Skip to content

[PM-31614] feat: Added new UI for the Email verification on sends#6488

Merged
aj-rosado merged 14 commits intomainfrom
PM-31614/update-add-edit-send-ui-email-auth
Feb 13, 2026
Merged

[PM-31614] feat: Added new UI for the Email verification on sends#6488
aj-rosado merged 14 commits intomainfrom
PM-31614/update-add-edit-send-ui-email-auth

Conversation

@aj-rosado
Copy link
Contributor

@aj-rosado aj-rosado commented Feb 6, 2026

🎟️ Tracking

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

📔 Objective

Initial PR to add the layout changes for the Email Verification option on Sends.
The changes are behind a feature flag and should not have any impact when it is turned off.

This does not add the changes to the server and misses validations that will be added on subsequent PRs.

📸 Screenshots

Screenshot_1770402169 Screenshot_1770402172 Screenshot_1770402191 Screenshot_1770402349

⏰ 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

@aj-rosado aj-rosado requested review from a team and david-livefront as code owners February 6, 2026 18:27
@github-actions github-actions bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context labels Feb 6, 2026
@aj-rosado aj-rosado added the ai-review Request a Claude code review label Feb 6, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

Logo
Checkmarx One – Scan Summary & Detailsf2a9cc03-24ac-41b9-9f1c-52ef2713cd29

New Issues (1)

Checkmarx found the following issues in this Pull Request

# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM Privacy_Violation /app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModelTest.kt: 1094
detailsMethod Lambda at line 1094 of /app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendViewModelTest.kt sends user inform...
Attack Vector

@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 89.76744% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.34%. Comparing base (b20622e) to head (dae2231).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...tools/feature/send/addedit/AddEditSendViewModel.kt 82.53% 6 Missing and 5 partials ⚠️
...d/addedit/components/AddEditSendAuthTypeChooser.kt 93.05% 1 Missing and 4 partials ⚠️
...ls/feature/send/addedit/util/SendViewExtensions.kt 28.57% 4 Missing and 1 partial ⚠️
...re/send/addedit/util/AddEditSendStateExtensions.kt 92.30% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6488      +/-   ##
==========================================
+ Coverage   86.31%   86.34%   +0.02%     
==========================================
  Files         790      790              
  Lines       56525    56620      +95     
  Branches     8175     8210      +35     
==========================================
+ Hits        48791    48888      +97     
+ Misses       4887     4874      -13     
- Partials     2847     2858      +11     

☔ 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.

@claude
Copy link
Contributor

claude bot commented Feb 9, 2026

Claude finished @aj-rosado's task in 4m 49s —— View job


Code Review: PM-31614 feat: Added new UI for the Email verification on sends

  • Gather context and read changed files
  • Analyze code changes for issues
  • Post review findings

Reviewed commit: dae2231

Summary

This review covers the latest push which includes formatting fixes and filtering of empty email entries. The PR adds a new UI for email verification on sends, behind the SendEmailVerification feature flag.

Key changes reviewed:

  • SendAuth sealed class hierarchy (None, Password, Email) encapsulating auth-related state
  • AuthEmail data class with UUID-based identity for safe add/change/remove operations
  • AddEditSendAuthTypeChooser composable with ColumnScope extension and segmented button UI
  • ViewModel handlers for auth type selection, email add/change/remove actions
  • Extension functions mapping between SendAuth and SDK SendView models
  • Feature flag gating to control new vs. legacy password-only UI
  • Comprehensive test coverage for ViewModel actions, UI components, and extension mappings

Findings

No new issues found in this latest push. The previous review findings have been addressed:

  • Empty email entries are now filtered out before sending to the server (the filter { it.isNotBlank() } was added in the latest commit)
  • Formatting has been cleaned up per reviewer feedback

Outstanding Review Threads

Most existing threads are marked as outdated (the code they pointed at has been updated). The following items from previous reviews may still warrant attention:

  1. Missing toViewState test coverage for EMAIL and NONE branches (SendViewExtensionsTest.kt): The tests still only cover the PASSWORD case via createMockSendView(hasPassword = true). The EMAIL and NONE branches of the when expression in SendViewExtensions.kt remain untested.

  2. david-livefront's pending feedback on several outdated threads — the author has addressed most comments through refactoring, but some threads remain unresolved and may need explicit resolution from the reviewer.


