Skip to content

Commit 034ce50

Browse files
authored
Fix identity management to support Leanplum.start with different userId (#513)
1 parent 660c82f commit 034ce50

File tree

2 files changed

+118
-57
lines changed

2 files changed

+118
-57
lines changed

AndroidSDKCore/src/main/java/com/leanplum/migration/wrapper/CTWrapper.kt

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.leanplum.migration.wrapper
22

33
import android.app.Application
44
import android.content.Context
5-
import android.text.TextUtils
65
import com.clevertap.android.sdk.ActivityLifecycleCallback
76
import com.clevertap.android.sdk.CleverTapAPI
87
import com.clevertap.android.sdk.CleverTapInstanceConfig
@@ -15,14 +14,13 @@ import com.leanplum.migration.MigrationManager
1514
import com.leanplum.migration.push.FcmMigrationHandler
1615
import com.leanplum.migration.push.HmsMigrationHandler
1716
import com.leanplum.migration.push.MiPushMigrationHandler
18-
import com.leanplum.utils.StringPreferenceNullable
1917

2018
internal class CTWrapper(
2119
private val accountId: String,
2220
private val accountToken: String,
2321
private val accountRegion: String,
24-
private val deviceId: String,
25-
private var userId: String?
22+
deviceId: String,
23+
userId: String?
2624
) : IWrapper by StaticMethodsWrapper {
2725

2826
override val fcmHandler: FcmMigrationHandler = FcmMigrationHandler()
@@ -32,36 +30,7 @@ internal class CTWrapper(
3230
private var cleverTapInstance: CleverTapAPI? = null
3331
private var instanceCallback: CleverTapInstanceCallback? = null
3432

35-
/**
36-
* Anonymous data will be merged to first user that logs-in, but CT ID will remain the same as
37-
* the anonymous' deviceId to allow the merge.
38-
*/
39-
private var firstLoginUserId: String? by StringPreferenceNullable("ct_first_login_userid")
40-
41-
/**
42-
* Needs to be calculated each time.
43-
*/
44-
private fun cleverTapId(): String {
45-
return when (userId) {
46-
null -> deviceId
47-
deviceId -> deviceId
48-
firstLoginUserId -> deviceId
49-
else -> "${deviceId}_${userId}"
50-
}
51-
}
52-
53-
/**
54-
* Needs to be calculated each time.
55-
*/
56-
private fun identity(): String {
57-
return when (val userId = userId) {
58-
null -> deviceId
59-
deviceId -> deviceId
60-
else -> userId
61-
}
62-
}
63-
64-
private fun isAnonymous() = userId == deviceId
33+
private var identityManager = IdentityManager(deviceId, userId ?: deviceId)
6534

6635
override fun launch(context: Context, callback: CleverTapInstanceCallback?) {
6736
instanceCallback = callback
@@ -74,19 +43,18 @@ internal class CTWrapper(
7443
enableCustomCleverTapId = true
7544
}
7645

77-
val cleverTapId = cleverTapId()
78-
val identity = identity()
46+
val cleverTapId = identityManager.cleverTapId()
47+
val profile = identityManager.profile()
7948
Log.d("Wrapper: using CleverTapID=__h$cleverTapId")
8049

8150
cleverTapInstance = CleverTapAPI.instanceWithConfig(context, config, cleverTapId)?.apply {
8251
setLibrary("Leanplum")
8352
if (!ActivityLifecycleCallback.registered) {
8453
ActivityLifecycleCallback.register(context.applicationContext as? Application)
8554
}
86-
if (isAnonymous()) {
55+
if (identityManager.isAnonymous()) {
8756
Log.d("Wrapper: identity not set for anonymous user")
8857
} else {
89-
val profile: Map<String, Any> = mutableMapOf(MigrationConstants.IDENTITY to identity)
9058
Log.d("Wrapper: will call onUserLogin with $profile and __h$cleverTapId")
9159
onUserLogin(profile, cleverTapId)
9260
}
@@ -110,25 +78,16 @@ internal class CTWrapper(
11078
}
11179

11280
override fun setUserId(userId: String?) {
113-
if (TextUtils.isEmpty(userId)) return
114-
if (this.userId == userId) return
115-
116-
val wasAnonymous = isAnonymous()
117-
this.userId = userId
118-
119-
val identity = identity()
120-
val profile: Map<String, Any> = mutableMapOf(MigrationConstants.IDENTITY to identity)
121-
122-
if (wasAnonymous) {
123-
firstLoginUserId = userId
124-
Log.d("Wrapper: anonymous data will be merged to $firstLoginUserId")
125-
Log.d("Wrapper: Leanplum.setUserId will call onUserLogin with $profile and __h$deviceId")
126-
cleverTapInstance?.onUserLogin(profile, deviceId)
127-
} else {
128-
val cleverTapId = cleverTapId()
129-
Log.d("Wrapper: Leanplum.setUserId will call onUserLogin with $profile and __h$cleverTapId")
130-
cleverTapInstance?.onUserLogin(profile, cleverTapId)
131-
}
81+
if (userId == null || userId.isEmpty()) return
82+
if (identityManager.getUserId() == userId) return
83+
84+
identityManager.setUserId(userId)
85+
86+
val cleverTapId = identityManager.cleverTapId()
87+
val profile = identityManager.profile()
88+
89+
Log.d("Wrapper: Leanplum.setUserId will call onUserLogin with $profile and __h$cleverTapId")
90+
cleverTapInstance?.onUserLogin(profile, cleverTapId)
13291
}
13392

13493
/**
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.leanplum.migration.wrapper
2+
3+
import com.leanplum.internal.Log
4+
import com.leanplum.migration.MigrationConstants
5+
import com.leanplum.utils.StringPreference
6+
import com.leanplum.utils.StringPreferenceNullable
7+
8+
/**
9+
* Scheme for migrating user profile is as follows:
10+
* - anonymous is translated to <CTID=deviceId, Identity=null>
11+
* - non-anonymous to <CTID=deviceId_userId, Identity=userId>
12+
*
13+
* When you login, but previous profile is anonymous, a merge should happen. CT SDK allows merges
14+
* only when the CTID remains the same, meaning that the merged profile would get the anonymous
15+
* profile's CTID. This is the reason to save anonymousMergeUserId into SharedPreferences and to
16+
* allow it to restore when user is back.
17+
*
18+
* When you call Leanplum.start and pass userId, that is not currently logged in, there are several
19+
* cases that could happen:
20+
*
21+
* 1. "undefined" state
22+
*
23+
* Wrapper hasn't been started even once, meaning that anonymous profile doesn't exist, so use the
24+
* "deviceId_userId" scheme.
25+
*
26+
* 2. "anonymous" state
27+
*
28+
* Wrapper has been started and previous user is anonymous - use deviceId as CTID to allow merge of
29+
* anonymous data.
30+
*
31+
* 3. "identified" state
32+
*
33+
* Wrapper has been started and previous user is not anonymous - use the "deviceId_userId" scheme.
34+
*/
35+
internal class IdentityManager(
36+
private val deviceId: String,
37+
private var userId: String
38+
) {
39+
40+
companion object {
41+
private const val UNDEFINED = "undefined"
42+
private const val ANONYMOUS = "anonymous"
43+
private const val IDENTIFIED = "identified"
44+
45+
private var anonymousMergeUserId: String? by StringPreferenceNullable("ct_anon_merge_userid")
46+
private var state: String by StringPreference("ct_login_state", UNDEFINED)
47+
}
48+
49+
init {
50+
if (isAnonymous()) {
51+
loginAnonymously()
52+
} else {
53+
loginIdentified()
54+
}
55+
}
56+
57+
fun isAnonymous() = userId == deviceId
58+
59+
private fun loginAnonymously() {
60+
state = ANONYMOUS
61+
}
62+
63+
private fun loginIdentified() {
64+
if (state == UNDEFINED) {
65+
state = IDENTIFIED
66+
}
67+
else if (state == ANONYMOUS) {
68+
anonymousMergeUserId = userId
69+
Log.d("Wrapper: anonymous data will be merged to $anonymousMergeUserId")
70+
state = IDENTIFIED
71+
}
72+
}
73+
74+
fun cleverTapId(): String {
75+
return when (userId) {
76+
deviceId -> deviceId
77+
anonymousMergeUserId -> deviceId
78+
else -> "${deviceId}_${userId}"
79+
}
80+
}
81+
82+
private fun identity(): String {
83+
return when (val userId = userId) {
84+
deviceId -> deviceId
85+
else -> userId
86+
}
87+
}
88+
89+
fun profile() = mapOf(MigrationConstants.IDENTITY to identity())
90+
91+
fun setUserId(userId: String) {
92+
if (state == ANONYMOUS) {
93+
anonymousMergeUserId = userId
94+
Log.d("Wrapper: anonymous data will be merged to $anonymousMergeUserId")
95+
state = IDENTIFIED
96+
}
97+
this.userId = userId
98+
}
99+
100+
fun getUserId() = userId
101+
102+
}

0 commit comments

Comments
 (0)