Example Features

Todos, Home, Profile, Refer a Friend, and permission priming — the replace-me screens.

These are the app's Example screens — working features wired to real repositories that exist to be studied, copied, and then replaced with your own product. None of them is load-bearing: delete Todos and the app still builds. Each screen is a small, honest demonstration of the patterns documented elsewhere in these docs, so treat them as the reference implementations to copy from.

Example vs Removable demo: the screens here are meant to be replaced (you'll build your own Home, your own first feature). The Showcase tab is meant to be deleted wholesale. Both are non-core.

The main shell has four tabs — Home, Component, Showcase, Me — plus a side drawer for everything else (Uploads, Refer, Personal Info, Account & Security, Notifications). Todos, Refer, and Permissions are pushed onto a tab's NavigationStack, not tabs themselves.

Todos — the reference feature

Features/Todos/ is the one to copy first. It's a full CRUD feature built the "right way" end-to-end, so it exercises every layer described in Data Layer: Todo model → TodosRemoteDataSourceTodosRepositoryTodosViewModelTodosView.

What it demonstrates:

  • Full CRUD — add, inline-edit (tap to edit in place, save/cancel), toggle complete, delete, and reorder (move up/down) against the Supabase todos table.
  • Filter chips — All / Active / Completed, with a filter-aware empty state (todos.empty / todos.noActive / todos.noCompleted).
  • Clear-completed confirmation — a destructive bulk action gated behind a confirmation dialog.
  • Optimistic updates with rollback — toggle, delete, reorder, and clear-completed all mutate local state immediately, then reconcile with the server; on failure they restore the previous list and surface an inline error. This is the offline/latency story: the UI never blocks on the network.
  • UiState<[Todo]> — the view switches over .idle/.loading/.data/.error and renders the feedback trio accordingly, with pull-to-refresh.
  • Signed-out state — when there's no session, Todos shows a sign-in prompt instead of an empty list (RLS would reject the query anyway).
// Optimistic toggle: flip locally, reconcile with the server, roll back on error.
func toggle(id: UUID) {
    guard let existing = todos.first(where: { $0.id == id }) else { return }
    var optimistic = existing
    optimistic.completed.toggle()
    upsert(optimistic)

    Task {
        await runInlineMutation {
            let updated = try await repository.toggle(id: id)
            upsert(updated)
        } onError: {
            self.upsert(existing)   // restore on failure
        }
    }
}

Copying Todos into your own feature is the walkthrough in Data Layer → Adding your own feature. Swap the model, the table, and the repository; keep the view-model shape.

Home — the product landing page

Features/Home/HomeView.swift is deliberately not a generic app home — in the template it's a product pitch for someone evaluating the starter. It's the clearest screen to gut and rebuild for your own app. It has three parts:

  • Hero + stack badges — the product title, value prop, and a wrapping FlowLayout of technology badges (SwiftUI, Supabase, RevenueCat…). The badges are intentionally not localized (same reason "Supabase" isn't translated).
  • State-aware license card — a small, testable state machine (HomeLicenseState.from(isConfigured:isSignedIn:isEntitled:)) picks between a "buy" CTA and an "owned" card. It reads env.entitlements.isEntitled — the shared subscriptions read-model, not the RevenueCat SDK. With no RevenueCat key it falls back to the buy CTA rather than a broken "owned" state.
  • Quick actions — a grid that jumps to the Component gallery, Todos, and the Upload sample. Navigation is callback-driven (onOpenTodos, onOpenShowcase, …) so the shell owns cross-tab jumps and Home stays decoupled from the removable Showcase catalog.

When you replace Home, the pattern worth keeping is callback-driven navigation: the view takes () -> Void closures instead of reaching into the shell, which is what lets the Showcase be deleted without touching Home.

Profile — Me, Personal Info, Account & Security

Features/Profile/ is the account hub, reached from the Me tab. It splits into three screens by concern:

ScreenWhat it does
MeViewThe account landing page: avatar + display name (ProfileDisplay), entry points into the sections below, and sign-out. Shows a sign-in CTA when signed out.
PersonalInfoViewEdit the editable profile fields (display name, etc.) and view read-only account facts; writes through ProfileRepository.
AccountSecurityViewThe sensitive-actions screen — change email (with OTP confirm), change password, toggle the biometric app-lock, sign out other sessions, manage subscription, and delete account.

Each destructive or credential action routes through AuthRepository and is gated behind a confirmation dialog (AccountSecurityViewModel.Confirmation). Delete account calls the delete-account Edge Function, which removes the user's rows and storage objects before the auth user — Apple requires in-app account deletion, so keep this wired (see App Store Release).

Refer a Friend

Features/Refer/ReferView.swift is a minimal share flow: a ShareLink wrapping an invite URL built by ReferralLinkProvider. The URL is https://<LINKING_DOMAIN>/ when LINKING_DOMAIN is set, falling back to soar:/// otherwise — so it degrades gracefully with no secrets, the same as every other service.

It also ties into the rating trigger: when the share sheet has been opened and the app returns to .active, it records a referral_shared action via env.rating.recordAction(...), one of the signals feeding the StoreKit review prompt.

Permissions priming

Features/Permissions/PermissionsView.swift is a priming screen reached from Me — it explains the benefit of each permission before the native system dialog appears, which measurably improves opt-in rates. It covers four PermissionIds: notifications, photos, camera, and tracking (ATT).

Behavior per card:

  • Not yet requested → a button that triggers the native prompt.
  • Denied or restricted (requiresSettings) → the button switches to Open Settings, because iOS won't re-show a dialog the user already dismissed.
  • Statuses refresh on appear via PermissionsViewModel.refresh().

ATT (App Tracking Transparency) governs third-party, IDFA-linked tracking — not first-party analytics. PostHog does not depend on ATT consent; that card is reserved for future attribution work. Don't gate analytics behind it.

Replacing these screens

A pragmatic order when you start building your own app:

  1. Keep Todos as a template, build your first real feature by copying its layer stack, then delete Todos.
  2. Gut Home — replace the hero, license card, and quick actions with your own landing content; keep the callback-driven navigation shape.
  3. Keep Profile and Permissions mostly as-is — they're generic account plumbing you'll want regardless (especially account deletion, which Apple requires).
  4. Delete the Showcase once you no longer need the demo catalog.

On this page