7habits
The seven daily habits that matter most, one tap a day.
Problem
Most habit apps hand you a blank screen and ask you to invent your own habits. That is the hardest part, and it is where most people give up.
Solution
7habits removes that decision. It is built around seven daily habits that research links to a healthier life, prayer, gratitude, connection, exercise, hydration, sleep and nutrition. You do not design a system. You just tap once a day, and consistency turns into colour you can see.
What it is
A habit tracker for iPhone and web, built around seven fixed daily habits, so you track what matters instead of starting from nothing.
Design process
General research
I started from a simple idea. The habits that shape a healthier life are already known, prayer, gratitude, connection, exercise, hydration, sleep, and nutrition. The problem is not knowing them, it is keeping them.
Market validation
Most habit apps fail people by handing them a blank screen and asking them to design their own system. That is the hardest part, and it is where people quit. A fixed, ready made set of habits was the gap.
Ideation
I designed the app around those seven habits, one tap a day, with colour that deepens as your streak grows, so your consistency becomes something you can see.
Testing
I ran multi perspective audits of the app and the copy, tested purchases on the simulator and on TestFlight, and checked every flow with real screenshots.
Iteration
I fixed in batches from those audits and from real user reports, twenty one fixes in one pass, nine in another, and a full sweep that cleaned up the trial and pricing wording across the app, the site, and the store.
Build
I built the seven habit tracker, a stats dashboard with a heat map and streaks, milestone celebrations, an evening reminder, and the whole payment system on StoreKit with a monthly, yearly, and lifetime plan.
Test
Before launch I set up TestFlight groups, verified the purchase and restore paths, and made sure a paying user is never locked out by a transient or offline check.
Release
7habits is live on the App Store and on the web at 7habits.app, with all three plans approved.
Technical breakdown
7habits is a React and TypeScript app built with Vite and Tailwind, wrapped for iPhone with Capacitor. It is local first, so all of your habit data lives on the device with no account and no backend. Two small native Swift plugins bridge into Apple, one for the purchases through StoreKit, and one for the sound. There is also a web version of the app at 7habits.app.
A local-first web bundle wrapped in Capacitor, with two native plugins
The shipped iPhone app is a React and Vite bundle running inside Capacitor, with no server and no account, and all habit data held on the device through Zustand persisted to Capacitor Preferences. Only two jobs drop to native Swift, a StoreKit purchases plugin and an audio plugin, because those are the two things a web view genuinely cannot do well. Reaching for native only where the web layer actually fails keeps almost all of the app in one fast, portable codebase.
Owning the audio session the web view kept muting
The habit notes are synthesised in WebAudio, and iOS silenced them on the ring switch, because WebKit classes WebAudio inside a WKWebView as ambient audio and sets that category from its own process, out of my reach. So I wrote a native audio plugin with AVAudioEngine that claims the session as playback with mixWithOthers, while the web layer still decides the pitch and length of each note, so the sound is unchanged. When a web view fights you over a device capability, move only the capability to native and leave the logic in the web.
The purchase bug that was a missing plugin registration
On device, purchases did not work at all, and it was not the StoreKit code. The plugin was simply never registered with Capacitor, so the bridge did not exist. Registering it was the whole fix. On top of that I surfaced the real StoreKit error on the paywall instead of a generic failure, and retry product loading with a backoff. When a native bridge does nothing at all, suspect the wiring before the logic.
Reading entitlement so a paying user is never locked out
The app reads your entitlement silently from Transaction.currentEntitlements, with no AppStore.sync and no sign in prompt on launch, a lifetime purchase is never revoked, and a transient or offline check never removes access. Refunds and expiries re lock naturally, because they are simply absent from currentEntitlements. The rule is to fail open for a paying customer, a flaky network should never lock someone out of what they bought.
A trial that a clock change cannot reset
There is a short free preview after onboarding, then the paywall, and it is tracked by a last seen timestamp rather than a live countdown, so moving the device clock backward does not hand you a fresh trial. Any client side time gate has to assume the user can move the clock, so anchor it to a stamp you control, not to the wall clock.
Removing a dead trial layer and dishonest copy
Apple rejected a build over prices shown in screenshots, and separately I found the app still carried no credit card and free trial wording that no longer matched the real, card required StoreKit trial, spread across the app, the landing page, and the store metadata. A five reviewer sweep deleted the dead app managed trial layer and aligned every word to what actually happens. Dishonest copy is not only a review risk, it is a promise your product then has to keep.
The Lifetime plan that could not ship alone
After the subscriptions were approved, the 99 dollar Lifetime, a non consumable, hit the rule that a first non consumable must be submitted on a version, and could not ship on its own. I carried it on a new version 1.0.1 submitted together, then released all three plans. Some App Store rules are about which items ride on which version, and you tend to learn them only by hitting them.
Stopping a trial reminder from reaching people with no trial
A day twenty nine, your trial ends tomorrow notification could fire for people who never started a trial. I removed it and cancel any already scheduled copy on launch, so a message queued on an old build cannot still arrive. A scheduled local notification is a promise made in the past, so when the rule changes you have to go back and cancel what you already scheduled.
Links