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
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ internal class SendFlowViewModel @Inject constructor(
!unknown.displayName.contains(searchString, ignoreCase = true)) {
return@mapNotNull null
}
if (unknown.e164.isNotEmpty()) {
recentsE164s += unknown.e164
}
unknown
}

Expand All @@ -301,18 +304,23 @@ internal class SendFlowViewModel @Inject constructor(
chatId = chatId,
lastActivity = summary.metadata.lastActivity,
)
}.sortedWith(
compareByDescending<ContactListItem.ContactRow> { it.lastActivity }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.contact.displayName }
)
}

// On Flipcash — contacts that haven't chatted yet
// On Flipcash — contacts that haven't chatted yet, use joinedAt as their sort timestamp
val flipcashRows = filtered
.filter { it.e164 in contactState.flipcashE164s && it.e164 !in recentsE164s }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.displayName })
.map { ContactListItem.ContactRow(contact = it, isOnFlipcash = true) }
.map { contact ->
ContactListItem.ContactRow(
contact = contact,
isOnFlipcash = true,
lastActivity = contactState.joinedAtByE164[contact.e164],
)
}

val flipcashCombined = (recentRows + flipcashRows)
val flipcashCombined = (recentRows + flipcashRows).sortedWith(
compareByDescending<ContactListItem.ContactRow> { it.lastActivity }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.contact.displayName }
)

