feat: Add optional in-app tip jar - #41
Conversation
WidgetConfigurationIntent requires all @parameter types to be optional; Xcode rejected the non-optional range enum. Default to .oneWeek when nil. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a "Support LogWeight" screen with three consumable StoreKit 2 tip tiers, reachable from Settings and via a one-time prompt shown after enough successful entries (TipPromptCoordinator). No paywall, no recurring nagging. Also updates the screenshot workflow (new settings-tipjar scene, StoreKit test config wired into the Screenshots/UITest schemes) and drops two scenes that were flaky/no longer representative. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
New user-facing tip-jar strings are hardcoded English and bypass the app's fully-localized Localizable.strings catalogs, so they will render untranslated in all non-English locales.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR adds an optional in-app tip jar to LogWeight. It introduces a StoreKit 2–backed "Support LogWeight" screen with three consumable tip tiers reachable from Settings, plus a one-time, non-nagging prompt shown after a threshold of successful weight entries. It also fixes a widget build error and reworks the screenshot/App Store capture workflow.
Changes:
- New tip jar:
TipJarProduct(Core),TipJarStore(StoreKit 2 purchases),TipJarView,TipPromptSheet, andTipPromptCoordinator(one-time prompt gating viaUserDefaults), wired intoSettingsViewandEntryView. - Widget fix:
LogWeightChartConfigIntent.rangemade optional (required byWidgetConfigurationIntent), with a.oneWeekfallback in the provider. - Screenshot workflow: new
settings-tipjarIAP-review scene, StoreKit config file + scheme wiring, light-mode enforcement for store captures, and trimmingALL_SCENESto Apple's 10-per-device cap.
File summaries
| File | Description |
|---|---|
Packages/LogWeightCore/.../Support/TipJarProduct.swift |
Foundation-only tip product identifiers/metadata shared by App + tests |
Packages/LogWeightCore/.../Support/TipPromptCoordinator.swift |
Pure UserDefaults logic deciding when to show the one-time prompt |
Packages/LogWeightCore/.../Settings/SettingsKeys.swift |
Adds entry-count and prompt-shown keys |
Packages/LogWeightCore/Tests/.../TipPromptCoordinatorTests.swift |
Threshold/one-time behavior tests |
App/iOS/Support/TipJarStore.swift |
@Observable StoreKit 2 store: load/purchase/transaction updates |
App/iOS/Views/TipJarView.swift |
Tip tier list UI with thank-you/failure states |
App/iOS/Views/TipPromptSheet.swift |
One-time subtle prompt sheet |
App/iOS/Views/EntryView.swift |
Records entries, presents prompt/tip jar sheets |
App/Shared/Views/SettingsView.swift |
Adds iOS-only "Support" section linking to tip jar |
App/iOSWidget/LogWeightChartConfigIntent.swift / LogWeightChartWidget.swift |
Optional range parameter + .oneWeek fallback |
App/iOSScreenshots/* (Resources/LogWeight.storekit, ScreenshotTestCase.swift, SettingsScreenshots.swift, Entry/HistoryScreenshots.swift) |
StoreKit test session, new tip-jar scene, removed two scenes |
project.yml |
StoreKit config wired into schemes; excludes .storekit from bundle |
Tools/*.sh, docs/AIScreenshotWorkflow.md |
Scene catalog trimmed to 10, --appearance flag, light-mode store captures |
Review details
- Files reviewed: 21/21 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Section("Support") { | ||
| NavigationLink { | ||
| TipJarView() | ||
| } label: { | ||
| Label("Support LogWeight", systemImage: "heart") |
| Text("LogWeight is free, with no ads and no tracking. If it's useful to you, a tip helps keep it that way.") | ||
| .font(.footnote) | ||
| .foregroundStyle(.secondary) | ||
| } | ||
|
|
||
| if case .thankYou = store.purchaseState { | ||
| Section { | ||
| Label("Thank you for supporting LogWeight!", systemImage: "heart.fill") | ||
| .foregroundStyle(.pink) | ||
| .accessibilityIdentifier("tipjar.thankyou") | ||
| } | ||
| } | ||
|
|
||
| if case .failed = store.purchaseState { | ||
| Section { | ||
| Text("The purchase didn't go through. Please try again.") | ||
| .foregroundStyle(.red) | ||
| } | ||
| } | ||
| } | ||
| .navigationTitle("Support LogWeight") |
| Text("Enjoying LogWeight?") | ||
| .font(.title3.weight(.semibold)) | ||
| Text("It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way.") |
| public var displayName: String { | ||
| switch self { | ||
| case .small: return "Small Tip" | ||
| case .medium: return "Medium Tip" | ||
| case .large: return "Large Tip" | ||
| } | ||
| } |
Support section, tip jar screen, and TipPromptSheet used hardcoded English text, so they rendered untranslated in all non-English locales. Add the missing keys to every Localizable.strings catalog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
--all inherited whatever appearance the simulator happened to be in,
so scenes captured after a dark-mode run silently came out dark.
Default --appearance to light; pass --appearance dark to override.
Also fixes an "unbound variable" crash on --all under bash 3.2 (macOS
default): expanding an empty only_testing_flag array under set -u
needs the ${arr[@]+"${arr[@]}"} guard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The change introduces real-money StoreKit purchase flows and cross-sheet presentation behavior whose runtime correctness cannot be fully verified here, warranting human review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
Packages/LogWeightCore/Tests/LogWeightCoreTests/TipPromptCoordinatorTests.swift:41
- The test name and comment claim it verifies the prompt "does not prompt again without marking presented" / stays "gated by the shown flag," but the flag is never set here and the only assertion checks that the entry count equals
promptThreshold + 5. In fact, withoutmarkPresented,recordSuccessfulEntryreturnstrueon every call past the threshold, so the name/comment describe behavior contrary to the implementation and none of the claimed gating is actually asserted. Consider renaming to reflect what is tested (that each call increments the count exactly once), or add an assertion that actually exercises the shown-flag gating.
Packages/LogWeightCore/Sources/LogWeightCore/Support/TipJarProduct.swift:20
TipJarProduct.displayNameis unused:TipJarViewrendersproduct.displayNamefrom the StoreKitProduct, not this enum's value. If it is intended as a fallback (e.g. when StoreKit products fail to load), note that these hardcoded English strings would not be localized, unlike the rest of the app's user-facing strings. Consider removing it or wiring it up as a localized fallback. Optional.
public var displayName: String {
switch self {
case .small: return "Small Tip"
case .medium: return "Medium Tip"
case .large: return "Large Tip"
}
}
- Files reviewed: 32/32 changed files
- Comments generated: 1
- Review effort level: Balanced
| .sheet(isPresented: $showTipPrompt) { | ||
| TipPromptSheet(onOpenTipJar: { showTipJar = true }) | ||
| } | ||
| .sheet(isPresented: $showTipJar) { | ||
| NavigationStack { TipJarView() } |
|
@copilot Fix the code for all comments in this review thread. When a review comment includes a suggested change, apply the suggestion exactly. Do not make changes beyond what is described in the linked review thread. |
…playName Co-authored-by: Juuro <559017+Juuro@users.noreply.github.com>
All four review items addressed in the latest commit:
|
--use-in-memory-store already isolates HealthKit but left UserDefaults (notably the tip-jar successfulEntryCount counter) persisting across launches and reused CI simulators. Once the counter crossed TipPromptCoordinator.promptThreshold, the tip sheet popped mid-test and broke EntryViewSmokeTests.testKeyboardEntryUnavailableAfterFirstSave. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
TipPromptCoordinator, no paywall/naggingLogWeightChartConfigIntent.rangebuild error (WidgetConfigurationIntent requires optional@Parametertypes)settings-tipjarscene, StoreKit config wired into Screenshots/UITest schemesTest plan
cd Packages/LogWeightCore && swift testxcodebuild test -scheme LogWeight -destination 'platform=iOS Simulator,name=iPhone 15'Tools/CaptureScene.sh --scene settings-tipjar