Reviewed with ❤️ by Claude

Comment on lines 62 to 70
var selectedOption: SendAuthType by rememberSaveable {
mutableStateOf(
value = when {
hasPassword -> SendAuthType.PASSWORD
emails.isNotEmpty() -> SendAuthType.EMAIL
else -> SendAuthType.NONE
},
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 SUGGESTED: The selectedOption is managed locally with rememberSaveable and computed from hasPassword and emails props. This works correctly in most scenarios, but creates a potential state desync if the ViewModel's authType changes from an external action (e.g., if "Remove password" functionality were to reset the auth type).

Consider passing the current authType from the ViewModel as a parameter to ensure single source of truth:

fun AddEditSendAuthTypeChooser(
    selectedAuthType: SendAuthType, // Add this parameter
    onAuthTypeSelect: (SendAuthType) -> Unit,
    // ... other params
)

This is a minor suggestion - the current implementation works correctly for the current use cases since the auth type changes only occur through user interaction with this component.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with Claude here, Maybe we can just pass in the SendAuthType

@andrebispo5
Copy link
Contributor

Feature label is missing.

@andrebispo5
Copy link
Contributor

⛏️ Typo in the PR description

The chagnes are behind a feature flag and should not have any impact when it is turned off.

@aj-rosado aj-rosado added the t:feature Change Type - Feature Development label Feb 10, 2026
@github-actions github-actions bot removed the t:feature Change Type - Feature Development label Feb 10, 2026
authType = SendAuthType.PASSWORD,
),
selectedType = DEFAULT_TEXT_TYPE,
),
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 SUGGESTED: Missing test coverage for authType branches

The toViewState extension function has conditional logic for determining authType based on hasPassword and emails.isNotEmpty() (lines 39-43 in SendViewExtensions.kt):

authType = when {
    hasPassword -> SendAuthType.PASSWORD
    emails.isNotEmpty() -> SendAuthType.EMAIL
    else -> SendAuthType.NONE
}

The current tests only cover the PASSWORD case (via createMockSendView which has hasPassword = true). Consider adding tests for:

  1. EMAIL case: A SendView with hasPassword = false and non-empty emails list
  2. NONE case: A SendView with hasPassword = false and empty emails list

This would ensure all branches are covered and the authType mapping logic is correctly tested.

onAddNewEmailClick: () -> Unit,
onRemoveEmailClick: (Int) -> Unit,
password: String,
emails: List<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 we make this an immutable list


@Composable
private fun specificPeopleEmailContent(
emails: List<String>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Immutable list here too

.passwordInput
.takeIf {
common.authType == SendAuthType.PASSWORD
}.orNullIfBlank(),
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 use chain format here:

   .passwordInput
   .takeIf { common.authType == SendAuthType.PASSWORD }
   .orNullIfBlank(),

}

