Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions apps/HeartCoach/CUSTOMER_VALUE_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Thump Customer Value Plan

## Honest Product Read

Thump is promising more than it has proven.

Right now the strongest customer value is not "AI wellness" or "family heart platform." It is one narrow job:

- help me decide whether today should be a push day, a steady day, or a recovery day

That is the habit worth building around.

What is weak today:

- too much product breadth for the current depth
- trust asks happen before value is obvious
- monetization existed in code before it existed in the customer story
- family and multi-tier packaging overstate what is actually useful today

## Most Useful Feature For Customers

The most useful feature is the morning readiness decision:

- one quick snapshot
- one plain-language recommendation
- one reason why

The customer does not primarily want "more metrics."
The customer wants:

- confidence about how hard to train today
- a clear signal when recovery is slipping
- a simple weekly recap that helps them adjust behavior

## Keep / Cut / Delay

### Keep

- daily readiness / recovery snapshot
- plain-language coaching recommendation
- stress context when it changes today's recommendation
- weekly review
- PDF summary for users who want to share progress

### Cut From Marketing

- Family plan positioning
- caregiver claims
- broad "full insights" language with no concrete result
- any implication that the app is a medical evaluator

### Delay

- family dashboard
- caregiver workflows
- advanced multi-member features
- broad subscription ladders

## Customer Journey To Build Around

### First 60 Seconds

The customer should be able to answer:

- what is my state today?
- what should I do with that information?

### First Week

The customer should build a habit:

- open every morning
- check the recommendation
- notice one useful pattern

### First Month

The customer should feel:

- this catches overreaching earlier than I do
- this helps me train more consistently
- this is worth paying for because it changes decisions

## Product Strategy

### Free

- daily snapshot
- basic trends
- watch check-ins

### Coach

- full metrics dashboard
- stress and anomaly context
- weekly review
- deeper multi-week trends
- PDF wellness summaries

## 30 / 60 / 90 Day Build Plan

### First 30 Days

- make the daily snapshot the clearest part of the app
- tighten sign-in and trust copy
- remove stale marketing around Family and legacy pricing
- add paywall triggers at real premium boundaries

### Next 30 Days

- improve weekly review so it feels worth paying for
- validate restore purchases and billing flows
- instrument funnel metrics: snapshot completion, paywall views, trial starts, conversion

### Final 30 Days

- run TestFlight with real watch users
- tune onboarding based on permission drop-off
- ship only after retention on the morning-check habit is real

## Success Metrics

- install to first snapshot completion
- HealthKit grant rate
- percentage of users opening the app at least 4 mornings per week
- weekly review open rate
- paywall view to trial start
- trial to paid conversion

## Rule For Future Features

Every new feature should answer one question:

- does this make the daily decision clearer, more trusted, or more actionable?

If not, it is probably not a priority yet.
2 changes: 1 addition & 1 deletion apps/HeartCoach/Legal/privacy-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ When you sign in with Apple, we receive an anonymous, app-specific identifier is

### 1.3 Subscription Information

Thump is free for the first year with full access to all features. No payment information is collected during this period. If you choose to subscribe after the free period, Apple processes your payment. We only receive confirmation of your subscription tier and its status. We do not have access to your payment method, credit card number, or billing address.
Some users may have grandfathered launch access for a limited period. If you choose to subscribe, Apple processes your payment. We only receive confirmation of your subscription tier and its status. We do not have access to your payment method, credit card number, or billing address.

### 1.4 Usage Analytics (Opt-In)

Expand Down
10 changes: 5 additions & 5 deletions apps/HeartCoach/Legal/terms-of-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ Thump uses Sign in with Apple for authentication. You are responsible for mainta

---

## 5. Launch Offer and Subscriptions
## 5. Launch Access and Subscriptions

### 5.1 First-Year Free Access
### 5.1 Grandfathered Launch Access

All users who download Thump during the launch period receive **complimentary full access to all features for one (1) year** from the date of their first sign-in. No subscription or payment is required during this period.
Some users who joined Thump during the original launch period may retain **complimentary Coach access for one (1) year** from the date their launch access began. This launch access is grandfathered and is not guaranteed for all new users.

This includes access to all Pro and Coach tier features at no cost. You will be notified before the free period ends and given the option to subscribe to continue using premium features.
If you have grandfathered launch access, you will be notified before it ends and given the option to subscribe to continue using premium features.

### 5.2 Future Subscriptions

After the one-year free period, Thump may offer paid subscription tiers with different feature access levels. Subscription details and pricing will be displayed within the app before any charges apply. You will never be charged without your explicit consent.
Thump may offer paid subscriptions, including the Coach plan currently presented in the app. Subscription details and pricing will be displayed within the app before any charges apply. You will never be charged without your explicit consent.

