Skip to content

Commit 5b10954

Browse files
committed
feat(flipcash): add onboarding funnel and pool analytics events
Signed-off-by: Brandon McAnsh <[email protected]>
1 parent fcbe6cd commit 5b10954

File tree

7 files changed

+140
-9
lines changed

7 files changed

+140
-9
lines changed

apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import com.flipcash.app.router.Router
2626
import com.flipcash.app.session.LocalSessionController
2727
import com.flipcash.app.shareable.LocalShareController
2828
import com.flipcash.app.shareable.ShareSheetController
29+
import com.flipcash.services.analytics.FlipcashAnalyticsService
2930
import com.flipcash.services.user.UserManager
31+
import com.getcode.libs.analytics.LocalAnalytics
3032
import com.getcode.opencode.compose.LocalExchange
3133
import com.getcode.opencode.exchange.Exchange
3234
import com.getcode.util.permissions.LocalPermissionChecker
@@ -99,6 +101,9 @@ class MainActivity : FragmentActivity() {
99101
@Inject
100102
lateinit var paymentController: PaymentController
101103

104+
@Inject
105+
lateinit var analytics: FlipcashAnalyticsService
106+
102107
override fun onCreate(savedInstanceState: Bundle?) {
103108
super.onCreate(savedInstanceState)
104109
handleUncaughtException()
@@ -110,6 +115,7 @@ class MainActivity : FragmentActivity() {
110115
LocalSystemSettings provides settingsHelper,
111116
LocalNetworkObserver provides networkObserver,
112117
LocalExchange provides exchange,
118+
LocalAnalytics provides analytics,
113119
LocalCurrencyUtils provides currencyUtils,
114120
LocalVibrator provides vibrator,
115121
LocalRouter provides router,

apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/accesskey/LoginAccessKeyViewModel.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.flipcash.app.login.accesskey
33
import com.flipcash.app.accesskey.BaseAccessKeyViewModel
44
import com.flipcash.app.auth.AuthManager
55
import com.flipcash.app.core.storage.MediaScanner
6+
import com.flipcash.services.analytics.Action
7+
import com.flipcash.services.analytics.FlipcashAnalyticsService
68
import com.flipcash.services.user.UserManager
79
import com.getcode.libs.qr.QRCodeGenerator
810
import com.getcode.manager.TopBarManager
@@ -20,15 +22,29 @@ class LoginAccessKeyViewModel @Inject constructor(
2022
qrCodeGenerator: QRCodeGenerator,
2123
mediaScanner: MediaScanner,
2224
private val userManager: UserManager,
23-
private val authManager: AuthManager
25+
private val authManager: AuthManager,
26+
private val analytics: FlipcashAnalyticsService,
2427
): BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner, userManager, qrCodeGenerator) {
2528

26-
suspend fun saveImage(): Result<Boolean> = saveBitmapToFile()
29+
suspend fun saveImage(): Result<Boolean> = trackButton(Action.SaveAccessKey)
30+
.fold(
31+
onSuccess = { saveBitmapToFile() },
32+
onFailure = { Result.failure(it) }
33+
)
2734
.onSuccess { authManager.onUserAccessKeySeen() }
2835
.map { authManager.presentCredentialStorage() }
2936
.map { userManager.userFlags?.requiresIapForRegistration == true }
3037

31-
suspend fun onWroteDownInstead(): Result<Boolean> = authManager.onUserAccessKeySeen()
38+
suspend fun onWroteDownInstead(): Result<Boolean> = trackButton(Action.SaveAccessKey)
39+
.fold(
40+
onSuccess = { authManager.onUserAccessKeySeen() },
41+
onFailure = { Result.failure(it) }
42+
)
3243
.map { authManager.presentCredentialStorage() }
3344
.map { userManager.userFlags?.requiresIapForRegistration == true }
45+
46+
private fun trackButton(action: Action): Result<Unit> {
47+
analytics.action(action)
48+
return Result.success(Unit)
49+
}
3450
}

apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.flipcash.app.login.router
33
import androidx.lifecycle.viewModelScope
44
import com.flipcash.app.auth.AuthManager
55
import com.flipcash.features.login.R
6+
import com.flipcash.services.analytics.Action
7+
import com.flipcash.services.analytics.FlipcashAnalyticsService
68
import com.flipcash.services.controllers.AccountController
79
import com.getcode.manager.BottomBarManager
810
import com.getcode.util.resources.ResourceHelper
@@ -25,6 +27,7 @@ class LoginViewModel @Inject constructor(
2527
private val authManager: AuthManager,
2628
private val accounts: AccountController,
2729
private val resources: ResourceHelper,
30+
private val analytics: FlipcashAnalyticsService,
2831
) : BaseViewModel2<LoginViewModel.State, LoginViewModel.Event>(
2932
initialState = State(),
3033
updateStateForEvent = updateStateForEvent
@@ -61,6 +64,7 @@ class LoginViewModel @Inject constructor(
6164

6265
eventFlow
6366
.filterIsInstance<Event.CreateAccount>()
67+
.onEach { analytics.action(Action.CreateAccount) }
6468
.map {
6569
authManager.createAccount()
6670
.onFailure {

apps/flipcash/features/pools/src/main/kotlin/com/flipcash/app/pools/internal/betting/PoolBettingViewModel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.flipcash.app.pools.PoolsCoordinator
2121
import com.flipcash.app.shareable.ShareSheetController
2222
import com.flipcash.app.shareable.Shareable
2323
import com.flipcash.features.pools.R
24+
import com.flipcash.services.analytics.FlipcashAnalyticsService
2425
import com.flipcash.services.models.ClosePoolError
2526
import com.flipcash.services.user.UserManager
2627
import com.getcode.ed25519.Ed25519
@@ -54,6 +55,7 @@ internal class PoolBettingViewModel @Inject constructor(
5455
shareController: ShareSheetController,
5556
resources: ResourceHelper,
5657
payments: PaymentController,
58+
analytics: FlipcashAnalyticsService,
5759
) : BaseViewModel2<PoolBettingViewModel.State, PoolBettingViewModel.Event>(
5860
initialState = State(),
5961
updateStateForEvent = updateStateForEvent
@@ -219,6 +221,7 @@ internal class PoolBettingViewModel @Inject constructor(
219221
.filterIsInstance<Event.OnPoolRendezvousChanged>()
220222
.filter { it.fromUser }
221223
.map { it.rendezvous }
224+
.onEach { analytics.poolOpenedFromDeeplink(it.publicKeyBytes.toList()) }
222225
.map { rendezvous ->
223226
dispatchEvent(Event.OnLoadingChanged(true))
224227
poolsCoordinator.getPool(rendezvous)
@@ -381,6 +384,7 @@ internal class PoolBettingViewModel @Inject constructor(
381384
is PaymentEvent.OnPaymentSuccess -> {
382385
event.acknowledge(true) {
383386
viewModelScope.launch {
387+
analytics.placedBidInPool(stateFlow.value.metadata.id)
384388
poolsCoordinator.onBetPaidForInPool(
385389
poolId = (event.metadata as PoolBidPaymentMetadata).pool.id
386390
)
@@ -475,6 +479,7 @@ internal class PoolBettingViewModel @Inject constructor(
475479
}
476480
is PaymentEvent.OnRpcFailure -> Unit
477481
is PaymentEvent.OnPaymentSuccess -> {
482+
analytics.declaredOutcomeInPool(stateFlow.value.metadata.id)
478483
event.acknowledge(true) {
479484
dispatchEvent(Event.OnFundsDistributed(true))
480485
}
@@ -537,6 +542,7 @@ internal class PoolBettingViewModel @Inject constructor(
537542
}
538543
is PaymentEvent.OnPaymentSuccess -> {
539544
event.acknowledge(true) {
545+
analytics.declaredOutcomeInPool(stateFlow.value.metadata.id)
540546
dispatchEvent(Event.OnFundsDistributed(true))
541547
}
542548
}

apps/flipcash/features/pools/src/main/kotlin/com/flipcash/app/pools/internal/create/PoolCreateViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ internal class PoolCreateViewModel @Inject constructor(
302302
},
303303
onSuccess = {
304304
dispatchEvent(Event.UpdateCreatingState(loading = false, success = true))
305+
analytics.poolCreated(it)
305306
dispatchEvent(Event.OnPoolCreated(it))
306307
}
307308
).launchIn(viewModelScope)

apps/flipcash/shared/permissions/src/main/kotlin/com/flipcash/app/permissions/internal/PermissionScreenContent.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import androidx.compose.ui.tooling.preview.Preview
2929
import cafe.adriel.voyager.core.registry.ScreenRegistry
3030
import com.flipcash.app.core.NavScreenProvider
3131
import com.flipcash.app.theme.FlipcashDesignSystem
32+
import com.flipcash.services.analytics.Action
3233
import com.flipcash.shared.permissions.R
34+
import com.getcode.libs.analytics.LocalAnalytics
3335
import com.getcode.navigation.core.LocalCodeNavigator
3436
import com.getcode.theme.CodeTheme
3537
import com.getcode.theme.DesignSystem
@@ -41,7 +43,7 @@ import com.getcode.util.permissions.cameraPermissionCheck
4143
import com.getcode.util.permissions.notificationPermissionCheck
4244

4345
internal enum class Permission {
44-
Camera, Notifications
46+
Notifications, Camera;
4547
}
4648

4749
@Composable
@@ -51,12 +53,13 @@ internal fun PermissionScreenContent(
5153
) {
5254
val navigator = LocalCodeNavigator.current
5355
val permissionChecker = LocalPermissionChecker.current
56+
val analytics = LocalAnalytics.current
5457

5558
when (permission) {
5659
Permission.Camera -> CameraPermissionScreenContent(
5760
onGranted = {
5861
if (fromOnboarding) {
59-
// analytics.action(Action.CompletedOnboarding)
62+
analytics.action(Action.CompletedOnboarding)
6063
}
6164
navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.HomeScreen.Scanner()))
6265
},
@@ -65,12 +68,12 @@ internal fun PermissionScreenContent(
6568
}
6669
)
6770
Permission.Notifications -> NotificationScreenContent {
68-
if (fromOnboarding) {
69-
// analytics.action(Action.CompletedOnboarding)
70-
}
7171
if (permissionChecker.isDenied(Manifest.permission.CAMERA)) {
7272
navigator.push(ScreenRegistry.get(NavScreenProvider.Permissions.Camera()))
7373
} else {
74+
if (fromOnboarding) {
75+
analytics.action(Action.CompletedOnboarding)
76+
}
7477
navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.HomeScreen.Scanner()))
7578
}
7679
}
@@ -79,12 +82,14 @@ internal fun PermissionScreenContent(
7982

8083
@Composable
8184
internal fun CameraPermissionScreenContent(onGranted: () -> Unit, onNotGranted: () -> Unit) {
85+
val analytics = LocalAnalytics.current
8286
var isResultHandled by remember { mutableStateOf(false) }
8387
val onNotificationResult: (Boolean) -> Unit = { isGranted ->
8488
if (!isResultHandled) {
8589
isResultHandled = true
8690

8791
if (isGranted) {
92+
analytics.action(Action.AllowCamera)
8893
onGranted()
8994
} else {
9095
onNotGranted()
@@ -154,9 +159,13 @@ internal fun CameraPermissionScreenContent(onGranted: () -> Unit, onNotGranted:
154159

155160
@Composable
156161
internal fun NotificationScreenContent(onGranted: () -> Unit) {
162+
val analytics = LocalAnalytics.current
157163
val onNotificationResult: (Boolean) -> Unit = { isGranted ->
158164
if (isGranted) {
165+
analytics.action(Action.AllowPush)
159166
onGranted()
167+
} else {
168+
analytics.action(Action.SkipPush)
160169
}
161170
}
162171
val notificationPermissionCheck = notificationPermissionCheck(onResult = {

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

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import com.getcode.ed25519.Ed25519.KeyPair
44
import com.getcode.libs.analytics.AnalyticsService
55
import com.getcode.libs.analytics.AppAction
66
import com.getcode.libs.analytics.AppActionSource
7+
import com.getcode.opencode.model.core.ID
78
import com.getcode.opencode.model.financial.CurrencyCode
89
import com.getcode.opencode.model.financial.Fiat
910
import com.getcode.opencode.model.financial.LocalFiat
1011
import com.getcode.services.flipcash.BuildConfig
12+
import com.getcode.solana.keys.PublicKey
13+
import com.getcode.solana.keys.base58
1114
import com.getcode.utils.TraceType
15+
import com.getcode.utils.base58
1216
import com.getcode.utils.getPublicKeyBase58
1317
import com.getcode.utils.trace
1418
import com.google.firebase.ktx.Firebase
@@ -37,6 +41,11 @@ interface FlipcashAnalyticsService : AnalyticsService {
3741
currency: CurrencyCode,
3842
owner: KeyPair,
3943
)
44+
45+
fun poolOpenedFromDeeplink(id: ID)
46+
fun poolCreated(id: ID)
47+
fun placedBidInPool(id: ID)
48+
fun declaredOutcomeInPool(id: ID)
4049
}
4150

4251
class FlipcashAnalyticsManager @Inject constructor(
@@ -69,7 +78,9 @@ class FlipcashAnalyticsManager @Inject constructor(
6978

7079
override fun unintentionalLogout() = Unit
7180

72-
override fun action(action: AppAction, source: AppActionSource?) = Unit
81+
override fun action(action: AppAction, source: AppActionSource?) {
82+
track(name = action.value)
83+
}
7384

7485
override fun transfer(event: AnalyticsEvent.Transfer, amount: LocalFiat?, successful: Boolean, error: Throwable?) {
7586
val properties = event.properties(localizedAmount = amount, successful = successful, error = error)
@@ -87,6 +98,30 @@ class FlipcashAnalyticsManager @Inject constructor(
8798
track(event.name, *properties.toList().toTypedArray())
8899
}
89100

101+
override fun poolOpenedFromDeeplink(id: ID) {
102+
val event = AnalyticsEvent.PoolOpened(id)
103+
val properties = event.properties()
104+
track(event.name, *properties.toList().toTypedArray())
105+
}
106+
107+
override fun poolCreated(id: ID) {
108+
val event = AnalyticsEvent.PoolCreated(id)
109+
val properties = event.properties()
110+
track(event.name, *properties.toList().toTypedArray())
111+
}
112+
113+
override fun placedBidInPool(id: ID) {
114+
val event = AnalyticsEvent.PlacedBid(id)
115+
val properties = event.properties()
116+
track(event.name, *properties.toList().toTypedArray())
117+
}
118+
119+
override fun declaredOutcomeInPool(id: ID) {
120+
val event = AnalyticsEvent.DeclaredOutcome(id)
121+
val properties = event.properties()
122+
track(event.name, *properties.toList().toTypedArray())
123+
}
124+
90125
private fun track(name: String, vararg properties: Pair<String, String>) {
91126
if (BuildConfig.DEBUG) {
92127
trace(
@@ -104,6 +139,36 @@ class FlipcashAnalyticsManager @Inject constructor(
104139
}
105140
}
106141

142+
sealed class Action : AppAction {
143+
data object CreateAccount : Action() {
144+
override val value: String = "Button: Create Account"
145+
}
146+
147+
data object SaveAccessKey : Action() {
148+
override val value: String = "Button: Save Access Key"
149+
}
150+
151+
data object WroteAccessKey : Action() {
152+
override val value: String = "Button: Wrote Access Key"
153+
}
154+
155+
data object AllowCamera : Action() {
156+
override val value: String = "Button: Allow Camera"
157+
}
158+
159+
data object AllowPush : Action() {
160+
override val value: String = "Button: Allow Push"
161+
}
162+
163+
data object SkipPush : Action() {
164+
override val value: String = "Button: Skip Push"
165+
}
166+
167+
data object CompletedOnboarding : Action() {
168+
override val value: String = "Complete Onboarding"
169+
}
170+
}
171+
107172
sealed interface AnalyticsEvent {
108173

109174
val name: String
@@ -134,6 +199,26 @@ sealed interface AnalyticsEvent {
134199
data object ClaimedCashLink : Transfer {
135200
override val name: String = "Receive Cash Link"
136201
}
202+
203+
sealed interface PoolEvent : AnalyticsEvent {
204+
val id: ID
205+
}
206+
207+
data class PoolOpened(override val id: ID) : PoolEvent {
208+
override val name: String = "Pool: Opened From Deeplink"
209+
}
210+
211+
data class PoolCreated(override val id: ID) : PoolEvent {
212+
override val name: String = "Pool: Created"
213+
}
214+
215+
data class PlacedBid(override val id: ID) : PoolEvent {
216+
override val name: String = "Pool: Place Bet"
217+
}
218+
219+
data class DeclaredOutcome(override val id: ID) : PoolEvent {
220+
override val name: String = "Pool: Declared Outcome"
221+
}
137222
}
138223

139224
private fun AnalyticsEvent.properties(
@@ -170,6 +255,10 @@ private fun AnalyticsEvent.properties(
170255
put("Currency", event.currency.name)
171256
put("Owner Public Key", event.owner.getPublicKeyBase58())
172257
}
258+
259+
is AnalyticsEvent.PoolEvent -> {
260+
put("ID", event.id.base58)
261+
}
173262
}
174263

175264
if (localizedAmount != null) {

0 commit comments

Comments
 (0)