Skip to content

Commit 8568f2e

Browse files
[PM-21125] Enable navigation to view send (#1588)
1 parent dcbe3a2 commit 8568f2e

File tree

6 files changed

+201
-12
lines changed

6 files changed

+201
-12
lines changed

BitwardenShared/UI/Tools/Send/Send/SendList/SendListItemRowView.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ enum SendListItemRowAction: Equatable, Sendable {
4141

4242
/// The item was pressed.
4343
case sendListItemPressed(SendListItem)
44+
45+
/// The view send button was tapped.
46+
case viewSend(_ sendView: SendView)
4447
}
4548

4649
// MARK: - SendListItemRowEffect
@@ -155,6 +158,9 @@ struct SendListItemRowView: View {
155158
await store.perform(.copyLinkPressed(sendView))
156159
}
157160
.accessibilityIdentifier("Copy")
161+
Button(Localizations.view) {
162+
store.send(.viewSend(sendView))
163+
}
158164
Button(Localizations.edit) {
159165
store.send(.editPressed(sendView))
160166
}

BitwardenShared/UI/Tools/Send/Send/SendList/SendListProcessor.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ final class SendListProcessor: StateProcessor<SendListState, SendListAction, Sen
100100
case let .sendListItemPressed(item):
101101
switch item.itemType {
102102
case let .send(sendView):
103-
coordinator.navigate(to: .editItem(sendView), context: self)
103+
coordinator.navigate(to: .viewItem(sendView), context: self)
104104
case let .group(type, _):
105105
coordinator.navigate(to: .group(type))
106106
}
107+
case let .viewSend(sendView):
108+
coordinator.navigate(to: .viewItem(sendView), context: self)
107109
}
108110
case let .toastShown(toast):
109111
state.toast = toast

BitwardenShared/UI/Tools/Send/Send/SendList/SendListProcessorTests.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,14 +476,14 @@ class SendListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
476476
XCTAssertEqual(coordinator.routes.last, .editItem(sendView))
477477
}
478478

479-
/// `receive(_:)` with `.sendListItemRow(.sendListItemPressed())` navigates to the edit send route.
479+
/// `receive(_:)` with `.sendListItemRow(.sendListItemPressed())` navigates to the view send route.
480480
@MainActor
481481
func test_receive_sendListItemRow_sendListItemPressed_withSendView() {
482482
let sendView = SendView.fixture()
483483
let item = SendListItem(sendView: sendView)!
484484
subject.receive(.sendListItemRow(.sendListItemPressed(item)))
485485

486-
XCTAssertEqual(coordinator.routes.last, .editItem(sendView))
486+
XCTAssertEqual(coordinator.routes.last, .viewItem(sendView))
487487
}
488488

489489
/// `receive(_:)` with `.sendListItemRow(.sendListItemPressed())` navigates to the group send route.
@@ -495,6 +495,15 @@ class SendListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
495495
XCTAssertEqual(coordinator.routes.last, .group(.file))
496496
}
497497

