Skip to content

Commit

Permalink
fix: migrate in-app module from gist to CIO (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shahroz16 committed Jul 12, 2023
1 parent 1e289d7 commit d6fda6d
Show file tree
Hide file tree
Showing 33 changed files with 1,607 additions and 20 deletions.
4 changes: 0 additions & 4 deletions build.gradle
Expand Up @@ -32,10 +32,6 @@ allprojects {
repositories {
google()
mavenCentral()
// added for gist SDK
maven {
url 'https://maven.gist.build'
}
}
}

Expand Down
Expand Up @@ -23,7 +23,6 @@ object Dependencies {
const val hiltGradlePlugin = "com.google.dagger:hilt-android-gradle-plugin:${Versions.HILT}"
const val firebaseMessaging =
"com.google.firebase:firebase-messaging:${Versions.FIREBASE_MESSAGING}"
const val gist = "build:gist:${Versions.GIST}"
const val googlePlayServicesBase =
"com.google.android.gms:play-services-base:${Versions.GOOGLE_PLAY_SERVICES_BASE}"
const val googleServicesPlugin =
Expand Down
1 change: 0 additions & 1 deletion buildSrc/src/main/kotlin/io.customer/android/Versions.kt
Expand Up @@ -15,7 +15,6 @@ object Versions {
internal const val FIREBASE_MESSAGING = "23.1.0"
internal const val GRADLE_NEXUS_PUBLISH_PLUGIN = "1.1.0"
internal const val GRADLE_VERSIONS_PLUGIN = "0.39.0"
internal const val GIST = "3.2.2"
internal const val GOOGLE_PLAY_SERVICES_BASE = "17.6.0"
internal const val GOOGLE_SERVICES_PLUGIN = "4.3.15"
const val HILT = "2.44.2"
Expand Down
239 changes: 238 additions & 1 deletion messaginginapp/api/messaginginapp.api

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions messaginginapp/build.gradle
Expand Up @@ -34,6 +34,9 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
Expand All @@ -49,13 +52,17 @@ dependencies {
api project(":base")
api project(":sdk")

implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"

implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

implementation Dependencies.coroutinesCore
implementation Dependencies.coroutinesAndroid
implementation Dependencies.androidxAppCompat
implementation Dependencies.retrofit
api Dependencies.gist
implementation Dependencies.retrofitMoshiConverter
implementation Dependencies.okhttpLoggingInterceptor
testImplementation Dependencies.androidxTestJunit
androidTestImplementation Dependencies.junit4

}
6 changes: 6 additions & 0 deletions messaginginapp/src/main/AndroidManifest.xml
Expand Up @@ -2,4 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.customer.messaginginapp">

<application>
<activity
android:name=".gist.presentation.GistModalActivity"
android:theme="@style/GistActivityTheme" />
</application>

</manifest>
@@ -0,0 +1,27 @@
package io.customer.messaginginapp.gist

interface GistEnvironmentEndpoints {
fun getGistQueueApiUrl(): String
fun getEngineApiUrl(): String
fun getGistRendererUrl(): String
}

enum class GistEnvironment : GistEnvironmentEndpoints {
DEV {
override fun getGistQueueApiUrl() = "https://gist-queue-consumer-api.cloud.dev.gist.build"
override fun getEngineApiUrl() = "https://engine.api.dev.gist.build"
override fun getGistRendererUrl() = "https://renderer.gist.build/2.0"
},

LOCAL {
override fun getGistQueueApiUrl() = "http://queue.api.local.gist.build:86"
override fun getEngineApiUrl() = "http://engine.api.local.gist.build:82"
override fun getGistRendererUrl() = "https://renderer.gist.build/2.0"
},

PROD {
override fun getGistQueueApiUrl() = "https://gist-queue-consumer-api.cloud.gist.build"
override fun getEngineApiUrl() = "https://engine.api.gist.build"
override fun getGistRendererUrl() = "https://renderer.gist.build/2.0"
}
}
@@ -0,0 +1,9 @@
package io.customer.messaginginapp.gist.data

class NetworkUtilities {
companion object {
internal const val CIO_SITE_ID_HEADER = "X-CIO-Site-Id"
internal const val USER_TOKEN_HEADER = "X-Gist-Encoded-User-Token"
internal const val CIO_DATACENTER_HEADER = "X-CIO-Datacenter"
}
}
@@ -0,0 +1,185 @@
package io.customer.messaginginapp.gist.data.listeners

import android.util.Base64
import android.util.Log
import io.customer.messaginginapp.gist.data.NetworkUtilities
import io.customer.messaginginapp.gist.data.model.GistMessageProperties
import io.customer.messaginginapp.gist.data.model.Message
import io.customer.messaginginapp.gist.data.repository.GistQueueService
import io.customer.messaginginapp.gist.presentation.GIST_TAG
import io.customer.messaginginapp.gist.presentation.GistListener
import io.customer.messaginginapp.gist.presentation.GistSdk
import java.util.regex.PatternSyntaxException
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class Queue : GistListener {

private var localMessageStore: MutableList<Message> = mutableListOf()

init {
GistSdk.addListener(this)
}

private val gistQueueService by lazy {
val httpClient: OkHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
GistSdk.getUserToken()?.let { userToken ->
val request: Request = chain.request().newBuilder()
.addHeader(NetworkUtilities.CIO_SITE_ID_HEADER, GistSdk.siteId)
.addHeader(NetworkUtilities.CIO_DATACENTER_HEADER, GistSdk.dataCenter)
.addHeader(
NetworkUtilities.USER_TOKEN_HEADER,
// The NO_WRAP flag will omit all line terminators (i.e., the output will be on one long line).
Base64.encodeToString(userToken.toByteArray(), Base64.NO_WRAP)
)
.build()

chain.proceed(request)
} ?: run {
val request: Request = chain.request().newBuilder()
.addHeader(NetworkUtilities.CIO_SITE_ID_HEADER, GistSdk.siteId)
.addHeader(NetworkUtilities.CIO_DATACENTER_HEADER, GistSdk.dataCenter)
.build()

chain.proceed(request)
}
}
.build()

Retrofit.Builder()
.baseUrl(GistSdk.gistEnvironment.getGistQueueApiUrl())
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient)
.build()
.create(GistQueueService::class.java)
}

internal fun fetchUserMessagesFromLocalStore() {
handleMessages(localMessageStore)
}

internal fun clearUserMessagesFromLocalStore() {
localMessageStore.clear()
}

internal fun fetchUserMessages() {
GlobalScope.launch {

Check warning on line 71 in messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt

View workflow job for this annotation

GitHub Actions / Unit tests (messaginginapp)

This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.

Check warning on line 71 in messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt

View workflow job for this annotation

GitHub Actions / Android Lint (messaginginapp)

This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.

Check warning on line 71 in messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt

View workflow job for this annotation

GitHub Actions / instrumentation-test (java_layout)

This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.

Check warning on line 71 in messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt

View workflow job for this annotation

GitHub Actions / instrumentation-test (kotlin_compose)

This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.
try {
Log.i(GIST_TAG, "Fetching user messages")
val latestMessagesResponse = gistQueueService.fetchMessagesForUser()
// If there's no change (304), move on.
if (latestMessagesResponse.code() == 304) { return@launch }

// To prevent us from showing expired / revoked messages, clear user messages from local queue.
clearUserMessagesFromLocalStore()
if (latestMessagesResponse.code() == 204) {
// No content, don't do anything
Log.i(GIST_TAG, "No messages found for user")
} else if (latestMessagesResponse.isSuccessful) {
Log.i(
GIST_TAG,
"Found ${latestMessagesResponse.body()?.count()} messages for user"
)
latestMessagesResponse.body()?.let { handleMessages(it) }
}
} catch (e: Exception) {
Log.e(
GIST_TAG,
"Error fetching messages: ${e.message}"
)
}
}
}

private fun handleMessages(messages: List<Message>) {
for (message in messages) {
processMessage(message)
}
}

private fun processMessage(message: Message) {
val gistProperties = GistMessageProperties.getGistProperties(message)
gistProperties.routeRule?.let { routeRule ->
try {
if (!routeRule.toRegex().matches(GistSdk.currentRoute)) {
Log.i(
GIST_TAG,
"Message route: $routeRule does not match current route: ${GistSdk.currentRoute}"
)
addMessageToLocalStore(message)
return
}
} catch (e: PatternSyntaxException) {
Log.i(GIST_TAG, "Invalid route rule regex: $routeRule")
return
}
}
gistProperties.elementId?.let { elementId ->
Log.i(
GIST_TAG,
"Embedding message from queue with queue id: ${message.queueId}"
)
GistSdk.handleEmbedMessage(message, elementId)
} ?: run {
Log.i(
GIST_TAG,
"Showing message from queue with queue id: ${message.queueId}"
)
GistSdk.showMessage(message)
}
}

internal fun logView(message: Message) {
GlobalScope.launch {

Check warning on line 138 in messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt

View workflow job for this annotation

GitHub Actions / Android Lint (messaginginapp)

This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.
try {
if (message.queueId != null) {
Log.i(
GIST_TAG,
"Logging view for user message: ${message.messageId}, with queue id: ${message.queueId}"
)
removeMessageFromLocalStore(message)
gistQueueService.logUserMessageView(message.queueId)
} else {
Log.i(GIST_TAG, "Logging view for message: ${message.messageId}")
gistQueueService.logMessageView(message.messageId)
}
} catch (e: Exception) {
Log.e(GIST_TAG, "Failed to log message view: ${e.message}", e)
}
}
}

private fun addMessageToLocalStore(message: Message) {
val localMessage =
localMessageStore.find { localMessage -> localMessage.queueId == message.queueId }
if (localMessage == null) {
localMessageStore.add(message)
}
}

private fun removeMessageFromLocalStore(message: Message) {
localMessageStore.removeAll { it.queueId == message.queueId }
}

override fun onMessageShown(message: Message) {
val gistProperties = GistMessageProperties.getGistProperties(message)
if (gistProperties.persistent) {
Log.i(GIST_TAG, "Persistent message shown: ${message.messageId}, skipping logging view")
} else {
logView(message)
}
}

override fun embedMessage(message: Message, elementId: String) {}

override fun onMessageDismissed(message: Message) {}

override fun onError(message: Message) {}

override fun onAction(message: Message, currentRoute: String, action: String, name: String) {}
}
@@ -0,0 +1,10 @@
package io.customer.messaginginapp.gist.data.model

data class LogEvent(
val name: String,
val route: String,
val instanceId: String,
val queueId: String?,
val campaignId: String?,
val platform: String = "android"
)
@@ -0,0 +1,68 @@
package io.customer.messaginginapp.gist.data.model
import java.util.*

enum class MessagePosition(val position: String) {
TOP("top"),
CENTER("center"),
BOTTOM("bottom")
}

data class GistProperties(
val routeRule: String?,
val elementId: String?,
val campaignId: String?,
val position: MessagePosition,
val persistent: Boolean
)

data class Message(
val messageId: String = "",
val instanceId: String = UUID.randomUUID().toString(),
val queueId: String? = null,
val properties: Map<String, Any?>? = null
)

class GistMessageProperties {
companion object {
fun getGistProperties(message: Message): GistProperties {
var routeRule: String? = null
var elementId: String? = null
var campaignId: String? = null
var position: MessagePosition = MessagePosition.CENTER
var persistent = false

message.properties?.let { properties ->
properties["gist"]?.let { gistProperties ->
(gistProperties as Map<String, Any?>).let { gistProperties ->

Check warning on line 36 in messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt

View workflow job for this annotation

GitHub Actions / Android Lint (messaginginapp)

Unchecked cast: Any to Map<String, Any?>

Check warning on line 36 in messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt

View workflow job for this annotation

GitHub Actions / Android Lint (messaginginapp)

Name shadowed: gistProperties
gistProperties["routeRuleAndroid"]?.let { rule ->
(rule as String).let { stringRule ->
routeRule = stringRule
}
}
gistProperties["campaignId"]?.let { id ->
(id as String).let { stringId ->
campaignId = stringId
}
}
gistProperties["elementId"]?.let { id ->
(id as String).let { stringId ->
elementId = stringId
}
}
gistProperties["position"]?.let { messagePosition ->
(messagePosition as String).let { stringPosition ->
position = MessagePosition.valueOf(stringPosition.uppercase())
}
}
gistProperties["persistent"]?.let { id ->
(id as Boolean).let { persistentValue ->
persistent = persistentValue
}
}
}
}
}
return GistProperties(routeRule = routeRule, elementId = elementId, campaignId = campaignId, position = position, persistent = persistent)
}
}
}
@@ -0,0 +1,11 @@
package io.customer.messaginginapp.gist.data.model.engine

internal data class EngineWebConfiguration(
val siteId: String,
val dataCenter: String,
val messageId: String,
val instanceId: String,
val endpoint: String,
val livePreview: Boolean = false,
val properties: Map<String, Any?>? = null
)

0 comments on commit d6fda6d

Please sign in to comment.