### 5.3 Billing

Expand Down
45 changes: 45 additions & 0 deletions apps/HeartCoach/PRODUCTION_TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Thump Production TODO

## Pricing And Packaging

- [x] Reduce the public subscription surface to `Free + Coach`.
- [x] Set Coach pricing in code to `$2.99/month`.
- [x] Set Coach annual pricing in code to `$17.99/year` to land at about 50% off monthly billing.
- [ ] Mirror the same price points in App Store Connect product configuration.
- [ ] Remove or archive legacy `Pro` and `Family` products after legacy subscribers are migrated or retired.

## Monetization Enforcement

- [x] Stop auto-enrolling every new user into launch-year free access.
- [x] Preserve grandfathered launch access for users who already have it.
- [ ] Wire `SubscriptionTier` feature gates into the actual iOS views and flows.
- [ ] Decide exactly which free features remain available without Coach.
- [ ] Add paywall entry points at the feature boundaries, not only from Settings.

## Product Copy And Trust

- [x] Update Settings subscription copy so the app no longer says everything is free.
- [x] Update the paywall to sell one Coach plan instead of three tiers.
- [x] Update launch-access copy to make it clear that complimentary access is grandfathered.
- [ ] Rewrite onboarding trust copy so it does not over-promise on-device-only behavior.
- [ ] Align website marketing copy with the in-app promise and pricing.

## Legal And Compliance

- [x] Update markdown legal docs to stop promising a free first year to all new users.
- [x] Update in-app legal text to reflect the single Coach offering.
- [ ] Review website legal pages for any stale launch-offer or pricing language.
- [ ] Re-check App Store privacy nutrition labels against current Firebase, telemetry, and bug-report behavior.

## Technical Readiness

- [ ] Create and test a StoreKit configuration file for local subscription testing.
- [ ] Remove simulator-only Coach auto-grant before release builds are finalized.
- [ ] Add end-to-end tests that verify free users hit the intended paywall gates.
- [ ] Verify restore-purchase and cancellation UX on device and in TestFlight.

## Launch Readiness

