Business

I fancy myself a calm and collected person. Mellow. Only very few things on this good earth rile me up enough to make me want to punch my Mac’s screen. But boy, is Apple’s App Review consistently one of them.

Prologue

I recently released ScreenFloat v2.3.8. I submitted it to the Mac App Store on July 16th, 2026, and later that day, it was reviewed and approved by App Review without any issues. I published it on July 20th.

As sometimes sadly happens though, right after the release, I noticed a crashing bug. It’s sort of obscure, but prominent enough that I would have been uneasy had I not fixed it right away. I’ll skip over the part where I panic and curse myself for missing it – that’s my own personal hell.
I fixed the bug (one line of code!) and re-submitted to Apple with an expedited review request, which was granted.

Chapter I – The Devil Is In The Details. But Where Are They!?

The update eventually entered review and was fairly soon afterwards rejected for the following reason:

This is an automated message. The review of this submission cannot proceed. See below for more information.

An automated analysis of the submission indicates the app includes placeholder or otherwise insufficient purpose strings.

Purpose strings must clearly and completely describe your use of data.

Take one of the following actions:

- If the app needs access to a protected resource that requires a purpose string, revise the purpose string to explain how the app will use the resource and provide a specific example of how the data will be used. Then resubmit for review.

- If the app does not need access to a protected resource that requires a purpose string, remove the code that’s accessing the resource and the purpose string from the binary. Then resubmit for review.

Aside from the fact that it took me a second to understand what the automated message (a first for me, by the way) meant by “purpose strings”1, I was flabbergasted.
Which purpose string does the automated process deem “insufficient” or a “placeholder”?
What does “insufficient” even mean here? Is a purpose string not clear, or too short, or not easily understandable? Is it grammatically incorrect? Is it in a localization, or the English language version?
What’s a “placeholder” considered to be in this context?
Is it objecting to my use of the protected resource, or my user-facing sentence?
Which protected resource is it objecting to in the first place?

It’s painfully ironic that they require my strings to “clearly and completely describe” my use of data, when they themselves so marvelously fail to do so.

You know what’s “insufficient”? Your freaking automated message!
They might as well have told me “Something’s preventing us from further reviewing your app. Good luck finding it, asshole!”.

Even if I only had one purpose string in my app, this would have been annoying, because that detail would help quite a bit in knowing where to look and what to change.
But I have several of them. ScreenFloat is a screen capture app, so it can record your screen, and, if you tell it to, your microphone and system audio. (And, if you enable it, your key strokes during recording, but that doesn’t need a “purpose string”.)
In addition to those, the app has a Face ID purpose string, because folders in ScreenFloat can be locked and unlocked with biometrics. It has a Pictures folder purpose string for the specific case where a v1-library-import fails for certain files, so the app can move those there.
It has an Apple Events purpose string so the app can use and interact with Shortcuts. Lastly, it has a Contacts purpose string, because ScreenFloat can Quick Look vcards encoded in QR codes, and that only works with that Contacts entitlement, otherwise no preview is shown, or worse, the app crashes. Don’t ask me why.

Long story short, ScreenFloat needs all those purpose strings, and I believe they are worded correctly. After all, they have been like this ever since they first shipped.

So I reworded two purpose strings to demonstrate goodwill, and re-submitted.

Chapter II – They’re Just Trying to Break Me Now

Another rejection. This time from an actual person. Or so I believe. It didn’t say “automated message” this time around. I’m not sure if getting escalated from an automated rejection to a human one is reason to rejoice, or to worry even harder. But I digress.

The rejection this time was much clearer, but just as infuriating.

The app uses one or more entitlements which do not appear to have matching functionality within the app.
[…]
com.apple.security.personal-information.addressbook

I see.
Apparently, App Review completely ignored – or is ignorant of – the fact that in App Store Connect2, there’s a section called “App Sandbox Information”, specifically designed for the purpose of telling App Review what entitlements your app uses, and why.
I have this. See here:

