Skip to content

Conversation

@LRNcardozoWDF
Copy link
Member

@LRNcardozoWDF LRNcardozoWDF commented Nov 11, 2025

🎟️ Tracking

PM-19305

📔 Objective

Allow organizations to enforce session timeout policy in the iOS app.

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@LRNcardozoWDF LRNcardozoWDF requested review from a team and matt-livefront as code owners November 11, 2025 03:32
@claude
Copy link

claude bot commented Nov 11, 2025

Claude finished @LRNcardozoWDF's task —— View job


Code Review Complete

Summary

Reviewed changes since 2025-11-26. The latest commit (ade0228) addressed some previous findings by adding DocC documentation to policyTimeoutCustomMessage and cleaning up test comments. However, two critical findings remain unresolved that require attention before this PR can be merged.

Status of Previous Findings

✅ Resolved

  • Finding 2 (DocC documentation): Added comprehensive documentation to policyTimeoutCustomMessage in AccountSecurityState.swift:218-226
  • Test comment cleanup in AccountSecurityStateTests.swift

❌ Still Unresolved

  • Finding 1: All snapshot tests remain disabled (see below)
  • Finding 6: Divider visibility issue not addressed (see below)

Critical Findings Requiring Action

Finding 1: All snapshot tests are disabled

BLOCKER

All 7 snapshot tests in SettingsPickerField+SnapshotTests.swift have the disabletest_ prefix, meaning they never execute. This violates Testing.md guidelines requiring snapshot tests for accessibility and theme variations.

Location: BitwardenKit/UI/Platform/Application/Views/SettingsPickerField+SnapshotTests.swift:27-112

Required action: Remove the disable prefix from all test functions:

  • disabletest_snapshot_lightMode_noFooter()test_snapshot_lightMode_noFooter()
  • disabletest_snapshot_darkMode_noFooter() → (same pattern)
  • All 7 test functions need this fix

Why this matters: These tests verify UI correctness across light/dark modes and accessibility settings. Disabled tests provide zero protection against visual regressions.


Finding 2: Divider appears without footer text

⚠️ VISUAL BUG

The latest commit (ade0228) changed lines 57 and 66 from if let footer to if footer != nil, but this makes the problem worse. The dividers now show even when footer is an empty string, creating the visual issue Matt identified in his screenshot.

Location: BitwardenKit/UI/Platform/Application/Views/SettingsPickerField.swift:57,66

Current problematic code:

if footer != nil {  // Line 57: Shows divider even for empty strings
    Divider()
        .padding(.leading, 16)
}

// ...picker...

if footer != nil {  // Line 66: Same issue
    Divider()
        .padding(.leading, 16)
}

if let footer {  // Line 72: Only shows text when non-nil
    Text(footer)
        // ...
}

