From 482c9389c6b08eb8934b7fb20bea346ddfcb9cb2 Mon Sep 17 00:00:00 2001 From: Matt Czech Date: Fri, 17 Jul 2026 16:06:44 -0500 Subject: [PATCH 1/2] [PM-38792] feat: Enforce Send deletion date via Send Controls policy --- .../en.lproj/Localizable.strings | 1 + .../Models/Domain/SendPolicyOptions.swift | 19 +++++++--- .../Domain/SendPolicyOptionsTests.swift | 38 +++++++++++++++++++ .../Models/Enum/SendDeletionDateType.swift | 32 ++++++++++++++++ .../Enum/SendDeletionDateTypeTests.swift | 26 +++++++++++++ .../Vault/Models/Enum/PolicyOptionType.swift | 5 +++ .../AddEditSendItemProcessor.swift | 3 ++ .../AddEditSendItemProcessorTests.swift | 26 +++++++++++++ .../AddEditSendItemState.swift | 18 ++++++++- .../AddEditSendItemStateTests.swift | 34 +++++++++++++++-- ...dEditSendItemView+ViewInspectorTests.swift | 24 ++++++++++++ .../AddEditSendItem/AddEditSendItemView.swift | 5 ++- 12 files changed, 220 insertions(+), 11 deletions(-) diff --git a/BitwardenResources/Localizations/en.lproj/Localizable.strings b/BitwardenResources/Localizations/en.lproj/Localizable.strings index 1fe7b9c9b4..64b8707be8 100644 --- a/BitwardenResources/Localizations/en.lproj/Localizable.strings +++ b/BitwardenResources/Localizations/en.lproj/Localizable.strings @@ -422,6 +422,7 @@ "NewSendCreated" = "Send created"; "Custom" = "Custom"; "SendDisabledWarning" = "Due to an enterprise policy, you are only able to delete an existing Send."; +"SendDeletionDateEnforcedByOrganization" = "This date is enforced by your organization"; "AboutSend" = "About Send"; "HideEmail" = "Hide my email address from recipients"; "SendOptionsPolicyInEffect" = "One or more organization policies are affecting your Send options."; diff --git a/BitwardenShared/Core/Tools/Models/Domain/SendPolicyOptions.swift b/BitwardenShared/Core/Tools/Models/Domain/SendPolicyOptions.swift index f2ae6d477b..78e3cd795d 100644 --- a/BitwardenShared/Core/Tools/Models/Domain/SendPolicyOptions.swift +++ b/BitwardenShared/Core/Tools/Models/Domain/SendPolicyOptions.swift @@ -14,6 +14,10 @@ struct SendPolicyOptions: Equatable, Sendable { /// The access type the user is required to use, or `nil` if the access type is unrestricted. var enforcedAccessType: SendAccessType? + /// The number of hours from creation the user is required to use as the Send deletion date, or + /// `nil` if the deletion date is unrestricted. + var enforcedDeletionDateHours: Int? + /// The Send type the user is required to use, or `nil` if both types are allowed (unrestricted). var enforcedSendType: SendType? @@ -28,11 +32,15 @@ extension SendPolicyOptions { /// Creates the Send policy options from the Send Controls policies that apply to the user. /// /// When multiple policies apply, a restriction is enforced if *any* applying policy enables it. - /// The enforced access type is resolved to the most restrictive across all applying policies - /// (email verification > password protection > no access control, the `whoCanAccess` values are - /// ordered by restrictiveness and the highest value wins, and the enforced Send type is the - /// most restrictive across all applying policies (per the order text > file > - /// both/unrestricted). + /// Additionally: + /// - The enforced access type is resolved to the most restrictive across all applying policies + /// (email verification > password protection > no access control). + /// - The enforced access control (`whoCanAccess`) values are ordered by restrictiveness and the + /// highest value wins. + /// - The enforced Send type is the most restrictive across all applying policies (per the order + /// text > file > both/unrestricted). + /// - The enforced deletion date is the most restrictive (shortest timeframe, i.e. the minimum + /// number of hours) across all applying policies. /// /// - Parameter sendControlsPolicies: The `sendControls` policies applying to the active user. /// @@ -57,6 +65,7 @@ extension SendPolicyOptions { self.init( allowedDomains: allowedDomains, enforcedAccessType: enforcedAccessType, + enforcedDeletionDateHours: policies.compactMap { $0[.deletionHours]?.intValue }.min(), enforcedSendType: Self.enforcedSendType(from: policies), isHideEmailDisabled: policies.contains { $0[.disableHideEmail]?.boolValue == true }, isSendDisabled: policies.contains { $0[.disableSend]?.boolValue == true }, diff --git a/BitwardenShared/Core/Tools/Models/Domain/SendPolicyOptionsTests.swift b/BitwardenShared/Core/Tools/Models/Domain/SendPolicyOptionsTests.swift index c934219888..5fd7a89e3c 100644 --- a/BitwardenShared/Core/Tools/Models/Domain/SendPolicyOptionsTests.swift +++ b/BitwardenShared/Core/Tools/Models/Domain/SendPolicyOptionsTests.swift @@ -62,6 +62,7 @@ struct SendPolicyOptionsTests { let subject = SendPolicyOptions(sendControlsPolicies: []) #expect(subject.allowedDomains.isEmpty) #expect(subject.enforcedAccessType == nil) + #expect(subject.enforcedDeletionDateHours == nil) #expect(subject.enforcedSendType == nil) #expect(!subject.isHideEmailDisabled) #expect(!subject.isSendDisabled) @@ -149,6 +150,43 @@ struct SendPolicyOptionsTests { #expect(subject.allowedDomains == ["earlier.com"]) } + /// `init(sendControlsPolicies:)` reads the enforced deletion date hours from a policy's + /// `deletionHours` option. + @Test + func init_sendControlsPolicies_enforcedDeletionDateHours() { + let subject = SendPolicyOptions(sendControlsPolicies: [ + .fixture(data: [PolicyOptionType.deletionHours.rawValue: .int(168)], type: .sendControls), + ]) + #expect(subject.enforcedDeletionDateHours == 168) + } + + /// `init(sendControlsPolicies:)` enforces no deletion date when no policy specifies + /// `deletionHours`. + @Test + func init_sendControlsPolicies_enforcedDeletionDateHours_missing() { + let subject = SendPolicyOptions(sendControlsPolicies: [.fixture(type: .sendControls)]) + #expect(subject.enforcedDeletionDateHours == nil) + } + + /// `init(sendControlsPolicies:)` enforces the most restrictive deletion date (the shortest + /// timeframe, i.e. the minimum hours) across applying policies. + @Test + func init_sendControlsPolicies_enforcedDeletionDateHours_multiplePolicies_shortestWins() { + let subject = SendPolicyOptions(sendControlsPolicies: [ + .fixture( + data: [PolicyOptionType.deletionHours.rawValue: .int(720)], + id: "thirty-days", + type: .sendControls, + ), + .fixture( + data: [PolicyOptionType.deletionHours.rawValue: .int(168)], + id: "seven-days", + type: .sendControls, + ), + ]) + #expect(subject.enforcedDeletionDateHours == 168) + } + /// `init(sendControlsPolicies:)` maps a single-element `allowedSendTypes` array to the enforced /// Send type. @Test diff --git a/BitwardenShared/Core/Tools/Models/Enum/SendDeletionDateType.swift b/BitwardenShared/Core/Tools/Models/Enum/SendDeletionDateType.swift index cabdd644d5..de84539223 100644 --- a/BitwardenShared/Core/Tools/Models/Enum/SendDeletionDateType.swift +++ b/BitwardenShared/Core/Tools/Models/Enum/SendDeletionDateType.swift @@ -22,6 +22,9 @@ enum SendDeletionDateType: Menuable { /// A time period of seven days. case sevenDays + /// A time period of fourteen days. + case fourteenDays + /// A time period of thirty days. case thirtyDays @@ -37,11 +40,38 @@ enum SendDeletionDateType: Menuable { case .twoDays: Localizations.xDays(2) case .threeDays: Localizations.xDays(3) case .sevenDays: Localizations.xDays(7) + case .fourteenDays: Localizations.xDays(14) case .thirtyDays: Localizations.xDays(30) case let .custom(customDate): customDate.dateTimeDisplay } } + // MARK: Type Methods + + /// Returns the deletion date type matching the given number of hours. + /// + /// Maps to a preset case when `hours` matches a known preset (`1`, `24`, `48`, `72`, `168`, + /// `336`, `720`); otherwise returns a `.custom` date `hours` from `originDate`. Used to + /// represent a policy-enforced deletion date supplied as a number of hours. + /// + /// - Parameters: + /// - hours: The number of hours from `originDate` at which the Send should be deleted. + /// - originDate: The date the custom-fallback calculation is based on. Defaults to `Date()`. + /// - Returns: The matching `SendDeletionDateType`. + /// + static func from(hours: Int, originDate: Date = Date()) -> SendDeletionDateType { + switch hours { + case 1: .oneHour + case 24: .oneDay + case 48: .twoDays + case 72: .threeDays + case 168: .sevenDays + case 336: .fourteenDays + case 720: .thirtyDays + default: .custom(Calendar.current.date(byAdding: .hour, value: hours, to: originDate) ?? originDate) + } + } + // MARK: Methods /// Calculates the date representation of this value. @@ -60,6 +90,8 @@ enum SendDeletionDateType: Menuable { Calendar.current.date(byAdding: .day, value: 3, to: originDate) case .sevenDays: Calendar.current.date(byAdding: .day, value: 7, to: originDate) + case .fourteenDays: + Calendar.current.date(byAdding: .day, value: 14, to: originDate) case .thirtyDays: Calendar.current.date(byAdding: .day, value: 30, to: originDate) case let .custom(customDate): diff --git a/BitwardenShared/Core/Tools/Models/Enum/SendDeletionDateTypeTests.swift b/BitwardenShared/Core/Tools/Models/Enum/SendDeletionDateTypeTests.swift index 6989bcf2b9..adbc47198e 100644 --- a/BitwardenShared/Core/Tools/Models/Enum/SendDeletionDateTypeTests.swift +++ b/BitwardenShared/Core/Tools/Models/Enum/SendDeletionDateTypeTests.swift @@ -28,6 +28,9 @@ class SendDeletionDateTypeTests: BitwardenTestCase { let sevenDays = SendDeletionDateType.sevenDays.calculateDate(from: originDate) XCTAssertEqual(sevenDays, Date(year: 2023, month: 11, day: 12)) + let fourteenDays = SendDeletionDateType.fourteenDays.calculateDate(from: originDate) + XCTAssertEqual(fourteenDays, Date(year: 2023, month: 11, day: 19)) + let thirtyDays = SendDeletionDateType.thirtyDays.calculateDate(from: originDate) XCTAssertEqual(thirtyDays, Date(year: 2023, month: 12, day: 5)) @@ -35,6 +38,28 @@ class SendDeletionDateTypeTests: BitwardenTestCase { XCTAssertEqual(custom, customDate) } + /// `from(hours:)` maps a preset number of hours to the matching preset case. + func test_fromHours_presets() { + let originDate = Date(year: 2023, month: 11, day: 5) + XCTAssertEqual(SendDeletionDateType.from(hours: 1, originDate: originDate), .oneHour) + XCTAssertEqual(SendDeletionDateType.from(hours: 24, originDate: originDate), .oneDay) + XCTAssertEqual(SendDeletionDateType.from(hours: 48, originDate: originDate), .twoDays) + XCTAssertEqual(SendDeletionDateType.from(hours: 72, originDate: originDate), .threeDays) + XCTAssertEqual(SendDeletionDateType.from(hours: 168, originDate: originDate), .sevenDays) + XCTAssertEqual(SendDeletionDateType.from(hours: 336, originDate: originDate), .fourteenDays) + XCTAssertEqual(SendDeletionDateType.from(hours: 720, originDate: originDate), .thirtyDays) + } + + /// `from(hours:)` falls back to a custom date `hours` from the origin date when the number of + /// hours doesn't match a preset. + func test_fromHours_customFallback() { + let originDate = Date(year: 2023, month: 11, day: 5) + XCTAssertEqual( + SendDeletionDateType.from(hours: 100, originDate: originDate), + .custom(Date(year: 2023, month: 11, day: 9, hour: 4)), + ) + } + /// `localizedName` returns the localized name of the option to display in the menu. func test_localizedName() { XCTAssertEqual(SendDeletionDateType.oneHour.localizedName, Localizations.xHours(1)) @@ -42,6 +67,7 @@ class SendDeletionDateTypeTests: BitwardenTestCase { XCTAssertEqual(SendDeletionDateType.twoDays.localizedName, Localizations.xDays(2)) XCTAssertEqual(SendDeletionDateType.threeDays.localizedName, Localizations.xDays(3)) XCTAssertEqual(SendDeletionDateType.sevenDays.localizedName, Localizations.xDays(7)) + XCTAssertEqual(SendDeletionDateType.fourteenDays.localizedName, Localizations.xDays(14)) XCTAssertEqual(SendDeletionDateType.thirtyDays.localizedName, Localizations.xDays(30)) XCTAssertEqual( diff --git a/BitwardenShared/Core/Vault/Models/Enum/PolicyOptionType.swift b/BitwardenShared/Core/Vault/Models/Enum/PolicyOptionType.swift index b44e48edd6..af31e84778 100644 --- a/BitwardenShared/Core/Vault/Models/Enum/PolicyOptionType.swift +++ b/BitwardenShared/Core/Vault/Models/Enum/PolicyOptionType.swift @@ -90,6 +90,11 @@ enum PolicyOptionType: String { /// are allowed. case allowedSendTypes + /// A policy option for the deletion date users are required to use on Sends, encoded as an + /// `Int` number of hours from creation (e.g. `168` = 7 days). A missing key means the deletion + /// date is not restricted. + case deletionHours + /// A policy option for whether the send should disable the hide email option. case disableHideEmail diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemProcessor.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemProcessor.swift index f6d0ca36d3..4ac2587481 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemProcessor.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemProcessor.swift @@ -248,6 +248,9 @@ class AddEditSendItemProcessor: // swiftlint:disable:this type_body_length if let enforcedAccessType = state.sendPolicyOptions.enforcedAccessType { state.accessType = enforcedAccessType } + if let enforcedDeletionDate = state.policyEnforcedDeletionDate { + state.deletionDate = enforcedDeletionDate + } state.hasPremium = await services.sendRepository.doesActiveAccountHavePremium() await refreshProfileState() diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemProcessorTests.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemProcessorTests.swift index e18b76f434..d2f34d03aa 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemProcessorTests.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemProcessorTests.swift @@ -219,6 +219,32 @@ class AddEditSendItemProcessorTests: BitwardenTestCase { // swiftlint:disable:th XCTAssertTrue(coordinator.alertShown.isEmpty) } + /// `perform(_:)` with `loadData` applies a policy-enforced deletion date, forcing the deletion + /// date and marking it enforced. + @MainActor + func test_perform_loadData_enforcedDeletionDate() async { + await subject.perform(.loadData) + XCTAssertNil(subject.state.policyEnforcedDeletionDate) + XCTAssertFalse(subject.state.isDeletionDateEnforcedByPolicy) + + policyService.getSendPolicyOptionsResult.enforcedDeletionDateHours = 168 + await subject.perform(.loadData) + XCTAssertEqual(subject.state.policyEnforcedDeletionDate, .sevenDays) + XCTAssertEqual(subject.state.deletionDate, .sevenDays) + XCTAssertTrue(subject.state.isDeletionDateEnforcedByPolicy) + } + + /// `perform(_:)` with `loadData` leaves the deletion date unchanged when no deletion date is + /// enforced by policy. + @MainActor + func test_perform_loadData_noEnforcedDeletionDate() async { + subject.state.deletionDate = .oneDay + await subject.perform(.loadData) + XCTAssertNil(subject.state.policyEnforcedDeletionDate) + XCTAssertFalse(subject.state.isDeletionDateEnforcedByPolicy) + XCTAssertEqual(subject.state.deletionDate, .oneDay) + } + /// `perform(_:)` with `loadData` loads whether the Send Controls policy feature flag is enabled. @MainActor func test_perform_loadData_sendControlsPolicyFlag() async { diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemState.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemState.swift index 406ff90db2..8a92a557f9 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemState.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemState.swift @@ -126,9 +126,12 @@ struct AddEditSendItemState: Equatable, Sendable { var availableDeletionDateTypes: [SendDeletionDateType] { switch mode { case .add, .shareExtension: - [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .thirtyDays] + [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .fourteenDays, .thirtyDays] case .edit: - [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .thirtyDays, .custom(customDeletionDate)] + [ + .oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .fourteenDays, .thirtyDays, + .custom(customDeletionDate), + ] } } @@ -137,6 +140,11 @@ struct AddEditSendItemState: Equatable, Sendable { sendPolicyOptions.enforcedAccessType != nil } + /// Whether the deletion date is enforced by policy, which disables the deletion date menu. + var isDeletionDateEnforcedByPolicy: Bool { + sendPolicyOptions.enforcedDeletionDateHours != nil + } + /// Whether sends are disabled via a policy. var isSendDisabled: Bool { sendPolicyOptions.isSendDisabled @@ -182,6 +190,12 @@ struct AddEditSendItemState: Equatable, Sendable { sendPolicyOptions.enforcedAccessType } + /// The deletion date the user is required to use by policy, or `nil` if the deletion date is + /// not restricted by policy. + var policyEnforcedDeletionDate: SendDeletionDateType? { + sendPolicyOptions.enforcedDeletionDateHours.map { SendDeletionDateType.from(hours: $0) } + } + /// Whether the hide-email field should be shown. /// /// When the Send Controls policy disables hiding the sender's email, the field is hidden diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemStateTests.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemStateTests.swift index 76ebc293a1..82006b8306 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemStateTests.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemStateTests.swift @@ -22,6 +22,20 @@ class AddEditSendItemStateTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertTrue(subject.isAccessTypeEnforcedByPolicy) } + // MARK: isDeletionDateEnforcedByPolicy + + /// `isDeletionDateEnforcedByPolicy` is `true` when a deletion date is enforced by policy. + func test_isDeletionDateEnforcedByPolicy_enforced() { + let subject = AddEditSendItemState(sendPolicyOptions: SendPolicyOptions(enforcedDeletionDateHours: 168)) + XCTAssertTrue(subject.isDeletionDateEnforcedByPolicy) + } + + /// `isDeletionDateEnforcedByPolicy` is `false` when no deletion date is enforced by policy. + func test_isDeletionDateEnforcedByPolicy_notEnforced() { + let subject = AddEditSendItemState(sendPolicyOptions: SendPolicyOptions(enforcedDeletionDateHours: nil)) + XCTAssertFalse(subject.isDeletionDateEnforcedByPolicy) + } + // MARK: normalizedRecipientEmails /// `normalizedRecipientEmails` applies all transformations: trim, lowercase, and filter. @@ -56,6 +70,20 @@ class AddEditSendItemStateTests: BitwardenTestCase { // swiftlint:disable:this t XCTAssertEqual(subject.normalizedRecipientEmails, ["test@example.com", "another@example.com"]) } + // MARK: policyEnforcedDeletionDate + + /// `policyEnforcedDeletionDate` maps the enforced hours to the matching deletion date type. + func test_policyEnforcedDeletionDate_enforced() { + let subject = AddEditSendItemState(sendPolicyOptions: SendPolicyOptions(enforcedDeletionDateHours: 168)) + XCTAssertEqual(subject.policyEnforcedDeletionDate, .sevenDays) + } + + /// `policyEnforcedDeletionDate` is `nil` when no deletion date is enforced by policy. + func test_policyEnforcedDeletionDate_notEnforced() { + let subject = AddEditSendItemState(sendPolicyOptions: SendPolicyOptions(enforcedDeletionDateHours: nil)) + XCTAssertNil(subject.policyEnforcedDeletionDate) + } + // MARK: shouldShowTrashIcon /// `shouldShowTrashIcon(for:)` returns `false` when there's only one empty email field. @@ -91,7 +119,7 @@ class AddEditSendItemStateTests: BitwardenTestCase { // swiftlint:disable:this t let subject = AddEditSendItemState(mode: .add) XCTAssertEqual( subject.availableDeletionDateTypes, - [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .thirtyDays], + [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .fourteenDays, .thirtyDays], ) } @@ -102,7 +130,7 @@ class AddEditSendItemStateTests: BitwardenTestCase { // swiftlint:disable:this t let subject = AddEditSendItemState(customDeletionDate: deletionDate, mode: .edit) XCTAssertEqual( subject.availableDeletionDateTypes, - [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .thirtyDays, .custom(deletionDate)], + [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .fourteenDays, .thirtyDays, .custom(deletionDate)], ) } @@ -112,7 +140,7 @@ class AddEditSendItemStateTests: BitwardenTestCase { // swiftlint:disable:this t let subject = AddEditSendItemState(mode: .shareExtension(.singleAccount)) XCTAssertEqual( subject.availableDeletionDateTypes, - [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .thirtyDays], + [.oneHour, .oneDay, .twoDays, .threeDays, .sevenDays, .fourteenDays, .thirtyDays], ) } diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView+ViewInspectorTests.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView+ViewInspectorTests.swift index 89eb216baa..c6c9580c07 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView+ViewInspectorTests.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView+ViewInspectorTests.swift @@ -56,6 +56,30 @@ class AddEditSendItemViewTests: BitwardenTestCase { XCTAssertEqual(processor.dispatchedActions.last, .deletionDateChanged(.thirtyDays)) } + /// The deletion date menu remains visible but is disabled when the deletion date is enforced + /// by policy. + @MainActor + func test_deletionDateMenu_disabledWhenEnforcedByPolicy() throws { + processor.state.isOptionsExpanded = true + var menuField = try subject.inspect().find(bitwardenMenuField: Localizations.deletionDate) + XCTAssertFalse(menuField.isDisabled()) + + processor.state.sendPolicyOptions.enforcedDeletionDateHours = 168 + menuField = try subject.inspect().find(bitwardenMenuField: Localizations.deletionDate) + XCTAssertTrue(menuField.isDisabled()) + } + + /// The deletion date field shows the policy helper text when the deletion date is enforced by + /// policy. + @MainActor + func test_deletionDate_helperTextWhenEnforcedByPolicy() throws { + processor.state.isOptionsExpanded = true + processor.state.sendPolicyOptions.enforcedDeletionDateHours = 168 + XCTAssertNoThrow( + try subject.inspect().find(text: Localizations.sendDeletionDateEnforcedByOrganization), + ) + } + /// Updating the maximum access count stepper sends the `.maximumAccessCountChanged` action. @MainActor func test_maximumAccessCountStepper_updated() throws { diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift index a15249f9e6..a84b904dca 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift @@ -193,8 +193,11 @@ struct AddEditSendItemView: View { // swiftlint:disable:this type_body_length send: AddEditSendItemAction.deletionDateChanged, ), ) + .disabled(store.state.isDeletionDateEnforcedByPolicy) - Text(Localizations.deletionDateInfo) + Text(store.state.isDeletionDateEnforcedByPolicy + ? Localizations.sendDeletionDateEnforcedByOrganization + : Localizations.deletionDateInfo) .styleGuide(.footnote) .foregroundColor(SharedAsset.Colors.textSecondary.swiftUIColor) .padding(.horizontal, 16) From 61dc7dba07a49f202a8af8a267c98be95e7e50cb Mon Sep 17 00:00:00 2001 From: Matt Czech Date: Mon, 24 Aug 2026 14:43:05 -0500 Subject: [PATCH 2/2] [PM-38792] Update localization key --- BitwardenResources/Localizations/en.lproj/Localizable.strings | 2 +- .../AddEditSendItemView+ViewInspectorTests.swift | 2 +- .../Send/SendItem/AddEditSendItem/AddEditSendItemView.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BitwardenResources/Localizations/en.lproj/Localizable.strings b/BitwardenResources/Localizations/en.lproj/Localizable.strings index 64b8707be8..f6d58b84d6 100644 --- a/BitwardenResources/Localizations/en.lproj/Localizable.strings +++ b/BitwardenResources/Localizations/en.lproj/Localizable.strings @@ -422,7 +422,7 @@ "NewSendCreated" = "Send created"; "Custom" = "Custom"; "SendDisabledWarning" = "Due to an enterprise policy, you are only able to delete an existing Send."; -"SendDeletionDateEnforcedByOrganization" = "This date is enforced by your organization"; +"ThisDateIsEnforcedByYourOrganization" = "This date is enforced by your organization"; "AboutSend" = "About Send"; "HideEmail" = "Hide my email address from recipients"; "SendOptionsPolicyInEffect" = "One or more organization policies are affecting your Send options."; diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView+ViewInspectorTests.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView+ViewInspectorTests.swift index c6c9580c07..1fe999dd11 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView+ViewInspectorTests.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView+ViewInspectorTests.swift @@ -76,7 +76,7 @@ class AddEditSendItemViewTests: BitwardenTestCase { processor.state.isOptionsExpanded = true processor.state.sendPolicyOptions.enforcedDeletionDateHours = 168 XCTAssertNoThrow( - try subject.inspect().find(text: Localizations.sendDeletionDateEnforcedByOrganization), + try subject.inspect().find(text: Localizations.thisDateIsEnforcedByYourOrganization), ) } diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift index a84b904dca..0cc6dd10b3 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift @@ -196,7 +196,7 @@ struct AddEditSendItemView: View { // swiftlint:disable:this type_body_length .disabled(store.state.isDeletionDateEnforcedByPolicy) Text(store.state.isDeletionDateEnforcedByPolicy - ? Localizations.sendDeletionDateEnforcedByOrganization + ? Localizations.thisDateIsEnforcedByYourOrganization : Localizations.deletionDateInfo) .styleGuide(.footnote) .foregroundColor(SharedAsset.Colors.textSecondary.swiftUIColor)