So pardon my French, but it’s freaking right there.

But I did as they did, and ignored this.
I gave them the same (paraphrased) reason via reply-to-the-rejection, and now the app is “Waiting for Review” again. What a waste of time.

Epilogue

I cannot stress enough how frustrating these inconsistent app reviews are.
If you recall from the beginning of this post, just 5 days ago, on the 16th, I submitted basically the same binary, and it went through without a hitch.
Now, a few days later, with a one-line change in the binary to fix a crash, I have two rejections for stuff that has been in the app since I can’t even remember when.

What gives, App Review?
Why waste my (and your) time with asking for info on an entitlement, when it’s already there?
Why not object to it at the very first review of that entitlement with v2.0, instead of out-of-the-blue, after goodness knows how many updates in between?
What’s with that absolutely disgusting and disrespectful automated message with no details whatsoever?
What happened between July 16th and today, five days later!?

And most importantly of all: why won’t you let me release my one-code-line-crash-fixing app update for ScreenFloat already!?

[Update]: ScreenFloat v2.3.9 was accepted by App Review on July 22nd and subsequently released.

[Update]: Apple called. And now I feel bad. I hope it was clear that I’m upset with the process, not the people behind it.



  1. A “purpose string” is the text displayed to users for requesting protected resources, like the microphone, camera, etc.
    There should be a different term for this than “purpose strings”. How about “Usage Description string”, since that’s how they’re defined in code, like “NSMicrophoneUsageDescription”? That would make it way more recognizable to developers. ↩︎
  2. The place where developers submit their apps to App Review. ↩︎
Read more

I’ve been wanting to make my website a bit more “alive” for a while, and I finally had the time to do something about it.


App’s Prices

For the longest time, I didn’t show the prices for my apps on my website (crazy, I know), simply because I didn’t feel like going into the HTML source, updating the static price, and re-uploading the page every time I had a sale.

Now, I have a PHP script that takes care of it for me. It can load prices for different localities using the App Store API, or from Paddle. I currently just get them from Apple right now, because my apps’ prices are the same on the App Stores and my website. The results are cached for 6 hours so I don’t have to query the API for every page load. For time-sensitive sales, I can easily discard the cache and have the page (and thus, prices) reload for everyone instantly.

Prices are now shown on the main website (see above), and on each app’s webpage:


“Current Version” Indicator, Release Notes

For ScreenFloat 2.0, I built an entire API and database with PHP and MySQL for the release notes I show in-app:

It allows me to get all sorts of information about any release, the obvious ones being version, release date and download file size.
This powers my in-app update mechanisms, the in-app release notes you can see above, and now the “current version” of apps on my website (see below), along with a web version of my apps’ release notes.

A click onto the version brings you to the web release notes. I tried to replicate the in-app release notes as closely as possible, with image- and video previews of features, etc. You can switch between English and German, rich- and plain text, and browse through the release history.


Highlighted Blog Posts

Though I’ve always linked to my blog from my website, I thought it would be neat to feature the newest blog post on the main page and the latest app-related post on the app’s webpage. I hope this is more enticing to click than a simple “Blog” link.

This uses the basic WordPress API. It looks for posts with specific tags or within specific categories, published within the last 2 months. If there aren’t any blog posts with that criteria, nothing is shown. I thought that better than showing old, outdated posts. I don’t want to give off “deserted” vibes.


Friends Page

I have a new Friends page where I highlight a few long-time friends of Eternal Storms Software. I still need to find a way to include that into the main website better.


Auto-Applying Discount Codes on my Web Store

Probably a no-brainer for anyone else, but I coded my web store myself, and only for Black Friday 2025 had the idea of auto-applying discount codes. That way, everyone gets the benefit of a discount when a sale’s going on, even if they don’t know about it.
The way it works is, I have a specific naming scheme for site-wide and app-specific discount codes, and the web store looks for them and applies them automatically.


