Skip to content

Commit f85bc3d

Browse files
committed
feat(analytics): send the money-flow events
Sent Cash now carries Context at every call site (Direct, Group Send with Recipient Count, Payment Request), and paying a request share sends it for the first time on Android. Sent Boost, Payment Request: Created and Payment Request: Closed are sent on the result of the call they describe.
1 parent 6f93817 commit f85bc3d

13 files changed

Lines changed: 470 additions & 13 deletions

File tree

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.flipcash.analytics.GroupGateFunding
3434
import com.flipcash.analytics.GroupInviteMethod
3535
import com.flipcash.analytics.GroupInviteSheetSource
3636
import com.flipcash.analytics.GroupInviteSource
37+
import com.flipcash.analytics.SendContext
3738
import com.flipcash.analytics.State as AnalyticsState
3839
import com.flipcash.analytics.events.ChatEvents
3940
import com.flipcash.analytics.events.GroupEvents
@@ -3844,7 +3845,7 @@ internal class ChatViewModel @AssistedInject constructor(
38443845
val sentAmount = verifiedFiat.localFiat.analytics
38453846
when {
38463847
isCashLink -> TransferEvents.sendCashLink(state, sentAmount, CashLinkChoice.GROUP_CHAT, null, error)
3847-
else -> TransferEvents.sentCash(state, sentAmount, error, null, null)
3848+
else -> TransferEvents.sentCash(state, sentAmount, error, SendContext.DIRECT, null)
38483849
}
38493850
}
38503851

