Skip to content

Commit 66ee7a8

Browse files
authored
Merge pull request #467 from code-payments/chore/onboarding-funnel-metrics
chore: add onboarding funnel metrics
2 parents 8da143e + b7ce56a commit 66ee7a8

8 files changed

Lines changed: 83 additions & 14 deletions

File tree

api/src/main/java/com/getcode/analytics/AnalyticsManager.kt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ import timber.log.Timber
1717
import javax.inject.Inject
1818
import javax.inject.Singleton
1919

20+
enum class Action(val value: String) {
21+
CreateAccount("Action: Create Account"),
22+
EnterPhone("Action: Enter Phone"),
23+
VerifyPhone("Action: Verify Phone"),
24+
ConfirmAccessKey("Action: Confirm Access Key"),
25+
CompletedOnboarding("Action: Completed Onboarding"),
26+
}
27+
28+
enum class ActionSource(val value: String) {
29+
AccessKeySaved("Saved to Photos"),
30+
AccessKeyWroteDown("Wrote it Down")
31+
}
32+
2033
@Singleton
2134
class AnalyticsManager @Inject constructor(
2235
private val mixpanelAPI: MixpanelAPI
@@ -261,17 +274,32 @@ class AnalyticsManager @Inject constructor(
261274
)
262275
}
263276

277+
override fun action(action: Action, source: ActionSource?) {
278+
track(
279+
action = action,
280+
properties = source?.let { arrayOf(Property.Source to it.value) }.orEmpty()
281+
)
282+
}
283+
264284
private fun track(event: Name, vararg properties: Pair<Property, String>) {
285+
track(name = event.value, properties = properties)
286+
}
287+
288+
private fun track(action: Action, vararg properties: Pair<Property, String>) {
289+
track(name = action.value, properties = properties)
290+
}
291+
292+
private fun track(name: String, vararg properties: Pair<Property, String>) {
265293
if (BuildConfig.DEBUG) {
266-
Timber.d("debug track $event, ${properties.map { "${it.first.name}, ${it.second}" }}")
294+
Timber.d("debug track $name, ${properties.map { "${it.first.name}, ${it.second}" }}")
267295
return
268296
} //no logging in debug
269297

270298
val jsonObject = JSONObject()
271299
properties.forEach { property ->
272300
jsonObject.put(property.first.value, property.second)
273301
}
274-
mixpanelAPI.track(event.value, jsonObject)
302+
mixpanelAPI.track(name, jsonObject)
275303
}
276304