Tip Jar

Recently, I’ve been implementing Tip Jars into my apps, for anyone who’d like to completely voluntary further support my work, beyond the app’s one-time purchase price (I don’t offer subscriptions).

Tip Jar in Yoink for Mac

Since I implemented it for both the App Store and via Paddle for the direct-purchase versions of my apps, I figured I’d implement a small web version of the latter, too.
I’m linking to it from my blog, but I’m still undecided about adding it to the main page. Probably going to leave it out.


Things Still In the Works

For now, there are two things I still haven’t gotten around to including.

Dynamic App Reviews

I do include reviews from the App Stores on the main and app pages, but they’re static. I’d like it to be a bit more dynamic, where I’d show random 4- and 5-star reviews. Apple offers APIs for that, so it shouldn’t be too much of a hassle.

A “Sources” Page

I’d like to have a page up indicating official sources for my apps: my website, the App Store, Bundlehunt (on occasion), MacUpdate, etc.
Basically a single source of truth for figuring out if a website offering a download of or licenses for my apps is official and legit.


Now back to work. Since I’m not vibe-coding, my apps don’t develop themselves!


Read more

Having implemented the backend and licensing mechanism, there was still an important part missing: a way to download and install updates for the app.

A “safer” download

My app trial downloads have always been just a direct file download. But because I wanted the auto-updating mechanism in my app to use that same download path, I needed to add a little layer of validation/safe-guard there.
Step 1: sign the zip file cryptographically with a private key, and upload the signature data alongside the zip file.
Step 2: write a download PHP script. Instead of just initiating a direct file download, the script starts the download, only if the zip file’s signature could be verified with the public key. If there’s a signature mismatch, an email is sent to me so I know there’s something going on.
Step 3: implement the download and signature check in the app. The app downloads the update zip file using the PHP script, with the signature of it passed along as a header. That allows the app to verify the zip file’s signature with the public key, before unzipping it. If the signature can’t be verified (because it was tampered with, or the download got corrupted somehow), the zip file is discarded and an error presented.

I got you, “gotcha”s

With the PHP download script, I encountered two “gotcha”s.
One was that, no matter what I tried, I couldn’t set the Content-Length header, which lets the downloading client know how much data to expect (so a download progress can be displayed properly). A lot of googling later (I also tried ChatGPT for fun, but it was no help here), there’s an environment variable that needed to be set on my server:

SetEnv ap_trust_cgilike_cl 1

With that, it miraculously started working.

The second “gotcha” was resumable downloads.
For a file that is only a few megabytes, it’s not that big of a deal, but for larger downloads, it is convenient and responsible to have resumable downloads.
If a download halts because of, say, a loss of connection, the downloader is able to later pick up the download from where it left off, instead of discarding what it had already downloaded and having to start over.
It took a bit of trial and error, but thankfully, there are lots of code samples for this available, this one being a prime specimen, and I eventually got it working.

Release the Notes

I just recently revamped my release notes to be more interactive:

It gives a nice overview of new features, improvements and bug fixes, I can display a nice header or footer if I like, and I can show users new features in action on-the-fly as a video, an image, display a website for instructions, or take users to features or settings in-app right away.

But I hadn’t thought it through enough. All this had been powered by a single JSON file. For multiple releases, over the years, that file would become larger and larger. And to display information for one release, I’d have to download the entire file. So I had to go back in and change that.
Instead of a single JSON file, I now have it in a database, each release in a row, so I can obtain information for individual releases, instead of having to download all of them at once.
Interacting with this database is powered by a custom PHP API.
It lets me retrieve all version strings for an app, alongside their release dates and build numbers – crucial for self-updating.
I can then request an individual release’s information as an on-demand created JSON. I use this in the Mac App Store version to show what’s new after an update has happened, and in the non-Mac-App-Store version to display what’s new before an update.
Additionally, it can return more than one row of releases. That allows me to show multiple releases in the “Update available” screen:
Say a user is on version 2.1. Versions 2.1.1 and 2.2 have already been released, but the user never updated to them. Now version 2.2.1 is out, and the user gets notified. Not only does the update dialog show what’s new in version 2.2.1, users can also see what was new in v2.2 and v2.1.1. Yay.

