Skip to content

Commit ddec5b8

Browse files
committed
feat(contacts): detect first Flipcash contacts discovery (0→N signal)
Add hasDiscoveredFlipcashContacts flag persisted on ContactSyncStateEntity so the UI layer can surface a one-time UX moment when a user first discovers contacts on Flipcash. Fix clearAll() to delete all mappings on logout (prevents stale hasEverSynced on re-login) and sequence the permission- revocation check before sync on foreground resume to eliminate a race. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 9801d29 commit ddec5b8

7 files changed

Lines changed: 523 additions & 13 deletions

File tree

apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class ContactCoordinator @Inject constructor(
9595
val flipcashE164s: Set<String> = emptySet(),
9696
val syncState: SyncState = SyncState.Idle,
9797
val hasEverSynced: Boolean = false,
98+
val hasDiscoveredFlipcashContacts: Boolean = false,
9899
)
99100

100101
enum class SyncState { Idle, Syncing, Synced, Error }
@@ -136,9 +137,12 @@ class ContactCoordinator @Inject constructor(
136137

137138
override fun onStart(owner: LifecycleOwner) {
138139
if (cluster.value != null) {
139-
scope.launch { clearServerContactSetIfRevoked() }
140-
trace(tag = TAG, message = "Lifecycle resumed, triggering contact sync", type = TraceType.Process)
141-
launchSync()
140+
syncJob?.cancel()
141+
syncJob = scope.launch {
142+
clearServerContactSetIfRevoked()
143+
trace(tag = TAG, message = "Lifecycle resumed, triggering contact sync", type = TraceType.Process)
144+
performSync()
145+
}
142146
}
143147
}
144148

@@ -269,9 +273,10 @@ class ContactCoordinator @Inject constructor(
269273
val mappings = contactDataSource.get()
270274

271275
val hasEverSynced = syncState != null || mappings.isNotEmpty()
276+
val hasDiscoveredFlipcashContacts = syncState?.hasDiscoveredFlipcashContacts ?: false
272277
if (mappings.isEmpty()) {
273278
if (hasEverSynced) {
274-
_state.update { it.copy(hasEverSynced = true) }
279+
_state.update { it.copy(hasEverSynced = true, hasDiscoveredFlipcashContacts = hasDiscoveredFlipcashContacts) }
275280
}
276281
return
277282
}
@@ -288,7 +293,12 @@ class ContactCoordinator @Inject constructor(
288293
val flipcashE164s = mappings.filter { it.isOnFlipcash }.map { it.e164 }.toSet()
289294

290295
_state.update {
291-
it.copy(contacts = contacts, flipcashE164s = flipcashE164s, hasEverSynced = true)
296+
it.copy(
297+
contacts = contacts,
298+
flipcashE164s = flipcashE164s,
299+
hasEverSynced = true,
300+
hasDiscoveredFlipcashContacts = hasDiscoveredFlipcashContacts,
301+
)
292302
}
293303

294304
trace(tag = TAG, message = "Hydrated ${mappings.size} contacts from persistence", type = TraceType.Process)
@@ -440,6 +450,10 @@ class ContactCoordinator @Inject constructor(
440450
contactDataSource.clearFlipcashStatus()
441451
if (flipcashE164s.isNotEmpty()) {
442452
contactDataSource.markAsFlipcash(flipcashE164s.toList())
453+
if (!_state.value.hasDiscoveredFlipcashContacts) {
454+
contactDataSource.markFlipcashContactsDiscovered()
455+
_state.update { it.copy(hasDiscoveredFlipcashContacts = true) }
456+
}
443457
}
444458
_state.update { it.copy(flipcashE164s = flipcashE164s) }
445459
trace(tag = TAG, message = "Found ${flipcashE164s.size} contacts on Flipcash", type = TraceType.Process)

0 commit comments

Comments
 (0)