Skip to content

Commit 47d65ae

Browse files
committed
chore(flipcash): add base set of analytics events (transfers)
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent bead125 commit 47d65ae

4 files changed

Lines changed: 177 additions & 7 deletions

File tree

apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/seed/SeedInputViewModel.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ data class SeedInputUiModel(
3838

3939
@HiltViewModel
4040
class SeedInputViewModel @Inject constructor(
41-
private val analytics: FlipcashAnalyticsService,
4241
private val authManager: AuthManager,
4342
private val resources: ResourceHelper,
4443
private val mnemonicManager: MnemonicManager,

apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalViewModel.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import com.flipcash.app.activityfeed.ActivityFeedCoordinator
99
import com.flipcash.app.core.extensions.onResult
1010
import com.flipcash.app.core.ui.CurrencyHolder
1111
import com.flipcash.features.withdrawal.R
12+
import com.flipcash.services.analytics.AnalyticsEvent
13+
import com.flipcash.services.analytics.FlipcashAnalyticsService
1214
import com.flipcash.services.user.UserManager
1315
import com.getcode.manager.BottomBarAction
1416
import com.getcode.manager.BottomBarManager
@@ -68,6 +70,7 @@ internal class WithdrawalViewModel @Inject constructor(
6870
transactionController: TransactionController,
6971
clipboardManager: ClipboardManager,
7072
activityFeedCoordinator: ActivityFeedCoordinator,
73+
analytics: FlipcashAnalyticsService
7174
) : BaseViewModel2<WithdrawalViewModel.State, WithdrawalViewModel.Event>(
7275
initialState = State(),
7376
updateStateForEvent = updateStateForEvent
@@ -403,13 +406,23 @@ internal class WithdrawalViewModel @Inject constructor(
403406
)
404407
}.onResult(
405408
onError = {
409+
analytics.transfer(
410+
event = AnalyticsEvent.Withdrawal,
411+
amount = stateFlow.value.amountEntryState.selectedAmount,
412+
successful = false,
413+
error = it,
414+
)
406415
dispatchEvent(Event.UpdateWithdrawalState(loading = false))
407416
BottomBarManager.showError(
408417
title = resources.getString(R.string.error_title_failedWithdrawal),
409418
message = resources.getString(R.string.error_description_failedWithdrawal)
410419
)
411420
},
412421
onSuccess = {
422+
analytics.transfer(
423+
AnalyticsEvent.Withdrawal,
424+
amount = stateFlow.value.amountEntryState.selectedAmount
425+
)
413426
viewModelScope.launch {
414427
coroutineScope {
415428
activityFeedCoordinator.fetchSinceLatest()

apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import com.flipcash.app.shareable.ShareSheetController
2525
import com.flipcash.app.shareable.Shareable
2626
import com.flipcash.app.shareable.ShareableConfirmationController
2727
import com.flipcash.core.R
28+
import com.flipcash.services.analytics.AnalyticsEvent
29+
import com.flipcash.services.analytics.FlipcashAnalyticsService
2830
import com.flipcash.services.billing.BillingClient
2931
import com.flipcash.services.controllers.AccountController
3032
import com.flipcash.services.user.UserManager
@@ -91,8 +93,8 @@ class RealSessionController @Inject constructor(
9193
private val toastController: ToastController,
9294
private val billingClient: BillingClient,
9395
private val balanceController: BalanceController,
94-
private val exchange: Exchange,
9596
private val featureFlagController: FeatureFlagController,
97+
private val analytics: FlipcashAnalyticsService,
9698
appSettingsCoordinator: AppSettingsCoordinator,
9799
) : SessionController {
98100

@@ -345,6 +347,7 @@ class RealSessionController @Inject constructor(
345347
amount = bill.amount,
346348
owner = owner,
347349
onGrabbed = {
350+
analytics.transfer(AnalyticsEvent.GiveBill, bill.amount)
348351
toastController.enqueue(bill.amount, isDeposit = false)
349352
dismissBill(Grabbed)
350353
vibrator.vibrate()
@@ -358,7 +361,15 @@ class RealSessionController @Inject constructor(
358361
// CodeAnalyticsManager.BillPresentationStyle.Slide
359362
// )
360363
},
361-
onError = { dismissBill(action = PutInWallet) },
364+
onError = {
365+
analytics.transfer(
366+
event = AnalyticsEvent.GiveBill,
367+
amount = bill.amount,
368+
successful = false,
369+
error = it
370+
)
371+
dismissBill(action = PutInWallet)
372+
},
362373
present = { data ->
363374
if (!bill.didReceive) {
364375
trace(
@@ -405,7 +416,8 @@ class RealSessionController @Inject constructor(
405416
delay(CASH_LINK_CONFIRMATION_DELAY)
406417

407418
// confirm the result of the share
408-
val confirmResult = shareConfirmationController.confirm(shareable, result)
419+
val confirmResult =
420+
shareConfirmationController.confirm(shareable, result)
409421

410422
// reset isChecking after confirmation
411423
shareSheetController.reset(setChecked = false)
@@ -423,6 +435,7 @@ class RealSessionController @Inject constructor(
423435
dismissBill(Grabbed)
424436
vibrator.vibrate()
425437
bringActivityFeedCurrent()
438+
analytics.transfer(AnalyticsEvent.SentCashLink(clipboard = true), amount)
426439
trace(
427440
tag = "Session",
428441
message = "Cash link copied to clipboard",
@@ -438,6 +451,7 @@ class RealSessionController @Inject constructor(
438451
dismissBill(Grabbed)
439452
vibrator.vibrate()
440453
bringActivityFeedCurrent()
454+
analytics.transfer(AnalyticsEvent.SentCashLink(app = result.to), amount)
441455
trace(
442456
tag = "Session",
443457
message = "Cash link shared with ${result.to}",
@@ -549,7 +563,6 @@ class RealSessionController @Inject constructor(
549563
return
550564
}
551565

552-
// TODO: analytics
553566
claimGiftCard(owner = owner, entropy = entropy, claimIfOwned = false)
554567
}
555568

@@ -569,6 +582,7 @@ class RealSessionController @Inject constructor(
569582
owner = owner,
570583
claimIfOwned = claimIfOwned,
571584
onReceived = {
585+
analytics.transfer(AnalyticsEvent.ClaimedCashLink, amount = it)
572586
toastController.enqueue(it, isDeposit = true)
573587
showBill(
574588
bill = Bill.Cash(amount = it, didReceive = true),
@@ -578,6 +592,15 @@ class RealSessionController @Inject constructor(
578592
bringActivityFeedCurrent()
579593
},
580594
onError = { cause ->
595+
if (cause !is ReceiveGiftTransactorError.UsersGiftCard) {
596+
analytics.transfer(
597+
AnalyticsEvent.ClaimedCashLink,
598+
amount = null,
599+
successful = false,
600+
error = cause
601+
)
602+
}
603+
581604
when (cause) {
582605
is ReceiveGiftTransactorError.UsersGiftCard -> {
583606
// present confirmation to claim (cancel) own gift card
@@ -609,6 +632,7 @@ class RealSessionController @Inject constructor(
609632
}
610633
)
611634
}
635+
612636
is ReceiveGiftTransactorError.AlreadyClaimed -> {
613637
BottomBarManager.showError(
614638
resources.getString(R.string.error_title_alreadyCollected),
@@ -645,6 +669,7 @@ class RealSessionController @Inject constructor(
645669
owner = owner,
646670
payload = payload,
647671
onGrabbed = { amount ->
672+
analytics.transfer(AnalyticsEvent.GrabBill, amount)
648673
BottomBarManager.clear()
649674
toastController.enqueue(amount, isDeposit = true)
650675
showBill(
@@ -655,6 +680,12 @@ class RealSessionController @Inject constructor(
655680
bringActivityFeedCurrent()
656681
},
657682
onError = {
683+
analytics.transfer(
684+
event = AnalyticsEvent.GrabBill,
685+
fiat = payload.fiat,
686+
successful = false,
687+
error = it
688+
)
658689
scannedRendezvous.remove(payload.rendezvous.publicKey)
659690
ErrorUtils.handleError(it)
660691
}

services/flipcash/src/main/kotlin/com/flipcash/services/analytics/Analytics.kt

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,153 @@ package com.flipcash.services.analytics
33
import com.getcode.libs.analytics.AnalyticsService
44
import com.getcode.libs.analytics.AppAction
55
import com.getcode.libs.analytics.AppActionSource
6+
import com.getcode.opencode.model.financial.Fiat
7+
import com.getcode.opencode.model.financial.LocalFiat
8+
import com.getcode.services.flipcash.BuildConfig
9+
import com.getcode.utils.TraceType
10+
import com.getcode.utils.trace
11+
import com.google.firebase.ktx.Firebase
12+
import com.google.firebase.perf.ktx.performance
13+
import com.google.firebase.perf.metrics.Trace
614
import com.mixpanel.android.mpmetrics.MixpanelAPI
15+
import org.json.JSONObject
16+
import timber.log.Timber
717
import javax.inject.Inject
818

919
interface FlipcashAnalyticsService : AnalyticsService {
20+
fun transfer(
21+
event: AnalyticsEvent,
22+
amount: LocalFiat? = null,
23+
successful: Boolean = true,
24+
error: Throwable? = null
25+
)
26+
fun transfer(
27+
event: AnalyticsEvent,
28+
fiat: Fiat? = null,
29+
successful: Boolean = true,
30+
error: Throwable? = null
31+
)
1032
}
1133

1234
class FlipcashAnalyticsManager @Inject constructor(
1335
private val mixpanelAPI: MixpanelAPI
1436
) : FlipcashAnalyticsService {
1537

38+
private var traceAppInit: Trace? = null
39+
private var timeAppInit: Long? = null
40+
1641
override fun onAppStart() {
42+
timeAppInit = System.currentTimeMillis()
43+
traceAppInit = Firebase.performance.newTrace("Init")
44+
traceAppInit?.start()
1745
}
1846

1947
override fun onAppStarted() {
48+
traceAppInit ?: return
49+
traceAppInit?.stop()
50+
traceAppInit = null
51+
val duration = System.currentTimeMillis() - (timeAppInit ?: 0)
52+
trace(
53+
tag = "Analytics",
54+
message = "App started",
55+
metadata = {
56+
"duration" to duration
57+
},
58+
type = TraceType.Process
59+
)
60+
}
61+
62+
override fun unintentionalLogout() = Unit
63+
64+
override fun action(action: AppAction, source: AppActionSource?) = Unit
65+
66+
override fun transfer(event: AnalyticsEvent, amount: LocalFiat?, successful: Boolean, error: Throwable?) {
67+
val properties = event.properties(localizedAmount = amount, successful = successful, error = error)
68+
track(event.name, *properties.toList().toTypedArray())
2069
}
2170

22-
override fun unintentionalLogout() {
71+
override fun transfer(event: AnalyticsEvent, fiat: Fiat?, successful: Boolean, error: Throwable?) {
72+
val properties = event.properties(nativeAmount = fiat, successful = successful, error = error)
73+
track(event.name, *properties.toList().toTypedArray())
2374
}
2475

25-
override fun action(action: AppAction, source: AppActionSource?) {
76+
private fun track(name: String, vararg properties: Pair<String, String>) {
77+
if (BuildConfig.DEBUG) {
78+
trace(
79+
"debug track ${name}, ${properties.joinToString { "${it.first} => ${it.second}" }}",
80+
type = TraceType.Silent
81+
)
82+
return
83+
} //no logging in debug
84+
85+
val jsonObject = JSONObject()
86+
properties.forEach { property ->
87+
jsonObject.put(property.first, property.second)
88+
}
89+
mixpanelAPI.track(name, jsonObject)
2690
}
91+
}
92+
93+
sealed class AnalyticsEvent(val name: String) {
94+
data object GrabBill : AnalyticsEvent("Grab Bill")
95+
data object GiveBill : AnalyticsEvent("Give Bill")
96+
data object Withdrawal : AnalyticsEvent("Withdrawal")
97+
data class SentCashLink(val clipboard: Boolean? = null, val app: String? = null) : AnalyticsEvent("Sent Cash Link")
98+
data object ClaimedCashLink : AnalyticsEvent("Receive Cash Link")
99+
}
100+
101+
private fun AnalyticsEvent.properties(
102+
localizedAmount: LocalFiat? = null,
103+
nativeAmount: Fiat? = null,
104+
successful: Boolean,
105+
error: Throwable?
106+
): Map<String, String> {
107+
return buildMap {
108+
if (successful) {
109+
put("State", "Success")
110+
} else {
111+
put("State", "Failure")
112+
}
27113

114+
when (val event = this@properties) {
115+
is AnalyticsEvent.SentCashLink -> {
116+
if (event.clipboard == true) {
117+
put("Cash Link Choice", "Copied to clipboard")
118+
} else if (event.app != null) {
119+
put("Cash Link Choice", "Shared to app")
120+
put("App", event.app)
121+
}
122+
}
123+
AnalyticsEvent.Withdrawal,
124+
AnalyticsEvent.ClaimedCashLink,
125+
AnalyticsEvent.GiveBill,
126+
AnalyticsEvent.GrabBill -> Unit
127+
}
128+
129+
if (localizedAmount != null) {
130+
putAll(localizedAmount.asProperties())
131+
} else if (nativeAmount != null) {
132+
putAll(nativeAmount.asProperties())
133+
}
134+
135+
if (error != null) {
136+
put("Error", error.message.orEmpty())
137+
}
138+
}
139+
}
140+
141+
private fun LocalFiat.asProperties(): Map<String, String> {
142+
return buildMap {
143+
putAll(usdc.asProperties())
144+
"Fiat" to converted.doubleValue.toString()
145+
"Exchange Rate" to rate.fx.toString()
146+
"Currency" to rate.currency.name
147+
}
148+
}
149+
150+
private fun Fiat.asProperties(): Map<String, String> {
151+
return buildMap {
152+
"USDC" to doubleValue.toString()
153+
"Quarks" to quarks.toDouble().toString()
154+
}
28155
}

0 commit comments

Comments
 (0)