Skip to content

feat: Add optional in-app tip jar - #41

Merged
Juuro merged 6 commits into
mainfrom
feature/tip-jar
Sep 18, 2026
Merged

Juuro merged 6 commits into
mainfrom
feature/tip-jar

Conversation

@Juuro

@Juuro Juuro commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • New "Support LogWeight" screen (StoreKit 2, three consumable tip tiers), reachable from Settings
  • One-time prompt after enough successful entries via TipPromptCoordinator, no paywall/nagging
  • Fixes LogWeightChartConfigIntent.range build error (WidgetConfigurationIntent requires optional @Parameter types)
  • Screenshot workflow: new settings-tipjar scene, StoreKit config wired into Screenshots/UITest schemes

Test plan

  • cd Packages/LogWeightCore && swift test
  • xcodebuild test -scheme LogWeight -destination 'platform=iOS Simulator,name=iPhone 15'
  • Manual: trigger tip prompt, purchase each tier in simulator w/ StoreKit config
  • Tools/CaptureScene.sh --scene settings-tipjar

Juuro and others added 2 commits September 18, 2026 16:43
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>
@Juuro
Juuro requested a balanced review from Copilot September 18, 2026 14:45
@Juuro Juuro changed the title Add optional in-app tip jar feat: Add optional in-app tip jar Sep 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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, and TipPromptCoordinator (one-time prompt gating via UserDefaults), wired into SettingsView and EntryView.
  • Widget fix: LogWeightChartConfigIntent.range made optional (required by WidgetConfigurationIntent), with a .oneWeek fallback in the provider.
  • Screenshot workflow: new settings-tipjar IAP-review scene, StoreKit config file + scheme wiring, light-mode enforcement for store captures, and trimming ALL_SCENES to 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.

Comment on lines +84 to +88
Section("Support") {
NavigationLink {
TipJarView()
} label: {
Label("Support LogWeight", systemImage: "heart")
Comment on lines +31 to +51
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")
Comment on lines +16 to +18
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.")
Comment on lines +14 to +20
public var displayName: String {
switch self {
case .small: return "Small Tip"
case .medium: return "Medium Tip"
case .large: return "Large Tip"
}
}
Juuro and others added 2 commits September 18, 2026 16:58
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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, without markPresented, recordSuccessfulEntry returns true on 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.displayName is unused: TipJarView renders product.displayName from the StoreKit Product, 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

Comment thread App/iOS/Views/EntryView.swift Outdated
Comment on lines +115 to +119
.sheet(isPresented: $showTipPrompt) {
TipPromptSheet(onOpenTipJar: { showTipJar = true })
}
.sheet(isPresented: $showTipJar) {
NavigationStack { TipJarView() }
@Juuro

Juuro commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

@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>

Copilot AI commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

@copilot Fix the code for all comments in this review thread.

When a review comment includes a suggested change, apply the s...

All four review items addressed in the latest commit:

  1. Localization — all tip-jar strings ("Support", "Support LogWeight", footnote, thank-you, failure, prompt title/body, "Leave a tip", "Maybe later") were already present in all 11 locale catalogs from the previous commit.

  2. Dead displayName (TipJarProduct.swift) — removed the unused displayName computed property; TipJarView uses StoreKit's localized product.displayName directly.

  3. Fragile sheet handoff (EntryView.swift) — replaced the simultaneous dismiss() + showTipJar = true pattern with an onDismiss callback approach: added a pendingOpenTipJar flag that is set by the "Leave a tip" button, and showTipJar is set to true only after the tip-prompt sheet fully dismisses via onDismiss.

  4. TipPromptSheet — reordered the button action to call onOpenTipJar() (sets the pending flag) before dismiss(), ensuring the flag is set when onDismiss fires.

--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>
@Juuro
Juuro merged commit 0ddd810 into main Sep 18, 2026
8 of 9 checks passed
@Juuro
Juuro deleted the feature/tip-jar branch September 18, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants