Skip to content

Commit

Permalink
feat: persistant in-app messages (#269)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shahroz16 authored Oct 25, 2023
1 parent d17fab9 commit 4fcb781
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 24 deletions.
1 change: 1 addition & 0 deletions messaginginapp/api/messaginginapp.api
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ public final class io/customer/messaginginapp/gist/presentation/GistSdk : androi
public static field dataCenter Ljava/lang/String;
public static field siteId Ljava/lang/String;
public final fun addListener (Lio/customer/messaginginapp/gist/presentation/GistListener;)V
public final fun clearCurrentMessage ()V
public final fun clearListeners ()V
public final fun clearUserToken ()V
public final fun dismissMessage ()V
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.customer.messaginginapp.gist.data.listeners

import android.content.Context
import android.util.Base64
import android.util.Log
import io.customer.messaginginapp.gist.data.NetworkUtilities
Expand All @@ -9,47 +10,94 @@ 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.io.File
import java.util.regex.PatternSyntaxException
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class Queue : GistListener {

private var localMessageStore: MutableList<Message> = mutableListOf()
private val cacheMap = mutableMapOf<String, Response>()

init {
GistSdk.addListener(this)
}

private val cacheSize = 10 * 1024 * 1024 // 10 MB
private val cacheDirectory by lazy { File(GistSdk.application.cacheDir, "http_cache") }
private val cache by lazy { Cache(cacheDirectory, cacheSize.toLong()) }

private fun saveToPrefs(context: Context, key: String, value: String) {
val prefs = context.getSharedPreferences("network_cache", Context.MODE_PRIVATE)
prefs.edit().putString(key, value).apply()
}

private fun getFromPrefs(context: Context, key: String): String? {
val prefs = context.getSharedPreferences("network_cache", Context.MODE_PRIVATE)
return prefs.getString(key, null)
}

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()
// Interceptor to set up request headers like site ID, data center, and user token.
val httpClient: OkHttpClient =
OkHttpClient.Builder().cache(cache)
.addInterceptor { chain ->
val originalRequest = chain.request()

chain.proceed(request)
} ?: run {
val request: Request = chain.request().newBuilder()
val networkRequest = originalRequest.newBuilder()
.addHeader(NetworkUtilities.CIO_SITE_ID_HEADER, GistSdk.siteId)
.addHeader(NetworkUtilities.CIO_DATACENTER_HEADER, GistSdk.dataCenter)
.apply {
GistSdk.getUserToken()?.let { userToken ->
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)
)
}
}
.header("Cache-Control", "no-cache")
.build()

chain.proceed(request)
val response = chain.proceed(networkRequest)

when (response.code) {
200 -> {
response.body?.let { responseBody ->
val responseBodyString = responseBody.string()
saveToPrefs(
GistSdk.application,
originalRequest.url.toString(),
responseBodyString
)
return@addInterceptor response.newBuilder().body(
responseBodyString.toResponseBody(responseBody.contentType())
).build()
}
}

304 -> {
val cachedResponse =
getFromPrefs(GistSdk.application, originalRequest.url.toString())
cachedResponse?.let {
return@addInterceptor response.newBuilder()
.body(it.toResponseBody(null)).code(200).build()
} ?: return@addInterceptor response
}

else -> return@addInterceptor response
}

response
}
}
.build()
.build()

Retrofit.Builder()
.baseUrl(GistSdk.gistEnvironment.getGistQueueApiUrl())
Expand All @@ -72,8 +120,6 @@ class Queue : GistListener {
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class GistMessageProperties {
}
}
gistProperties["persistent"]?.let { id ->
(id as Boolean).let { persistentValue ->
(id as? Boolean)?.let { persistentValue ->
persistent = persistentValue
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd
import com.google.gson.Gson
Expand Down Expand Up @@ -61,6 +62,12 @@ class GistModalActivity : AppCompatActivity(), GistListener, GistViewListener {
} ?: run {
finish()
}

// Update back button to handle in-app message behavior, disable back press for persistent messages, true otherwise
val onBackPressedCallback = object : OnBackPressedCallback(isPersistentMessage()) {
override fun handleOnBackPressed() {}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}

override fun onResume() {
Expand Down Expand Up @@ -91,10 +98,21 @@ class GistModalActivity : AppCompatActivity(), GistListener, GistViewListener {

override fun onDestroy() {
GistSdk.removeListener(this)
GistSdk.dismissMessage()
// If the message is not persistent, dismiss it and inform the callback
if (!isPersistentMessage()) {
GistSdk.dismissMessage()
} else {
GistSdk.clearCurrentMessage()
}
super.onDestroy()
}

private fun isPersistentMessage(): Boolean = currentMessage?.let {
GistMessageProperties.getGistProperties(
it
).persistent
} ?: false

override fun onMessageShown(message: Message) {
runOnUiThread {
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ internal class GistModalManager : GistListener {

internal fun showModalMessage(message: Message, position: MessagePosition? = null): Boolean {
currentMessage?.let { currentMessage ->
Log.i(GIST_TAG, "Message ${message.messageId} not shown, ${currentMessage.messageId} is already showing.")
Log.i(
GIST_TAG,
"Message ${message.messageId} not shown, ${currentMessage.messageId} is already showing."
)
return false
}

Expand Down Expand Up @@ -55,4 +58,8 @@ internal class GistModalManager : GistListener {
override fun onMessageShown(message: Message) {}

override fun onAction(message: Message, currentRoute: String, action: String, name: String) {}

internal fun clearCurrentMessage() {
currentMessage = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ object GistSdk : Application.ActivityLifecycleCallbacks {
gistModalManager.dismissActiveMessage()
}

fun clearCurrentMessage() {
gistModalManager.clearCurrentMessage()
}

// Listeners

fun addListener(listener: GistListener) {
Expand Down

0 comments on commit 4fcb781

Please sign in to comment.