Skip to content

Commit bf3c15a

Browse files
authored
Merge pull request #4931 from woocommerce/issue/4333-integrate-srf-modal
[Mobile Payments] [Several Readers Found] Integrate choose-a-reader list modal
2 parents dadb564 + f13d68d commit bf3c15a

14 files changed

+414
-63
lines changed

WooCommerce/Classes/Extensions/Array+Helpers.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ extension Array {
1818
var isNotEmpty: Bool {
1919
return !isEmpty
2020
}
21+
22+
/// A Bool indicating if the collection has at least two elements
23+
var containsMoreThanOne: Bool {
24+
return count > 1
25+
}
2126
}
2227

2328

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift

Lines changed: 126 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ final class CardReaderConnectionController {
2323
///
2424
case searching
2525

26-
/// Found a card reader
26+
/// Found one card reader
2727
///
2828
case foundReader
2929

30+
/// Found two or more card readers
31+
///
32+
case foundSeveralReaders
33+
3034
/// Attempting to connect to a card reader. The completion passed to `searchAndConnect`
3135
/// will be called with a `success` `Bool` `True` result if successful, after which the view controller
3236
/// passed to `searchAndConnect` will be dereferenced and the state set to `idle`
@@ -77,6 +81,13 @@ final class CardReaderConnectionController {
7781
///
7882
private var candidateReader: CardReader?
7983

84+
/// Since the number of readers can go greater than 1 and then back to 1, and we don't
85+
/// want to keep changing the UI from the several-readers-found list to a single prompt
86+
/// and back (as this would be visually quite annoying), this flag will tell us that we've
87+
/// already switched to list format for this discovery flow, so that stay in list mode
88+
/// even if the number of found readers drops to less than 2
89+
private var showSeveralFoundReaders: Bool = false
90+
8091
private var subscriptions = Set<AnyCancellable>()
8192

8293
private var onCompletion: ((Result<Bool, Error>) -> Void)?
@@ -123,6 +134,8 @@ private extension CardReaderConnectionController {
123134
onSearching()
124135
case .foundReader:
125136
onFoundReader()
137+
case .foundSeveralReaders:
138+
onFoundSeveralReaders()
126139
case .cancel:
127140
onCancel()
128141
case .connectToReader:
@@ -134,17 +147,49 @@ private extension CardReaderConnectionController {
134147
}
135148
}
136149

137-
/// Updates the found readers list by removing any the user has asked
138-
/// us to ignore (aka keep searching) during this discovery session
150+
/// To avoid presenting the "Do you want to connect to reader XXXX" prompt
151+
/// repeatedly for the same reader, keep track of readers the user has tapped
152+
/// "Keep Searching" for.
153+
///
154+
/// If we have switched to the list view, however, don't prune
139155
///
140156
func pruneSkippedReaders() {
141-
self.foundReaders = self.foundReaders.filter({!skippedReaderIDs.contains($0.id)})
157+
guard !showSeveralFoundReaders else {
158+
return
159+
}
160+
foundReaders = foundReaders.filter({!skippedReaderIDs.contains($0.id)})
142161
}
143162

144163
/// Returns the list of found readers which are also known
145164
///
146165
func getFoundKnownReaders() -> [CardReader] {
147-
self.foundReaders.filter({knownReaderIDs.contains($0.id)})
166+
foundReaders.filter({knownReaderIDs.contains($0.id)})
167+
}
168+
169+
/// A helper to return an array of found reader IDs
170+
///
171+
func getFoundReaderIDs() -> [String] {
172+
foundReaders.compactMap({$0.id})
173+
}
174+
175+
/// A helper to return a specific CardReader instance based on the reader ID
176+
///
177+
func getFoundReaderByID(readerID: String) -> CardReader? {
178+
foundReaders.first(where: {$0.id == readerID})
179+
}
180+
181+
/// Updates the show multiple readers flag to indicate that, for this discovery flow,
182+
/// we have already shown the multiple readers UI (so we don't switch back to the
183+
/// single reader found UI for this particular discovery)
184+
///
185+
func updateShowSeveralFoundReaders() {
186+
guard ServiceLocator.featureFlagService.isFeatureFlagEnabled(.cardPresentSeveralReadersFound) else {
187+
return
188+
}
189+
190+
if foundReaders.containsMoreThanOne {
191+
showSeveralFoundReaders = true
192+
}
148193
}
149194

150195
/// Initial state of the controller
@@ -161,6 +206,7 @@ private extension CardReaderConnectionController {
161206
///
162207
skippedReaderIDs = []
163208
candidateReader = nil
209+
showSeveralFoundReaders = false
164210

165211
/// Fetch the list of known readers - i.e. readers we should automatically connect to when we see them
166212
///
@@ -181,8 +227,10 @@ private extension CardReaderConnectionController {
181227
/// Begins the search for a card reader
182228
/// Does NOT open any modal
183229
/// Transitions state to `.searching`
184-
/// Later, when a reader is found, state transitions to `.foundReader` if it is unknown,
185-
/// or to `.connectToReader` if it is known
230+
/// Later, when a reader is found, state transitions to
231+
/// `.foundReader` if one unknown reader is found,
232+
/// `.foundMultipleReaders` if two or more readers are found,
233+
/// or to `.connectToReader` if one known reader is found
186234
///
187235
func onBeginSearch() {
188236
self.state = .searching
@@ -193,16 +241,25 @@ private extension CardReaderConnectionController {
193241
return
194242
}
195243

196-
/// First, update our copy of the foundReaders and prune
197-
/// skipped ones
244+
/// Update our copy of the foundReaders, evaluate if we should switch to the list view,
245+
/// and prune skipped ones
198246
///
199247
self.foundReaders = cardReaders
248+
self.updateShowSeveralFoundReaders()
200249
self.pruneSkippedReaders()
201250

202-
/// This completion can be called repeatedly as more readers
203-
/// become discovered. To avoid interrupting connecting to
204-
/// a known reader, or interrupting the user prompt for a unknown
205-
/// reader, ensure we are in the searching state first
251+
/// Note: This completion will be called repeatedly as the list of readers
252+
/// discovered changes, so some care around state must be taken here.
253+
///
254+
255+
/// If the found-several-readers view is already presenting, update its list of found readers
256+
///
257+
if case .foundSeveralReaders = self.state {
258+
self.alerts.updateSeveralReadersList(readerIDs: self.getFoundReaderIDs())
259+
}
260+
261+
/// To avoid interrupting connecting to a known reader, ensure we are
262+
/// in the searching state before proceeding further
206263
///
207264
guard case .searching = self.state else {
208265
return
@@ -216,11 +273,19 @@ private extension CardReaderConnectionController {
216273
return
217274
}
218275

276+
/// If we have found multiple readers, advance to foundMultipleReaders
277+
///
278+
if self.showSeveralFoundReaders {
279+
self.state = .foundSeveralReaders
280+
return
281+
}
282+
219283
/// If we have a found (but unknown) reader, advance to foundReader
220284
///
221285
if self.foundReaders.isNotEmpty {
222286
self.candidateReader = self.foundReaders.first
223287
self.state = .foundReader
288+
return
224289
}
225290
},
226291
onError: { [weak self] error in
@@ -243,15 +308,36 @@ private extension CardReaderConnectionController {
243308
return
244309
}
245310

246-
/// In the case of multiple found readers, we may have another reader to show
247-
/// to the user at this point, so don't open the searching modal, but go to
248-
/// onFoundReader
311+
/// If we enter this state and another reader was discovered while the
312+
/// "Do you want to connect to" modal was being displayed and if that reader
313+
/// is known and the merchant tapped keep searching on the first
314+
/// (unknown) reader, auto-connect to that known reader
315+
if self.getFoundKnownReaders().isNotEmpty {
316+
self.candidateReader = self.getFoundKnownReaders().first
317+
self.state = .connectToReader
318+
return
319+
}
320+
321+
/// If we already have found readers
322+
/// display the list view if so enabled, or...
323+
///
324+
if showSeveralFoundReaders {
325+
self.state = .foundSeveralReaders
326+
return
327+
}
328+
329+
/// Display the single view and ask the merchant if they'd
330+
/// like to connect to it
331+
///
249332
if foundReaders.isNotEmpty {
250333
self.candidateReader = foundReaders.first
251334
self.state = .foundReader
252335
return
253336
}
254337

338+
/// If all else fails, display the "scanning" modal and
339+
/// stay in this state
340+
///
255341
alerts.scanningForReader(from: from, cancel: {
256342
self.state = .cancel
257343
})
@@ -283,6 +369,30 @@ private extension CardReaderConnectionController {
283369
})
284370
}
285371

372+
/// Several readers have been found
373+
/// Opens a continually updating list modal for the user to pick one (or cancel the search)
374+
///
375+
func onFoundSeveralReaders() {
376+
guard let from = fromController else {
377+
return
378+
}
379+
380+
alerts.foundSeveralReaders(
381+
from: from,
382+
readerIDs: getFoundReaderIDs(),
383+
connect: { [weak self] readerID in
384+
guard let self = self else {
385+
return
386+
}
387+
self.candidateReader = self.getFoundReaderByID(readerID: readerID)
388+
self.state = .connectToReader
389+
},
390+
cancelSearch: { [weak self] in
391+
self?.state = .cancel
392+
}
393+
)
394+
}
395+
286396
/// End the search for a card reader
287397
///
288398
func onCancel() {
@@ -296,10 +406,6 @@ private extension CardReaderConnectionController {
296406
/// Connect to the candidate card reader
297407
///
298408
func onConnectToReader() {
299-
/// We always work with the first reader in the foundReaders array
300-
/// The array will have already had skipped (aka "keep searching") readers removed
301-
/// by time we get here
302-
///
303409
guard let candidateReader = candidateReader else {
304410
return
305411
}

WooCommerce/Classes/ViewRelated/CardPresentPayments/SeveralReadersFoundViewController.swift

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,13 @@ final class SeveralReadersFoundViewController: UIViewController, UITableViewDele
1616
private var sections = [Section]()
1717

1818
private var readerIDs = [String]()
19+
private var onConnect: ((String) -> Void)?
20+
private var onCancel: (() -> Void)?
1921

2022
init() {
2123
super.init(nibName: Self.nibName, bundle: nil)
2224

2325
modalPresentationStyle = .overFullScreen
24-
25-
// TODO: Dummy data for now
26-
self.readerIDs = [
27-
"CHB204909005931",
28-
"CHB204909005942",
29-
"CHB204909005953",
30-
"CHB204909005964",
31-
]
3226
}
3327

3428
required init?(coder: NSCoder) {
@@ -62,9 +56,17 @@ final class SeveralReadersFoundViewController: UIViewController, UITableViewDele
6256
}
6357
}
6458

65-
// TODO - accept updates to the list of CardReaderIDs to present and reloadData
59+
func configureController(readerIDs: [String], connect: @escaping ((String) -> Void), cancelSearch: @escaping (() -> Void)) {
60+
self.readerIDs = readerIDs
61+
onConnect = connect
62+
onCancel = cancelSearch
63+
}
6664

67-
// TODO - call completion with selected CardReaderID? (nil on cancel)
65+
func updateReaderIDs(readerIDs: [String]) {
66+
self.readerIDs = readerIDs
67+
configureSections()
68+
tableView?.reloadData()
69+
}
6870
}
6971

7072
// MARK: - View Configuration
@@ -77,6 +79,9 @@ private extension SeveralReadersFoundViewController {
7779
func configureNavigation() {
7880
headlineLabel.text = Localization.headline
7981
cancelButton.setTitle(Localization.cancel, for: .normal)
82+
cancelButton.on(.touchUpInside) { [weak self] _ in
83+
self?.didTapCancel()
84+
}
8085
}
8186

8287
/// Setup the sections in this table view
@@ -156,16 +161,21 @@ private extension SeveralReadersFoundViewController {
156161
guard let cell = cell as? LabelAndButtonTableViewCell else {
157162
return
158163
}
159-
cell.label.text = readerID
160-
cell.button.setTitle(Localization.connect, for: .normal)
164+
cell.configure(
165+
labelText: readerID,
166+
buttonTitle: Localization.connect,
167+
didTapButton: {
168+
self.didTapConnect(readerID: readerID)
169+
}
170+
)
161171
cell.selectionStyle = .none
162172
}
163173

164174
func configureScanningRow(cell: UITableViewCell) {
165175
guard let cell = cell as? ActivitySpinnerAndLabelTableViewCell else {
166176
return
167177
}
168-
cell.label.text = Localization.scanningLabel
178+
cell.configure(labelText: Localization.scanningLabel)
169179
cell.selectionStyle = .none
170180
}
171181

@@ -184,6 +194,22 @@ private extension SeveralReadersFoundViewController {
184194
}
185195
}
186196

197+
// MARK: - Actions
198+
//
199+
private extension SeveralReadersFoundViewController {
200+
@objc func didTapConnect(readerID: String) {
201+
self.dismiss(animated: true, completion: {
202+
self.onConnect?(readerID)
203+
})
204+
}
205+
206+
@objc func didTapCancel() {
207+
self.dismiss(animated: true, completion: {
208+
self.onCancel?()
209+
})
210+
}
211+
}
212+
187213
// MARK: - Convenience Methods
188214
//
189215
private extension SeveralReadersFoundViewController {

0 commit comments

Comments
 (0)