Appearance
Trial System — Architecture & Design
Overview
LucidPal runs three independent trial meters in parallel: an app-wide 7-day AI trial (AppTrialManager), a 30-day email integration trial (EmailTrialManager), and a 30-minute Live Notes trial (LiveTrialManager). The three trials never interfere with each other — each has its own trigger, duration, and gating logic. All active trials are always surfaced to the user in TrialStatusCard inside Settings.
Trial Inventory
| Trial | Manager | Trigger | Duration | Cloud AI | Daily limit | Server enforced? |
|---|---|---|---|---|---|---|
| App AI | AppTrialManager | Onboarding completion | 7 days | Yes | 30 messages/day | No (client-side) |
| Email integrations | EmailTrialManager | First Gmail or Exchange connect | 30 days | Yes | None | No (client-side) |
| Live Notes | LiveTrialManager | First transcription session | 30 minutes total | No (Deepgram cost) | N/A | Yes (Durable Object) |
Independence Guarantee
The three trials serve different product goals and are intentionally decoupled:
- Live Notes is Deepgram-cost-gated (real-time transcription incurs per-second billing). The server's Durable Object is the authoritative counter — the client syncs via
syncFromServer(remainingSeconds:). It cannot be extended by other trials. - Email integration trial is habit-formation-gated. The goal is to get the user to connect Gmail or Exchange and experience the calendar-aware AI queries. It starts only when a real integration is made, and its 30-day window is designed to cover multiple work weeks.
- App AI trial is feature-preview-gated. Every new user gets 7 days of cloud AI (Gemini 2.5 Flash) to experience the difference vs. on-device inference. It starts at onboarding completion and applies to all users regardless of email integration status.
Each manager is a standalone ObservableObject. PremiumManager holds optional references to both client-side managers and queries them directly — there is no shared state or event bus between the three.
Trial Stacking — The Retention Funnel
The email trial was deliberately designed to extend cloud AI access beyond the 7-day app trial:
Day 0 Sign-up → onboarding complete → AppTrialManager.startTrial()
Cloud AI active (7 days, 30 msg/day)
Day N (N < 7) User connects Gmail/Exchange
→ EmailTrialManager.startTrial()
→ Email trial starts (30 days)
Day 7 App trial exhausted
→ EmailTrialManager.isActive == true (still running)
→ canUseCloudAI remains true
→ canSendCloudMessage = true (email trial path)
Day 7+N Email trial exhausted
→ Both trials expired
→ canUseCloudAI = false (unless paid)
→ canSendCloudMessage = false (unless paid or credits > 0)The email trial is a retention hook that extends cloud AI access beyond the app trial — by design. This is communicated explicitly in the day-5 app trial warning: if an email integration is active, the warning body tells the user their email trial will continue for N more days after the AI trial ends.
Cost Model
| Source | Per-user cost | Cap mechanism |
|---|---|---|
| Gemini 2.5 Flash (app trial) | ~$0.01–0.07 | 30 msg/day hard cap in AppTrialManager |
| Gemini 2.5 Flash (email trial) | ~$0.01–0.07 | emailTrialManager.isActive (no daily cap) |
| Deepgram (Live Notes) | max ~$0.13 | Server Durable Object (30 min hard cap) |
| Total worst case | ~$0.20 | Combined across all trials |
Abuse risk: bot signups that create many accounts to get repeated 7-day free trials. Mitigation: AppTrialManager.dailyMessageLimit = 30 hard-caps the blast radius per account per day. The email trial has no daily cap because it requires a real OAuth connection (Gmail) or Exchange authentication — these act as natural friction.
Transparency Rules (Non-Negotiable)
Every future contributor must follow these rules. Violating them erodes user trust:
- Every active trial must appear in
TrialStatusCard— no hidden state, no invisible timers. - Trial interactions must be surfaced in warning copy (the day-5 app trial warning explicitly mentions the email trial's remaining days if it is active).
- Post-trial tier requirements must be stated before the trial ends — not at the paywall gate.
- Live Notes minutes run in parallel — never paused or extended by other trials.
- No trial resets on reinstall for Live Notes (server-enforced); app/email trials are client-side UserDefaults (acceptable tradeoff — complexity of server-enforcing these exceeds the abuse risk for first-party use cases).
AppTrialManager — Implementation Notes
Key design decisions:
- Daily message cap resets at midnight local time.
recordMessageSent()compareslastCountDatetoDate()usingCalendar.current.isDate(_:inSameDayAs:). This uses the device's current timezone — intentional, not a bug. canSendMessageshort-circuits on trial exhaustion. Even ifdailyMessageCount < dailyMessageLimit,canSendMessagereturnsfalsewhenisExhausted(day 8+). The check isisActive && dailyMessagesRemaining > 0.startTrial()is idempotent. It guards onhasStarted— calling it multiple times (e.g. from bothinitandonChange(of: settings.hasCompletedOnboarding)) is safe.- PremiumManager priority order for
canSendCloudMessage: ultimate → emailTrial → appTrial → credits. This means an ultimate subscriber is never credit-gated, email trial supersedes the app trial's daily limit, and the app trial supersedes the credit bucket.
TrialStatusCard — Visibility Rules
TrialStatusCard renders when appTrialManager.isActive || emailTrialManager.isActive || liveTrialManager.secondsRemaining > 0.
Each row has its own visibility condition:
- App AI row:
appTrialManager.hasStarted && !appTrialManager.isExhausted - Email row:
emailTrialManager.hasStarted && !emailTrialManager.isExhausted - Live Notes row:
liveTrialManager.secondsRemaining > 0
Pill color logic (same as EmailTrialPill):
> 10 days→ blue6–10 days→ amber≤ 5 days→ orange- Last day / exhausted → red
Live Notes uses a time-based label: "MM min left" when ≥ 1 minute, "SS sec left" when under 1 minute.
Files
| File | Purpose |
|---|---|
Services/AppTrialManager.swift | App-wide 7-day AI trial — start, daily message tracking, exhaustion |
Services/EmailTrialManager.swift | 30-day email integration trial — start on OAuth connect, query tracking |
Services/LiveTrialManager.swift | 30-minute Live Notes trial — server-synced second counter |
Views/TrialStatusCard.swift | Unified trial status card shown in Settings — all active trials in one view |
Views/AppTrialWarningView.swift | Warning sheet for app AI trial (day ≤ 2 and final exhaustion) |
Views/EmailTrialOnboardingSheet.swift | Onboarding sheet explaining the email trial at connection time |
Views/EmailTrialWarningModal.swift | Warning modal for email trial approaching expiry |
Views/EmailHintCard.swift | Day-paced hint cards shown during email trial to drive habit formation |
Views/EmailTrialPill.swift | Compact countdown capsule (reused in TrialStatusCard rows) |
Views/LiveNotesTrialModal.swift | Modal shown when Live Notes trial approaches exhaustion |