Skip to content

Commit c254337

Browse files
authored
Add sample appsflyer destination plugin (#44)
* add appsflyer * update appsflyer tests * added implementation yo main app * update tests and fix implementation
1 parent 3341857 commit c254337

File tree

4 files changed

+593
-0
lines changed

4 files changed

+593
-0
lines changed

samples/kotlin-android-app-destinations/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ dependencies {
7272
// Intercom
7373
implementation 'io.intercom.android:intercom-sdk-base:10.1.1'
7474
implementation 'io.intercom.android:intercom-sdk-fcm:10.1.1'
75+
76+
// appsflyer
77+
implementation 'com.appsflyer:af-android-sdk:6.3.2'
78+
implementation 'com.android.installreferrer:installreferrer:2.2'
7579
}
7680

7781
// Test Dependencies

samples/kotlin-android-app-destinations/src/main/java/com/segment/analytics/destinations/MainApplication.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Application
44
import com.segment.analytics.destinations.plugins.*
55
import com.segment.analytics.kotlin.android.Analytics
66
import com.segment.analytics.kotlin.core.Analytics
7+
import com.segment.analytics.kotlin.core.platform.plugins.log
78
import java.util.concurrent.Executors
89

910
class MainApplication : Application() {
@@ -40,5 +41,29 @@ class MainApplication : Application() {
4041

4142
// Try out Intercom destination
4243
analytics.add(IntercomDestination(this))
44+
45+
val appsflyerDestination = AppsFlyerDestination(applicationContext, true)
46+
analytics.add(appsflyerDestination)
47+
48+
appsflyerDestination.conversionListener =
49+
object : AppsFlyerDestination.ExternalAppsFlyerConversionListener {
50+
override fun onConversionDataSuccess(map: Map<String, Any>) {
51+
// Process Deferred Deep Linking here
52+
for (attrName in map.keys) {
53+
analytics.log("Appsflyer: attribute: " + attrName + " = " + map[attrName])
54+
}
55+
}
56+
57+
override fun onConversionDataFail(s: String?) {}
58+
override fun onAppOpenAttribution(map: Map<String, String>) {
59+
// Process Direct Deep Linking here
60+
for (attrName in map.keys) {
61+
analytics.log("Appsflyer: attribute: " + attrName + " = " + map[attrName])
62+
}
63+
}
64+
65+
override fun onAttributionFailure(s: String?) {}
66+
}
67+
4368
}
4469
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package com.segment.analytics.destinations.plugins
2+
3+
import android.app.Activity
4+
import android.content.Context
5+
import com.segment.analytics.kotlin.core.*
6+
import com.segment.analytics.kotlin.core.platform.DestinationPlugin
7+
import com.segment.analytics.kotlin.core.platform.Plugin
8+
import kotlinx.serialization.Serializable
9+
import com.appsflyer.AppsFlyerLib
10+
import com.appsflyer.AppsFlyerConversionListener
11+
import android.content.SharedPreferences
12+
import android.os.Bundle
13+
import com.appsflyer.AFInAppEventParameterName
14+
import com.segment.analytics.kotlin.android.plugins.AndroidLifecycle
15+
import com.segment.analytics.kotlin.core.platform.plugins.LogType
16+
import com.segment.analytics.kotlin.core.platform.plugins.log
17+
import com.appsflyer.deeplink.DeepLinkListener
18+
import com.segment.analytics.kotlin.core.utilities.getString
19+
import com.segment.analytics.kotlin.core.utilities.mapTransform
20+
import com.segment.analytics.kotlin.core.utilities.toContent
21+
import kotlinx.serialization.json.*
22+
23+
/*
24+
This is an example of the AppsFlyer device-mode destination plugin that can be integrated with
25+
Segment analytics.
26+
Note: This plugin is NOT SUPPORTED by Segment. It is here merely as an example,
27+
and for your convenience should you find it useful.
28+
To use it in your codebase, we suggest copying this file over and include the following
29+
dependencies in your `build.gradle` file:
30+
```
31+
dependencies {
32+
...
33+
implementation 'com.appsflyer:af-android-sdk:6.3.2'
34+
implementation 'com.android.installreferrer:installreferrer:2.2'
35+
}
36+
```
37+
MIT License
38+
Copyright (c) 2021 Segment
39+
Permission is hereby granted, free of charge, to any person obtaining a copy
40+
of this software and associated documentation files (the "Software"), to deal
41+
in the Software without restriction, including without limitation the rights
42+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
43+
copies of the Software, and to permit persons to whom the Software is
44+
furnished to do so, subject to the following conditions:
45+
The above copyright notice and this permission notice shall be included in all
46+
copies or substantial portions of the Software.
47+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
48+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
49+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
50+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
51+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
52+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
53+
SOFTWARE.
54+
*/
55+
56+
@Serializable
57+
data class AppsFlyerSettings(
58+
var appsFlyerDevKey: String,
59+
var trackAttributionData: Boolean = false
60+
)
61+
62+
class AppsFlyerDestination(
63+
private val applicationContext: Context,
64+
private var isDebug: Boolean = false
65+
) : DestinationPlugin(), AndroidLifecycle {
66+
67+
internal var settings: AppsFlyerSettings? = null
68+
internal var appsflyer: AppsFlyerLib? = null
69+
70+
internal var customerUserId: String = ""
71+
internal var currencyCode: String = ""
72+
var conversionListener: ExternalAppsFlyerConversionListener? = null
73+
var deepLinkListener: ExternalDeepLinkListener? = null
74+
75+
override val key: String = "AppsFlyer"
76+
77+
override fun setup(analytics: Analytics) {
78+
super.setup(analytics)
79+
}
80+
81+
override fun update(settings: Settings, type: Plugin.UpdateType) {
82+
super.update(settings, type)
83+
if (settings.isDestinationEnabled(key)) {
84+
analytics.log("Appsflyer Destination is enabled")
85+
this.settings = settings.destinationSettings(key)
86+
if (type == Plugin.UpdateType.Initial) {
87+
appsflyer = AppsFlyerLib.getInstance()
88+
analytics.log("Appsflyer Destination loaded")
89+
var listener: AppsFlyerConversionListener? = null
90+
this.settings?.let {
91+
if (it.trackAttributionData) {
92+
listener = ConversionListener()
93+
}
94+
appsflyer?.setDebugLog(isDebug)
95+
appsflyer?.init(it.appsFlyerDevKey, listener, applicationContext)
96+
}
97+
}
98+
deepLinkListener?.let {
99+
appsflyer?.subscribeForDeepLink(it)
100+
}
101+
}
102+
}
103+
104+
override fun identify(payload: IdentifyEvent): BaseEvent? {
105+
val userId: String = payload.userId
106+
val traits: JsonObject = payload.traits
107+
108+
customerUserId = userId
109+
currencyCode = traits.getString("currencyCode") ?: ""
110+
111+
updateEndUserAttributes()
112+
113+
return payload
114+
}
115+
116+
override fun track(payload: TrackEvent): BaseEvent? {
117+
val event: String = payload.event
118+
val properties: Properties = payload.properties
119+
120+
val afProperties = properties.mapTransform(propertiesMapper).mapValues { (_, v) -> v.toContent() }
121+
122+
appsflyer?.logEvent(applicationContext, event, afProperties)
123+
analytics.log("appsflyer.logEvent(context, $event, $properties)", type = LogType.INFO)
124+
return payload
125+
}
126+
127+
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
128+
super.onActivityCreated(activity, savedInstanceState)
129+
if (activity != null) {
130+
AppsFlyerLib.getInstance().start(activity)
131+
analytics.log("AppsFlyerLib.getInstance().start($activity)")
132+
}
133+
updateEndUserAttributes()
134+
}
135+
136+
private fun updateEndUserAttributes() {
137+
appsflyer?.setCustomerUserId(customerUserId)
138+
analytics.log("appsflyer.setCustomerUserId($customerUserId)", type = LogType.INFO)
139+
appsflyer?.setCurrencyCode(currencyCode)
140+
analytics.log("appsflyer.setCurrencyCode($currencyCode)", type = LogType.INFO)
141+
appsflyer?.setDebugLog(isDebug)
142+
analytics.log("appsflyer.setDebugLog($isDebug)", type = LogType.INFO)
143+
}
144+
145+
146+
companion object {
147+
const val AF_SEGMENT_SHARED_PREF = "appsflyer-segment-data"
148+
const val CONV_KEY = "AF_onConversion_Data"
149+
}
150+
151+
private val propertiesMapper = mapOf(
152+
"revenue" to AFInAppEventParameterName.REVENUE,
153+
"price" to AFInAppEventParameterName.PRICE,
154+
"currency" to AFInAppEventParameterName.CURRENCY
155+
)
156+
157+
interface ExternalAppsFlyerConversionListener : AppsFlyerConversionListener
158+
interface ExternalDeepLinkListener : DeepLinkListener
159+
160+
inner class ConversionListener : AppsFlyerConversionListener {
161+
override fun onConversionDataSuccess(conversionData: Map<String, Any>) {
162+
if (!getFlag(CONV_KEY)) {
163+
trackInstallAttributed(conversionData)
164+
setFlag(CONV_KEY, true)
165+
}
166+
conversionListener?.onConversionDataSuccess(conversionData)
167+
}
168+
169+
override fun onConversionDataFail(errorMessage: String) {
170+
conversionListener?.onConversionDataFail(errorMessage)
171+
}
172+
173+
override fun onAppOpenAttribution(attributionData: Map<String, String>) {
174+
conversionListener?.onAppOpenAttribution(attributionData)
175+
}
176+
177+
override fun onAttributionFailure(errorMessage: String) {
178+
conversionListener?.onAttributionFailure(errorMessage)
179+
}
180+
181+
private fun convertToPrimitive(value: Any?): JsonElement {
182+
return when (value) {
183+
is Boolean -> JsonPrimitive(value)
184+
is Number -> JsonPrimitive(value)
185+
is String -> JsonPrimitive(value)
186+
is Map<*, *> -> buildJsonObject {
187+
value.forEach { (k, v) ->
188+
put(k.toString(), convertToPrimitive(v))
189+
}
190+
}
191+
is List<*> -> buildJsonArray {
192+
value.forEach { v ->
193+
add(convertToPrimitive(v))
194+
}
195+
}
196+
is Array<*> -> buildJsonArray {
197+
value.forEach { v ->
198+
add(convertToPrimitive(v))
199+
}
200+
}
201+
else -> JsonPrimitive(value.toString())
202+
}
203+
}
204+
205+
private fun trackInstallAttributed(attributionData: Map<String, Any>) {
206+
// See https://segment.com/docs/spec/mobile/#install-attributed.
207+
val properties = buildJsonObject {
208+
put("provider", key)
209+
attributionData.forEach { (k, v) ->
210+
if (k !in setOf("media_source", "adgroup")) {
211+
put(k, convertToPrimitive(v))
212+
}
213+
}
214+
put("campaign", buildJsonObject {
215+
put("source", convertToPrimitive(attributionData["media_source"]))
216+
put("name", convertToPrimitive(attributionData["campaign"]))
217+
put("ad_group", convertToPrimitive(attributionData["adgroup"]))
218+
})
219+
}
220+
221+
// If you are working with networks that don't allow passing user level data to 3rd parties,
222+
// you will need to apply code to filter out these networks before calling
223+
// `analytics.track("Install Attributed", properties);`
224+
analytics.track("Install Attributed", properties)
225+
}
226+
227+
private fun getFlag(key: String): Boolean {
228+
val sharedPreferences: SharedPreferences =
229+
applicationContext.getSharedPreferences(AF_SEGMENT_SHARED_PREF, 0)
230+
return sharedPreferences.getBoolean(key, false)
231+
}
232+
233+
private fun setFlag(key: String, value: Boolean) {
234+
val sharedPreferences: SharedPreferences =
235+
applicationContext.getSharedPreferences(AF_SEGMENT_SHARED_PREF, 0)
236+
val editor = sharedPreferences.edit()
237+
editor.putBoolean(key, value).apply()
238+
}
239+
}
240+
241+
}

0 commit comments

Comments
 (0)