val excludedE164s = recentsE164s + contactState.flipcashE164s
val other = filtered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,5 @@ private fun ResolvedUserFlags.editableEntries(): List<EditableEntry<*>> = listOf
EditableEntry(Field.WithdrawalFeeAmount, withdrawalFeeAmount),
EditableEntry(Field.PreferredUsdcOnRampLiquidityPool, usdcOnRampLiquidityPool),
EditableEntry(Field.MinimumHolderAmountForLeaderboard, minimumHolderAmountForLeaderboard),
EditableEntry(Field.RequireCoinbaseEmailVerification, requireCoinbaseEmailVerification),
)
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,7 @@ class EventStreamDelegate @Inject constructor(
val lastMsg = if (resolvedMessages.isNotEmpty()) {
trace(tag = TAG, message = "Upserting ${resolvedMessages.size} messages for $chatId", type = TraceType.Process)
messageDataSource.upsert(chatId, resolvedMessages)
resolvedMessages.maxByOrNull { it.messageId }?.also { msg ->
metadataDataSource.updateLastMessageId(chatId, msg.messageId)
metadataDataSource.updateLastActivity(chatId, msg.timestamp.toEpochMilliseconds())
}
resolvedMessages.maxByOrNull { it.messageId }
} else null

// Advance event sequence cursor — gap-aware
Expand All @@ -252,6 +249,9 @@ class EventStreamDelegate @Inject constructor(
memberDataSource.updatePointers(chatId, pointer)
}

// Process metadata updates before message-derived fields so that a
// FullRefresh (which replaces the entire entity) doesn't overwrite
// the lastActivity/lastMessageId set from the incoming message.
for (metaUpdate in update.metadataUpdates) {
when (metaUpdate) {
is MetadataUpdate.FullRefresh -> {
Expand All @@ -271,6 +271,14 @@ class EventStreamDelegate @Inject constructor(
}
}

// Update lastMessageId and lastActivity AFTER metadata updates so that
// incoming message timestamps always take precedence over a potentially
// stale FullRefresh.
lastMsg?.let { msg ->
metadataDataSource.updateLastMessageId(chatId, msg.messageId)
metadataDataSource.updateLastActivity(chatId, msg.timestamp.toEpochMilliseconds())
}

// --- Process reaction updates ---

if (update.reactionUpdates.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.map
import kotlin.time.Instant
import kotlin.collections.mapValues


Expand Down Expand Up @@ -98,6 +99,7 @@ class ContactCoordinator @Inject constructor(
val contacts: Map<String, DeviceContact> = emptyMap(),
val flipcashE164s: Set<String> = emptySet(),
val dmChatIds: Map<String, String> = emptyMap(),
val joinedAtByE164: Map<String, Instant> = emptyMap(),
val syncState: SyncState = SyncState.Idle,
val hasEverSynced: Boolean = false,
val hasDiscoveredFlipcashContacts: Boolean = false,
Expand Down Expand Up @@ -370,12 +372,16 @@ class ContactCoordinator @Inject constructor(
val dmChatIds = mappings
.filter { it.dmChatId.isNotEmpty() }
.associate { it.e164 to it.dmChatId }
val joinedAtByE164 = mappings
.filter { it.joinedAtEpochSeconds > 0 }
.associate { it.e164 to Instant.fromEpochSeconds(it.joinedAtEpochSeconds) }

_state.update {
it.copy(
contacts = contacts,
flipcashE164s = flipcashE164s,
dmChatIds = dmChatIds,
joinedAtByE164 = joinedAtByE164,
hasEverSynced = true,
hasDiscoveredFlipcashContacts = hasDiscoveredFlipcashContacts,
)
Expand Down Expand Up @@ -551,12 +557,21 @@ class ContactCoordinator @Inject constructor(
}
}
val dmChatIds = mutableMapOf<String, String>()
val joinedAts = mutableMapOf<String, Instant>()
entries.forEach { entry ->
contactDataSource.updateJoinedAt(entry.phoneNumber, entry.joinedAt.epochSeconds)
joinedAts[entry.phoneNumber] = entry.joinedAt
val chatIdStr = entry.dmChatId?.toString() ?: return@forEach
contactDataSource.updateDmChatId(entry.phoneNumber, chatIdStr)
dmChatIds[entry.phoneNumber] = chatIdStr
}
_state.update { it.copy(flipcashE164s = flipcashE164s, dmChatIds = it.dmChatIds + dmChatIds) }
_state.update {
it.copy(
flipcashE164s = flipcashE164s,
dmChatIds = it.dmChatIds + dmChatIds,
joinedAtByE164 = it.joinedAtByE164 + joinedAts,
)
}
trace(tag = TAG, message = "Found ${flipcashE164s.size} contacts on Flipcash", type = TraceType.Process)
}?.onFailure { error ->
when (error) {
Expand Down
1 change: 1 addition & 0 deletions apps/flipcash/shared/onramp/coinbase/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies {
implementation(libs.kotlinx.coroutines.play.services)
implementation(project(":libs:messaging"))
implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:userflags"))
implementation(project(":apps:flipcash:shared:web"))
api(project(":libs:network:coinbase:onramp"))
implementation(project(":libs:network:jwt"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.getcode.opencode.model.transactions.SwapFundingSource
import com.getcode.solana.keys.Mint
import com.getcode.solana.keys.base58
import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.getcode.utils.CodeServerError
import com.getcode.utils.ErrorUtils
import com.getcode.utils.NotifiableError
Expand All @@ -54,8 +55,8 @@ import javax.inject.Inject

sealed class PurchaseGate : Throwable() {
data class WebViewWarning(val channel: WebViewChannel) : PurchaseGate()
data object GooglePayNotSupported : PurchaseGate()
data object GooglePayNoPaymentMethod : PurchaseGate()
class GooglePayNotSupported : PurchaseGate()
class GooglePayNoPaymentMethod : PurchaseGate()
}

typealias OrderWithPaymentLink = Pair<String, OnRampPurchaseResponse.PaymentLink>
Expand All @@ -76,6 +77,7 @@ class CoinbaseOnRampController @Inject constructor(
private val transactionController: TransactionOperations,
private val googlePayReadiness: GooglePayReadiness,
private val webViewChannelDetector: WebViewChannelDetector,
private val userFlags: UserFlagsCoordinator,
) {

private val _state = MutableStateFlow<CoinbaseOnRampState>(CoinbaseOnRampState.Idle)
Expand Down Expand Up @@ -115,8 +117,8 @@ class CoinbaseOnRampController @Inject constructor(

suspend fun checkPurchaseGates(): Result<Unit> {
when (googlePayReadiness.check()) {
GooglePayReadiness.Status.NotSupported -> return Result.failure(PurchaseGate.GooglePayNotSupported)
GooglePayReadiness.Status.NoPaymentMethod -> return Result.failure(PurchaseGate.GooglePayNoPaymentMethod)
GooglePayReadiness.Status.NotSupported -> return Result.failure(PurchaseGate.GooglePayNotSupported())
GooglePayReadiness.Status.NoPaymentMethod -> return Result.failure(PurchaseGate.GooglePayNoPaymentMethod())
GooglePayReadiness.Status.Ready -> Unit
}

Expand Down Expand Up @@ -231,8 +233,8 @@ class CoinbaseOnRampController @Inject constructor(

val destination = destinationForToken(owner, token)

val email = userManager.profile?.verifiedEmailAddress
val phone = userManager.profile?.verifiedPhoneNumber
val email = resolveEmail(phone)

if (email == null || phone == null) {
return Result.failure(
Expand Down Expand Up @@ -276,8 +278,8 @@ class CoinbaseOnRampController @Inject constructor(

val destination = destinationForToken(owner, token)

val email = userManager.profile?.verifiedEmailAddress
val phone = userManager.profile?.verifiedPhoneNumber
val email = resolveEmail(phone)

if (email == null || phone == null) {
return Result.failure(
Expand Down Expand Up @@ -339,6 +341,16 @@ class CoinbaseOnRampController @Inject constructor(
)
}

private fun resolveEmail(phone: String?): String? {
val verified = userManager.profile?.verifiedEmailAddress
if (verified != null) return verified

val requireEmail = userFlags.resolvedFlags.value.requireCoinbaseEmailVerification.effectiveValue
if (!requireEmail && phone != null) return "[email protected]"

return null
}

private fun destinationForToken(owner: AccountCluster, token: Token): String {
return if (token.address == Mint.usdf) {
owner.depositAddressFor(token).base58()
Expand Down Expand Up @@ -379,9 +391,7 @@ class CoinbaseOnRampController @Inject constructor(
)
when (error) {
is GetJwtError.EmailVerificationRequired -> Result.failure(
OnRampAuthError.VerificationRequired(
email = true
)
OnRampAuthError.VerificationRequired(email = true)
)

is GetJwtError.PhoneVerificationRequired -> Result.failure(
Expand Down
Loading
Loading