- [ ] Update App Store screenshots and website pricing section to match `Free + Coach`.
- [ ] Add conversion analytics for paywall view, trial start, purchase, restore, and cancellation.
- [ ] Run TestFlight with real Apple Watch users before spending on acquisition.
49 changes: 35 additions & 14 deletions apps/HeartCoach/Shared/Models/UserModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ public struct UserProfile: Codable, Equatable, Sendable {
/// Email address from Sign in with Apple (optional, only provided on first sign-in).
public var email: String?

/// Date when the launch free year started (first sign-in).
/// Date when grandfathered launch access started.
/// Nil if the user signed up after the launch promotion ends.
public var launchFreeStartDate: Date?

Expand Down Expand Up @@ -323,14 +323,14 @@ public struct UserProfile: Codable, Equatable, Sendable {
steadyStreakDays >= 14
}

/// Whether the user is currently within the launch free year.
/// Whether the user is currently within the grandfathered launch year.
public var isInLaunchFreeYear: Bool {
guard let start = launchFreeStartDate else { return false }
guard let expiryDate = Calendar.current.date(byAdding: .year, value: 1, to: start) else { return false }
return Date() < expiryDate
}

/// Days remaining in the launch free year. Returns 0 if expired or not enrolled.
/// Days remaining in the grandfathered launch year. Returns 0 if expired or not enrolled.
public var launchFreeDaysRemaining: Int {
guard let start = launchFreeStartDate else { return 0 }
guard let expiryDate = Calendar.current.date(byAdding: .year, value: 1, to: start) else { return 0 }
Expand All @@ -348,6 +348,9 @@ public enum SubscriptionTier: String, Codable, Equatable, Sendable, CaseIterable
case coach
case family

/// The only tier currently sold to new subscribers.
public static let merchandisedTier: SubscriptionTier = .coach

/// User-facing tier name.
public var displayName: String {
switch self {
Expand All @@ -363,7 +366,7 @@ public enum SubscriptionTier: String, Codable, Equatable, Sendable, CaseIterable
switch self {
case .free: return 0.0
case .pro: return 3.99
case .coach: return 6.99
case .coach: return 2.99
case .family: return 0.0 // Family is annual-only
}
}
Expand All @@ -373,7 +376,7 @@ public enum SubscriptionTier: String, Codable, Equatable, Sendable, CaseIterable
switch self {
case .free: return 0.0
case .pro: return 29.99
case .coach: return 59.99
case .coach: return 17.99
case .family: return 79.99
}
}
Expand All @@ -398,7 +401,9 @@ public enum SubscriptionTier: String, Codable, Equatable, Sendable, CaseIterable
]
case .coach:
return [
"Everything in Pro",
"Full wellness dashboard (HRV, Recovery, VO2, zone activity)",
"Personalized daily suggestions and nudges",
"Stress pattern awareness and anomaly alerts",
"Weekly wellness review and gentle plan tweaks",
"Multi-week trend exploration and progress snapshots",
"Shareable PDF wellness summaries",
Expand All @@ -414,26 +419,42 @@ public enum SubscriptionTier: String, Codable, Equatable, Sendable, CaseIterable
}

/// Whether this tier grants access to full metric dashboards.
/// NOTE: All features are currently free for all users.
public var canAccessFullMetrics: Bool {
return true
switch self {
case .free:
return false
case .pro, .coach, .family:
return true
}
}

/// Whether this tier grants access to personalized nudges.
/// NOTE: All features are currently free for all users.
public var canAccessNudges: Bool {
return true
switch self {
case .free:
return false
case .pro, .coach, .family:
return true
}
}

/// Whether this tier grants access to weekly reports and trend analysis.
/// NOTE: All features are currently free for all users.
public var canAccessReports: Bool {
return true
switch self {
case .coach, .family:
return true
case .free, .pro:
return false
}
}

/// Whether this tier grants access to activity-trend correlation analysis.
/// NOTE: All features are currently free for all users.
public var canAccessCorrelations: Bool {
return true
switch self {
case .free:
return false
case .pro, .coach, .family:
return true
}
}
}
2 changes: 1 addition & 1 deletion apps/HeartCoach/Tests/ClickableDataFlowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1394,7 +1394,7 @@ final class SettingsOnboardingDataFlowTests: XCTestCase {
// MARK: - Profile: Launch Free Year

func testLaunchFreeYear_showsCorrectPlan() {
// When isInLaunchFreeYear is true, subscription section shows "Coach (Free)"
// Grandfathered launch users should still report an active complimentary plan.
let isInFreeYear = localStore.profile.isInLaunchFreeYear
// Just verify the property is accessible and returns a boolean
XCTAssertTrue(isInFreeYear == true || isInFreeYear == false)
Expand Down
20 changes: 10 additions & 10 deletions apps/HeartCoach/Tests/ConfigServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,30 +78,30 @@ final class ConfigServiceTests: XCTestCase {
)
}

// MARK: - Test: All Tiers Can Access All Features (all features are free)
// MARK: - Test: Tier-Based Feature Access

func testFreeTierCanAccessAllFeatures() {
XCTAssertTrue(ConfigService.canAccessFullMetrics(tier: .free))
XCTAssertTrue(ConfigService.canAccessNudges(tier: .free))
XCTAssertTrue(ConfigService.canAccessReports(tier: .free))
XCTAssertTrue(ConfigService.canAccessCorrelations(tier: .free))
func testFreeTierCanAccessOnlyStarterFeatures() {
XCTAssertFalse(ConfigService.canAccessFullMetrics(tier: .free))
XCTAssertFalse(ConfigService.canAccessNudges(tier: .free))
XCTAssertFalse(ConfigService.canAccessReports(tier: .free))
XCTAssertFalse(ConfigService.canAccessCorrelations(tier: .free))
}

func testProTierCanAccessAllFeatures() {
func testProTierCanAccessCorePremiumFeatures() {
XCTAssertTrue(ConfigService.canAccessFullMetrics(tier: .pro))
XCTAssertTrue(ConfigService.canAccessNudges(tier: .pro))
XCTAssertTrue(ConfigService.canAccessReports(tier: .pro))
XCTAssertFalse(ConfigService.canAccessReports(tier: .pro))
XCTAssertTrue(ConfigService.canAccessCorrelations(tier: .pro))
}

func testCoachTierCanAccessAllFeatures() {
func testCoachTierCanAccessAllPremiumFeatures() {
XCTAssertTrue(ConfigService.canAccessFullMetrics(tier: .coach))
XCTAssertTrue(ConfigService.canAccessNudges(tier: .coach))
XCTAssertTrue(ConfigService.canAccessReports(tier: .coach))
XCTAssertTrue(ConfigService.canAccessCorrelations(tier: .coach))
}

func testFamilyTierCanAccessAllFeatures() {
func testFamilyTierCanAccessAllPremiumFeatures() {
XCTAssertTrue(ConfigService.canAccessFullMetrics(tier: .family))
XCTAssertTrue(ConfigService.canAccessNudges(tier: .family))
XCTAssertTrue(ConfigService.canAccessReports(tier: .family))
Expand Down
Loading
Loading