Skip to content

Commit c1fe45c

Browse files
committed
Support CSAT forms
1 parent ccce738 commit c1fe45c

File tree

12 files changed

+1103
-1
lines changed

12 files changed

+1103
-1
lines changed

green/src/main/java/com/blockstream/green/data/Countly.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.content.SharedPreferences
66
import android.content.res.Configuration
77
import android.os.Parcelable
88
import androidx.core.content.edit
9+
import androidx.fragment.app.FragmentManager
910
import androidx.lifecycle.MutableLiveData
1011
import com.android.installreferrer.api.InstallReferrerClient
1112
import com.android.installreferrer.api.InstallReferrerStateListener
@@ -24,11 +25,15 @@ import com.blockstream.green.gdk.SessionManager
2425
import com.blockstream.green.settings.ApplicationSettings
2526
import com.blockstream.green.settings.SettingsManager
2627
import com.blockstream.green.ui.AppActivity
28+
import com.blockstream.green.ui.dialogs.CountlyNpsDialogFragment
29+
import com.blockstream.green.ui.dialogs.CountlySurveyDialogFragment
2730
import com.blockstream.green.utils.isDevelopmentFlavor
2831
import com.blockstream.green.utils.isDevelopmentOrDebug
2932
import com.blockstream.green.utils.isProductionFlavor
3033
import com.blockstream.green.utils.toList
3134
import com.blockstream.green.views.GreenAlertView
35+
import kotlinx.coroutines.flow.MutableStateFlow
36+
import kotlinx.coroutines.flow.asStateFlow
3237
import kotlinx.coroutines.flow.launchIn
3338
import kotlinx.coroutines.flow.onEach
3439
import kotlinx.parcelize.Parcelize
@@ -39,6 +44,8 @@ import kotlinx.serialization.json.buildJsonObject
3944
import kotlinx.serialization.json.put
4045
import ly.count.android.sdk.Countly
4146
import ly.count.android.sdk.CountlyConfig
47+
import ly.count.android.sdk.ModuleFeedback
48+
import ly.count.android.sdk.ModuleFeedback.CountlyFeedbackWidget
4249
import ly.count.android.sdk.RemoteConfigCallback
4350
import mu.NamedKLogging
4451
import java.net.URLDecoder
@@ -113,6 +120,7 @@ class Countly constructor(
113120
private val userProfile = countly.userProfile()
114121
private val remoteConfig = countly.remoteConfig()
115122
private val attribution = countly.attribution()
123+
private val feedback = countly.feedback()
116124

117125
private var analyticsConsent : Boolean by Delegates.observable(settingsManager.getApplicationSettings().analytics) { _, oldValue, newValue ->
118126
if(oldValue != newValue){
@@ -180,6 +188,40 @@ class Countly constructor(
180188
}
181189
}
182190
}
191+
updateFeedbackWidget()
192+
}
193+
194+
private val _feedbackWidgetStateFlow = MutableStateFlow<CountlyFeedbackWidget?>(null)
195+
val feedbackWidgetStateFlow get() = _feedbackWidgetStateFlow.asStateFlow()
196+
val feedbackWidget get() = _feedbackWidgetStateFlow.value
197+
198+
fun sendFeedbackWidgetData(widget: CountlyFeedbackWidget, data: Map<String, Any>?){
199+
feedback.reportFeedbackWidgetManually(widget, null, data)
200+
// can't use updateFeedback() as the data are sent async
201+
_feedbackWidgetStateFlow.value = null
202+
}
203+
204+
fun getFeedbackWidgetData(widget: CountlyFeedbackWidget, callback: (CountlyWidget?) -> Unit){
205+
countly.feedback().getFeedbackWidgetData(widget) { data, _ ->
206+
try{
207+
callback.invoke(GreenWallet.JsonDeserializer.decodeFromString<CountlyWidget>(data.toString()).also{
208+
it.widget = widget
209+
})
210+
211+
// Set it to null to hide it from UI, this way user can know that this is a temporary FAB
212+
_feedbackWidgetStateFlow.value = null
213+
}catch (e: Exception){
214+
logger.info { data.toString() }
215+
e.printStackTrace()
216+
callback.invoke(null)
217+
}
218+
}
219+
}
220+
221+
private fun updateFeedbackWidget(){
222+
countly.feedback().getAvailableFeedbackWidgets { countlyFeedbackWidgets, _ ->
223+
_feedbackWidgetStateFlow.value = countlyFeedbackWidgets?.firstOrNull()
224+
}
183225
}
184226

