Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/flipcash/features/login/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies {
implementation(project(":apps:flipcash:shared:accesskey"))
implementation(project(":apps:flipcash:shared:analytics"))
implementation(project(":apps:flipcash:shared:authentication"))
implementation(project(":apps:flipcash:shared:contacts"))
implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:permissions"))
implementation(project(":apps:flipcash:shared:userflags"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import com.flipcash.app.login.internal.screens.LoginRouterScreenContent
import com.flipcash.app.login.internal.screens.SeedInputContent
import com.flipcash.app.login.router.LoginViewModel
import com.flipcash.app.login.internal.SeedInputViewModel
import androidx.compose.runtime.rememberCoroutineScope
import com.flipcash.app.contacts.LocalContactCoordinator
import com.flipcash.app.permissions.asContactAccessHandle
import com.flipcash.app.permissions.internal.contacts.ContactScreenContent
import com.flipcash.app.permissions.internal.notifications.NotificationRationalePermissionContent
Expand All @@ -64,6 +66,7 @@ import com.getcode.util.permissions.PermissionResult
import com.getcode.util.permissions.rememberContactPermission
import com.getcode.util.permissions.rememberNotificationPermission
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
Expand Down Expand Up @@ -446,11 +449,14 @@ private fun PurchaseStepContent() {
private fun ContactPermissionStepContent() {
val flowNavigator = rememberFlowNavigator<OnboardingStep, OnboardingResult>()
val analytics = LocalAnalytics.current
val contactCoordinator = LocalContactCoordinator.current
val scope = rememberCoroutineScope()

val permissionState = rememberContactPermission { result ->
when (result) {
PermissionResult.Granted -> {
analytics.action(Button.AllowContacts)
scope.launch { contactCoordinator.sync() }
flowNavigator.proceed()
}
PermissionResult.Denied,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,14 @@ class AuthManager @Inject constructor(
}

val seenAccessKey = credentialManager.hasSeenAccessKey()
val completedOnboarding = credentialManager.hasCompletedOnboarding()
// Interactive logins (seed input, deep link, credential picker)
// always route through the onboarding permissions flow, which
// sets Ready on completion. Don't trust the completedOnboarding
// default (true for backward compat) here — it would skip the
// permissions phase and set Ready too early.
// Soft logins (app restart) can trust the persisted flag.
val completedOnboarding = if (!isSoftLogin) false
else credentialManager.hasCompletedOnboarding()
if (flags != null) {
userManager.set(flags)
if (flags.isRegistered && seenAccessKey && completedOnboarding) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ class AuthManagerTest {
Result.success(flags)
)

val result = authManager.login(entropyB64 = entropy)
val result = authManager.login(entropyB64 = entropy, isSoftLogin = true)

assertTrue(result.isSuccess)
verify { userManager.set(flags) }
Expand Down Expand Up @@ -281,7 +281,7 @@ class AuthManagerTest {
coEvery { accountController.getUserFlags() } returns Result.failure(RuntimeException("persistent failure"))
coEvery { credentialManager.hasCompletedOnboarding() } returns true

val result = authManager.login(entropyB64 = entropy)
val result = authManager.login(entropyB64 = entropy, isSoftLogin = true)

assertTrue(result.isSuccess)
verify { userManager.set(authState = AuthState.Ready) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,29 +211,36 @@ class ContactCoordinator @Inject constructor(
* | After logout → re-login | `reset()` clears flag, fires on first foreground if conditions met |
* | Phone number changed (unlink + re-verify) | Foreground path won't re-fire (flag is `true`), but the verification flow calls `linkForPayment` directly |
*/
private val linkMutex = kotlinx.coroutines.sync.Mutex()

fun linkForPaymentIfNeeded() {
scope.launch {
val featureFlag = featureFlagController.get(FeatureFlag.PhoneNumberSend)
val serverFlag = userManager.state.value.flags?.enablePhoneNumberSend == true
val enabled = featureFlag || serverFlag
if (!enabled) return@launch

val alreadyLinked = contactPrefs.data
.map { it[KEY_LINKED_FOR_PAYMENT] ?: false }
.first()
if (alreadyLinked) return@launch

// Profile may not be loaded yet on the first Ready transition;
// wait for the verified phone number to arrive.
val phone = userManager.state
.map { it.userProfile?.verifiedPhoneNumber }
.filterNotNull()
.first()

contactVerificationController.linkForPayment(ContactMethod.Phone(phone))
.onSuccess {
contactPrefs.edit { it[KEY_LINKED_FOR_PAYMENT] = true }
}
if (!linkMutex.tryLock()) return@launch
try {
val featureFlag = featureFlagController.get(FeatureFlag.PhoneNumberSend)
val serverFlag = userManager.state.value.flags?.enablePhoneNumberSend == true
val enabled = featureFlag || serverFlag
if (!enabled) return@launch

val alreadyLinked = contactPrefs.data
.map { it[KEY_LINKED_FOR_PAYMENT] ?: false }
.first()
if (alreadyLinked) return@launch

// Profile may not be loaded yet on the first Ready transition;
// wait for the verified phone number to arrive.
val phone = userManager.state
.map { it.userProfile?.verifiedPhoneNumber }
.filterNotNull()
.first()

contactVerificationController.linkForPayment(ContactMethod.Phone(phone))
.onSuccess {
contactPrefs.edit { it[KEY_LINKED_FOR_PAYMENT] = true }
}
} finally {
linkMutex.unlock()
}
}
}

Expand Down Expand Up @@ -343,7 +350,7 @@ class ContactCoordinator @Inject constructor(

if (deviceContacts.isEmpty()) {
trace(tag = TAG, message = "No device contacts found", type = TraceType.Process)
_state.update { it.copy(syncState = SyncState.Synced) }
_state.update { it.copy(syncState = SyncState.Synced, hasEverSynced = true) }
return Result.success(Unit)
}

Expand Down
Loading