doneface turns the time you just spent into one selfie, printed like a receipt and kept on your device.

Targeting: HAMM Award, RevenueCat Design Award, RevenueCat Peace Prize.

Inspiration

I kept meeting the same person in app reviews and in myself: someone who had already tried the restriction apps and stopped using them, not because they stopped working but because of how they felt. Their words were always some version of "I felt like I was being made to hold back." Then one missed day resets the count to zero, and the app that was supposed to help starts keeping score against you.

The insight came from an ordinary detail. Many focus apps make you do something to unlock your phone again — a photo, a code, a math problem. That thing is treated as a toll: a condition you satisfy to get your phone back, and then it is thrown away.

doneface keeps it instead. The selfie you take at the end is not a condition. It is evidence that you passed through that time, with your own face on it. Nothing else in the app is allowed to be more important than that.

What it does

You choose a duration and what the time is for, and optionally pick apps to put down (Apple's Screen Time / Family Controls). When the time ends, you take one selfie. A proof card prints like a receipt into your log — the photo inside a paper frame, with EXIF-style fields recording the duration, the purpose, and the time it closed.

If you end early, doneface keeps that card the same way, and the card's RESULT field reads CLOSED HERE instead of COMPLETED. No apology, no partial credit, no different color. That behavior is a written rule in the project's constitution, not a preference.

What doneface refuses to have: streaks, day counters, rankings, comparison with other people, friend feeds, face recognition, AI face analysis, emotion scoring, beauty filters, celebration animations, and any wording from the vocabulary of blame. Photos and logs live only on your device.

How we built it

SwiftUI and SwiftData on iOS 18, Swift 6, with the project file generated by XcodeGen so project.yml stays the single source of truth. Screen Time locking uses FamilyControls, ManagedSettings and DeviceActivity, with two app extensions that draw the custom shield and its "Open doneface" button. AVFoundation handles capture and the quiet month-in-review movie export; ActivityKit powers the Live Activity and Dynamic Island; WidgetKit shows the latest proof card on the home screen. 600 tests run on every change (581 passing, 19 skipped, 0 failing on the release branch).

RevenueCat. Purchases and restores go through the RevenueCat SDK (purchases-ios 5.87.1). Purchases.configure runs once at launch with StoreKit 2 pinned; every purchase in the app is Purchases.shared.purchase(product: StoreProduct(sk2Product:)), and the restore button calls Purchases.shared.restorePurchases(). Existing subscribers from the pre-RevenueCat versions are migrated with a single syncPurchases() call, gated by a pure function so it can run exactly once and retry only if it failed.

What RevenueCat deliberately does not do here is decide whether you have Plus. That evaluation stays on-device with StoreKit 2's Transaction.currentEntitlements, and CustomerInfo has no write access to the entitlement flag at all. This is written down as a binding invariant in the project's change process (PROP-029, decision D2) with machine-checkable acceptance criteria: the cache-preserving code path must show zero diff lines, and the seven views reading the entitlement flag must show zero diff lines. The reason is simple. A network outage on any vendor's side — including RevenueCat's — must never be able to take away something a person already paid for. Routing purchases through RevenueCat while keeping the verdict local gives us the analytics and the purchase infrastructure without ever handing over the yes/no that a paying user depends on.

Challenges we ran into

The extension nobody was looking for. The custom lock screen refused to appear, with no error anywhere. Twelve hypotheses died before the real cause turned up: one word wrong in NSExtensionPointIdentifier (ManagedSettings where it had to be ManagedSettingsUI). The system was not failing to load our extension — it was not looking for it. Complete silence, we learned, is the signature of "nothing is searching," not of "something broke."

Local StoreKit configuration that quietly lied. Screenshots of the paywall kept showing the wrong prices. The .storekit configuration file only takes effect through the Xcode scheme's run action, so anything launched another way silently fell back to live App Store metadata. Once we understood that, a second fact followed: correct paywall screenshots can only be taken after App Store Connect itself is correct.

Three review cycles, each teaching something different. The first version was auto-rejected over Family Controls and was approved on resubmission with evidence and zero code change. A later rejection under Guideline 5.1.1(iv) named exactly one screen, but the fix was to trace every call site of the permission APIs — button labels can be reworded, API calls cannot — and three screens had the same shape. Then removing the word "Allow" from a settings row quietly destroyed the reader's sense of where the row led, which no test and no build could detect. We had to look at the screen.

Accomplishments that we're proud of

Shipped on the App Store on August 11, 2026, and kept shipping: a redesigned first run, a home screen widget, monthly grouping in the log, a weekly plan, and now the RevenueCat integration in review.

The invariants held. "Ending early is kept the same way" and "the selfie unlock is never behind a paywall" are not aspirations in a README — they are rules that a change proposal has to survive before it can touch them, and the entitlement invariant is enforced as a diff constraint that a machine checks.

The vocabulary is enforced too. A separate audit pass reviews any copy change against a written list of words the app does not use, with the reason attached to each one: 依存 (dependence) because it pathologizes, 未達 (unachieved) because it imports goal-chasing, サボり (slacking) because it is self-negation.

What we learned

A monetization principle is only real when it costs you something. The obvious way to make money in this category is to sell stronger locking. Writing down that we would not, before we needed the revenue, is what made every later paywall decision easy.

Decide who owns the verdict before you integrate anyone. Adding RevenueCat was a two-hour code change. Deciding that it would drive purchases but never determine entitlement took a documented proposal, and it is the decision that will still matter in five years.

Confirm the mechanism, not the number. Twice we accepted a plausible explanation because the numbers happened to line up — a price that looked like a currency conversion, an extension that looked signed correctly. Both were wrong. Matching numbers are not proof of a mechanism.

What's next

Version 1.2.0 with RevenueCat went live on September 6, 2026, so we finally have purchase data to look at instead of guesses. The weekly plan at ¥300 was introduced specifically to test whether a low-commitment entry converts better than a discontinued free trial did, and that is the first question the RevenueCat dashboard will answer. Beyond that: the month-in-review movie for Plus, English-market ASO, and an iPad layout.

Built With

  • activitykit
  • app-extensions
  • avfoundation
  • avkit
  • deviceactivity
  • familycontrols
  • managedsettings
  • managedsettingsui
  • observation
  • photokit
  • revenuecat
  • storekit-2
  • swift
  • swift-6
  • swift-charts
  • swift-concurrency
  • swift-testing
  • swiftdata
  • swiftui
  • uikit
  • usernotifications
  • widgetkit
  • xcodegen
Share this project:

Updates

Submission history