277305
enum class Name(val value: String) {
@@ -343,6 +371,8 @@ class AnalyticsManager @Inject constructor(
343371
VoidingSend("Voiding Send"),
344372

345373
PercentDelta("Percent Delta"),
374+
375+
Source("Source"),
346376
}
347377

348378
enum class BillPresentationStyle(val value: String) {

api/src/main/java/com/getcode/analytics/AnalyticsService.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ interface AnalyticsService {
4949
fun unintentionalLogout()
5050

5151
fun appSettingToggled(setting: AppSetting, value: Boolean)
52+
53+
fun action(action: Action, source: ActionSource? = null)
5254
}
5355

5456
class AnalyticsServiceNull : AnalyticsService {
@@ -92,4 +94,5 @@ class AnalyticsServiceNull : AnalyticsService {
9294
override fun backgroundSwapInitiated() = Unit
9395
override fun unintentionalLogout() = Unit
9496
override fun appSettingToggled(setting: AppSetting, value: Boolean) = Unit
97+
override fun action(action: Action, source: ActionSource?) = Unit
9598
}

app/src/main/java/com/getcode/navigation/screens/LoginScreens.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import androidx.compose.ui.res.stringResource
77
import cafe.adriel.voyager.core.screen.ScreenKey
88
import cafe.adriel.voyager.core.screen.uniqueScreenKey
99
import cafe.adriel.voyager.hilt.getViewModel
10+
import com.getcode.LocalAnalytics
1011
import com.getcode.R
12+
import com.getcode.analytics.Action
13+
import com.getcode.analytics.AnalyticsManager
1114
import com.getcode.navigation.core.LocalCodeNavigator
1215
import com.getcode.ui.utils.getStackScopedViewModel
1316
import com.getcode.view.login.AccessKey
@@ -35,11 +38,14 @@ data class LoginScreen(val seed: String? = null) : LoginGraph {
3538
@Composable
3639
override fun Content() {
3740
val navigator = LocalCodeNavigator.current
41+
val analytics = LocalAnalytics.current
42+
3843
if (seed != null) {
3944
SeedDeepLink(getViewModel(), seed)
4045
} else {
4146
LoginHome(
4247
createAccount = {
48+
analytics.action(Action.CreateAccount)
4349
navigator.push(LoginPhoneVerificationScreen(isNewAccount = true))
4450
},
4551
login = {
@@ -165,7 +171,7 @@ sealed interface CodeLoginPermission: Parcelable {
165171
}
166172

167173
@Parcelize
168-
data class PermissionRequestScreen(val permission: CodeLoginPermission) : LoginGraph {
174+
data class PermissionRequestScreen(val permission: CodeLoginPermission, val fromOnboarding: Boolean = false) : LoginGraph {
169175

170176
@IgnoredOnParcel
171177
override val key: ScreenKey = uniqueScreenKey
@@ -174,11 +180,11 @@ data class PermissionRequestScreen(val permission: CodeLoginPermission) : LoginG
174180
override fun Content() {
175181
when (permission) {
176182
CodeLoginPermission.Camera -> {
177-
CameraPermission()
183+
CameraPermission(fromOnboarding = fromOnboarding)
178184
}
179185

180186
CodeLoginPermission.Notifications -> {
181-
NotificationPermission()
187+
NotificationPermission(fromOnboarding = fromOnboarding)
182188
}
183189
}
184190

app/src/main/java/com/getcode/view/login/AccessKeyViewModel.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package com.getcode.view.login
33
import android.Manifest
44
import android.annotation.SuppressLint
55
import android.os.Build
6+
import com.getcode.analytics.Action
7+
import com.getcode.analytics.ActionSource
8+
import com.getcode.analytics.AnalyticsManager
69
import com.getcode.analytics.AnalyticsService
710
import com.getcode.manager.AuthManager
811
import com.getcode.media.MediaScanner
@@ -45,14 +48,18 @@ class AccessKeyViewModel @Inject constructor(
4548
.concatWith(
4649
if (isSaveImage) {
4750
Completable.create { c ->
51+
analytics.action(Action.ConfirmAccessKey, source = ActionSource.AccessKeySaved)
4852
val result = saveBitmapToFile()
4953
if (result) c.onComplete() else c.onError(IllegalStateException())
5054
}.subscribeOn(Schedulers.computation())
5155
} else {
56+
analytics.action(Action.ConfirmAccessKey, source = ActionSource.AccessKeyWroteDown)
5257
Completable.complete()
5358
}
5459
)
5560
.doOnComplete {
61+
val owner = mnemonicManager.getKeyPair(entropyB64)
62+
analytics.createAccount(true, owner.getPublicKeyBase58())
5663
uiFlow.value = uiFlow.value.copy(isLoading = false, isSuccess = true)
5764
}
5865
.delay(2L, TimeUnit.SECONDS)
@@ -71,24 +78,23 @@ class AccessKeyViewModel @Inject constructor(
7178
}
7279

7380
private fun onComplete(navigator: CodeNavigator, entropyB64: String) {
74-
val owner = mnemonicManager.getKeyPair(entropyB64)
75-
analytics.createAccount(true, owner.getPublicKeyBase58())
76-
7781
val cameraPermissionDenied = permissions.isDenied(Manifest.permission.CAMERA)
7882

7983
if (cameraPermissionDenied) {
80-
navigator.push(PermissionRequestScreen(CodeLoginPermission.Camera))
84+
navigator.push(PermissionRequestScreen(CodeLoginPermission.Camera, true))
8185
} else {
8286
if (Build.VERSION.SDK_INT < 33) {
87+
analytics.action(Action.CompletedOnboarding)
8388
navigator.replaceAll(HomeScreen())
8489
} else {
8590
val notificationsPermissionDenied = permissions.isDenied(
8691
Manifest.permission.POST_NOTIFICATIONS
8792
)
8893

8994
if (notificationsPermissionDenied) {
90-
navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications))
95+
navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications, true))
9196
} else {
97+
analytics.action(Action.CompletedOnboarding)
9298
navigator.replaceAll(HomeScreen())
9399
}
94100
}

app/src/main/java/com/getcode/view/login/CameraPermission.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import androidx.compose.ui.res.painterResource
1010
import androidx.compose.ui.res.stringResource
1111
import androidx.compose.ui.text.style.TextAlign
1212
import androidx.constraintlayout.compose.ConstraintLayout
13+
import com.getcode.LocalAnalytics
1314
import com.getcode.R
15+
import com.getcode.analytics.Action
1416
import com.getcode.navigation.screens.CodeLoginPermission
1517
import com.getcode.navigation.core.CodeNavigator
1618
import com.getcode.navigation.screens.HomeScreen
@@ -21,16 +23,20 @@ import com.getcode.ui.components.ButtonState
2123
import com.getcode.ui.components.CodeButton
2224

2325
@Composable
24-
fun CameraPermission(navigator: CodeNavigator = LocalCodeNavigator.current) {
26+
fun CameraPermission(navigator: CodeNavigator = LocalCodeNavigator.current, fromOnboarding: Boolean = false) {
2527
var isResultHandled by remember { mutableStateOf(false) }
28+
val analytics = LocalAnalytics.current
2629
val onNotificationResult: (Boolean) -> Unit = { isGranted ->
2730
if (!isResultHandled) {
2831
isResultHandled = true
2932

3033
if (isGranted) {
34+
if (fromOnboarding) {
35+
analytics.action(Action.CompletedOnboarding)
36+
}
3137
navigator.replaceAll(HomeScreen())
3238
} else {
33-
navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications))
39+
navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications, fromOnboarding))
3440
}
3541
}
3642
}

app/src/main/java/com/getcode/view/login/NotificationPermission.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import androidx.compose.ui.res.painterResource
1010
import androidx.compose.ui.res.stringResource
1111
import androidx.compose.ui.text.style.TextAlign
1212
import androidx.constraintlayout.compose.ConstraintLayout
13+
import com.getcode.LocalAnalytics
1314
import com.getcode.R
15+
import com.getcode.analytics.Action
1416
import com.getcode.navigation.core.CodeNavigator
1517
import com.getcode.navigation.screens.HomeScreen
1618
import com.getcode.navigation.core.LocalCodeNavigator
@@ -19,9 +21,13 @@ import com.getcode.ui.components.ButtonState
1921
import com.getcode.ui.components.CodeButton
2022

2123
@Composable
22-
fun NotificationPermission(navigator: CodeNavigator = LocalCodeNavigator.current) {
24+
fun NotificationPermission(navigator: CodeNavigator = LocalCodeNavigator.current, fromOnboarding: Boolean = false) {
25+
val analytics = LocalAnalytics.current
2326
val onNotificationResult: (Boolean) -> Unit = { isGranted ->
2427
if (isGranted) {
28+
if (fromOnboarding) {
29+
analytics.action(Action.CompletedOnboarding)
30+
}
2531
navigator.replaceAll(HomeScreen())
2632
}
2733
}

app/src/main/java/com/getcode/view/login/PhoneConfirmViewModel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import com.codeinc.gen.phone.v1.PhoneVerificationService
77
import com.codeinc.gen.user.v1.IdentityService
88
import com.getcode.App
99
import com.getcode.R
10+
import com.getcode.analytics.Action
11+
import com.getcode.analytics.AnalyticsManager
12+
import com.getcode.analytics.AnalyticsService
1013
import com.getcode.ed25519.Ed25519
1114
import com.getcode.manager.MnemonicManager
1215
import com.getcode.manager.SessionManager
@@ -60,6 +63,7 @@ data class PhoneConfirmUiModel(
6063

6164
@HiltViewModel
6265
class PhoneConfirmViewModel @Inject constructor(
66+
private val analytics: AnalyticsService,
6367
private val identityRepository: IdentityRepository,
6468
private val phoneRepository: PhoneRepository,
6569
private val phoneUtils: PhoneUtils,
@@ -211,6 +215,7 @@ class PhoneConfirmViewModel @Inject constructor(
211215
return if (isOtpValid) {
212216
Single.just(true)
213217
} else {
218+
analytics.action(Action.VerifyPhone)
214219
phoneRepository.checkVerificationCode(phoneNumber, otpInput)
215220
.map { res ->
216221
when (res) {

app/src/main/java/com/getcode/view/login/PhoneVerifyViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import androidx.compose.ui.text.TextRange
66
import androidx.compose.ui.text.input.TextFieldValue
77
import com.codeinc.gen.phone.v1.PhoneVerificationService
88
import com.getcode.R
9+
import com.getcode.analytics.Action
10+
import com.getcode.analytics.AnalyticsManager
11+
import com.getcode.analytics.AnalyticsService
912
import com.getcode.manager.TopBarManager
1013
import com.getcode.navigation.core.CodeNavigator
1114
import com.getcode.navigation.screens.LoginPhoneConfirmationScreen
@@ -49,6 +52,7 @@ data class PhoneVerifyUiModel(
4952

5053
@HiltViewModel
5154
class PhoneVerifyViewModel @Inject constructor(
55+
private val analytics: AnalyticsService,
5256
private val phoneRepository: PhoneRepository,
5357
private val phoneUtils: PhoneUtils,
5458
private val resources: ResourceHelper,
@@ -165,7 +169,10 @@ class PhoneVerifyViewModel @Inject constructor(
165169
phoneRepository.sendVerificationCode(phoneNumber)
166170
.firstElement()
167171
.observeOn(AndroidSchedulers.mainThread())
168-
.doOnSubscribe { setIsLoading(true) }
172+
.doOnSubscribe {
173+
analytics.action(Action.EnterPhone)
174+
setIsLoading(true)
175+
}
169176
.doOnComplete { setIsLoading(false) }
170177
.map { res ->
171178
when (res) {

0 commit comments

Comments
 (0)