For feeding information into the database, I wrote a little PHP web “app”:

I can add localized release notes for a feature, improvement or bug fix, and manipulate the JSON (on the right side) directly, too, if I need to change the order, or fix a typo. I can go back into any version, any time, in an organized manner.

Lastly, I can download a nicely-formatted .txt file for each localization I can copy-paste into the App Store’s What’s New section (or anywhere else I like):

Self-Updating App

The simple and sane thing to do would have been to go with Sparkle. It’s fantastic. It can work with zips and other compression formats, dmgs, can do diff updates and all that stuff. It’s really good, and if you need any of that, or just want an easy way to do updates, save yourself the headache and just use Sparkle.

All I needed, though, was to unpack a zip file and install that update. And with all that custom code I wrote on the backend, to perfectly integrate and take advantage of it all, I figured I’d write my own updating mechanism.

The app has to:
1) Check if a new version is available
2) Display to the user what’s new and ask whether they’d like to download the update
3) Download the update
4) Verify the zip file’s signature
5) Unzip the zip file if the signature is okay
6) Verify that the new bundle is code signed, and that the code signature’s team of the new bundle is the same as the old one’s
7) Replace the old copy with the new one
8) Relaunch

Items 1 through 4 were easily done – all the heavy lifting is already taken care of by the server.
Item 5 – unzipping – is where it began to become tricky.
Actually, it already became tricky zipping up the app in the first place.
Like I stated before, I upload a zip file and its signature. To create both, I use a Shortcuts.app shortcut – I select the app produced by Xcode, it gets zipped up, a signature file is created, and I manually upload both to my server.
Alas, the “Make Archive” shortcut is teh suckage when it comes to apps.
Try it yourself: Create a “Make Archive” shortcut that creates a zip archive from an app, then unzip that archive and double-click – it won’t work (or, even worse, it’ll work on your Mac, but on a different one, it won’t. Thanks to UTM, I was able to figure that out).
So instead, I use a Run shell script phase with the ditto CLI:

ditto -ckX --sequesterRsrc --keepParent <app-file-path> <zip-file-path>

Unzipping that and running the app worked right away both on my Mac and in the UTM virtual machine, so the first hurdle had been taken.
Unzipping, then, in the app, also had to be done using that CLI:

ditto -xk <zip-file-path> <unzip-file-path>

Because I didn’t test each step right away and implemented the flow in one go, when I first tried it, I wondered where my mistake was when the app didn’t launch. It was only after quite a bit of backtracking and research that I came to the conclusion that my way of zipping and unzipping was the culprit.

My current iteration of my self-updating mechanism isn’t complete yet.
It can update itself when the user account that first installed the app is also the one updating it. But if a different user account runs the update, it fails, because of insufficient privileges. I had one implementation where it worked once, and then nobody could update anymore – yikes. I had another implementation where, when replacing the old app with the new one, for some reason the /Contents/MacOS/ folder could not be replaced (but the files in MacOS were deleted), corrupting both the old and the new copy.
Let’s just say, I hit some weird edge cases here.

This is obviously something I’m working on still, but in the meantime, when I suspect it’s a different user account attempting the update, I present the user with the new version of my app in Finder, and show instructions on how to manually update.
It’s nowhere near a perfect user experience yet, but it was a trade-off I was willing to accept for the time being (on behalf of the user, I’m afraid), knowing the upside of having control over the entire thing and eventually figuring it out.

It’s my main drive at work – figuring stuff out.

Next Time

Next time, I’ll talk about how and if it all worked out. I hope to see you then!

Read more

