Skip to content

[PM-40532] feat: Lock the Send deletion date when enforced by the SendControls policy - #7258

Merged
andrebispo5 merged 13 commits into
PM-40534-restrict-send-creation-when-one-type-allowedfrom
PM-40532-lock-deletion-date-when-enforced
Aug 12, 2026
Merged

[PM-40532] feat: Lock the Send deletion date when enforced by the SendControls policy#7258
andrebispo5 merged 13 commits into
PM-40534-restrict-send-creation-when-one-type-allowedfrom
PM-40532-lock-deletion-date-when-enforced

Conversation

@andrebispo5

@andrebispo5 andrebispo5 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-40532

📔 Objective

When an org's SendControls policy sets deletionHours, the deletion date stops being the user's to pick. The chooser on the Add/Edit Send screen is now locked to the enforced value, and the helper text under it changes to "This date is enforced by your organization" so it's clear why the field can't be changed.

A new Send picks up the enforced window instead of the usual 7 days. An existing Send keeps the deletion date it was created with — editing a Send for some unrelated reason shouldn't quietly push its deletion date out.

The View screen is left alone. The ticket's technical breakdown asked for the same helper text there, but the Figma acceptance criteria says to keep showing the date as normal, and the ticket itself flags this as an open question for design (item G3). Happy to add it if design comes back the other way.

All of it sits behind the pm-31885-send-controls flag. With the flag off the field behaves exactly as before: editable, default 7-day window, original helper text. If the flag or the policy is switched off while the screen is open, a new Send's date falls back to the 7-day default, so the chooser and the state can't end up showing different things.

Stacked on #7249.

📸 Screenshots

Before After

@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context t:feature Change Type - Feature Development labels Aug 11, 2026
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.97%. Comparing base (9808658) to head (e01bb15).

Additional details and impacted files
@@                                    Coverage Diff                                    @@
##           PM-40534-restrict-send-creation-when-one-type-allowed    #7258      +/-   ##
=========================================================================================
+ Coverage                                                  84.47%   85.97%   +1.50%     
=========================================================================================
  Files                                                       1114      894     -220     
  Lines                                                      68737    65509    -3228     
  Branches                                                    9923     9808     -115     
=========================================================================================
- Hits                                                       58068    56324    -1744     
+ Misses                                                      7150     5683    -1467     
+ Partials                                                    3519     3502      -17     
Flag Coverage Δ
app-data 17.88% <0.00%> (+0.20%) ⬆️
app-ui-auth-tools 18.76% <100.00%> (+0.01%) ⬆️
app-ui-platform 16.41% <0.00%> (-0.72%) ⬇️
app-ui-vault 27.32% <0.00%> (-0.59%) ⬇️
authenticator 6.08% <0.00%> (-0.01%) ⬇️
lib-core-network-bridge 4.10% <0.00%> (+<0.01%) ⬆️
lib-data-ui 1.19% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andrebispo5
andrebispo5 marked this pull request as ready for review August 11, 2026 13:38
@andrebispo5
andrebispo5 requested review from a team and david-livefront as code owners August 11, 2026 13:38
Copilot AI lite review requested due to automatic review settings August 11, 2026 13:38
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the SendControls deletion-date enforcement across AddEditSendViewModel, the two deletion date chooser composables, and their tests. The flag gating (isSendControlsEnabled) is applied consistently at both initial-state construction and on policy re-emission, add-vs-edit mode behavior matches the stated intent, and the revert-to-default path when enforcement is lifted is covered by tests. No security, correctness, or breaking-change issues found in the changed code.

Code Review Details
  • ❓ : Locked chooser silently displays "7 days" when the enforced deletionHours does not map to a DeletionOption
    • app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt:45

Minor, not posted inline: the deletionHours KDoc in EffectiveSendPolicy.kt:21 and AddEditSendViewModel.kt:979 still says "Currently unused by the UI", which this PR makes stale.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Add/Edit Send flow to respect the SendControls policy’s deletionHours enforcement by locking the deletion-date picker to the enforced window (behind the pm-31885-send-controls feature flag), while ensuring edits to existing Sends do not silently change their deletion date.

Changes:

  • Adds UI helper text and disables the deletion-date chooser when an enforced deletion window is present.
  • Updates AddEditSendViewModel to default new Sends to the enforced deletion window (or 7-day default when not enforced) and to avoid shifting deletion dates for existing Sends.
  • Adds ViewModel and Compose UI tests covering enforcement, toggling, and enabled/disabled picker behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ui/src/main/res/values/strings.xml Adds helper text for enforced deletion date messaging.
