@@ -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 }
0 commit comments