Now that I’d decided on a game plan, I had to put my code where my mouth was.
I thought it best to begin with the one thing that could make or break my entire endeavor: integrating my backend with my new Merchant of Record, Paddle.

Paddle Billing

Paddle Billing is very obviously targeted at recurring payments (“subscriptions”) rather than one-time purchases, underlined by the fact that Paddle Classic did have direct support for creating and/or handling license keys, whereas Paddle Billing does not. I know subscriptions are all the rage these days, but I just don’t like them.
A Quick-Start guide on how to handle one-time purchases would have been nice, or at least some sort of direction on what API to look into. The documentation in general is very good and detailed, but it isn’t outright obvious how all the pieces fit and work together. It feels like each piece is described nicely on its own, but how it all fits into a whole flow of a user purchasing something is up for the developer to figure out.
I would have preferred a use-case approach. Like, for subscriptions, what does a typical, recurring payment flow look like from first billing, to recurring billing, to the eventual cancellation by the customer? What does a one-time purchase flow look like? What APIs are involved? How are refunds handled? What are the caveats, and what should I pay close attention to? That would have helped a lot here.
Being a developer myself, I understand the situation too well. When you develop an app, you know it inside and out. That’s good, but it makes you prone to omitting the very fundamentals of your product when explaining it to others, because it’s second nature and obvious to you. Like, when a 3-Michelin-star-chef explains cooking soup to you by talking about how to plate it up and make it look nice, and you’re there wondering how to tell when the water is boiling.

Anyway, I did figure it out – Paddle’s sandbox environment made it beautifully painless. And once I did, I was very positively surprised by how extensive the API is and what you can build with it. It’s based on events and webhooks, where your server endpoints get called when certain events in your checkout flow happen. For instance, when a customer completes a purchase of a product, you get a transaction.complete event, which means you can now fulfill the order – by sending a license key, for example. That transaction persists on their server, so you can use it again to implement a “re-send license” feature, when a customer loses the email you sent. Neat.
You could also use this to implement a reminder feature, where you mail potential customers with an incomplete transaction, asking them if they’d like to complete their order. (Local and international laws apply, of course. And if you go down this route, do it very sparingly. Most people nowadays hate getting nag-mail out of the blue).


I went about implementing my backend using…

PHP, the old (t)rusty

Apparently, PHP is dead. Every year anew, it seems. And yet, it’s always been there when I needed it.
It has a huge community of very helpful developers behind it, is very well documented, and there’s a vast library of frameworks and code to take inspiration from. Personally, I love it. It’s also well integrated into and supported by my web server, regularly maintained and updated, so it was a no-brainer for me to use it for this implementation.

But I am not a backend developer. I usually leave backend development to those who really know it. That being said, I didn’t want to hand off something so crucial to be implemented by someone else; I would have little control over the code, I would have had to give them access to at least my Paddle credentials, and to parts of my web server – I’d rather never eat Käseleberkäse again. And I love Käseleberkäse. When it comes to my work, I keep it all as much first-party as I possibly can.
Does that mean a task that would have taken a professional fifteen minutes takes me three hours?

Yes.
But I sleep so much better at night.

I second-guessed every line of PHP code I wrote to ensure it runs smoothly and safely.
Cross every ‘t’, dot every ‘i’, and htmlspecialcharacter every ‘<‘.
And PDO. Why had I never heard about PDO before? In the past, I always interfaced with databases directly using mysqli (carefully, of course), but now, with PDO (PHP Data Objects), it’s way better – and allegedly safer.

To sell ScreenFloat and, in time, my other apps directly from my website (in addition to the Mac App Store), I had to implement the following:
The Backend
– Get a webhook callback from Paddle when a successful purchase occurs
– – Validate the callback, create the license(s) from the information in the transaction and send them to the customer
– – Save minimum information in a database for the license activation, deactivation, resetting, and recovery mechanisms
– Get a webhook callback from Paddle for refunds, to disable licenses created from that purchase transaction

