Redeem codes, and the background service I deleted on purpose
Two things shipped on 9 October: a redeem-code flow, and the removal of always-on background auto-booking. The first exists because mainland China cannot use Google Play, so store billing does not exist for those users; the second was me deciding that notification parsing could not be trusted.
PFEx takes payments through Google Play billing. But mainland China cannot use Google Play services, so users there install the APK and store billing does not exist for them. So the 9 October release filled that gap: redeem codes, an entitlement channel that skips the store. In the same commit I also deleted the background-resident service, and that one was not a gap to fill. It was me deciding I did not want that piece any more.
The redeem code
The UI is just a "use redeem code" field. The trouble was not the UI. It was the order.
I had the order backwards at first: I granted the entitlement before logging the user in to the subscription service. The backend then tried to write an entitlement for a user it did not know yet, and the service answered "subscriber not found". Once I had that error in hand, the order was obvious: log in to the subscription service first, then grant. The grant is asynchronous, so the client waits a moment before the entitlement arrives.
The other trap was how coarse the binding is: one code binds one device. Someone switching phones has to migrate first. The prompt says exactly that: "This code is already bound to another device. After migrating, the original device loses Pro access." Confirming resets the local device id, and the old phone falls back to the free tier at its next check.
Offline, I did not kick anyone out immediately: the paid tier keeps working for 3 days offline, and only then expires.
Requests are signed. To be clear about what that buys: a signature raises the effort of tampering with a packet, and that is all. The real check is on the server. Whether the entitlement works depends on the backend, not on what the client stored locally.
Deleting the background service
Let me be honest about the motive: this was not to pass a review.
PFEx used to have a "smart watch" feature: a resident background service that polled every few seconds, recognised income and spending out of the notification bar, dropped them into a local draft queue, and kept a persistent notification. Its cost was certain. Its payoff was not.
The weak link is recognition itself. A payment notification often says nothing more than "payment successful", and of the three things I need, amount, merchant and time, at least one is missing. An app changes the wording of its template once and every rule written against the old one stops matching. Some pushes are pure marketing text, and the actual debit never appears in a notification at all. Missing information has to be guessed at, a wrong guess becomes a wrong entry, and the user ends up fixing by hand what the feature was supposed to spare them. By the end it generated more corrections than it saved taps.
On top of that sits the cost of residency: a service that never stops, a notification that cannot be dismissed, and keep-alive behaviour that differs by manufacturer. Certain overhead against unreliable input. I did not have to think twice.
So this release switched the whole thing off, not merely hid it. The notification listener is not even registered any more, so no notification reaches the app. Boot-time auto-start is gone. The background dependency is out of the dependency list entirely. The checks that used to run in the background now run once, when you open the app. I need it to be correct, not to stay awake.
There was also a genuine Android 14 trap along the way. It has nothing to do with why I removed the feature, but it is worth writing down: the plugin's own manifest declared a background service missing a required field. Commenting out my own calls was not enough; the system still threw when it started the service, and I had to strip that declaration out of the merged manifest to get it clean.
A bookkeeping app does not need to live in the background. The data is local; computing once on open is enough. That is a product judgement, not a compliance conclusion.
So it is not an optional feature any more: auto-booking produces nothing today. Booking by hand is not a fallback here, it is the product.
Also in that commit
Backup import now verifies row counts. Previously "success" meant "no exception thrown", which allowed a quietly partial restore: import completed, ledger short by a chunk. Now the imported row counts are compared against the export and a mismatch aborts with a message.
The recurring-flow entry also moved. It used to hide inside the growth chart; now the amount row itself is tappable and opens the period flows list. One less menu, one more obvious gesture.