feat(profile): real two-way reminder settings and a profile with content - #37
Merged
Merged
Conversation
Notification settings were one-way and forgetful: the toggle only ever cancelled the expense reminder, never re-scheduled it, and reset to "on" at every launch while the pending request stayed cancelled; goalAlert had no UI at all; the reminder times chosen in onboarding were @State, used once and stored nowhere. - ReminderSettings.swift: ReminderKind (the two request identifiers, unchanged since v1), ReminderPreferences, ReminderPlanner (the pure schedule/cancel decision, unit-tested), ReminderStore (UserDefaults, explicit — no @AppStorage inside the ObservableObject). - NotificationManager: apply(_:to:), authorizationStatus(), pendingReminderTime(for:) so installs that onboarded before preferences existed are seeded from their pending requests once. - ProfileViewModel: reminders are published, persisted and two-way; refreshReminderState() reports denied permission; deleteAllData(). - Onboarding's notifications step gets a per-reminder opt-out and writes the same store, so onboarding and Profile can never disagree. - ProfileView: dead bell removed; nested NavigationStack removed; identity card is the edit affordance and says "Saving since <month>"; stats strip (saved · movements · active goals); the *next* achievement as a row with a progress bar instead of six padlocks; Settings shows both reminders with editable times, or a denied-permission hint with Open Settings; new Data & privacy section (local-first line, weekly PDF, delete-all with a two-step confirm). Toggles own their titles so VoiceOver reads them; decorative tiles are hidden from it. - Dark Mode row hidden behind FeatureFlags.darkModeEnabled = false until the adaptive palette (PR D2); the @AppStorage plumbing stays. - Strings: new keys added with en + es-419; the two onboarding time labels this change orphaned are removed.
Owner's request: the monogram + "User" card on Profile and the person.fill circle in the Dashboard greeting stand in for an account the app does not have. D2 gets a commit 0 that removes both, moves the "Saving since" line into the lifetime income card, and deletes the display-name plumbing that nothing would reach anymore.
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Executes
docs/plans/pr-d-profile.md.Savely/Utilities/ReminderSettings.swift:ReminderKind(the twoUNNotificationRequestidentifiers, unchanged since v1 — unit-tested so they never drift),ReminderPreferences,ReminderPlanner(the pure schedule/cancel decision),ReminderStore(explicitUserDefaults, not@AppStorageinside theObservableObject— that was the re-render trap).NotificationManagergainsapply(_:to:),authorizationStatus()andpendingReminderTime(for:).ProfileView.swift): dead bell removed; nestedNavigationStackremoved; identity card is the edit affordance and says "Saving since " / "Just getting started"; stats strip (saved · movements · active goals); the next achievement as a row with a real progress bar instead of six padlocks (unlocked ones stay as tiles); Settings shows both reminders with editable times — or, when iOS permission is denied, a hint + Open Settings instead of a dead switch; new Data & privacy section (local-first line, Weekly PDF, Delete all data with a two-step confirm).Toggleowns its title (no moreToggle("")), decorative tiles hidden, rows combined, section headers carry.isHeader.FeatureFlags.darkModeEnabled = falseuntil PR D2 flips it; the@AppStorage/preferredColorSchemeplumbing stays.Strings.swiftwith es-419 in the catalog; the two onboarding time-label keys this change orphaned are removed.pr-d2-adaptive-palette.mdgains commit 0 — remove the placeholder "User" identity card and the Dashboard avatar (owner's request during review).Why
Verified live before this PR: the Notifications toggle only ever cancelled (console showed
cancelledon off and nothing on on), reset to "on" every launch while the reminder stayed cancelled,goalAlertcould never be turned off, reminder times were@Stateused once and stored nowhere, the bell wasButton(action: {}), and Dark Mode persisted but changed nothing (palette is light-only → D2). Full evidence table in the plan.Test plan
xcodebuild build— BUILD SUCCEEDEDxcodebuild test -skip-testing:SavelyUITests— TEST SUCCEEDED (7 newReminderSettingsTests: planner both ways, identifiers pinned, defaults, store round-trip)swiftlint lint --strict— 0plutil -lint project.pbxprojOK; catalog JSON valid, 289 keys, every new key has en + es-419Scheduledand no request for the othercancelled; toggle on → consoleScheduled; change time →Scheduledagain; relaunch → state persistsRisks
expenseEnabled = falseseeded (no pending request) — matches what they actually had.deleteAllData()usesmodelContext.delete(model:)on all four models; keeps name + reminders on purpose (dialog says so).Togglenow renders its own label (wasToggle("")+ separateText); layout is the same HStack, worth an eyeball.Checklist
feat/branch, Conventional Commits, no AI attributionStrings.swift+ catalog (en + es-419)CLAUDE.md: no new invariant