app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt Locks picker and updates supporting text when deletion date is enforced.
app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt Locks custom picker and updates supporting text when deletion date is enforced.
app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModel.kt Applies enforced deletion window for new Sends and handles enforcement changes without affecting existing Sends.
app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendScreen.kt Threads enforced deletion window state down into content.
app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendContent.kt Passes enforcement state into the add/edit deletion date chooser components.
app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModelTest.kt Adds unit tests for enforced/default deletion window behavior and enforcement toggling.
app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendScreenTest.kt Adds Compose tests asserting the chooser is locked/unlocked and helper text changes under enforcement.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 44 to +48
val options = DeletionOption.entries.associateWith { it.text() }
val enforcedOption = enforcedDeletionHours?.let { hours ->
DeletionOption.entries.firstOrNull {
it.offsetMillis == hours.hours.inWholeMilliseconds
}
Comment thread ui/src/main/res/values/strings.xml
Comment on lines +45 to +49
val enforcedOption = enforcedDeletionHours?.let { hours ->
DeletionOption.entries.firstOrNull {
it.offsetMillis == hours.hours.inWholeMilliseconds
}
}

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.

QUESTION: Is deletionHours guaranteed to map to one of the seven DeletionOption values?

Details

enforcedDeletionHours comes from server policy JSON as an arbitrary Int (PolicyInformation.SendControls.deletionHours). If an org sets a value outside the fixed set (1, 24, 48, 72, 168, 336, 720), enforcedOption is null, so the locked button falls back to selectedOption and displays "7 days" — while the ViewModel state has deletionDate = now + enforcedHours. The Send would then be created with a window the user was never shown, and the field is disabled so they cannot correct it.

If the admin console constrains the value to those options, this is moot. If not, one option is to display the resolved instant when no option matches, the way AddEditSendCustomDateChooser's CustomDeletionOption.Current formats originalSelection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Option are fixed and are rolled out to all clients when needed.

…into PM-40532-lock-deletion-date-when-enforced
Comment on lines +464 to +467
val hours = when {
enforcedDeletionHours != null -> enforcedDeletionHours.toLong()
this.enforcedDeletionHours != null -> DEFAULT_DELETION_HOURS
else -> return null

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.

I am a little bit confused with this.
Does not the state already contain the enforcedDeletionHourse?
Why if it not null are we using the DEFAULT_DELETION_HOURS instead of the value that exists on the state?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This updates the state based on the new deletion date coming from the policy. The param is the
new value, this.enforcedDeletionHours is the previous one, so that second branch only hits
when the new value is null, i.e. enforcement was just dropped, and we fall back to the DEFAULT_DELETION_HOURS. If it was never enforced we return null and leave the date alone.

Will rename the param to newEnforcedDeletionHours since the shadowing makes it confusing.

Comment on lines +238 to +239
assertEquals(ENFORCED_DELETION_HOURS, state.enforcedDeletionHours)
assertEquals(ENFORCED_DELETION_DATE, state.deletionDateOrNull())

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.

Compare state

Comment on lines +250 to +251
assertNull(state.enforcedDeletionHours)
assertEquals(DEFAULT_COMMON_STATE.deletionDate, state.deletionDateOrNull())

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.

Assert the whole state

assertEquals(ENFORCED_DELETION_DATE, state.deletionDateOrNull())
}

@Suppress("MaxLineLength")

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.

Suppress not needed

assertEquals(DEFAULT_COMMON_STATE.deletionDate, state.deletionDateOrNull())
}

@Suppress("MaxLineLength")

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.

not needed

Comment on lines +262 to +267
assertEquals(DEFAULT_COMMON_STATE.deletionDate, awaitItem().deletionDateOrNull())

mutableEffectiveSendPolicyFlow.value =
DEFAULT_EFFECTIVE_SEND_POLICY.copy(deletionHours = ENFORCED_DELETION_HOURS)

assertEquals(ENFORCED_DELETION_DATE, awaitItem().deletionDateOrNull())

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.

Assert whole state

}
}

@Suppress("MaxLineLength")

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.

Not necessary


@Suppress("MaxLineLength")
@Test
fun `deletion date should not change when the enforced deletion window changes in edit mode`() =

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.

Use whole state on all asserts here

}
}

@Suppress("MaxLineLength")

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.

Not necessary


mutableSendControlsFlagFlow.value = true

assertEquals(

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.

Whole state

)
}

@Suppress("MaxLineLength")

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.

Unnecessary

disableHideEmail = true,
)

val updatedState = awaitItem()

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.

Assert whole state

}
}

@Suppress("MaxLineLength")

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.

Not needed

Comment on lines +357 to +358
assertNull(updatedState.enforcedDeletionHours)
assertEquals(DEFAULT_COMMON_STATE.deletionDate, updatedState.deletionDateOrNull())

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.

Compare whole state


mutableEffectiveSendPolicyFlow.value = DEFAULT_EFFECTIVE_SEND_POLICY

assertEquals(DEFAULT_COMMON_STATE.deletionDate, awaitItem().deletionDateOrNull())

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.

Compare state

}
}

@Suppress("MaxLineLength")

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.

not needed

…into PM-40532-lock-deletion-date-when-enforced

# Conflicts:
#	app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModel.kt
#	app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModelTest.kt
…into PM-40532-lock-deletion-date-when-enforced
…into PM-40532-lock-deletion-date-when-enforced
@andrebispo5
andrebispo5 requested a review from aj-rosado August 12, 2026 15:56

val flagOnState = createViewModel().stateFlow.value
assertEquals(ENFORCED_DELETION_STATE, flagOnState)
assertEquals(ENFORCED_DELETION_HOURS, flagOnState.enforcedDeletionHours)

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.

Is not this second assert redundant as we are already checking it on assertEquals(ENFORCED_DELETION_STATE, flagOnState) ?

@aj-rosado aj-rosado 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.

LGTM

@andrebispo5
andrebispo5 added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 5c0764e Aug 12, 2026
26 checks passed
@andrebispo5
andrebispo5 deleted the PM-40532-lock-deletion-date-when-enforced branch August 12, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants