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