Skip to content

feat(profile): real two-way reminder settings and a profile with content - #37

Merged
Ivan-LB merged 2 commits into
devfrom
feat/profile-real-settings
Aug 16, 2026
Merged

Ivan-LB merged 2 commits into
devfrom
feat/profile-real-settings

Conversation

@Ivan-LB

@Ivan-LB Ivan-LB commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Summary

Executes docs/plans/pr-d-profile.md.

  • Reminder settings are real, two-way and persisted. New Savely/Utilities/ReminderSettings.swift: ReminderKind (the two UNNotificationRequest identifiers, unchanged since v1 — unit-tested so they never drift), ReminderPreferences, ReminderPlanner (the pure schedule/cancel decision), ReminderStore (explicit UserDefaults, not @AppStorage inside the ObservableObject — that was the re-render trap). NotificationManager gains apply(_:to:), authorizationStatus() and pendingReminderTime(for:).
  • Onboarding notifications step gets a per-reminder opt-out + time and writes the same store the Profile reads, so the two can never disagree. Finishing onboarding schedules only what was left on.
  • Migration for installs that onboarded before preferences existed: on first Profile visit the times are seeded once from the still-pending requests (a missing request means the old one-way toggle turned it off).
  • Profile tab (ProfileView.swift): dead bell removed; nested NavigationStack removed; 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).
  • VoiceOver on what this PR touches: every Toggle owns its title (no more Toggle("")), decorative tiles hidden, rows combined, section headers carry .isHeader.
  • Dark Mode row hidden behind FeatureFlags.darkModeEnabled = false until PR D2 flips it; the @AppStorage/preferredColorScheme plumbing stays.
  • Strings: all new copy through Strings.swift with es-419 in the catalog; the two onboarding time-label keys this change orphaned are removed.
  • Docs: pr-d2-adaptive-palette.md gains 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 cancelled on off and nothing on on), reset to "on" every launch while the reminder stayed cancelled, goalAlert could never be turned off, reminder times were @State used once and stored nowhere, the bell was Button(action: {}), and Dark Mode persisted but changed nothing (palette is light-only → D2). Full evidence table in the plan.

Test plan

  • xcodebuild build — BUILD SUCCEEDED
  • xcodebuild test -skip-testing:SavelyUITests — TEST SUCCEEDED (7 new ReminderSettingsTests: planner both ways, identifiers pinned, defaults, store round-trip)
  • swiftlint lint --strict — 0
  • plutil -lint project.pbxproj OK; catalog JSON valid, 289 keys, every new key has en + es-419
  • Simulator walk-through NOT completed by me — the Claude Code simulator panel crashed mid-session and its own error said retrying would not help. Please verify (or re-open the panel and I will):
    • fresh install → onboarding last step shows two toggles + times; turn one off → after Start, console shows one Scheduled and no request for the other
    • Profile → toggle off → console cancelled; toggle on → console Scheduled; change time → Scheduled again; relaunch → state persists
    • deny notification permission in iOS Settings → Profile shows the hint + Open Settings, no switches
    • fresh-install Profile looks deliberate; populated shows stats + "Next up" bar
    • Delete all data → two confirms → dashboard empty; name + reminder settings kept
    • VoiceOver: "Expense Reminders, switch, on"; identity card reads name + hint
    • tap the toggles with a finger (harness flipped them on drag, not tap — likely an artifact)

Risks

  • Behavior change: users who had turned the old toggle off get expenseEnabled = false seeded (no pending request) — matches what they actually had.
  • deleteAllData() uses modelContext.delete(model:) on all four models; keeps name + reminders on purpose (dialog says so).
  • Toggle now renders its own label (was Toggle("") + separate Text); layout is the same HStack, worth an eyeball.

Checklist

  • feat/ branch, Conventional Commits, no AI attribution
  • New user-facing strings through Strings.swift + catalog (en + es-419)
  • No secrets staged
  • Tests added for behavior changes
  • CLAUDE.md: no new invariant

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.
@Ivan-LB
Ivan-LB merged commit 56e5cc9 into dev Aug 16, 2026
1 check passed
@Ivan-LB
Ivan-LB deleted the feat/profile-real-settings branch August 16, 2026 04:53
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.

1 participant