Skip to content

Commit e91f36b

Browse files
authored
fix(verification): await linkForPayment before advancing phone flow (#799)
Fix race where linkForPayment was fire-and-forget via eventFlow but the flow exited before the network call completed. Introduce OnPhoneVerificationComplete event that gates navigation until linkForPayment finishes. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 78aabbd commit e91f36b

4 files changed

Lines changed: 78 additions & 23 deletions

File tree

apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/phone/PhoneVerificationViewModel.kt

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ import com.flipcash.services.user.UserManager
1717
import com.flipcash.services.models.PhoneVerificationError
1818
import com.getcode.manager.BottomBarManager
1919
import com.getcode.util.resources.ResourceHelper
20+
import com.getcode.utils.TraceType
21+
import com.getcode.utils.trace
2022
import com.getcode.view.BaseViewModel
2123
import com.getcode.view.LoadingSuccessState
2224
import dagger.hilt.android.lifecycle.HiltViewModel
2325
import kotlinx.coroutines.delay
2426
import kotlinx.coroutines.flow.distinctUntilChanged
25-
import kotlinx.coroutines.flow.filter
2627
import kotlinx.coroutines.flow.filterIsInstance
2728
import kotlinx.coroutines.flow.flatMapLatest
2829
import kotlinx.coroutines.flow.launchIn
@@ -95,6 +96,7 @@ internal class PhoneVerificationViewModel @Inject constructor(
9596
data object OnVerifyCodeClicked : Event
9697
data object OnCodeVerified : Event
9798
data object LinkForPayment : Event
99+
data object OnPhoneVerificationComplete : Event
98100

99101
data object OnMaxAttemptsReached : Event
100102
}
@@ -166,11 +168,15 @@ internal class PhoneVerificationViewModel @Inject constructor(
166168
val cleanedNumber = phoneUtils.cleanNumber(number, locale)
167169
ContactMethod.Phone(cleanedNumber)
168170
}
169-
.onEach { dispatchEvent(Event.OnVerifyingCodeChanged(loading = true)) }
171+
.onEach {
172+
trace(message = "Verifying code", type = TraceType.Process)
173+
dispatchEvent(Event.OnVerifyingCodeChanged(loading = true))
174+
}
170175
.map { method ->
171176
verificationController.checkVerificationCode(method, stateFlow.value.codeTextFieldState.text.toString())
172177
}.onResult(
173178
onSuccess = {
179+
trace(message = "Code verified successfully", type = TraceType.Process)
174180
stopTimer()
175181
dispatchEvent(Event.OnVerifyingCodeChanged(success = true))
176182
viewModelScope.launch {
@@ -184,6 +190,7 @@ internal class PhoneVerificationViewModel @Inject constructor(
184190
}
185191
},
186192
onError = {
193+
trace(message = "Code verification failed: $it", type = TraceType.Error)
187194
dispatchEvent(Event.OnVerifyingCodeChanged())
188195
val (title, message) = when (it) {
189196
is PhoneVerificationError -> when (it) {
@@ -206,18 +213,24 @@ internal class PhoneVerificationViewModel @Inject constructor(
206213

207214
eventFlow
208215
.filterIsInstance<Event.LinkForPayment>()
209-
.filter {
210-
featureFlags.observe(FeatureFlag.PhoneNumberSend).value ||
211-
userManager.state.value.flags?.enablePhoneNumberSend == true
212-
}
213216
.map {
214217
val number = stateFlow.value.numberTextFieldState.text.toString()
215218
val locale = stateFlow.value.selectedLocale
216219
val cleanedNumber = phoneUtils.cleanNumber(number, locale)
220+
trace(message = "Calling linkForPayment", type = TraceType.Process)
217221
ContactMethod.Phone(cleanedNumber)
218222
}
219-
.map { verificationController.linkForPayment(it) }
220-
.launchIn(viewModelScope)
223+
.map { method -> verificationController.linkForPayment(method) }
224+
.onResult(
225+
onSuccess = {
226+
trace(message = "linkForPayment succeeded", type = TraceType.Process)
227+
dispatchEvent(Event.OnPhoneVerificationComplete)
228+
},
229+
onError = {
230+
trace(message = "linkForPayment failed: $it", type = TraceType.Error)
231+
dispatchEvent(Event.OnPhoneVerificationComplete)
232+
}
233+
).launchIn(viewModelScope)
221234
}
222235

223236
private suspend fun handleSendVerificationCode(method: ContactMethod) {
@@ -268,6 +281,17 @@ internal class PhoneVerificationViewModel @Inject constructor(
268281
}
269282
}
270283

284+
suspend fun awaitLinkForPayment() {
285+
val isEnabled = featureFlags.observe(FeatureFlag.PhoneNumberSend).value ||
286+
userManager.state.value.flags?.enablePhoneNumberSend == true
287+
if (!isEnabled) return
288+
289+
val number = stateFlow.value.numberTextFieldState.text.toString()
290+
val locale = stateFlow.value.selectedLocale
291+
val cleanedNumber = phoneUtils.cleanNumber(number, locale)
292+
verificationController.linkForPayment(ContactMethod.Phone(cleanedNumber))
293+
}
294+
271295
private fun startTimer() {
272296
dispatchEvent(
273297
Event.OnTimerTick(
@@ -319,6 +343,7 @@ internal class PhoneVerificationViewModel @Inject constructor(
319343
Event.OnVerifyCodeClicked -> { state -> state }
320344
Event.OnCodeVerified -> { state -> state }
321345
Event.LinkForPayment -> { state -> state }
346+
Event.OnPhoneVerificationComplete -> { state -> state }
322347
is Event.OnPhoneNumberFormatted -> { state -> state.copy(formattedPhone = event.formatted) }
323348
Event.OnSendCodeClicked -> { state -> state.copy(attempts = state.attempts + 1) }
324349
Event.OnMaxAttemptsReached -> { state -> state }

apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneCodeScreen.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,23 @@ fun PhoneCodeContent(
5656
.launchIn(this)
5757
}
5858

59-
LaunchedEffect(viewModel, includeEmail, linkForPayment) {
59+
LaunchedEffect(viewModel, linkForPayment) {
6060
viewModel.eventFlow
6161
.filterIsInstance<PhoneVerificationViewModel.Event.OnCodeVerified>()
6262
.onEach {
6363
if (linkForPayment) {
6464
viewModel.dispatchEvent(PhoneVerificationViewModel.Event.LinkForPayment)
65+
} else {
66+
viewModel.dispatchEvent(PhoneVerificationViewModel.Event.OnPhoneVerificationComplete)
6567
}
68+
}
69+
.launchIn(this)
70+
}
71+
72+
LaunchedEffect(viewModel, includeEmail) {
73+
viewModel.eventFlow
74+
.filterIsInstance<PhoneVerificationViewModel.Event.OnPhoneVerificationComplete>()
75+
.onEach {
6676
if (includeEmail) {
6777
flowNavigator.navigateTo(VerificationStep.EmailEntry)
6878
} else {

apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/MyAccountScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ fun MyAccountScreen() {
7777

7878
LaunchedEffect(viewModel) {
7979
viewModel.eventFlow
80-
.filterIsInstance<MyAccountScreenViewModel.Event.OnVerifyPhoneClicked>()
80+
.filterIsInstance<MyAccountScreenViewModel.Event.ConnectPhoneClicked>()
8181
.onEach {
8282
val flow = AppRoute.Verification(
8383
origin = AppRoute.Menu.MyAccount,
8484
includePhone = true,
8585
includeEmail = false,
86+
linkForPayment = it.linkForPayment
8687
)
8788

8889
navigator.push(flow) }

apps/flipcash/features/myaccount/src/main/kotlin/com/flipcash/app/myaccount/internal/MyAccountScreenViewModel.kt

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.ClipboardManager
44
import androidx.lifecycle.viewModelScope
55
import com.flipcash.app.auth.AuthManager
66
import com.flipcash.app.core.extensions.setText
7+
import com.flipcash.app.featureflags.FeatureFlag
78
import com.flipcash.app.featureflags.FeatureFlagController
89
import com.flipcash.app.menu.MenuItem
910
import com.flipcash.features.myaccount.R
@@ -54,14 +55,16 @@ internal class MyAccountScreenViewModel @Inject constructor(
5455
val accountId: String? = null,
5556
val publicKey: String? = null,
5657
val pushToken: String? = null,
58+
val linkForPayment: Boolean = false,
5759
val items: List<MenuItem<Event>> = FullMenuList
5860
)
5961

6062
internal sealed interface Event {
6163
data class OnUserAssociated(
6264
val userId: String?,
6365
val publicKey: String?,
64-
val pushToken: String? = null
66+
val pushToken: String? = null,
67+
val linkForPayment: Boolean = false,
6568
) : Event
6669

6770
data class OnBetaFeaturesUnlocked(val unlocked: Boolean) : Event
@@ -71,6 +74,7 @@ internal class MyAccountScreenViewModel @Inject constructor(
7174
data object OnViewAccessKey : Event
7275
data object OnVerifyEmailClicked : Event
7376
data object OnVerifyPhoneClicked : Event
77+
data class ConnectPhoneClicked(val linkForPayment: Boolean) : Event
7478
data object OnDeleteAccountClicked : Event
7579
data object OnAccountDeleted : Event
7680
data object CopyPublicKey : Event
@@ -81,19 +85,26 @@ internal class MyAccountScreenViewModel @Inject constructor(
8185
}
8286

8387
init {
84-
userManager.state
85-
.onEach {
86-
val userId = it.accountId?.base64
87-
val publicKey = it.cluster?.authorityPublicKey?.base58()
88+
combine(
89+
userManager.state,
90+
featureFlagController.observe(FeatureFlag.PhoneNumberSend),
91+
) { state, sendEnabled ->
92+
val userId = state.accountId?.base64
93+
val publicKey = state.cluster?.authorityPublicKey?.base58()
8894

89-
dispatchEvent(
90-
Event.OnUserAssociated(
91-
userId = userId,
92-
publicKey = publicKey,
93-
pushToken = it.pushToken
94-
)
95+
val linkForPayment = sendEnabled ||
96+
state.flags?.enablePhoneNumberSend == true
97+
98+
dispatchEvent(
99+
Event.OnUserAssociated(
100+
userId = userId,
101+
publicKey = publicKey,
102+
pushToken = state.pushToken,
103+
linkForPayment = linkForPayment,
95104
)
96-
}.launchIn(viewModelScope)
105+
)
106+
107+
}.launchIn(viewModelScope)
97108

98109
combine(
99110
featureFlagController.observeOverride(),
@@ -183,6 +194,12 @@ internal class MyAccountScreenViewModel @Inject constructor(
183194
)
184195
}.launchIn(viewModelScope)
185196

197+
eventFlow
198+
.filterIsInstance<Event.OnVerifyPhoneClicked>()
199+
.onEach {
200+
dispatchEvent(Event.ConnectPhoneClicked(stateFlow.value.linkForPayment))
201+
}.launchIn(viewModelScope)
202+
186203
eventFlow
187204
.filterIsInstance<Event.OnLogOutClicked>()
188205
.onEach {
@@ -228,13 +245,15 @@ internal class MyAccountScreenViewModel @Inject constructor(
228245
state.copy(
229246
accountId = event.userId,
230247
publicKey = event.publicKey,
231-
pushToken = event.pushToken
248+
pushToken = event.pushToken,
249+
linkForPayment = event.linkForPayment,
232250
)
233251
}
234252

235253
Event.OnLogOutClicked,
236254
Event.OnLoggedOutCompletely,
237255
Event.OnVerifyPhoneClicked,
256+
is Event.ConnectPhoneClicked,
238257
Event.OnVerifyEmailClicked,
239258
Event.OnViewAccessKey,
240259
Event.CopyPublicKey,

0 commit comments

Comments
 (0)