The Website
– Integrate Paddle’s overlay into my website for checkout
– – Have a checkout “preflight”, where customers enter their name, email, and the type of license they want to purchase, to set up the transaction used in the Paddle checkout overlay

The App
– Make ScreenFloat accept licenses to be registered with
– – Have a license reset and lost-license-retrieval mechanism in-app

What was important to me here was that (a) I could easily include other apps for purchase later, (b) the license creation and all associated functions were independent of Paddle, so I could use it with other platforms (for bundles, mostly) and subsequently (c), if ever need be, switch from Paddle to a different Merchant of Record without having to re-implement everything.


Setting up the webhook on Paddle is straight forward. Select the events you’re interested in getting, supply your server endpoint for them, and it’s done.

After I verify the call is actually coming from Paddle and contains a valid transaction payload, I create the cryptographically signed license(s) from the customer’s information in that transaction, along with app-related information. After I verify it is signed correctly, I send it off to the customer in a nicely formatted plain-text and html-text multi-part email.
Initially I played around with encrypting the entire license and decrypting it in the app for validation and unlocking, but in the end settled on just signing a more-or-less plaintext payload to keep it simple.

Like I said in the first part of this blog post series, personal licenses can be used to register two copies of my app (on accounts on the same Mac, or on different Macs), whereas commercial licenses can be used to register one copy, but used by any account on that Mac.
There’s quite a bit of overhead in the implementation here. If you want to keep track of activations, you need a way to do that (a database and an “API” to manage all the necessary info), and will have to have a way to reset individual or all activations of a license: if a customer gets a new Mac, they might want to move their registration over from the old one, for example.
When it comes to commercial licenses, I figured administrators wouldn’t want employees to be able to mess with a copy’s registration, so those require a “key” to be reset, which is individually created and sent alongside the license keys.


For the customer to get to that part, I first needed some sort of checkout on my website. Paddle provides a drop-in storefront, which handles it all. Sweet. I wanted to precede that with a simple form where the customer can select what type of license they want (personal; personal as a gift for someone else; commercial), and enter their name and email, to make sure a name is supplied for the license to be personalized with.

Here comes Paddle’s API into play – I create a new transaction from the supplied information and pass it on to the Paddle storefront, with all information already set. All that’s left then is for the customer to enter their payment details, and they’re done.

This “preflight” also allows me to provide special discounts for Apple employees, like I’ve already been doing for Yoink for Mac, (although in a very different way). I do plan to transition that over to Paddle and licenses as well, just to have it all in one place, handled by the same mechanic in the background.
I thought about implementing Paddle’s storefront in-app directly, too, but I decided against that – I believe users feel safer and more comfortable entering payment details when they’re in the browser of their choice.


All that work is useless if users can’t use a license in my app to unlock it. I already offer trial versions for most of my Mac apps as downloads from my website, with ScreenFloat being no exception, so I already had a head start in creating an out-of-app-store branch. All that was missing from it was the license validation and app-registration that removes the trial limitations.

The license a user receives by mail is a custom app-url-scheme link which, when clicked, automatically launches my app and fills out the required license key field, for the user to be confirmed for registration. Alternatively, I also include a download link for a license file. I figured some people might like a backup copy of it somewhere, and it can make it easier to share a gifted license.

Since licenses aren’t stored anywhere, that download link is basically the same as the custom app-url-scheme link that has the payload as a url query parameter, but instead of pointing to my app, it’s pointing to an endpoint on my server – a PHP script that turns that query parameter into a file download. I found that to be a neat workaround.
Naturally, that file can be double-clicked to register the app, if it’s already installed.
The app itself then verifies the payload locally and with the server and, if everything checks out, unlocks itself.


Next Time

There’s still the matter of self-updating the app, displaying release notes, and verifying app update downloads, so that’s what we’ll go over next time. I hope to see you then!


Read more