@Composable
private fun specificPeopleEmailContent(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should start with a capital S

Can we also add the ColumnScope. extension. I know the IDE will say it's not needed but this composable does not make sense outside of a ColumnScope and we should enforce that it is always applied there.

label = stringResource(id = BitwardenString.add_email),
onClick = {
onAddNewEmailClick()
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we simplify this:

onClick = onAddNewEmailClick,

@david-livefront
Copy link
Collaborator

One of your screenshots show that part of the card has the incorrect style

val expirationDate: ZonedDateTime?,
val sendUrl: String?,
val hasPassword: Boolean,
val authEmails: List<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 we make this an ImmutableList

* The user selected an authentication type.
*/
data class AuthTypeSelect(val authType: SendAuthType) :
AddEditSendAction()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we format this a bit nicer:

    data class AuthTypeSelect(val authType: SendAuthType) : AddEditSendAction()

@aj-rosado aj-rosado changed the title [PM-31614] Added new UI for the Email verification on sends [PM-31614] feat: Added new UI for the Email verification on sends Feb 11, 2026
*/
enum class SendAuthType(
val text: Text,
val supportingTextRes: Int?,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we make this a Text as well?

@github-actions github-actions bot added the t:feature Change Type - Feature Development label Feb 11, 2026
// Initialize with one empty email field if list is empty
commonContent.copy(
authEmails = commonContent.authEmails.ifEmpty { listOf("") }
.toImmutableList(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this be simplified:

authEmails = commonContent.authEmails.ifEmpty { persistentListOf("") },

onPasswordChange: (String) -> Unit,
onEmailValueChange: (String, Int) -> Unit,
onAddNewEmailClick: () -> Unit,
onRemoveEmailClick: (Int) -> Unit,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of the index, can we [ass back the actual email to remove?

val onPasswordCopyClick: (String) -> Unit,
val onAuthTypeSelect: (SendAuthType) -> Unit,
val onAuthPasswordChange: (String) -> Unit,
val onEmailValueChange: (String, Int) -> Unit,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Using the index is unsafe and can lead to errors.

Can we wrap the emails in a data class and create ids to track them. That will make this all much safer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have made a refactor on this. Added a new class like you suggested but also decided to remove the SendAuthType and aggregate all the data related to the Send Authentication on a class.

Will add a tech-debt task to also include the passwordInput there.

@Parcelize
data object None : SendAuth() {
@IgnoredOnParcel
override val text: Text = BitwardenString.anyone_with_the_link.asText()
Copy link
Collaborator

Choose a reason for hiding this comment

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

You don't need these annotations if you make them getters

val email1 = AuthEmail(id = "id1", value = "test@example.com")
val initialCommonState = DEFAULT_COMMON_STATE.copy(
passwordInput = "oldpassword",
sendAuth = SendAuth.Password,

Check warning

Code scanning / Checkmarx One

Privacy Violation Medium test

Privacy Violation
onPasswordChange: (String) -> Unit,
onEmailValueChange: (String, String) -> Unit,
onAddNewEmailClick: () -> Unit,
onRemoveEmailClick: (String) -> Unit,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we updates these to just emit the AuthEmail

    onEmailValueChange: (AuthEmail) -> Unit,
    onRemoveEmailClick: (AuthEmail) -> Unit,

contentDescription = stringResource(id = BitwardenString.delete),
contentColor = BitwardenTheme.colorScheme.status.error,
onClick = {
onRemoveEmailClick(authEmail.id)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just emit the whole AuthEmail object

hasPassword -> SendAuth.Password
emails.isNotEmpty() -> SendAuth.Email(
emails = this.emails.map { AuthEmail(value = it) }.toImmutableList(),
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any multi-line case like this should be wrapped in curly braces

if (authEmail.id == action.id) {
authEmail.copy(value = action.email)
if (authEmail.id == action.authEmail.id) {
authEmail.copy(value = action.authEmail.value)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why copy it?

Can we just replace it?

val updatedEmails = currentAuth.emails.map { authEmail ->
    if (authEmail.id == action.authEmail.id) {
        action.authEmail
    } else {
        authEmail
    }

},
emails = emptyList(),
authType = AuthType.NONE,
emails = (common.sendAuth as? SendAuth.Email)?.emails?.map { it.value }.orEmpty(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: Empty/blank email strings are included in the emails list sent to the server.

Since handleAuthEmailsRemove always keeps at least one empty AuthEmail when the list would become empty (ViewModel line 624), saving with "Specific people" selected but no emails entered sends [""] to the server. This is also inconsistent with how empty passwords are handled -- newPassword uses .orNullIfBlank() to filter out empty values (line 28), but emails get no such filtering.

Consider filtering out blank entries before sending:

Suggested change
emails = (common.sendAuth as? SendAuth.Email)?.emails?.map { it.value }.orEmpty(),
emails = (common.sendAuth as? SendAuth.Email)
?.emails
?.map { it.value }
?.filter { it.isNotBlank() }
.orEmpty(),

sendAuth = currentAuth.copy(
emails = currentAuth.emails.plus(
AuthEmail(value = ""),
).toImmutableList(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Formatting:

      emails = currentAuth
          .emails
          .plus(AuthEmail(value = ""))
          .toImmutableList(),

?.emails
?.map { it.value }
?.filter { it.isNotBlank() }
.orEmpty(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

@aj-rosado aj-rosado added this pull request to the merge queue Feb 13, 2026
Merged via the queue into main with commit ce3f0ac Feb 13, 2026
22 checks passed
@aj-rosado aj-rosado deleted the PM-31614/update-add-edit-send-ui-email-auth branch February 13, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review 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