185227
fun handleReferrer(onComplete: (referrer: String) -> Unit) {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.blockstream.green.data
2+
3+
import com.blockstream.gdk.GAJson
4+
import kotlinx.serialization.SerialName
5+
import kotlinx.serialization.Serializable
6+
import ly.count.android.sdk.ModuleFeedback
7+
8+
@Serializable
9+
data class CountlyWidget(
10+
@SerialName("_id") val id: String,
11+
@SerialName("app_id") val appId: String,
12+
@Serializable(with = StringHtmlSerializer::class)
13+
@SerialName("name") val name: String,
14+
@SerialName("questions") val questions: List<Question>? = null,
15+
@SerialName("type") val type: SurveyType,
16+
@SerialName("msg") val msg: Messages,
17+
@SerialName("appearance") val appearance: Appearance,
18+
@SerialName("followUpType") val followUpType: FollowUpType? = null,
19+
) : GAJson<CountlyWidget>() {
20+
override val keepJsonElement: Boolean = true
21+
22+
override fun kSerializer() = serializer()
23+
24+
@kotlinx.serialization.Transient
25+
lateinit var widget : ModuleFeedback.CountlyFeedbackWidget
26+
27+
val text: Question? get() = questions?.find { it.type == "text" }
28+
val rating: Question? get() = questions?.find { it.type == "rating" }
29+
}
30+
31+
32+
@Serializable
33+
enum class FollowUpType{
34+
@SerialName("score") Score, @SerialName("one") One, @SerialName("none") None
35+
}
36+
37+
@Serializable
38+
enum class SurveyType{
39+
@SerialName("nps") NPS, @SerialName("survey") Survey
40+
}
41+
42+
@Serializable
43+
data class Messages(
44+
@Serializable(with = StringHtmlSerializer::class)
45+
@SerialName("thanks") val thanks: String,
46+
@Serializable(with = StringHtmlSerializer::class)
47+
@SerialName("mainQuestion") val mainQuestion: String = "",
48+
@Serializable(with = StringHtmlSerializer::class)
49+
@SerialName("followUpAll") val followUpAll: String = "",
50+
@Serializable(with = StringHtmlSerializer::class)
51+
@SerialName("followUpPromoter") val followUpPromoter: String = "",
52+
@Serializable(with = StringHtmlSerializer::class)
53+
@SerialName("followUpPassive") val followUpPassive: String = "",
54+
@Serializable(with = StringHtmlSerializer::class)
55+
@SerialName("followUpDetractor") val followUpDetractor: String = "",
56+
) : GAJson<Messages>() {
57+
override fun kSerializer() = serializer()
58+
}
59+
60+
@Serializable
61+
data class Appearance(
62+
@Serializable(with = StringHtmlSerializer::class)
63+
@SerialName("followUpInput") val followUpInput: String? = null,
64+
@Serializable(with = StringHtmlSerializer::class)
65+
@SerialName("notLikely") val notLikely: String? = null,
66+
@Serializable(with = StringHtmlSerializer::class)
67+
@SerialName("likely") val likely: String? = null,
68+
) : GAJson<Appearance>() {
69+
override fun kSerializer() = serializer()
70+
}
71+
72+
@Serializable
73+
data class Question(
74+
@SerialName("id") val id: String,
75+
@SerialName("type") val type: String,
76+
@Serializable(with = StringHtmlSerializer::class)
77+
@SerialName("question") val question: String,
78+
@SerialName("required") val required: Boolean = false,
79+
@Serializable(with = StringHtmlSerializer::class)
80+
@SerialName("followUpInput") val followUpInput: String? = null,
81+
@Serializable(with = StringHtmlSerializer::class)
82+
@SerialName("notLikely") val notLikely: String? = null,
83+
@Serializable(with = StringHtmlSerializer::class)
84+
@SerialName("likely") val likely: String? = null,
85+
) : GAJson<Question>() {
86+
override fun kSerializer() = serializer()
87+
}
88+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.blockstream.green.data
2+
3+
import androidx.core.text.TextUtilsCompat
4+
import com.blockstream.green.utils.fromHtml
5+
import kotlinx.serialization.KSerializer
6+
import kotlinx.serialization.descriptors.PrimitiveKind
7+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
8+
import kotlinx.serialization.descriptors.SerialDescriptor
9+
import kotlinx.serialization.encoding.Decoder
10+
import kotlinx.serialization.encoding.Encoder
11+
12+
object StringHtmlSerializer : KSerializer<String> {
13+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StringHtml", PrimitiveKind.STRING)
14+
override fun serialize(encoder: Encoder, value: String) = encoder.encodeString(TextUtilsCompat.htmlEncode(value))
15+
override fun deserialize(decoder: Decoder): String = decoder.decodeString().fromHtml().toString()
16+
}
17+

green/src/main/java/com/blockstream/green/ui/MainActivity.kt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,33 @@ import com.blockstream.green.database.WalletRepository
3434
import com.blockstream.green.databinding.MainActivityBinding
3535
import com.blockstream.green.gdk.SessionManager
3636
import com.blockstream.green.ui.devices.DeviceInfoBottomSheetDialogFragment
37+
import com.blockstream.green.ui.dialogs.CountlyNpsDialogFragment
38+
import com.blockstream.green.ui.dialogs.CountlySurveyDialogFragment
3739
import com.blockstream.green.ui.wallet.LoginFragment
38-
import com.blockstream.green.utils.*
40+
import com.blockstream.green.utils.AppKeystore
41+
import com.blockstream.green.utils.AuthenticationCallback
42+
import com.blockstream.green.utils.ConsumableEvent
43+
import com.blockstream.green.utils.getVersionName
44+
import com.blockstream.green.utils.handleException
45+
import com.blockstream.green.utils.isDevelopmentFlavor
46+
import com.blockstream.green.utils.navigate
3947
import com.blockstream.green.views.GreenToolbar
4048
import com.google.android.material.snackbar.Snackbar
4149
import dagger.hilt.android.AndroidEntryPoint
50+
import kotlinx.coroutines.delay
51+
import kotlinx.coroutines.flow.combine
52+
import kotlinx.coroutines.flow.distinctUntilChanged
53+
import kotlinx.coroutines.flow.launchIn
54+
import kotlinx.coroutines.flow.onEach
4255
import kotlinx.coroutines.launch
4356
import kotlinx.serialization.json.Json
4457
import kotlinx.serialization.json.jsonObject
4558
import kotlinx.serialization.json.jsonPrimitive
59+
import ly.count.android.sdk.ModuleFeedback
4660
import mu.KLogging
4761
import javax.inject.Inject
62+
import kotlin.time.DurationUnit
63+
import kotlin.time.toDuration
4864

4965

5066
@AndroidEntryPoint
@@ -210,6 +226,25 @@ class MainActivity : AppActivity() {
210226
showUnlockPrompt()
211227
}
212228

229+
combine(countly.feedbackWidgetStateFlow, navController.currentBackStackEntryFlow) { feedback, currentBackStackEntry ->
230+
(feedback != null && currentBackStackEntry.destination.id == R.id.overviewFragment)
231+
}.distinctUntilChanged().onEach {showDialog ->
232+
if(showDialog) {
233+
lifecycleScope.launchWhenResumed {
234+
// Delay a bit
235+
delay(1.toDuration(DurationUnit.SECONDS))
236+
237+
countly.feedbackWidget?.type.also { type ->
238+
if(type == ModuleFeedback.FeedbackWidgetType.nps){
239+
CountlyNpsDialogFragment.show(supportFragmentManager)
240+
}else if(type == ModuleFeedback.FeedbackWidgetType.survey){
241+
CountlySurveyDialogFragment.show(supportFragmentManager)
242+
}
243+
}
244+
}
245+
}
246+
}.launchIn(lifecycleScope)
247+
213248
navController.addOnDestinationChangedListener { _, destination, _ ->
214249

215250
getVisibleFragment()?.let {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.blockstream.green.ui.dialogs
2+
3+
import android.app.Dialog
4+
import android.os.Bundle
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import android.view.ViewGroup
8+
import android.view.WindowManager
9+
import androidx.databinding.ViewDataBinding
10+
import androidx.fragment.app.DialogFragment
11+
import androidx.fragment.app.FragmentManager
12+
import com.blockstream.green.data.Countly
13+
import com.blockstream.green.data.ScreenView
14+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
15+
import mu.KLogging
16+
import javax.inject.Inject
17+
18+
// Based on https://dev.to/bhullnatik/how-to-use-material-dialogs-with-dialogfragment-28i1
19+
abstract class AbstractDialogFragment<T : ViewDataBinding> : DialogFragment(), ScreenView{
20+
@Inject
21+
lateinit var countly: Countly
22+
23+
private var bindingOrNull: T? = null
24+
protected val binding: T get() = bindingOrNull!!
25+
26+
override var screenIsRecorded = false
27+
override val segmentation: HashMap<String, Any>? = null
28+
29+
open val isFullWidth: Boolean = false
30+
31+
abstract fun inflate(layoutInflater: LayoutInflater): T
32+
33+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
34+
return MaterialAlertDialogBuilder(requireContext())
35+
.setView(onCreateView(layoutInflater, null, savedInstanceState))
36+
.create()
37+
}
38+
39+
// Warning: onCreateView can be called multiple times
40+
final override fun onCreateView(
41+
inflater: LayoutInflater,
42+
container: ViewGroup?,
43+
savedInstanceState: Bundle?
44+
): View {
45+
return bindingOrNull?.root ?: inflate(inflater).also {
46+
bindingOrNull = it
47+
// binding.lifecycleOwner = viewLifecycleOwner
48+
// viewLifecycleOwner is unavailable for MaterialAlertDialogBuilder based dialogs
49+
binding.lifecycleOwner = parentFragment?.viewLifecycleOwner ?: activity
50+
}.root
51+
}
52+
53+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
54+
super.onViewCreated(view, savedInstanceState)
55+
56+
if(isFullWidth){
57+
dialog?.window?.apply {
58+
setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT)
59+
}
60+
}
61+
}
62+
63+
override fun onResume() {
64+
super.onResume()
65+
countly.screenView(this)
66+
}
67+
68+
companion object : KLogging() {
69+
fun show(instance: AbstractDialogFragment<*>, fragmentManager: FragmentManager){
70+
instance.show(fragmentManager, instance.javaClass.simpleName)
71+
}
72+
73+
// Open a single instance
74+
fun showSingle(instance: AbstractDialogFragment<*>, fragmentManager: FragmentManager){
75+
val tag = instance.javaClass.simpleName
76+
if (fragmentManager.findFragmentByTag(tag) == null) {
77+
show(instance, fragmentManager)
78+
} else {
79+
logger.info { "There is already an open instance of ${instance.javaClass.simpleName}" }
80+
}
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)