in defense of the tip jar
why I chose voluntary tips for EXIF.ER, and how three buttons became a small StoreKit project
I wanted to add a small thank-you to EXIF.ER. Three buttons in the Info and Credits screen, priced as a coffee, a snack, or lunch. Somewhere along the way, this acquired a transaction listener, duplicate detection, a migration for a stored boolean, and tests for what happens when the store returns absolutely nothing. There is a respectable amount of machinery behind buying someone an imaginary croissant.
I made EXIF.ER for my own photo backups. It renames photos and videos using their capture dates, and the tip jar shipped in the 3.0 update. A tip adds a thank-you and some hearts to the Info and Credits screen. All the renaming features are free.
paying for a chore
Sometimes I have a batch of files to organize. Then I leave the app alone. A subscription would give me another thing to remember to cancel after finishing a chore, which is a peculiar improvement to my backup routine.
I would like to be paid for the work. Bugs survive the version where I was sure I'd caught them, and keeping up with iOS takes time. But when I imagine charging for this app, I keep coming back to someone with a folder much like mine, wanting to sort out their photos and get on with something else.
I couldn't bring myself to build a counter that stops a batch at the eleventh photo until someone pays. I'd have to decide where to interrupt a task I'd spent all this time trying to make less annoying. So the invitation lives in Info and Credits, where someone can leave a tip if the app has been useful.
when the store returns nothing
The tips are consumable products, with their identifiers and translated names in
TipTier. I sort the returned products so coffee always comes before snack and
lunch. For prices, the buttons use StoreKit's Product.displayPrice, already formatted for the
storefront's locale.
Drawing the buttons is straightforward once those products arrive. They might also fail to load,
or the store might send back an empty list. The TipJar service keeps track of that
with a small enum:
enum State: Equatable {
case loading
case unavailable
case ready([TipOffer])
}
A failed or empty response leaves a message and a retry button in the screen. Once the offers
arrive, the interface gets TipOffer values with the text and emoji for each tile;
the StoreKit products stay in the service. While a purchase is open, the other tip buttons are
disabled.
That also gave me a way to prepare screenshots without waiting for the store. The debug harness supplies offers, including their prices, to the same views the app uses. I could check the Portuguese buttons even when StoreKit wasn't answering.
keeping count
The thank-you needs to work even when a purchase finishes later. EXIF.ER listens to Transaction.updates and sends those updates to
the same handler as a successful purchase() result. The handler checks
verification, records the tip, then calls finish().
Two ways in meant I had to guard against counting the same transaction twice. The recording method keeps the IDs it has seen during that launch, and ignores unknown products and revoked transactions. Each accepted tip adds a heart, up to five.
The count lives in UserDefaults on that installation. An earlier version stored
only whether someone had tipped, so there is a migration that turns the old “yes” into one
heart.
I test the recording method with plain transaction IDs and flags. Send the same ID twice, check that it adds one heart; send a revoked transaction, check that it adds none. Other tests cover the saved count and the old boolean. For loading, a supplied closure lets the tests return nothing or throw an error, then try again. These tests exercise my code. Buying a tip through the store still needs a separate check.
room for the name
Three prices in a row look tidy until one translated name wraps and pushes its price down. The
tile uses lineLimit(2, reservesSpace: true) to give every name two lines, even when
it only needs one. At accessibility text sizes, AnyLayout stacks the buttons
vertically and the names can take as much room as they need.
I had thought of the tip jar as a small addition to the app, then found myself adjusting the space above a price. It is easy to spend less care on a screen I rarely open myself.
Version 3.0 is now on the App Store. As of September 10, 2026, no one has left a tip yet. I’m still hoping for that coffee.