Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 7.13.0

- Added Gamify Widget SDK module: a WebView-based bottom sheet that loads Optimove widget URLs and communicates with the widget via a JavaScript bridge

## 7.12.3

- Wraps `isLaunchActivity()` in try/catch and safely returns false on failure to prevent crashes.
Expand Down
2 changes: 2 additions & 0 deletions OptimoveSDK/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ android {
compileSdk 34
defaultConfig {
applicationId "com.optimove.android.optimovemobilesdk"
multiDexEnabled true
minSdk 21
targetSdk 34
versionCode Integer.parseInt("$sdk_version_code")
Expand Down Expand Up @@ -40,6 +41,7 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':optimove-sdk')
implementation project(':gamify-widget-sdk')
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
Expand Down
4 changes: 4 additions & 0 deletions OptimoveSDK/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
<activity
android:name="com.optimove.android.optimovemobilesdk.EmbeddedMessagingActivity"
android:parentActivityName=".MainActivity"/>
<activity
android:name="com.optimove.android.optimovemobilesdk.GamifyWidgetActivity"
android:exported="false"
android:parentActivityName=".MainActivity"/>
<activity
android:name="com.optimove.android.optimovemobilesdk.DeeplinkTargetActivity"
android:parentActivityName=".MainActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.optimove.android.optimovemobilesdk

import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.optimove.android.gamifywidgetsdk.GamifyWidgetSDK
import com.optimove.android.optimovemobilesdk.ui.GamifyWidgetScreen
import com.optimove.android.optimovemobilesdk.ui.theme.AppTheme

enum class GamifyEnv(val label: String, val baseUrl: String) {
DEV("Dev", "https://opti-ls-widget-dev.optimove.net"),
PROD_US("Prod US", "https://opti-ls-widget-us.optimove.net"),
PROD_EU("Prod EU", "https://opti-ls-widget-eu.optimove.net")
}

class GamifyWidgetActivity : AppCompatActivity() {

private lateinit var prefs: SharedPreferences

private var tenant by mutableStateOf("")
private var userId by mutableStateOf("")
private var env by mutableStateOf(GamifyEnv.DEV)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
tenant = prefs.getString(KEY_TENANT, "") ?: ""
userId = prefs.getString(KEY_USER_ID, "") ?: ""
env = enumValues<GamifyEnv>().find { it.name == prefs.getString(KEY_ENV, null) } ?: GamifyEnv.DEV

setContent {
AppTheme {
GamifyWidgetScreen(
tenant = tenant,
userId = userId,
env = env,
onTenantChange = { tenant = it; save() },
onWidgetIdChange = { userId = it; save() },
onEnvChange = { env = it; save() },
onOpenWidget = ::openWidget
)
}
}
}

private fun save() {
prefs.edit()
.putString(KEY_TENANT, tenant)
.putString(KEY_USER_ID, userId)
.putString(KEY_ENV, env.name)
.apply()
}

private fun openWidget() {
val widgetUrl = "${env.baseUrl}/$tenant/$userId"
GamifyWidgetSDK.init(widgetUrl = widgetUrl)
GamifyWidgetSDK.open(supportFragmentManager)
}

companion object {
private const val PREFS_NAME = "gamify_widget_config"
private const val KEY_TENANT = "tenant"
private const val KEY_USER_ID = "user_id"
private const val KEY_ENV = "env"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.optimove.android.preferencecenter.OptimovePreferenceCenter
import com.optimove.android.preferencecenter.PreferenceUpdate
import com.optimove.android.preferencecenter.Topic
import com.optimove.android.optimovemobilesdk.ui.MainScreen

import com.optimove.android.optimovemobilesdk.ui.theme.AppTheme
import org.json.JSONObject

Expand Down Expand Up @@ -146,7 +147,8 @@ class MainActivity : AppCompatActivity() {
"Delayed init enabled — restart app to apply"
else
"Immediate init enabled — restart app to apply"
}
},
onOpenGamifyWidget = ::openGamifyWidget
)
}
}
Expand Down Expand Up @@ -316,6 +318,10 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, EmbeddedMessagingActivity::class.java))
}

private fun openGamifyWidget() {
startActivity(Intent(this, GamifyWidgetActivity::class.java))
}

