Skip to content

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 ​

TrialManagerTriggerDurationCloud AIDaily limitServer enforced?
App AIAppTrialManagerOnboarding completion7 daysYes30 messages/dayNo (client-side)
Email integrationsEmailTrialManagerFirst Gmail or Exchange connect30 daysYesNoneNo (client-side)
Live NotesLiveTrialManagerFirst transcription session30 minutes totalNo (Deepgram cost)N/AYes (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 ​

SourcePer-user costCap mechanism
Gemini 2.5 Flash (app trial)~$0.01–0.0730 msg/day hard cap in AppTrialManager
Gemini 2.5 Flash (email trial)~$0.01–0.07emailTrialManager.isActive (no daily cap)
Deepgram (Live Notes)max ~$0.13Server Durable Object (30 min hard cap)
Total worst case~$0.20Combined 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:

  1. Every active trial must appear in TrialStatusCard — no hidden state, no invisible timers.
  2. 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).
  3. Post-trial tier requirements must be stated before the trial ends — not at the paywall gate.
  4. Live Notes minutes run in parallel — never paused or extended by other trials.
  5. 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() compares lastCountDate to Date() using Calendar.current.isDate(_:inSameDayAs:). This uses the device's current timezone — intentional, not a bug.
  • canSendMessage short-circuits on trial exhaustion. Even if dailyMessageCount < dailyMessageLimit, canSendMessage returns false when isExhausted (day 8+). The check is isActive && dailyMessagesRemaining > 0.
  • startTrial() is idempotent. It guards on hasStarted — calling it multiple times (e.g. from both init and onChange(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 → blue
  • 6–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 ​

FilePurpose
Services/AppTrialManager.swiftApp-wide 7-day AI trial — start, daily message tracking, exhaustion
Services/EmailTrialManager.swift30-day email integration trial — start on OAuth connect, query tracking
Services/LiveTrialManager.swift30-minute Live Notes trial — server-synced second counter
Views/TrialStatusCard.swiftUnified trial status card shown in Settings — all active trials in one view
Views/AppTrialWarningView.swiftWarning sheet for app AI trial (day ≤ 2 and final exhaustion)
Views/EmailTrialOnboardingSheet.swiftOnboarding sheet explaining the email trial at connection time
Views/EmailTrialWarningModal.swiftWarning modal for email trial approaching expiry
Views/EmailHintCard.swiftDay-paced hint cards shown during email trial to drive habit formation
Views/EmailTrialPill.swiftCompact countdown capsule (reused in TrialStatusCard rows)
Views/LiveNotesTrialModal.swiftModal shown when Live Notes trial approaches exhaustion

Internal — not for distribution