498+
/// `receive(_:)` with `.sendListItemRow(.viewSend())` navigates to the view send route.
499+
@MainActor
500+
func test_receive_sendListItemRow_viewSend() {
501+
let sendView = SendView.fixture()
502+
subject.receive(.sendListItemRow(.viewSend(sendView)))
503+
504+
XCTAssertEqual(coordinator.routes.last, .viewItem(sendView))
505+
}
506+
498507
/// `receive(_:)` with `.toastShown` updates the toast value in the state.
499508
@MainActor
500509
func test_receive_toastShown() {

BitwardenShared/UI/Tools/Send/SendItem/SendItemCoordinator.swift

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ final class SendItemCoordinator: Coordinator, HasStackNavigator {
1212
// MARK: Types
1313

1414
typealias Module = FileSelectionModule
15+
& NavigatorBuilderModule
16+
& SendItemModule
1517

1618
typealias Services = HasAuthRepository
1719
& HasErrorAlertServices.ErrorAlertServices
@@ -77,6 +79,8 @@ final class SendItemCoordinator: Coordinator, HasStackNavigator {
7779
delegate?.sendItemCancelled()
7880
case .deleted:
7981
delegate?.sendItemDeleted()
82+
case let .dismiss(dismissAction):
83+
stackNavigator?.dismiss(completion: dismissAction?.action)
8084
case let .complete(sendView):
8185
delegate?.sendItemCompleted(with: sendView)
8286
case let .edit(sendView):
@@ -95,6 +99,22 @@ final class SendItemCoordinator: Coordinator, HasStackNavigator {
9599

96100
// MARK: Private methods
97101

102+
/// Present a child `SendItemCoordinator` on top of the existing coordinator.
103+
///
104+
/// Presenting a view on top of an already presented view within the same coordinator causes
105+
/// problems when dismissing only the top view. So instead, present a new coordinator and
106+
/// show the view to navigate to within that coordinator's navigator.
107+
///
108+
/// - Parameter route: The route to navigate to in the presented coordinator.
109+
///
110+
private func presentChildSendItemCoordinator(route: SendItemRoute, context: AnyObject?) {
111+
let navigationController = module.makeNavigationController()
112+
let coordinator = module.makeSendItemCoordinator(delegate: self, stackNavigator: navigationController)
113+
coordinator.navigate(to: route, context: context)
114+
coordinator.start()
115+
stackNavigator?.present(navigationController)
116+
}
117+
98118
/// Shows the add item screen.
99119
///
100120
/// - Parameter content: Optional content to pre-fill the add item screen.
@@ -131,14 +151,19 @@ final class SendItemCoordinator: Coordinator, HasStackNavigator {
131151
/// - Parameter sendView: The send to edit.
132152
///
133153
private func showEditItem(for sendView: SendView) {
134-
let state = AddEditSendItemState(sendView: sendView)
135-
let processor = AddEditSendItemProcessor(
136-
coordinator: asAnyCoordinator(),
137-
services: services,
138-
state: state
139-
)
140-
let view = AddEditSendItemView(store: Store(processor: processor))
141-
stackNavigator?.replace(view)
154+
guard let stackNavigator else { return }
155+
if stackNavigator.isEmpty {
156+
let state = AddEditSendItemState(sendView: sendView)
157+
let processor = AddEditSendItemProcessor(
158+
coordinator: asAnyCoordinator(),
159+
services: services,
160+
state: state
161+
)
162+
let view = AddEditSendItemView(store: Store(processor: processor))
163+
stackNavigator.replace(view)
164+
} else {
165+
presentChildSendItemCoordinator(route: .edit(sendView), context: nil)
166+
}
142167
}
143168

144169
/// Navigates to the specified `FileSelectionRoute`.
@@ -193,3 +218,39 @@ final class SendItemCoordinator: Coordinator, HasStackNavigator {
193218
extension SendItemCoordinator: HasErrorAlertServices {
194219
var errorAlertServices: ErrorAlertServices { services }
195220
}
221+
222+
// MARK: - SendItemDelegate
223+
224+
extension SendItemCoordinator: SendItemDelegate {
225+
func handle(_ authAction: AuthAction) async {
226+
await delegate?.handle(authAction)
227+
}
228+
229+
func sendItemCancelled() {
230+
stackNavigator?.dismiss()
231+
}
232+
233+
func sendItemCompleted(with sendView: SendView) {
234+
// The dismiss and share sheet presentation needs to occur here rather than passing it onto
235+
// the delegate to handle the case where the edit view is presented on the view Send view.
236+
// The edit view is dismissed and the share sheet is presented on the view Send view.
237+
Task {
238+
do {
239+
guard let url = try await self.services.sendRepository.shareURL(for: sendView) else {
240+
navigate(to: .dismiss(nil))
241+
return
242+
}
243+
navigate(to: .dismiss(DismissAction {
244+
self.navigate(to: .share(url: url))
245+
}))
246+
} catch {
247+
services.errorReporter.log(error: error)
248+
navigate(to: .dismiss(nil))
249+
}
250+
}
251+
}
252+
253+
func sendItemDeleted() {
254+
delegate?.sendItemDeleted()
255+
}
256+
}

BitwardenShared/UI/Tools/Send/SendItem/SendItemCoordinatorTests.swift

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import BitwardenKitMocks
12
import BitwardenSdk
3+
import TestHelpers
24
import XCTest
35

46
@testable import BitwardenShared
@@ -9,6 +11,7 @@ class SendItemCoordinatorTests: BitwardenTestCase {
911
// MARK: Properties
1012

1113
var delegate: MockSendItemDelegate!
14+
var errorReporter: MockErrorReporter!
1215
var module: MockAppModule!
1316
var sendRepository: MockSendRepository!
1417
var stackNavigator: MockStackNavigator!
@@ -19,20 +22,25 @@ class SendItemCoordinatorTests: BitwardenTestCase {
1922
override func setUp() {
2023
super.setUp()
2124
delegate = MockSendItemDelegate()
25+
errorReporter = MockErrorReporter()
2226
module = MockAppModule()
2327
sendRepository = MockSendRepository()
2428
stackNavigator = MockStackNavigator()
2529
subject = SendItemCoordinator(
2630
delegate: delegate,
2731
module: module,
28-
services: ServiceContainer.withMocks(sendRepository: sendRepository),
32+
services: ServiceContainer.withMocks(
33+
errorReporter: errorReporter,
34+
sendRepository: sendRepository
35+
),
2936
stackNavigator: stackNavigator
3037
)
3138
}
3239

3340
override func tearDown() {
3441
super.tearDown()
3542
delegate = nil
43+
errorReporter = nil
3644
module = nil
3745
sendRepository = nil
3846
stackNavigator = nil
@@ -125,6 +133,15 @@ class SendItemCoordinatorTests: BitwardenTestCase {
125133
XCTAssertEqual(delegate.sendItemCompletedSendView, sendView)
126134
}
127135

136+
/// `navigate(to:)` with `.dismiss` dismisses the current modally presented screen.
137+
@MainActor
138+
func test_navigateTo_dismiss() throws {
139+
subject.navigate(to: .dismiss())
140+
141+
let action = try XCTUnwrap(stackNavigator.actions.last)
142+
XCTAssertEqual(action.type, .dismissedWithCompletionHandler)
143+
}
144+
128145
/// `navigate(to:)` with `.edit` shows the edit screen.
129146
@MainActor
130147
func test_navigateTo_edit() throws {
@@ -138,6 +155,19 @@ class SendItemCoordinatorTests: BitwardenTestCase {
138155
XCTAssertEqual(view.store.state.mode, .edit)
139156
}
140157

158+
/// `navigate(to:)` with `.edit` with a non empty stack presents a new send item coordinator.
159+
@MainActor
160+
func test_navigateTo_edit_presentsCoordinator() throws {
161+
stackNavigator.isEmpty = false
162+
163+
subject.navigate(to: .edit(.fixture()), context: nil)
164+
165+
let action = try XCTUnwrap(stackNavigator.actions.last)
166+
XCTAssertEqual(action.type, .presented)
167+
XCTAssertTrue(action.view is UINavigationController)
168+
XCTAssertEqual(module.sendItemCoordinator.routes, [.edit(.fixture())])
169+
}
170+
141171
/// `navigate(to:)` with `.fileSelection` and with a file selection delegate presents the file
142172
/// selection screen.
143173
@MainActor
@@ -169,4 +199,78 @@ class SendItemCoordinatorTests: BitwardenTestCase {
169199
let view = try XCTUnwrap(action.view as? ViewSendItemView)
170200
XCTAssertEqual(view.store.state.sendView, sendView)
171201
}
202+
203+
/// `handle(_:)` calls `handle(_:)` on the delegate.
204+
@MainActor
205+
func test_sendItemDelegate_handleAuthAction() async {
206+
let action = AuthAction.logout(userId: "1", userInitiated: true)
207+
await subject.handle(action)
208+
XCTAssertEqual(delegate.handledAuthActions, [action])
209+
}
210+
211+
/// `sendItemCancelled()` dismisses the presented view.
212+
@MainActor
213+
func test_sendItemDelegate_sendItemCancelled() throws {
214+
subject.sendItemCancelled()
215+
216+
let action = try XCTUnwrap(stackNavigator.actions.last)
217+
XCTAssertEqual(action.type, .dismissed)
218+
}
219+
220+
/// `sendItemCompleted(with:)` dismisses the view and presents the share sheet.
221+
@MainActor
222+
func test_sendItemDelegate_sendItemCompleted() throws {
223+
sendRepository.shareURLResult = .success(.example)
224+
225+
subject.sendItemCompleted(with: .fixture())
226+
227+
waitFor { stackNavigator.actions.count == 2 }
228+
229+
XCTAssertEqual(stackNavigator.actions.count, 2)
230+
231+
let shareAction = try XCTUnwrap(stackNavigator.actions[0])
232+
XCTAssertEqual(shareAction.type, .presented)
233+
XCTAssertTrue(shareAction.view is UIActivityViewController)
234+
235+
let dismissAction = try XCTUnwrap(stackNavigator.actions[1])
236+
XCTAssertEqual(dismissAction.type, .dismissedWithCompletionHandler)
237+
}
238+
239+
/// `sendItemCompleted(with:)` logs an error if generating the share URL fails.
240+
@MainActor
241+
func test_sendItemDelegate_sendItemCompleted_error() throws {
242+
sendRepository.shareURLResult = .failure(BitwardenTestError.example)
243+
244+
subject.sendItemCompleted(with: .fixture())
245+
246+
waitFor { !stackNavigator.actions.isEmpty }
247+
248+
XCTAssertEqual(stackNavigator.actions.count, 1)
249+
250+
let action = try XCTUnwrap(stackNavigator.actions.last)
251+
XCTAssertEqual(action.type, .dismissedWithCompletionHandler)
252+
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
253+
}
254+
255+
/// `sendItemCompleted(with:)` dismisses the view if the share URL is `nil`.
256+
@MainActor
257+
func test_sendItemDelegate_sendItemCompleted_nilURL() throws {
258+
sendRepository.shareURLResult = .success(nil)
259+
260+
subject.sendItemCompleted(with: .fixture())
261+
262+
waitFor { !stackNavigator.actions.isEmpty }
263+
264+
XCTAssertEqual(stackNavigator.actions.count, 1)
265+
266+
let action = try XCTUnwrap(stackNavigator.actions.last)
267+
XCTAssertEqual(action.type, .dismissedWithCompletionHandler)
268+
}
269+
270+
/// `sendItemDeleted()` calls `sendItemDeleted()` on the delegate.
271+
@MainActor
272+
func test_sendItemDelegate_sendItemDeleted() {
273+
subject.sendItemDeleted()
274+
XCTAssertTrue(delegate.didSendItemDeleted)
275+
}
172276
}

BitwardenShared/UI/Tools/Send/SendItem/SendItemRoute.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ public enum SendItemRoute: Equatable, Hashable {
4040
/// A route specifying that the send item flow completed by deleting a send.
4141
case deleted
4242

43+
/// A route that dismisses a presented sheet.
44+
///
45+
/// - Parameter action: An optional `DismissAction` that is executed after the sheet has been
46+
/// dismissed.
47+
///
48+
case dismiss(_ action: DismissAction? = nil)
49+
4350
/// A route to the edit item screen for the provided send.
4451
///
4552
/// - Parameter sendView: The `SendView` to edit.

0 commit comments

Comments
 (0)