private fun openDeeplinkTest() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(DeeplinkTargetActivity.DEEPLINK_TEST_URI)))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.optimove.android.optimovemobilesdk.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.optimove.android.optimovemobilesdk.GamifyEnv
import androidx.compose.foundation.layout.Row

private val CardShape = RoundedCornerShape(12.dp)
private val SectionPadding = 16.dp

@Composable
fun GamifyWidgetScreen(
tenant: String,
userId: String,
env: GamifyEnv,
onTenantChange: (String) -> Unit,
onWidgetIdChange: (String) -> Unit,
onEnvChange: (GamifyEnv) -> Unit,
onOpenWidget: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(SectionPadding)
) {
Text(
"Gamify Widget",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Surface(
modifier = Modifier.fillMaxWidth(),
shape = CardShape,
color = MaterialTheme.colorScheme.surfaceVariant
) {
Column(
modifier = Modifier.padding(SectionPadding),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = tenant,
onValueChange = onTenantChange,
label = { Text("Tenant") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
focusedLabelColor = MaterialTheme.colorScheme.primary,
cursorColor = MaterialTheme.colorScheme.primary
)
)
OutlinedTextField(
value = userId,
onValueChange = onWidgetIdChange,
label = { Text("User ID") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
focusedLabelColor = MaterialTheme.colorScheme.primary,
cursorColor = MaterialTheme.colorScheme.primary
)
)
Text(
"Environment",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
enumValues<GamifyEnv>().forEach { option ->
if (env == option) {
Button(
onClick = { onEnvChange(option) },
shape = RoundedCornerShape(10.dp)
) {
Text(option.label)
}
} else {
OutlinedButton(
onClick = { onEnvChange(option) },
shape = RoundedCornerShape(10.dp),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
) {
Text(option.label)
}
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onOpenWidget,
enabled = tenant.isNotBlank() && userId.isNotBlank(),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(10.dp)
) {
Text("Open Widget")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -70,7 +69,8 @@ fun MainScreen(
onRegisterPush: () -> Unit,
onUnregisterPush: () -> Unit,
isDelayedInit: Boolean,
onDelayedInitToggle: (Boolean) -> Unit
onDelayedInitToggle: (Boolean) -> Unit,
onOpenGamifyWidget: () -> Unit
) {
var optimoveCred by remember {
mutableStateOf(if (showDelayedConfig) MyApplication.DEFAULT_OPTIMOVE_CRED else "")
Expand Down Expand Up @@ -331,6 +331,14 @@ fun MainScreen(
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Button(
onClick = onOpenGamifyWidget,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(10.dp)
) {
Text("Open Gamify Widget")
}
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = { showGeolocationDialog = true },
Expand Down
41 changes: 41 additions & 0 deletions OptimoveSDK/gamify-widget-sdk/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
namespace 'com.optimove.android.gamifywidgetsdk'
compileSdk 34

defaultConfig {
minSdk 21
targetSdk 34
consumerProguardFiles 'proguard-rules.pro'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}


compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}

dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'com.google.android.material:material:1.9.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20230227'
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.3.1'
}
1 change: 1 addition & 0 deletions OptimoveSDK/gamify-widget-sdk/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-keep class com.optimove.android.gamifywidgetsdk.** { *; }
3 changes: 3 additions & 0 deletions OptimoveSDK/gamify-widget-sdk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.optimove.android.gamifywidgetsdk

import android.util.Log
import android.webkit.JavascriptInterface
import org.json.JSONException
import org.json.JSONObject

/**
* Native bridge exposed to the widget's JavaScript as `window.AndroidBridge`.
*
* The widget calls:
* window.AndroidBridge.closeWidget() — to dismiss the bottom sheet
* window.AndroidBridge.receiveMessage(json) — generic widget → SDK messages,
* including the READY handshake (type = "READY")
*/
internal class AndroidBridge(
private val onClose: () -> Unit,
private val onReady: () -> Unit
) {

@JavascriptInterface
fun closeWidget() {
onClose()
}
Comment thread
dmytro-b-optimove marked this conversation as resolved.

@JavascriptInterface
fun receiveMessage(json: String) {
try {
if (JSONObject(json).getString("type") == "READY") {
onReady()
}
} catch (e: JSONException) {
Log.d(TAG, "Incorrect message format: $json")
}
}
Comment thread
dmytro-b-optimove marked this conversation as resolved.

companion object {
private const val TAG = "GamifyWidget"
}
}
Loading
Loading