diff --git a/CHANGELOG.md b/CHANGELOG.md index 29823b9..0e58bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,24 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -[0.1.0] - 2024-07-31 +## [1.0.0] - 2024-11-11 ### Added + +- Added support for Personalise rules within `Mutually Exclusive Groups`. +- Settings cache: Cached settings will be used till it expires. Client can set the expiry time of cache. +- Storage support: Built-in local storage will be used by default if client doesn't provide their own. Client’s storage will be used if it is provided. +- Call backs added to avoid busy waiting for server call to complete. +- Changed variable access to method access for Flag - setIsEnabled & isEnabled + +## [0.1.0] - 2024-07-31 + +### Added + - First release of VWO Feature Management and Experimentation capabilities. ```kotlin @@ -53,9 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Log.d("Vwo", "vwoInitFailed: $message") } }) - - ``` + ``` - **Error handling** - - Gracefully handle any kind of error - TypeError, NetworkError, etc. \ No newline at end of file + - Gracefully handle any kind of error - TypeError, NetworkError, etc. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9534303..24375ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:supportsRtl="true" android:theme="@style/Theme.FME" tools:targetApi="31"> + @@ -21,6 +22,8 @@ + + \ No newline at end of file diff --git a/app/src/main/java/com/vwo/fme/JavaMainActivity.java b/app/src/main/java/com/vwo/fme/JavaMainActivity.java new file mode 100644 index 0000000..a07a7df --- /dev/null +++ b/app/src/main/java/com/vwo/fme/JavaMainActivity.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024 Wingify Software Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.vwo.fme; + +import android.os.Bundle; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import com.vwo.VWO; +import com.vwo.fme.databinding.ActivityMainBinding; +import com.vwo.interfaces.IVwoInitCallback; +import com.vwo.interfaces.IVwoListener; +import com.vwo.models.user.GetFlag; +import com.vwo.models.user.VWOContext; +import com.vwo.models.user.VWOInitOptions; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JavaMainActivity extends AppCompatActivity { + + TestApp prod = new TestApp(0, + "", + "flag-name", + "variable-name", + "event-name", + "attribute-name"); + + TestApp server = prod; + String SDK_KEY = server.getSdkKey(); + int ACCOUNT_ID = server.getAccountId(); + + private VWO vwo; + private GetFlag featureFlag; + private VWOContext userContext; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + binding.tvName.setText("FME Java"); + + binding.btnInitSdk.setOnClickListener(view -> { + VWOInitOptions vwoInitOptions = new VWOInitOptions(); + vwoInitOptions.setSdkKey(SDK_KEY); + vwoInitOptions.setAccountId(ACCOUNT_ID); + + Map loggerOptions = new HashMap<>(); + loggerOptions.put("level", "TRACE"); + vwoInitOptions.setLogger(loggerOptions); + + VWO.init(vwoInitOptions, new IVwoInitCallback() { + @Override + public void vwoInitSuccess(@NonNull VWO vwo, @NonNull String message) { + Log.d("Flag", "vwoInitSuccess " + message); + JavaMainActivity.this.vwo = vwo; + } + + @Override + public void vwoInitFailed(@NonNull String message) { + Log.d("Flag", "vwoInitFailed: " + message); + } + }); + }); + binding.btnGetFlag.setOnClickListener(v -> { + if (vwo != null) { + getFlag(vwo); + } + }); + + binding.btnGetVariable.setOnClickListener(v -> { + if (featureFlag != null) { + getVariable(featureFlag); + } + }); + + binding.btnTrack.setOnClickListener(v -> track()); + + binding.btnAttribute.setOnClickListener(v -> sendAttribute()); + binding.btnJavaScreen.setVisibility(View.GONE); + } + + private void getFlag(@NonNull VWO vwo) { + userContext = new VWOContext(); + userContext.setId("unique_user_id"); + + Map customVariables = new HashMap<>(); + customVariables.put("Username", "Swapnil"); + customVariables.put("userType", "trial"); + userContext.setCustomVariables(customVariables); + + vwo.getFlag("feature-key", userContext, new IVwoListener() { + public void onSuccess(Object data) { + featureFlag = (GetFlag) data; + if (featureFlag != null) { + boolean isFeatureFlagEnabled = featureFlag.isEnabled(); + Log.d("FME-App", "Received getFlag isFeatureFlagEnabled=" + isFeatureFlagEnabled); + } + } + + public void onFailure(@NonNull String message) { + Log.d("FME-App", "getFlag " + message); + } + }); + if (featureFlag == null) + return; + boolean isFeatureFlagEnabled = featureFlag.isEnabled(); + + Log.d("Flag", "isFeatureFlagEnabled=" + isFeatureFlagEnabled); + } + + private void getVariable(@NonNull GetFlag featureFlag) { + boolean isFeatureFlagEnabled = featureFlag.isEnabled(); + Log.d("Flag", "isFeatureFlagEnabled=" + isFeatureFlagEnabled); + + if (isFeatureFlagEnabled) { + String variable1 = (String) featureFlag.getVariable("variable_key", "default-value1"); + + List> getAllVariables = featureFlag.getVariables(); + Log.d("Flag", "variable1=" + variable1 + " getAllVariables=" + getAllVariables); + } else { + Log.d("Flag", "Feature flag is disabled: " + featureFlag.isEnabled() + " " + featureFlag.getVariables()); + } + } + + private void track() { + if (userContext == null) return; + + Map properties = new HashMap<>(); + properties.put("cartvalue", 120); + properties.put("productCountInCart", 2); + + // Track the event for the given event name, user context and properties + Map trackResponse = vwo.trackEvent("productViewed", userContext, properties); + Log.d("Flag", "track=" + trackResponse); + // Track the event for the given event name and user context + //Map trackResponse = vwo.trackEvent("vwoevent", userContext); + } + + private void sendAttribute() { + if (vwo != null) { + vwo.setAttribute("userType", "paid", userContext); + vwo.setAttribute("attribute-name-float", 1.01, userContext); + vwo.setAttribute("attribute-name-boolean", true, userContext); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vwo/fme/MainActivity.kt b/app/src/main/java/com/vwo/fme/MainActivity.kt index 7629d14..8d265ce 100644 --- a/app/src/main/java/com/vwo/fme/MainActivity.kt +++ b/app/src/main/java/com/vwo/fme/MainActivity.kt @@ -1,22 +1,37 @@ package com.vwo.fme +import android.content.Intent import android.os.Bundle +import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.vwo.VWO import com.vwo.fme.databinding.ActivityMainBinding import com.vwo.interfaces.IVwoInitCallback +import com.vwo.interfaces.IVwoListener import com.vwo.models.user.GetFlag +import com.vwo.models.user.Recommendation import com.vwo.models.user.VWOContext import com.vwo.models.user.VWOInitOptions +import com.vwo.services.LoggerService +val prod = TestApp( + accountId = 0, + sdkKey = "", + flagName = "", + variableName = "", + eventName = "", + attributeName = "" +) -private const val SDK_KEY = "" -private const val ACCOUNT_ID = 0 +val server = prod +private val SDK_KEY = server.sdkKey +private val ACCOUNT_ID = server.accountId class MainActivity : AppCompatActivity() { + private val USER_ID = "" private var vwo: VWO? = null private var featureFlag: GetFlag? = null private lateinit var userContext: VWOContext @@ -37,8 +52,16 @@ class MainActivity : AppCompatActivity() { // Set SDK Key and Account ID vwoInitOptions.sdkKey = SDK_KEY vwoInitOptions.accountId = ACCOUNT_ID + vwoInitOptions.context = this@MainActivity.applicationContext vwoInitOptions.logger = mutableMapOf().apply { put("level", "TRACE") } + /*vwoInitOptions.gatewayService = mutableMapOf().apply { + put("url", "http://10.0.2.2:8000") + }*/ + //vwoInitOptions.pollInterval = 60000 + vwoInitOptions.cachedSettingsExpiryTime = 2 * 60 * 1000 // 2 min + + //vwoInitOptions.storage = StorageTest() // Create VWO instance with the vwoInitOptions VWO.init(vwoInitOptions, object : IVwoInitCallback { override fun vwoInitSuccess(vwo: VWO, message: String) { @@ -50,24 +73,30 @@ class MainActivity : AppCompatActivity() { // Log error here } }) - binding.btnGetFlag.setOnClickListener { - vwo?.let { getFlag(it) } - } - binding.btnGetVariable.setOnClickListener { - featureFlag?.let { getVariable(it) } - } - binding.btnTrack.setOnClickListener { - track() - } - binding.btnAttribute.setOnClickListener { - sendAttribute() - } + } + binding.btnGetFlag.setOnClickListener { + vwo?.let { getFlag(it) } + } + binding.btnGetVariable.setOnClickListener { + featureFlag?.let { getVariable(it) } + } + binding.btnTrack.setOnClickListener { + track() + } + binding.btnAttribute.setOnClickListener { + sendAttribute() + } + binding.btnJavaScreen.setOnClickListener { + startActivity(Intent(this, JavaMainActivity::class.java)) } } private fun getFlag(vwo: VWO) { userContext = VWOContext() - userContext.id = "unique_user_id" + userContext.id = USER_ID + userContext.ipAddress = "182.69.183.212" + //userContext.userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" + userContext.userAgent = "AppName/1.0 (Linux; Android 12; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Mobile Safari/537.36" userContext.customVariables = mutableMapOf( "name1" to 21, "name2" to 0, @@ -76,42 +105,105 @@ class MainActivity : AppCompatActivity() { ) // Get feature flag object - featureFlag = vwo.getFlag("feature_flag_name", userContext) + vwo.getFlag(server.flagName, userContext, object : IVwoListener { + override fun onSuccess(data: Any) { + featureFlag = data as? GetFlag + val isFeatureFlagEnabled = featureFlag?.isEnabled() + Log.d("FME-App", "Received getFlag isFeatureFlagEnabled=$isFeatureFlagEnabled") + } - val isFeatureFlagEnabled = featureFlag?.isEnabled + override fun onFailure(message: String) { + Log.d("FME-App", "getFlag $message") + } + }) } private fun getVariable(featureFlag: GetFlag) { - val isFeatureFlagEnabled = featureFlag.isEnabled + val isFeatureFlagEnabled = featureFlag.isEnabled() // Determine the application flow based on feature flag status if (isFeatureFlagEnabled) { // To get value of a single variable - val variable1 = featureFlag.getVariable("feature_flag_variable1", "default-value1") - val variable2 = featureFlag.getVariable("feature_flag_variable2", "default-value2") + recommendation(featureFlag) + val variable2 = featureFlag.getVariable(server.variableName, "default-value2") // To get value of all variables in object format val getAllVariables = featureFlag.getVariables() + println("Variable values: getAllVariables=$getAllVariables") } else { // Your code when feature flag is disabled } } + private fun recommendation(featureFlag: GetFlag) { + + val recommendationWrapper = featureFlag.getVariable(server.variableName, "default") + + if (recommendationWrapper is Recommendation) { + val options = mapOf( + "userId" to USER_ID, + "productIds" to "1,2,3,4", + "pageType" to "shopping-cart-page-view" + ) + recommendationWrapper.getRecommendations( + options, + category = "Clothing", + productIds = listOf(1501), + object : IVwoListener { + + override fun onSuccess(data: Any) { + println("response is -- $data") + } + + override fun onFailure(message: String) { + println("error is -- $message") + } + }) + recommendationWrapper.getRecommendationWidget( + featureFlag, + emptyMap(), + object : IVwoListener { + override fun onSuccess(data: Any) { + println("getRecommendationWidget response is -- $data") + } + + override fun onFailure(message: String) { + println("getRecommendationWidget error is -- $message") + } + }) + println("RecommendationBlock=${recommendationWrapper.recommendationBlock}") + } + } + private fun track() { if (!::userContext.isInitialized) return val properties = mutableMapOf("cartvalue" to 10) // Track the event for the given event name and user context - val trackResponse = vwo?.trackEvent("vwoevent", userContext, properties) - //val trackResponse = vwo?.trackEvent("vwoevent", userContext) + val map: MutableMap = mutableMapOf() + map["category"] = "electronics" + map["isWishlisted"] = false + map["price"] = 21 + map["productId"] = 1 + val trackResponse = vwo?.trackEvent(server.eventName, userContext, map) + //val trackResponse = vwo?.trackEvent(server.eventName, userContext) } private fun sendAttribute() { if (!::userContext.isInitialized) return - vwo?.setAttribute("attribute-name", "attribute-value1", userContext) + vwo?.setAttribute(server.attributeName, "attribute-value1", userContext) vwo?.setAttribute("attribute-name-float", 1.01, userContext) vwo?.setAttribute("attribute-name-boolean", true, userContext) } -} \ No newline at end of file +} + +data class TestApp( + val accountId: Int, + val sdkKey: String, + val flagName: String, + val variableName: String, + val eventName: String, + val attributeName: String +) \ No newline at end of file diff --git a/app/src/main/java/com/vwo/fme/StorageTest.kt b/app/src/main/java/com/vwo/fme/StorageTest.kt new file mode 100644 index 0000000..1923eb5 --- /dev/null +++ b/app/src/main/java/com/vwo/fme/StorageTest.kt @@ -0,0 +1,73 @@ +/** + * Copyright 2024 Wingify Software Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vwo.fme + +import com.vwo.packages.storage.Connector + +/** + * A class for storing and retrieving data from a storage. + */ +class StorageTest : Connector() { + /** + * A In-memory mutable map to store the data. + * The keys are strings representing a combination of feature key and user ID. + * The values are maps containing the data for each key. + */ + private val storage: MutableMap> = HashMap() + + /** + * Stores the data in the storage. + * + * @param data A map containing the data to be stored. + * The map should contain the following keys: + * - "featureKey": The feature key for the data. + * - "user": The user ID for the data. + * - "rolloutKey": The rollout key for the data. + * - "rolloutVariationId": The rollout variation ID for the data. + * - "experimentKey": The experimentkey for the data. + * - "experimentVariationId": The experiment variation ID for the data. + */ + override fun set(data: Map) { + val key = data["featureKey"].toString() + "_" + data["userId"] + + // Create a map to store the data + val value: MutableMap = HashMap() + value["rolloutKey"] = data["rolloutKey"] + value["rolloutVariationId"] = data["rolloutVariationId"] + value["experimentKey"] = data["experimentKey"] + value["experimentVariationId"] = data["experimentVariationId"] + + // Store the value in the storage + storage[key] = value + } + + /** + * Retrieves the data from the storage. + * + * @param featureKey The feature key for the data. + * @param userId The user ID for the data. + * @return The data if found, or null otherwise. + */ + override fun get(featureKey: String?, userId: String?): Any? { + val key = featureKey + "_" + userId + + // Check if the key exists in the storage + if (storage.containsKey(key)) { + return storage[key] + } + return null + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 60eed50..4459418 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -8,6 +8,7 @@ tools:context=".MainActivity"> + +