@@ -25,6 +25,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.PAIRING_CANCELLED
2525import com.duckduckgo.sync.impl.AccountErrorCodes.PAIRING_FAILED
2626import com.duckduckgo.sync.impl.AccountErrorCodes.PAIRING_REJECTED
2727import com.duckduckgo.sync.impl.AccountErrorCodes.PAIRING_UNAVAILABLE
28+ import com.duckduckgo.sync.impl.AccountErrorCodes.UNEXPECTED_EVENT
2829import com.duckduckgo.sync.impl.exchange.v2.ExchangeV2CodeParseResult
2930import com.duckduckgo.sync.impl.exchange.v2.ExchangeV2Event
3031import com.duckduckgo.sync.impl.exchange.v2.ExchangeV2Message
@@ -120,40 +121,9 @@ class RealSyncCodeDispatcher @Inject constructor(
120121 }
121122 }
122123
123- /* *
124- * Translate one runner event into a [DispatchOutcome] for the v2 Presenter flow,
125- * or null for intermediate events the caller should ignore. Both Host and Joiner roles are
126- * reachable here; mirrors [mapV2LinkingEventToOutcome] (login on Joiner.Done, preserve abort reason).
127- */
128124 private fun mapV2PresentEventToOutcome (event : ExchangeV2Event , peerKind : PeerKind ? ): DispatchOutcome ? = when (event) {
129125 is ExchangeV2Event .SessionStarted -> event.linkingCode?.let { DispatchOutcome .LinkingCodeReady (it) }
130- is ExchangeV2Event .Transition -> when (event.to) {
131- ExchangeV2State .Joiner .Confirming ->
132- DispatchOutcome .JoinerConfirmationRequested (peerName = runner.peerName, peerKind = peerKind)
133- ExchangeV2State .Host .Confirming ->
134- DispatchOutcome .HostConfirmationRequested (peerName = runner.peerName, peerKind = peerKind)
135- ExchangeV2State .Host .Done -> DispatchOutcome .LoggedIn (SetupPath .PAIRING , SetupRole .HOST , peerKind)
136- ExchangeV2State .Host .Aborted -> hostAbortedToOutcome(event.localTrigger)
137- ExchangeV2State .SameAccountAbort -> DispatchOutcome .AlreadyConnected
138- ExchangeV2State .Joiner .Done -> {
139- val received = (event.trigger as ? ExchangeV2Message .RecoveryCodeResponse )?.recoveryCode
140- if (received.isNullOrBlank()) {
141- DispatchOutcome .Failed (" Pairing completed without a recovery code" , NO_RECOVERY_CODE .code)
142- } else {
143- loginWithV2RecoveryCode(received, peerKind)
144- }
145- }
146- ExchangeV2State .Joiner .AbortedByHost -> when (event.trigger) {
147- is ExchangeV2Message .RecoveryCodeDenied ->
148- DispatchOutcome .Failed (" Pairing declined by peer" , PAIRING_REJECTED .code)
149- is ExchangeV2Message .RecoveryCodeUnavailable ->
150- DispatchOutcome .Failed (" Peer has no recovery code" , PAIRING_UNAVAILABLE .code)
151- else -> DispatchOutcome .Failed (" Pairing aborted by peer" , PAIRING_REJECTED .code)
152- }
153- ExchangeV2State .Joiner .AbortedLocal -> DispatchOutcome .Failed (" Pairing cancelled on this device" , PAIRING_CANCELLED .code)
154- ExchangeV2State .Aborted -> DispatchOutcome .Failed (" negotiation_aborted" , NEGOTIATION_ABORTED .code)
155- else -> null
156- }
126+ is ExchangeV2Event .Transition -> mapV2Transition(event, peerKind)
157127 is ExchangeV2Event .SessionError -> sessionErrorToOutcome(event.message)
158128 else -> null
159129 }
@@ -317,41 +287,41 @@ class RealSyncCodeDispatcher @Inject constructor(
317287 -> true
318288 }
319289
320- /* *
321- * Translate one runner event into a terminal [DispatchOutcome] for the v2 scanner flow,
322- * or null for intermediate events the caller should ignore.
323- */
324290 private fun mapV2LinkingEventToOutcome (event : ExchangeV2Event , peerKind : PeerKind ? ): DispatchOutcome ? = when (event) {
325- is ExchangeV2Event .Transition -> when (event.to) {
326- ExchangeV2State .Joiner .Confirming ->
327- DispatchOutcome .JoinerConfirmationRequested (peerName = runner.peerName, peerKind = peerKind)
328- ExchangeV2State .Host .Confirming ->
329- DispatchOutcome .HostConfirmationRequested (peerName = runner.peerName, peerKind = peerKind)
330- ExchangeV2State .Joiner .Done -> {
331- val received = (event.trigger as ? ExchangeV2Message .RecoveryCodeResponse )?.recoveryCode
332- if (received.isNullOrBlank()) {
333- DispatchOutcome .Failed (" Pairing completed without a recovery code" , NO_RECOVERY_CODE .code)
334- } else {
335- loginWithV2RecoveryCode(received, peerKind)
336- }
337- }
338- ExchangeV2State .Joiner .AbortedByHost -> when (event.trigger) {
339- is ExchangeV2Message .RecoveryCodeDenied ->
340- DispatchOutcome .Failed (" Pairing declined by peer" , PAIRING_REJECTED .code)
341- is ExchangeV2Message .RecoveryCodeUnavailable ->
342- DispatchOutcome .Failed (" Peer has no recovery code" , PAIRING_UNAVAILABLE .code)
343- else -> DispatchOutcome .Failed (" Pairing aborted by peer" , PAIRING_REJECTED .code)
291+ is ExchangeV2Event .Transition -> mapV2Transition(event, peerKind)
292+ is ExchangeV2Event .SessionError -> sessionErrorToOutcome(event.message)
293+ else -> null
294+ }
295+
296+ private fun mapV2Transition (
297+ transition : ExchangeV2Event .Transition ,
298+ peerKind : PeerKind ? ,
299+ ): DispatchOutcome ? = when (transition.to) {
300+ ExchangeV2State .Joiner .Confirming ->
301+ DispatchOutcome .JoinerConfirmationRequested (peerName = runner.peerName, peerKind = peerKind)
302+ ExchangeV2State .Host .Confirming ->
303+ DispatchOutcome .HostConfirmationRequested (peerName = runner.peerName, peerKind = peerKind)
304+ ExchangeV2State .Host .Done -> DispatchOutcome .LoggedIn (SetupPath .PAIRING , SetupRole .HOST , peerKind)
305+ ExchangeV2State .Host .Aborted -> hostAbortedToOutcome(transition.localTrigger, transition.trigger)
306+ // Per spec §"Same-account case": not an abort; both devices share an account already.
307+ ExchangeV2State .SameAccountAbort -> DispatchOutcome .AlreadyConnected
308+ ExchangeV2State .Joiner .Done -> {
309+ val received = (transition.trigger as ? ExchangeV2Message .RecoveryCodeResponse )?.recoveryCode
310+ if (received.isNullOrBlank()) {
311+ DispatchOutcome .Failed (" joiner_done_missing_recovery_code" , NO_RECOVERY_CODE .code)
312+ } else {
313+ loginWithV2RecoveryCode(received, peerKind)
344314 }
345- ExchangeV2State .Joiner .AbortedLocal -> DispatchOutcome .Failed (" Pairing cancelled on this device" , PAIRING_CANCELLED .code)
346- ExchangeV2State .Host .Aborted -> hostAbortedToOutcome(event.localTrigger)
347- // Per spec §"Same-account case": not an abort; both devices share an account already.
348- ExchangeV2State .SameAccountAbort -> DispatchOutcome .AlreadyConnected
349- // Elected Host and shared a recovery code — success from this device's perspective.
350- ExchangeV2State .Host .Done -> DispatchOutcome .LoggedIn (SetupPath .PAIRING , SetupRole .HOST , peerKind)
351- ExchangeV2State .Aborted -> DispatchOutcome .Failed (" negotiation_aborted" , NEGOTIATION_ABORTED .code)
352- else -> null
353315 }
354- is ExchangeV2Event .SessionError -> sessionErrorToOutcome(event.message)
316+ ExchangeV2State .Joiner .AbortedByHost -> when (transition.trigger) {
317+ is ExchangeV2Message .RecoveryCodeDenied ->
318+ DispatchOutcome .Failed (" peer_denied_recovery_code" , PAIRING_REJECTED .code)
319+ is ExchangeV2Message .RecoveryCodeUnavailable ->
320+ DispatchOutcome .Failed (" peer_recovery_code_unavailable" , PAIRING_UNAVAILABLE .code)
321+ else -> DispatchOutcome .Failed (" peer_aborted" , PAIRING_REJECTED .code)
322+ }
323+ ExchangeV2State .Joiner .AbortedLocal -> joinerAbortedLocalToOutcome(transition.localTrigger, transition.trigger)
324+ ExchangeV2State .Aborted -> DispatchOutcome .Failed (" negotiation_aborted" , NEGOTIATION_ABORTED .code)
355325 else -> null
356326 }
357327
@@ -364,12 +334,25 @@ class RealSyncCodeDispatcher @Inject constructor(
364334 }
365335 }
366336
367- private fun hostAbortedToOutcome (localTrigger : LocalTrigger ? ): DispatchOutcome = when (localTrigger) {
368- LocalTrigger .UserDeniedHost -> DispatchOutcome .Failed (" user_denied" , PAIRING_CANCELLED .code)
369- LocalTrigger .HostUnavailable -> DispatchOutcome .Failed (" host_unavailable" , PAIRING_UNAVAILABLE .code)
337+ private fun hostAbortedToOutcome (
338+ localTrigger : LocalTrigger ? ,
339+ wireTrigger : ExchangeV2Message ? ,
340+ ): DispatchOutcome = when {
341+ localTrigger == LocalTrigger .UserDeniedHost -> DispatchOutcome .Failed (" user_denied" , PAIRING_CANCELLED .code)
342+ localTrigger == LocalTrigger .HostUnavailable -> DispatchOutcome .Failed (" host_unavailable" , PAIRING_UNAVAILABLE .code)
343+ wireTrigger != null -> DispatchOutcome .Failed (" host_protocol_error" , UNEXPECTED_EVENT .code)
370344 else -> DispatchOutcome .Failed (" host_aborted" , NEGOTIATION_ABORTED .code)
371345 }
372346
347+ private fun joinerAbortedLocalToOutcome (
348+ localTrigger : LocalTrigger ? ,
349+ wireTrigger : ExchangeV2Message ? ,
350+ ): DispatchOutcome = when {
351+ localTrigger == LocalTrigger .UserDeniedJoiner -> DispatchOutcome .Failed (" user_denied_joiner" , PAIRING_CANCELLED .code)
352+ wireTrigger != null -> DispatchOutcome .Failed (" joiner_protocol_error" , UNEXPECTED_EVENT .code)
353+ else -> DispatchOutcome .Failed (" joiner_local_aborted" , PAIRING_FAILED .code)
354+ }
355+
373356 /* *
374357 * Apply a v2 recovery code received over the LinkingV2 channel (raw base64
375358 * `{recovery:{user_id, secret, cid, v}}`). Per spec §"Exchange Share Recovery Code → Joiner":
@@ -433,17 +416,6 @@ class RealSyncCodeDispatcher @Inject constructor(
433416 Base64 .encodeToString(bytes, Base64 .NO_WRAP )
434417 }.getOrNull()
435418
436- /* *
437- * Map a repo [Result] to a [DispatchOutcome].
438- *
439- * If [codeForAccountSwitch] is supplied and the repo failed with [ALREADY_SIGNED_IN], switch
440- * accounts transparently via [SyncAccountRepository.logoutAndJoinNewAccount] with no prompt:
441- * the Confirmations phase is already the consent step, so the v1 `AskToSwitchAccount` dialog
442- * does not apply.
443- *
444- * [codeForAccountSwitch] must be a v1 b64 shape [SyncAccountRepository.parseSyncAuthCode] can
445- * re-parse, so v2 callers convert first (see [encodeV1RecoveryCodeAsB64]).
446- */
447419 /* * Map the runner's raw peer credential id ("ddg"/"3party") to the telemetry [PeerKind], or null. */
448420 private fun String?.toPeerKind (): PeerKind ? = when (this ) {
449421 CID_DDG -> PeerKind .DDG
0 commit comments