The issue: Lines 57 and 66 check != nil (which passes for empty strings), but line 72 uses if let (which doesn't render text for empty strings). This creates dividers without accompanying footer content.

Suggested fix:

if let footer, !footer.isEmpty {
    Divider()
        .padding(.leading, 16)
}

Apply to both lines 57 and 66. This ensures dividers only appear when footer text will actually render.


Additional Observations

Good Practices

  • Comment added to SyncService.swift:464-465 explaining business logic for onAppRestart/never policy types
  • DocC documentation properly added per project guidelines
  • Test cleanup removed unnecessary inline comments

Notes

💭 Spacing verification needed: Matt's comment about 8pt spacing between ContentBlocks (AccountSecurityView.swift:148) should be verified at runtime. The contentSpacing: 8 parameter should provide correct spacing, but visual confirmation is recommended.


Action Items

Before merging:

  1. ✅ Enable all 7 snapshot tests (remove disable prefix)
  2. ✅ Fix divider visibility logic in SettingsPickerField.swift
  3. ℹ️ Verify 8pt spacing displays correctly in UI

Test coverage: Codecov shows 90.41% patch coverage with 95 lines missing. The disabled snapshot tests account for 81 of those missing lines - enabling them will significantly improve coverage.



// MARK: Initialization

/// Initialize `EnvironmentURLData` with the specified URLs.
Copy link

Choose a reason for hiding this comment

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

Finding 1: Copy-paste error in documentation

The documentation still references EnvironmentURLData and mentions an api parameter, but this is the SessionTimeoutPolicy initializer with completely different parameters.

Suggested change
/// Initialize `EnvironmentURLData` with the specified URLs.
/// Initialize `SessionTimeoutPolicy` with the specified values.
///
/// - Parameters:
/// - timeoutAction: The action to perform on session timeout.
/// - timeoutType: The type of session timeout.
/// - timeoutValue: The session timeout value.

"ThisSettingIsManagedByYourOrganization" = "This setting is managed by your organization.";
"YourOrganizationHasSetTheDefaultSessionTimeoutToX" = "Your organization has set the default session timeout to %1$@.";
"YourOrganizationHasSetTheDefaultSessionTimeoutToXAndY" = "Your organization has set the default session timeout to %1$@ and %2$@.";
"XMinutes" = "%1$@ minute(s)";
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 2: Missing pluralization rules

The string "XMinutes" = "%1$@ minute(s)"; uses generic "(s)" pluralization which won't work correctly for all languages. iOS supports proper pluralization via .stringsdict files.

Consider creating a Localizable.stringsdict entry:

<key>XMinutes</key>
<dict>
    <key>NSStringLocalizedFormatKey</key>
    <string>%#@minutes@</string>
    <key>minutes</key>
    <dict>
        <key>NSStringFormatSpecTypeKey</key>
        <string>NSStringPluralRuleType</string>
        <key>NSStringFormatValueTypeKey</key>
        <string>d</string>
        <key>one</key>
        <string>%d minute</string>
        <key>other</key>
        <string>%d minutes</string>
    </dict>
</dict>

The same applies to XHours if it exists.

// Only update the user's stored vault timeout value if
// their stored timeout value is > the policy's timeout value.
if timeoutValue.rawValue > value || timeoutValue.rawValue < 0 {
if type == SessionTimeoutType.onAppRestart || type == SessionTimeoutType.never {
Copy link

Choose a reason for hiding this comment

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

Finding 3: Logic inconsistency bypasses policy enforcement

When the policy type is onAppRestart or never, this code sets the timeout value to whatever the user currently has stored (timeoutValue), without any validation against the policy. This means:

  1. If a user previously had a custom timeout that violates the new policy, it won't be corrected
  2. The policy value is essentially ignored for these types

The logic should validate that the user's stored value is compatible with the policy type. For example:

  • If policy type is never, user's value should be set to .never
  • If policy type is onAppRestart, user's value should be set to .onAppRestart

Otherwise, the user could have a stored value of "5 minutes" when the policy says "never", which is inconsistent.

Copy link
Member Author

Choose a reason for hiding this comment

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

The implemented behaviour is correct, the user's value can in those cases be different than the policy since the policy type also allows those values.

state.policyTimeoutValue = policy.value
state.isTimeoutPolicyEnabled = true
state.policyTimeoutAction = policy.timeoutAction
state.policyTimeoutType = policy.timeoutType ?? SessionTimeoutType.custom
Copy link

Choose a reason for hiding this comment

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

💭 Finding 4: Unclear default handling for nil policy type

When policy.timeoutType is nil (legacy servers), this defaults to SessionTimeoutType.custom. However, the logic in PolicyService.swift:284-286 suggests that nil type should be handled differently. Is custom always the correct default for legacy servers?

Consider documenting why .custom is the appropriate fallback, or if the logic should check the policy value to determine a more appropriate type (e.g., if value matches a predefined timeout like 60 minutes, should it be .predefined instead?).

Copy link
Member Author

Choose a reason for hiding this comment

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

.custom is always the correct default for legacy servers since they will send the timeoutType as null.

Comment on lines 103 to 105
var policyTimeoutValue: Int = 0 {
didSet {
availableTimeoutOptions = SessionTimeoutValue.allCases
.filter { $0 != .never }
.filter { $0 != .onAppRestart }
.filter { $0.rawValue <= policyTimeoutValue }
availableTimeoutOptions = SessionTimeoutValue.allCases.filter { option in
Copy link

Choose a reason for hiding this comment

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

🎨 Finding 7: Complex logic in property observer reduces testability

The didSet observer contains significant filtering logic (27 lines) that modifies availableTimeoutOptions based on policyTimeoutType and policyTimeoutValue. This makes the state harder to test and understand.

Consider extracting this to a computed property or dedicated method:

var policyTimeoutValue: Int = 0

var availableTimeoutOptions: [SessionTimeoutValue] {
    computeAvailableTimeoutOptions(
        type: policyTimeoutType,
        value: policyTimeoutValue
    )
}

private func computeAvailableTimeoutOptions(
    type: SessionTimeoutType?,
    value: Int
) -> [SessionTimeoutValue] {
    // Filtering logic here
}

This approach:

  • Makes the logic independently testable
  • Clarifies the dependency on both policyTimeoutType and policyTimeoutValue
  • Removes side effects from property setters

///
/// - Parameter rawValue: The string representation of the type raw value.
///
public init(rawValue: String?) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 6: Missing test coverage for SessionTimeoutType

This enum lacks dedicated unit tests, particularly for:

  1. The init(rawValue:) method with all possible string values
  2. The legacy "onSystemLock" → "onAppRestart" mapping (line 70)
  3. The default case handling (line 73) which falls back to .custom
  4. The init(value:) method with all SessionTimeoutValue cases

These initialization paths are critical for policy enforcement compatibility with legacy servers. Add tests in a new file: BitwardenKit/Core/Platform/Models/Enum/SessionTimeoutTypeTests.swift

var timeoutValue: SessionTimeoutValue?

for policy in policies {
// This is never null, but maybe it should be revised
Copy link

Choose a reason for hiding this comment

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

💭 Finding 8: Comment suggests uncertainty about business logic

The comment "This is never null, but maybe it should be revised" indicates uncertainty about whether the policy's minutes value can be null. This warrants clarification:

  1. If it truly can never be null per the API contract, remove the guard and the comment
  2. If it can be null and represents a valid state, the continue is correct but should be documented
  3. If the server contract is unclear, add error logging before continuing

This is important for policy enforcement - skipping a policy without logging could hide configuration issues.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 11, 2025

Logo
Checkmarx One – Scan Summary & Details8c447cc0-f201-4c2b-9620-50f0b2350ced

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Nov 11, 2025

Codecov Report

❌ Patch coverage is 90.33571% with 95 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.15%. Comparing base (8ce5d66) to head (3823fc3).

Files with missing lines Patch % Lines
...tion/Views/SettingsPickerField+SnapshotTests.swift 0.00% 81 Missing ⚠️
...Settings/AccountSecurity/AccountSecurityView.swift 77.14% 8 Missing ⚠️
...atform/Application/Views/SettingsPickerField.swift 80.00% 2 Missing ⚠️
...ntSecurity/AccountSecurityView+SnapshotTests.swift 0.00% 2 Missing ⚠️
...rdenShared/Core/Vault/Services/PolicyService.swift 95.00% 1 Missing ⚠️
...ettings/AccountSecurity/AccountSecurityState.swift 99.04% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2127      +/-   ##
==========================================
- Coverage   85.39%   84.15%   -1.24%     
==========================================
  Files        1731     1989     +258     
  Lines      145718   161985   +16267     
==========================================
+ Hits       124433   136326   +11893     
- Misses      21285    25659    +4374     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@LRNcardozoWDF LRNcardozoWDF marked this pull request as draft November 11, 2025 05:13
@LRNcardozoWDF LRNcardozoWDF force-pushed the cmcg/pm-19305-vault-timeout-policy branch from 60abeb5 to 8bb7d69 Compare November 13, 2025 02:06
@LRNcardozoWDF LRNcardozoWDF marked this pull request as ready for review November 18, 2025 11:06
Comment on lines 75 to 84
if footer != nil {
Group {
if let footer {
Text(footer)
.styleGuide(.subheadline)
.foregroundColor(Color(asset: SharedAsset.Colors.textSecondary))
}
}
.padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

🎨 You could simplify this by unwrapping footer once and removing the Group:

Suggested change
if footer != nil {
Group {
if let footer {
Text(footer)
.styleGuide(.subheadline)
.foregroundColor(Color(asset: SharedAsset.Colors.textSecondary))
}
}
.padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
}
if let footer {
Text(footer)
.styleGuide(.subheadline)
.foregroundColor(Color(asset: SharedAsset.Colors.textSecondary))
.padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
}

Comment on lines 19 to 21
/// - Returns: The timeout value in minutes, and the action to take upon timeout.
///
func fetchTimeoutPolicyValues() async throws -> (action: SessionTimeoutAction?, value: Int)?
func fetchTimeoutPolicyValues() async throws -> SessionTimeoutPolicy?
Copy link
Collaborator

Choose a reason for hiding this comment

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

⛏️ Could you update the documentation for the return value here?


try await subject.fetchSync(forceSync: false)

XCTAssertEqual(stateService.timeoutAction["1"], .logout)
Copy link
Collaborator

Choose a reason for hiding this comment

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

🎨 Should this verify the timeout value as well?

!removeUnlockWithPinPolicyEnabled || isUnlockWithPINCodeOn
}

var customTimeoutMessage: String {
Copy link
Collaborator

Choose a reason for hiding this comment

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

⛏️ Can you add docs to this and alphabetize it among the other properties?

Comment on lines 148 to 152
SectionView(Localizations.sessionTimeout, contentSpacing: 8) {
VStack(spacing: 16) {
if let policyTimeoutMessage = store.state.policyTimeoutMessage {
InfoContainer(policyTimeoutMessage)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

⚠️ I believe there should be 8 pt of padding between the Session timeout and Session timeout action blocks in this section. Since we don't need the InfoContainer anymore, I think you can remove the VStack here and the SectionView will give you 8 pt of padding between the elements.

Image

Comment on lines +270 to +288
SessionTimeoutValue.allCases.filter { option in
switch type {
case .never:
return true
case .onAppRestart:
return option != .never
case .immediately:
return option == .immediately
case .custom:
if option.isCustomPlaceholder { return true }
guard let time = option.minutesValue else { return false }
return time <= value
case nil:
if value > 0 {
if option.isCustomPlaceholder { return true }
guard let time = option.minutesValue else { return false }
return time <= value
} else {
return true
Copy link
Collaborator

Choose a reason for hiding this comment

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

🎨 This logic took me a bit to figure out, would it be good to write some tests for this?

Copy link
Member Author

Choose a reason for hiding this comment

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

I test the results of the filtering here . I can also try and return the array with the right options given a type:

Suggested change
SessionTimeoutValue.allCases.filter { option in
switch type {
case .never:
return true
case .onAppRestart:
return option != .never
case .immediately:
return option == .immediately
case .custom:
if option.isCustomPlaceholder { return true }
guard let time = option.minutesValue else { return false }
return time <= value
case nil:
if value > 0 {
if option.isCustomPlaceholder { return true }
guard let time = option.minutesValue else { return false }
return time <= value
} else {
return true
var availableTimeoutOptions: [SessionTimeoutValue] {
switch policyTimeoutType {
case .never:
SessionTimeoutValue.allCases
case .onAppRestart:
[
.immediately,
.oneMinute,
.fiveMinutes,
.fifteenMinutes,
.thirtyMinutes,
.oneHour,
.fourHours,
.onAppRestart,
.custom(-100),
]
case .immediately:
[.immediately]
case .custom:
SessionTimeoutValue.allCases.filter { option in
option.isCustomPlaceholder || (option.minutesValue.map { $0 <= policyTimeoutValue } ?? false)
}
case nil:
if policyTimeoutValue > 0 {
SessionTimeoutValue.allCases.filter { option in
option.isCustomPlaceholder || (option.minutesValue.map { $0 <= policyTimeoutValue } ?? false)
}
} else {
SessionTimeoutValue.allCases
}
}
}

send: AccountSecurityAction.customTimeoutValueSecondsChanged,
),
hasDivider: false,
hasDivider: true,
Copy link
Collaborator

Choose a reason for hiding this comment

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

🤔 Should the divider only show if there's a footer? I see it here without a footer, which I don't think is right. Since we use ContentBlock to provide the dividers now across the app, it might be easier to remove the property and just add the divider in the component if there's a footer?

Image

.styleGuide(.subheadline)
.foregroundColor(Color(asset: SharedAsset.Colors.textSecondary))
.padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
}
Copy link

Choose a reason for hiding this comment

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

🎨 Finding 1: Redundant footer nil checks create visual noise

The current implementation checks footer != nil three times (lines 57, 66, 72). Matt's suggestion to simplify by unwrapping once would improve readability:

if let footer {
    Text(footer)
        .styleGuide(.subheadline)
        .foregroundColor(Color(asset: SharedAsset.Colors.textSecondary))
        .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
}

Then update lines 57 and 66 to simply check the unwrapped footer variable instead of footer != nil.

ContentBlock(dividerLeadingPadding: 16) {
BitwardenMenuField(
title: Localizations.sessionTimeout,
footer: store.state.policyTimeoutMessage,
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 2: Missing 8pt spacing between Session timeout blocks

As noted in Matt's review, there should be 8pt of padding between the "Session timeout" and "Session timeout action" ContentBlocks. Currently, they appear directly adjacent without proper spacing.

Since you removed the VStack wrapper, the contentSpacing: 8 parameter on the SectionView (line 148) should provide the spacing. However, verify this displays correctly - you may need to test that the spacing between ContentBlocks matches the design spec.

}
}
}
}
Copy link

Choose a reason for hiding this comment

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

💭 Finding 3: Complex filtering logic in computed property has limited test coverage

The availableTimeoutOptions computed property contains 27 lines of filtering logic. While there are tests for the results in AccountSecurityProcessorTests.swift:116-125, Matt suggested adding dedicated tests for this logic.

Consider extracting this to a testable function and adding unit tests that cover all branches:

  • .never type (returns all cases)
  • .onAppRestart type (filters out .never)
  • .immediately type (returns only .immediately)
  • .custom type with various values
  • nil type with value > 0
  • nil type with value == 0

This would make the logic easier to verify and maintain.

// Only update the user's stored vault timeout value if
// their stored timeout value is > the policy's timeout value.
if timeoutValue.rawValue > value || timeoutValue.rawValue < 0 {
if type == SessionTimeoutType.onAppRestart || type == SessionTimeoutType.never {
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 4: Unclear logic when policy type is onAppRestart or never

When the policy type is onAppRestart or never, this code sets timeoutValue to whatever the user currently has stored, without any validation. While the author clarified this is intentional behavior, the code lacks documentation explaining why.

This could lead to confusion: if a user had a stored value of "5 minutes" and the policy changes to "never", the stored value remains "5 minutes" even though it doesn't match the policy intent.

Add a comment explaining the business logic:

// For onAppRestart and never policy types, preserve the user's current timeout value
// as these policy types don't restrict the value itself, only the behavior

var timeoutValue: SessionTimeoutValue?

for policy in policies {
guard let policyTimeoutValue = policy[.minutes]?.intValue else { continue }
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 5: Missing documentation update for return value

Matt noted that the return value documentation needs updating. The function now returns SessionTimeoutPolicy? instead of the previous return type, but the DocC comment above this function likely still reflects the old signature.

Update the documentation to accurately describe what the function returns and under what conditions it returns nil.


try await subject.fetchSync(forceSync: false)

XCTAssertEqual(stateService.timeoutAction["1"], .logout)
Copy link

Choose a reason for hiding this comment

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

💭 Finding 6: Test should verify timeout value

Matt suggested this test should verify the timeout value as well. Currently the test only checks that the sync completed successfully, but doesn't validate that the timeout policy was applied correctly.

Add an assertion to verify the expected timeout value was set by the policy.

}

/// The message to display if a timeout policy is in effect for the user.
var policyTimeoutCustomMessage: String? {
Copy link

Choose a reason for hiding this comment

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

⛏️ Finding 7: Missing documentation and alphabetization for policyTimeoutCustomMessage

Matt noted this property needs:

  1. DocC documentation explaining its purpose
  2. Alphabetization among other properties

The property should be documented with /// comments and placed in alphabetical order with the other computed properties in this section.

@LRNcardozoWDF LRNcardozoWDF force-pushed the cmcg/pm-19305-vault-timeout-policy branch from fc4e4a8 to 1fde466 Compare November 24, 2025 23:28
.styleGuide(.subheadline)
.foregroundColor(Color(asset: SharedAsset.Colors.textSecondary))
.padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 1: Divider appears even without footer text

The current logic shows dividers whenever footer != nil, but this creates the visual issue Matt identified where a divider appears without accompanying footer text.

The issue: Lines 57 and 66 check if footer != nil to show dividers, and line 72 uses if let footer to show the actual text. This means dividers render even when footer is an empty string or when the text won't display.

Suggested fix:
Since the footer text block (lines 72-77) already uses if let footer, the dividers should follow the same pattern. The divider before the picker (line 57) and after the picker (line 66) should only appear when the footer text will actually render.

Alternatively, remove the divider checks on lines 57 and 66 entirely if dividers aren't needed when there's no footer.

}

/// The message to display if a timeout policy is in effect for the user.
var policyTimeoutCustomMessage: String? {
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 2: Missing DocC documentation

Per project guidelines and Matt's review comment, this computed property needs DocC documentation. The property has complex conditional logic returning different messages based on policy type.

Add documentation like:

/// The message to display in the custom timeout field when a policy is in effect.
///
/// Returns different messages based on the policy type:
/// - `.custom`: Shows the specific timeout duration from the policy
/// - `.immediately`: Indicates the setting is managed by the organization
/// - `.never`/`.onAppRestart`: Shows the policy's timeout type
/// - `nil`: Returns `nil` if no policy is in effect
///
var policyTimeoutCustomMessage: String? {

// MARK: Tests

/// Test a snapshot of the picker field in light mode without footer.
func disabletest_snapshot_lightMode_noFooter() {
Copy link

Choose a reason for hiding this comment

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

Finding 1: All snapshot tests are disabled

All 7 snapshot tests have the disabletest_ prefix instead of test_, which means they never execute. This violates the testing guidelines (Testing.md:172-174) requiring snapshot tests for light mode, dark mode, and large dynamic type.

Required action: Remove the disable prefix from all test function names:

  • test_snapshot_lightMode_noFooter()
  • test_snapshot_darkMode_noFooter()
  • test_snapshot_largeDynamicType_noFooter()
  • etc.

}

if hasDivider {
if let footer {
Copy link

Choose a reason for hiding this comment

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

Finding 2: Divider appears even when footer text won't render

This divider shows whenever footer != nil, but the actual footer text (lines 72-77) only renders when footer has content. This creates the visual issue Matt identified where a divider appears without accompanying text.

Suggested fix:

if let footer, !footer.isEmpty {
    Divider()
        .padding(.leading, 16)
}

Apply the same fix to line 66.


/// The session timeout section.
private var sessionTimeoutSection: some View {
SectionView(Localizations.sessionTimeout, contentSpacing: 8) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Finding 3: Verify 8pt spacing between ContentBlocks

Matt identified that there should be 8pt of padding between the "Session timeout" and "Session timeout action" blocks. While contentSpacing: 8 should provide this, please verify at runtime that the spacing displays correctly per the design spec.

}

/// The message to display if a timeout policy is in effect for the user.
var policyTimeoutCustomMessage: String? {
Copy link

Choose a reason for hiding this comment

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

⛏️ Finding 4 & 5: Missing DocC documentation and needs alphabetization

Per project guidelines (CLAUDE.md:13) and Matt's review:

  1. Add DocC documentation explaining the property's purpose and return values:
/// The message to display in the custom timeout field when a policy is in effect.
///
/// Returns different messages based on the policy type:
/// - `.custom`: Shows the organization's configured timeout duration
/// - `.immediately`: Indicates the setting is managed by the organization
/// - `.never`/`.onAppRestart`: Shows the policy's timeout type
/// - Returns `nil` if no policy is in effect
///
var policyTimeoutCustomMessage: String? {
  1. Alphabetize this property among other computed properties in this section.


try await subject.fetchSync(forceSync: false)

XCTAssertEqual(stateService.timeoutAction["1"], .logout)
Copy link

Choose a reason for hiding this comment

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

💭 Finding 7: Test should verify timeout value

Matt suggested this test should verify the timeout value was set correctly, not just the timeout action. Consider adding:

XCTAssertEqual(stateService.vaultTimeout["1"], /* expected SessionTimeoutValue based on policy */)

Copy link
Member Author

Choose a reason for hiding this comment

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

Claude, I added that validation in the line below.

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