@@ -4080,15 +4081,15 @@ internal class ChatViewModel @AssistedInject constructor(
40804081
holdPaymentSuccess(
40814082
show = { dispatchEvent(Event.SendStateUpdated(success = true)) },
40824083
release = {
4083-
analytics.track(TransferEvents.sentCash(AnalyticsState.SUCCESS, total.analytics, null, null, null))
4084+
analytics.track(TransferEvents.sentCash(AnalyticsState.SUCCESS, total.analytics, null, SendContext.GROUP_SEND, groupSend.selected.size))
40844085
// The send state stays until the sheet has gone, so Select People doesn't flash back to
40854086
// the keypad on the way out. The next "$" starts a fresh one.
40864087
dispatchEvent(Dispatchers.Main, Event.SendComplete(total))
40874088
},
40884089
)
40894090
}.onFailure { cause ->
40904091
dispatchEvent(Event.SendStateUpdated())
4091-
analytics.track(TransferEvents.sentCash(AnalyticsState.FAILURE, total.analytics, cause.analytics, null, null))
4092+
analytics.track(TransferEvents.sentCash(AnalyticsState.FAILURE, total.analytics, cause.analytics, SendContext.GROUP_SEND, groupSend.selected.size))
40924093
val (title, message) = when (cause) {
40934094
is ComputeVerifiedFiatError.AmountBelowMinimum ->
40944095
R.string.error_title_amountTooSmall to R.string.error_description_amountTooSmall

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/currencysend/CurrencySendViewModel.kt‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.getcode.view.PaymentSuccessHoldDuration
55
import com.getcode.view.holdPaymentSuccess
66
import androidx.lifecycle.ViewModel
77
import androidx.lifecycle.viewModelScope
8+
import com.flipcash.analytics.SendContext
89
import com.flipcash.analytics.State as AnalyticsState
910
import com.flipcash.analytics.events.TransferEvents
1011
import com.flipcash.app.analytics.FlipcashAnalytics
@@ -319,7 +320,7 @@ internal class CurrencySendViewModel @Inject constructor(
319320
show = { _state.update { it.copy(sendProgress = LoadingSuccessState(success = true)) } },
320321
release = {
321322
analytics.track(
322-
TransferEvents.sentCash(AnalyticsState.SUCCESS, verifiedFiat.localFiat.analytics, null, null, null)
323+
TransferEvents.sentCash(AnalyticsState.SUCCESS, verifiedFiat.localFiat.analytics, null, SendContext.DIRECT, null)
323324
)
324325
_done.trySend(Unit)
325326
},
@@ -331,7 +332,7 @@ internal class CurrencySendViewModel @Inject constructor(
331332
AnalyticsState.FAILURE,
332333
verifiedFiat.localFiat.analytics,
333334
cause.analytics,
334-
null,
335+
SendContext.DIRECT,
335336
null,
336337
)
337338
)

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/payment/StartChattingPayer.kt‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.flipcash.app.messenger.internal.payment
33
import androidx.annotation.StringRes
44
import com.flipcash.analytics.AddMoneySource
55
import com.flipcash.analytics.Amount
6+
import com.flipcash.analytics.SendContext
67
import com.flipcash.analytics.State as AnalyticsState
78
import com.flipcash.analytics.events.AddMoneyEvents
89
import com.flipcash.analytics.events.TransferEvents
@@ -191,7 +192,7 @@ internal class DefaultStartChattingPayer @Inject constructor(
191192
val sent = { state: AnalyticsState, amount: Amount, error: String? ->
192193
when (action) {
193194
TipAction.TIP -> TransferEvents.sentTip(state, amount, error)
194-
TipAction.SEND -> TransferEvents.sentCash(state, amount, error, null, null)
195+
TipAction.SEND -> TransferEvents.sentCash(state, amount, error, SendContext.DIRECT, null)
195196
}
196197
}
197198
analytics.track(

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/request/RequestDetailsViewModel.kt‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import com.getcode.view.PaymentSuccessHoldDuration
55
import com.getcode.view.holdPaymentSuccess
66
import androidx.lifecycle.ViewModel
77
import androidx.lifecycle.viewModelScope
8+
import com.flipcash.analytics.State as AnalyticsState
9+
import com.flipcash.analytics.events.PaymentRequestEvents
10+
import com.flipcash.app.analytics.FlipcashAnalytics
11+
import com.flipcash.app.analytics.analytics
812
import com.flipcash.app.core.AppRoute
913
import com.flipcash.app.core.internal.errors.showNetworkError
1014
import com.flipcash.app.messenger.internal.payment.StartChattingPayer
@@ -69,6 +73,7 @@ internal class RequestDetailsViewModel @Inject constructor(
6973
private val exchange: Exchange,
7074
private val userManager: UserManager,
7175
private val resources: ResourceHelper,
76+
private val analytics: FlipcashAnalytics,
7277
) : ViewModel() {
7378

7479
sealed interface Sheet {
@@ -172,6 +177,7 @@ internal class RequestDetailsViewModel @Inject constructor(
172177
exchange = exchange,
173178
userManager = userManager,
174179
resources = resources,
180+
analytics = analytics,
175181
openScreen = { route -> effects.trySend(Effect.OpenScreen(route)) },
176182
onNothingToPay = { _state.update { it.copy(hasPaymentIntent = false) } },
177183
)
@@ -194,7 +200,16 @@ internal class RequestDetailsViewModel @Inject constructor(
194200

195201
fun close() {
196202
val id = _state.value.requestId ?: return
197-
runCreatorAction { requests.close(id) }
203+
runCreatorAction {
204+
requests.close(id).also { result ->
205+
analytics.track(
206+
PaymentRequestEvents.closed(
207+
if (result.isSuccess) AnalyticsState.SUCCESS else AnalyticsState.FAILURE,
208+
result.exceptionOrNull()?.analytics,
209+
),
210+
)
211+
}
212+
}
198213
}
199214

200215
/** A tap on a row: only the creator, on an open request, on someone else's unpaid share. */

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/request/RequestMoneyViewModel.kt‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package com.flipcash.app.messenger.internal.request
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5+
import com.flipcash.analytics.State as AnalyticsState
6+
import com.flipcash.analytics.events.PaymentRequestEvents
7+
import com.flipcash.app.analytics.FlipcashAnalytics
8+
import com.flipcash.app.analytics.analytics
59
import com.flipcash.app.core.internal.errors.showNetworkError
610
import com.flipcash.app.core.ui.ConfirmationStyle
711
import com.flipcash.features.messenger.R
@@ -65,6 +69,7 @@ internal class RequestMoneyViewModel @Inject constructor(
6569
private val crypto: GroupContentCrypto,
6670
private val chatCoordinator: ChatCoordinator,
6771
private val userManager: UserManager,
72+
private val analytics: FlipcashAnalytics,
6873
) : ViewModel() {
6974

7075
enum class Stage { History, Amount, Summary, People, Name }
@@ -251,10 +256,15 @@ internal class RequestMoneyViewModel @Inject constructor(
251256
viewModelScope.launch {
252257
val requestId = draft.createdRequestId ?: run {
253258
val text = crypto.sealPaymentRequestText(chatId, viewerId, draft.title, "")
254-
.getOrElse { return@launch failed(it, chatId) }
259+
.getOrElse { return@launch failed(it, chatId, draft) }
255260
val split = draft.toSplit(chatId, text, Clock.System.now())
256-
?: return@launch failed(IllegalStateException("draft not sendable"), chatId)
257-
requests.create(split, key).getOrElse { return@launch failed(it, chatId) }.id
261+
?: return@launch failed(IllegalStateException("draft not sendable"), chatId, draft)
262+
val created = requests.create(split, key).getOrElse { return@launch failed(it, chatId, draft) }
263+
// Once per confirm: a retry reuses createdRequestId and never reaches here.
264+
analytics.track(
265+
PaymentRequestEvents.created(draft.requestedCount, AnalyticsState.SUCCESS, draft.perPerson?.analytics, null),
266+
)
267+
created.id
258268
}
259269
_state.update { it.copy(draft = it.draft?.copy(createdRequestId = requestId)) }
260270
// The request is live, so the deck shows it whatever happens here. Only the message is
@@ -283,7 +293,15 @@ internal class RequestMoneyViewModel @Inject constructor(
283293
)
284294
}
285295

286-
private suspend fun failed(error: Throwable, chatId: ChatId) {
296+
private suspend fun failed(error: Throwable, chatId: ChatId, draft: NewRequestDraft) {
297+
analytics.track(
298+
PaymentRequestEvents.created(
299+
draft.requestedCount,
300+
AnalyticsState.FAILURE,
301+
draft.perPerson?.analytics,
302+
error.analytics,
303+
),
304+
)
287305
_state.update { it.copy(sheet = Sheet.None, sendProgress = LoadingSuccessState()) }
288306
val message = when (error) {
289307
is CreatePaymentRequestError.Denied -> R.string.error_description_requestDenied

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/request/RequestPayFlow.kt‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package com.flipcash.app.messenger.internal.request
22

3+
import com.flipcash.analytics.SendContext
4+
import com.flipcash.analytics.State as AnalyticsState
5+
import com.flipcash.analytics.events.TransferEvents
6+
import com.flipcash.app.analytics.FlipcashAnalytics
7+
import com.flipcash.app.analytics.analytics
38
import com.flipcash.app.core.AppRoute
49
import com.flipcash.app.messenger.internal.payment.StartChattingPayer
510
import com.flipcash.app.messenger.internal.swipepay.SwipePayActions
@@ -46,6 +51,7 @@ internal class RequestPayFlow(
4651
private val exchange: Exchange,
4752
private val userManager: UserManager,
4853
private val resources: ResourceHelper,
54+
private val analytics: FlipcashAnalytics,
4955
/** Where "Add Money" goes, once the sheet has closed. */
5056
private val openScreen: (AppRoute) -> Unit,
5157
/** The server has no payment intent for the viewer on this request any more. */
@@ -118,7 +124,10 @@ internal class RequestPayFlow(
118124
.onSuccess {
119125
holdPaymentSuccess(
120126
show = { _state.update { it.copy(progress = LoadingSuccessState(success = true)) } },
121-
release = { dismiss() },
127+
release = {
128+
analytics.track(sentCash(AnalyticsState.SUCCESS, pending, null))
129+
dismiss()
130+
},
122131
)
123132
}
124133
.onFailure { cause -> afterFailedPayment(pending, cause) }
@@ -130,6 +139,7 @@ internal class RequestPayFlow(
130139
* paid, and a request closed meanwhile is "Request Closed".
131140
*/
132141
private suspend fun afterFailedPayment(pending: ShareToPay.Ready, cause: Throwable) {
142+
analytics.track(sentCash(AnalyticsState.FAILURE, pending, cause.analytics))
133143
_state.update { it.copy(progress = LoadingSuccessState()) }
134144
val viewer = viewerId ?: return
135145
when (val found = payments.prepare(pending.request.id, viewer).getOrNull()) {
@@ -146,6 +156,9 @@ internal class RequestPayFlow(
146156
}
147157
}
148158

159+
private fun sentCash(state: AnalyticsState, pending: ShareToPay.Ready, error: String?) =
160+
TransferEvents.sentCash(state, pending.share.amount.analytics, error, SendContext.PAYMENT_REQUEST, null)
161+
149162
fun dismiss() {
150163
if (!_state.value.progress.isIdle && !_state.value.progress.success) return
151164
ready = null

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/request/RequestPayViewModel.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.combine
2626
import kotlinx.coroutines.flow.receiveAsFlow
2727
import kotlinx.coroutines.flow.stateIn
2828
import kotlinx.coroutines.launch
29+
import com.flipcash.app.analytics.FlipcashAnalytics
2930
import javax.inject.Inject
3031

3132
/**
@@ -42,6 +43,7 @@ internal class RequestPayViewModel @Inject constructor(
4243
exchange: Exchange,
4344
userManager: UserManager,
4445
resources: ResourceHelper,
46+
analytics: FlipcashAnalytics,
4547
) : ViewModel() {
4648

4749
private val effects = Channel<AppRoute>(Channel.BUFFERED)
@@ -58,6 +60,7 @@ internal class RequestPayViewModel @Inject constructor(
5860
exchange = exchange,
5961
userManager = userManager,
6062
resources = resources,
63+
analytics = analytics,
6164
openScreen = { route -> effects.trySend(route) },
6265
)
6366

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/boost/ReactionBoostViewModel.kt‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package com.flipcash.app.messenger.internal.screens.boost
22

33
import androidx.lifecycle.viewModelScope
4+
import com.flipcash.analytics.ChatType as AnalyticsChatType
5+
import com.flipcash.analytics.State as AnalyticsState
6+
import com.flipcash.analytics.events.TransferEvents
7+
import com.flipcash.app.analytics.FlipcashAnalytics
8+
import com.flipcash.app.analytics.analytics
49
import com.flipcash.app.messenger.internal.payment.ReactionBoostPayer
510
import com.flipcash.app.messenger.internal.payment.StartChattingPayer
611
import com.flipcash.libs.coroutines.DispatcherProvider
@@ -11,6 +16,7 @@ import com.flipcash.shared.chat.MessageReactions
1116
import com.flipcash.shared.chat.reactions.SelfReaction
1217
import com.flipcash.shared.payments.boost.BoostChat
1318
import com.flipcash.shared.payments.boost.ReactionBoostPlan
19+
import com.flipcash.shared.payments.boost.centsToUsd
1420
import com.getcode.opencode.model.core.ID
1521
import com.getcode.opencode.model.financial.Token
1622
import com.getcode.utils.TraceType
@@ -51,6 +57,7 @@ internal class ReactionBoostViewModel @Inject constructor(
5157
private val payer: ReactionBoostPayer,
5258
private val splitResolver: ReactionBoostSplitResolver,
5359
startChattingPayer: StartChattingPayer,
60+
private val analytics: FlipcashAnalytics,
5461
dispatchers: DispatcherProvider,
5562
) : BaseViewModel<ReactionBoostViewModel.State, ReactionBoostViewModel.Event>(
5663
initialState = State(),
@@ -213,15 +220,32 @@ internal class ReactionBoostViewModel @Inject constructor(
213220
totalCents = cents,
214221
onAddMoney = { dispatchEvent(Event.AddMoney) },
215222
).onSuccess {
223+
trackBoost(metadata?.type, cents, AnalyticsState.SUCCESS, null)
216224
dispatchEvent(Event.SendProgressChanged(LoadingSuccessState(success = true)))
217225
delay(SUCCESS_HOLD_MS)
218226
dispatchEvent(Event.Completed)
219-
}.onFailure {
227+
}.onFailure { cause ->
228+
// A PaymentBlocked is a pre-flight stop (minimum, balance, limit, no account) that the
229+
// user was already told about; no payment was attempted.
230+
if (cause !is StartChattingPayer.PaymentBlocked) {
231+
trackBoost(metadata?.type, cents, AnalyticsState.FAILURE, cause.analytics)
232+
}
220233
dispatchEvent(Event.SendProgressChanged(LoadingSuccessState()))
221234
}
222235
}
223236
}
224237

238+
private fun trackBoost(type: ChatType?, totalCents: Long, state: AnalyticsState, error: String?) {
239+
analytics.track(
240+
TransferEvents.sentBoost(
241+
type?.analytics ?: AnalyticsChatType.UNKNOWN,
242+
state,
243+
centsToUsd(totalCents).analytics,
244+
error,
245+
),
246+
)
247+
}
248+
225249
private fun notSent(reason: String) {
226250
trace(tag = TAG, message = "Boost not sent: $reason", type = TraceType.Error)
227251
payer.alertPaymentFailed()

0 commit comments

Comments
 (0)