Skip to content

feat: settings design updates v60#490

Open
pwltr wants to merge 4 commits intomasterfrom
feat/settings-v60
Open

feat: settings design updates v60#490
pwltr wants to merge 4 commits intomasterfrom
feat/settings-v60

Conversation

@pwltr
Copy link
Contributor

@pwltr pwltr commented Mar 13, 2026

Description

From design prototype changelog v60:

  • Added tab navigation, renamed, reordered, and regrouped multiple settings and options for clarity
  • Added icons to all major settings for faster at-a-glance scanning
  • Expanded the amount of displayed selected values (gray, displayed right side of row) for faster at-a-glance scanning
  • Reworked the UX and logic for enabling, disabling, and changing the PIN
  • Removed Support from Settings and moved it to the main menu
  • Redesigned the Support screen layout and visual style

Closes #496

Screenshot / Video

Simulator.Screen.Recording.-.iPhone.17.-.2026-03-26.at.17.53.02.mov

QA Notes

Mostly a redesign. Testing is needed mostly in the updated PIN setup flow.

@pwltr pwltr force-pushed the feat/settings-v60 branch from 40c4c3d to 196106c Compare March 13, 2026 09:19
@pwltr pwltr self-assigned this Mar 13, 2026
@pwltr pwltr force-pushed the feat/settings-v60 branch from 196106c to ac406bf Compare March 16, 2026 15:47
@pwltr pwltr added this to the 2.2.0 milestone Mar 16, 2026
@pwltr pwltr force-pushed the feat/settings-v60 branch 2 times, most recently from 922a3fa to dc6aa77 Compare March 24, 2026 13:36
@pwltr pwltr linked an issue Mar 25, 2026 that may be closed by this pull request
@pwltr pwltr force-pushed the feat/settings-v60 branch 2 times, most recently from bd45417 to 74aa869 Compare March 26, 2026 16:51
@pwltr pwltr marked this pull request as ready for review March 26, 2026 16:55
@claude

This comment has been minimized.

@pwltr pwltr force-pushed the feat/settings-v60 branch from 74aa869 to 5177dbe Compare March 27, 2026 09:45
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Code review

Found 3 issues. See inline comments below.


CustomButton(title: t("common__ok")) {
sheets.hideSheet()
navigation.navigateBack()
Copy link

Choose a reason for hiding this comment

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

Bug: navigateBack() pops an unrelated route when the security sheet is opened without a prior navigation push

The CustomButton action unconditionally calls navigation.navigateBack() after hiding the sheet. This is correct when the sheet is opened from ChangePinScreen — which pushes .changePin onto the app navigation stack first. But SecuritySuccess is also the final screen of the initial PIN setup flow, triggered via:

  • sheets.showSheet(.security) (from suggestion cards) — no route is pushed
  • sheets.showSheet(.security, data: SecurityConfig(initialRoute: .setupPin)) in SecuritySettingsView — no route is pushed

In those cases, navigateBack() pops whatever route happens to be at the top of the app navigation stack (e.g. Settings), sending the user to an unexpected screen after completing PIN setup.

The fix should make the navigateBack() call conditional — for example, pass a shouldNavigateBack: Bool parameter to SecuritySuccess (defaulting to false) and only call it when true. SecuritySheet can derive this from SecurityConfig.initialRoute.

Comment on lines +113 to +120
do {
try settings.removePin(pin: currentPin, resetSettings: false)
try settings.setPin(newPin)
pinInput = ""
errorMessage = ""
errorIdentifier = nil
navigationPath.append(.changePinSuccess)
} catch {
Copy link

Choose a reason for hiding this comment

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

Bug: Non-atomic PIN change can silently remove PIN protection

try settings.removePin(pin: currentPin, resetSettings: false)  // deletes PIN from Keychain, sets pinEnabled = false
try settings.setPin(newPin)                                      // can throw

If removePin succeeds (the old PIN is permanently deleted from Keychain and pinEnabled is set to false) but setPin throws (e.g. a Keychain write error), the catch block only shows a generic error and the user silently loses all PIN protection — no rollback occurs.

Suggested fix: reverse the order to "set new, then remove old", so a failure on the first step leaves the old PIN intact:

try settings.setPin(newPin)
try settings.removePin(pin: currentPin, resetSettings: false)

Alternatively, on catch after a successful removePin, attempt to restore the old PIN before surfacing the error.

Comment on lines +262 to +269
private func requestPinCheck() async -> Bool {
// Prevent stacking multiple PIN screens if already presented.
if navigationPath.last != .pin {
navigationPath.append(.pin)
}

return await withCheckedContinuation { continuation in
pinCheckContinuation = continuation
Copy link

Choose a reason for hiding this comment

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

Bug: CheckedContinuation is overwritten without being resumed, causing a task leak

private func requestPinCheck() async -> Bool {
    if navigationPath.last != .pin {
        navigationPath.append(.pin)
    }
    // ⚠️ withCheckedContinuation runs unconditionally even when .pin is already shown
    return await withCheckedContinuation { continuation in
        pinCheckContinuation = continuation  // overwrites any existing unresolved continuation
    }
}

If requestPinCheck() is called a second time while the PIN screen is already presented (e.g. two rapid taps on send), the guard only prevents appending a duplicate .pin route — the withCheckedContinuation block still executes unconditionally. The old continuation stored in pinCheckContinuation is replaced without ever being resumed, causing the first awaiting task to hang forever. Swift's CheckedContinuation requires exactly one resume; a leaked continuation triggers a runtime warning in debug builds.

Fix: move withCheckedContinuation inside the if block so it only runs when a new PIN screen is actually being pushed:

private func requestPinCheck() async -> Bool {
    if navigationPath.last != .pin {
        navigationPath.append(.pin)
        return await withCheckedContinuation { continuation in
            pinCheckContinuation = continuation
        }
    }
    // PIN screen already showing — a result will arrive via resolvePinCheck
    return await withCheckedContinuation { continuation in
        pinCheckContinuation = continuation
    }
}

Or more simply, guard early and return false (or surface an error) if already pending.

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.

[Bug]: App not wiped when exceeding maximum PIN attempts

2 participants