diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..df56946
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# gradle directory
+.gradle
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# built application files
+*.apk
+*.ap_
+*.jar
+
+!gradle-wrapper.jar
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..73303b4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,9 @@
+The Affise SDK is licensed under the MIT License.
+
+Copyright (c) 2022 Affise, Inc. | https://affise.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dee5806
--- /dev/null
+++ b/README.md
@@ -0,0 +1,780 @@
+# Affise Attribution Android library
+
+- [Description](#description)
+- [Quick start](#quick-start)
+- [Integration](#integration)
+ - [Integrate as file dependency](#integrate-as-file-dependency)
+ - [Initialize](#initialize)
+- [Features](#features)
+ - [Device identifiers collection](#device-identifiers-collection)
+ - [Events tracking](#events-tracking)
+ - [Custom events tracking](#custom-events-tracking)
+ - [Predefined event parameters](#predefined-event-parameters)
+ - [Events buffering](#events-buffering)
+ - [Install referrer tracking](#install-referrer-tracking)
+ - [Advertising Identifier (google) tracking](#advertising-identifier-google-tracking)
+ - [Open Advertising Identifier (huawei) tracking](#open-advertising-identifier-huawei-tracking)
+ - [Push token tracking](#push-token-tracking)
+ - [Reinstall Uninstall tracking](#reinstall-uninstall-tracking)
+ - [APK preinstall tracking](#apk-preinstall-tracking)
+ - [Deeplinks](#deeplinks)
+ - [Offline mode](#offline-mode)
+ - [Disable tracking](#disable-tracking)
+ - [Disable background tracking](#disable-background-tracking)
+ - [GDPR right to be forgotten](#gdpr-right-to-be-forgotten)
+ - [Get referrer](#get-referrer)
+ - [Get referrer parameter](#get-referrer-parameter)
+ - [Referrer keys](#referrer-keys)
+ - [Web view tracking](#webview-tracking)
+ - [Initialize webview](#initialize-webview)
+ - [Events tracking JS](#events-tracking-js)
+ - [Predefined event parameters JS](#predefined-event-parameters-js)
+ - [Custom events JS](#custom-events-js)
+- [Requirements](#requirements)
+
+# Description
+
+Affise SDK is a software you can use to collect app usage statistics, device identifiers, deeplink usage, track install
+referrer.
+
+## Quick start
+
+## Integration
+
+### Integrate as file dependency
+
+Download latest Affise SDK (`attribution-release.aar`)
+from [releases page](https://github.com/affise/sdk-android-family/releases) and place this binary to gradle application
+module lib directory `app/libs/attribution-release.aar`
+
+Add library as gradle file dependency to application module build script
+Add install referrer library
+
+For kotlin build script build.gradle.kts use:
+
+```kotlin
+dependencies {
+ // ...
+ implementation(files("libs/attribution-release.aar"))
+ // Add install referrer
+ implementation("com.android.installreferrer:installreferrer:2.2")
+}
+```
+
+For groovy build script build.gradle use:
+
+```groovy
+dependencies {
+ // ...
+ implementation files('libs/attribution-release.aar')
+ // Add install referrer
+ implementation 'com.android.installreferrer:installreferrer:2.2'
+}
+```
+
+### Initialize
+
+After library is added as dependency sync project with gradle files and initialize.
+
+For kotlin use:
+
+```kotlin
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ val properties = AffiseInitProperties(
+ "Your appId", //Change to your app id
+ !BuildConfig.DEBUG, //Add your custom rule to determine if this is a production build
+ null, //Change to your partParamName
+ null, //Change to your partParamNameToken
+ null, //Change to your appToken
+ "Your secretId", //Change to your secretId
+ emptyList(), // Types to handles interception of clicks on activity
+ false // Affise metrics
+ )
+ Affise.init(this, properties)
+ }
+}
+```
+
+For java use:
+
+```java
+public class App extends Application {
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ AffiseInitProperties properties = new AffiseInitProperties(
+ "Your appId", //Change to your app id
+ !BuildConfig.DEBUG, //Add your custom rule to determine if this is a production build
+ null, //Change to your partParamName
+ null, //Change to your partParamNameToken
+ null, //Change to your appToken
+ "Your secretId", //Change to your secretId
+ Collections.emptyList(), // Types to handles interception of clicks on activity
+ false // Affise metrics
+ );
+ Affise.init(this, properties);
+ }
+}
+```
+
+
+### Requirements
+
+For a minimal working functionality your app needs to declare internet permission:
+
+```xml
+
+
+
+
+
+```
+
+# Features
+
+### Device identifiers collection
+
+To match users with events and data library is sending, these identifiers are collected:
+
+- `AFFISE_APP_ID`
+- `AFFISE_PKG_APP_NAME`
+- `AFFISE_APP_NAME_DASHBOARD`
+- `APP_VERSION`
+- `APP_VERSION_RAW`
+- `STORE`
+- `TRACKER_TOKEN`
+- `TRACKER_NAME`
+- `FIRST_TRACKER_TOKEN`
+- `FIRST_TRACKER_NAME`
+- `LAST_TRACKER_TOKEN`
+- `LAST_TRACKER_NAME`
+- `OUTDATED_TRACKER_TOKEN`
+- `INSTALLED_TIME`
+- `FIRST_OPEN_TIME`
+- `INSTALLED_HOUR`
+- `FIRST_OPEN_HOUR`
+- `INSTALL_BEGIN_TIME`
+- `INSTALL_FINISH_TIME`
+- `REFERRAL_TIME`
+- `CREATED_TIME`
+- `CREATED_TIME_MILLI`
+- `CREATED_TIME_HOUR`
+- `UNINSTALL_TIME`
+- `REINSTALL_TIME`
+- `LAST_SESSION_TIME`
+- `CONNECTION_TYPE`
+- `CPU_TYPE`
+- `HARDWARE_NAME`
+- `NETWORK_TYPE`
+- `DEVICE_MANUFACTURER`
+- `PROXY_IP_ADDRESS`
+- `DEEPLINK_CLICK`
+- `DEVICE_ATLAS_ID`
+- `AFFISE_DEVICE_ID`
+- `AFFISE_ALT_DEVICE_ID`
+- `ADID`
+- `ANDROID_ID`
+- `ANDROID_ID_MD5`
+- `MAC_SHA1`
+- `MAC_MD5`
+- `GAID_ADID`
+- `GAID_ADID_MD5`
+- `OAID`
+- `OAID_MD5`
+- `REFTOKEN`
+- `REFTOKENS`
+- `REFERRER`
+- `USER_AGENT`
+- `MCCODE`
+- `MNCODE`
+- `ISP`
+- `REGION`
+- `COUNTRY`
+- `LANGUAGE`
+- `DEVICE_NAME`
+- `DEVICE_TYPE`
+- `OS_NAME`
+- `PLATFORM`
+- `API_LEVEL_OS`
+- `AFFISE_SDK_VERSION`
+- `OS_VERSION`
+- `RANDOM_USER_ID`
+- `AFFISE_SDK_POS`
+- `TIMEZONE_DEV`
+- `LAST_TIME_SESSION`
+- `TIME_SESSION`
+- `AFFISE_SESSION_COUNT`
+- `LIFETIME_SESSION_COUNT`
+- `AFFISE_DEEPLINK`
+- `AFFISE_PART_PARAM_NAME`
+- `AFFISE_PART_PARAM_NAME_TOKEN`
+- `AFFISE_APP_TOKEN`
+- `LABEL`
+- `AFFISE_SDK_SECRET_ID`
+- `UUID`
+- `AFFISE_APP_OPENED`
+- `PUSHTOKEN`
+- `EVENTS`
+- `AFFISE_EVENTS_COUNT`
+
+### Events tracking
+
+For example, we want to track what items usually user adds to shopping cart. To send event first create it with
+following code
+
+```kotlin
+class Presenter {
+ fun onUserAddsItemsToCart(items: String) {
+ val items = JSONObject().apply {
+ put("items", "cookies, potato, milk")
+ }
+ Affise.sendEvent(AddToCartEvent(items, System.currentTimeMillis(), "groceries"))
+ }
+}
+```
+
+For java use:
+
+```java
+class Presenter {
+ void onUserAddsItemsToCart(String items) {
+ JSONObject items = new JSONObject();
+ items.put("items", items);
+ Affise.sendEvent(new AddToCartEvent(items, System.currentTimeMillis(), "groceries"));
+ }
+}
+```
+
+With above example you can implement other events:
+
+- `AchieveLevelEvent`
+- `AddPaymentInfoEvent`
+- `AddToCartEvent`
+- `AddToWishlistEvent`
+- `ClickAdvEvent`
+- `CompleteRegistrationEvent`
+- `CompleteStreamEvent`
+- `CompleteTrialEvent`
+- `CompleteTutorialEvent`
+- `ContentItemsViewEvent`
+- `DeepLinkedEvent`
+- `InitiatePurchaseEvent`
+- `InitiateStreamEvent`
+- `InviteEvent`
+- `LastAttributedTouchEvent`
+- `ListViewEvent`
+- `LoginEvent`
+- `OpenedFromPushNotificationEvent`
+- `PurchaseEvent`
+- `RateEvent`
+- `ReEngageEvent`
+- `ReserveEvent`
+- `SalesEvent`
+- `SearchEvent`
+- `ShareEvent`
+- `SpendCreditsEvent`
+- `StartRegistrationEvent`
+- `StartTrialEvent`
+- `StartTutorialEvent`
+- `SubscribeEvent`
+- `TravelBookingEvent`
+- `UnlockAchievementEvent`
+- `UnsubscribeEvent`
+- `UnsubscriptionEvent`
+- `UpdateEvent`
+- `ViewAdvEvent`
+- `ViewCartEvent`
+- `ViewItemEvent`
+- `ViewItemsEvent`
+
+### Custom events tracking
+
+Use any of custom events if default doesn't fit your scenario:
+
+- `CustomId01Event`
+- `CustomId02Event`
+- `CustomId03Event`
+- `CustomId04Event`
+- `CustomId05Event`
+- `CustomId06Event`
+- `CustomId07Event`
+- `CustomId08Event`
+- `CustomId09Event`
+- `CustomId10Event`
+
+### Predefined event parameters
+
+To enrich your event with another dimension, you can use predefined parameters for most common cases.
+Add it to any event:
+
+```kotlin
+class Presenter {
+ fun onUserAddsItemsToCart(items: String) {
+ val items = JSONObject().apply {
+ put("items", "cookies, potato, milk")
+ }
+
+ val event = AddToCartEvent(items, System.currentTimeMillis()).apply {
+ addPredefinedParameter(PredefinedParameters.DESCRIPTION, "best before 2029")
+ }
+ Affise.sendEvent(event)
+ }
+}
+```
+
+For java use:
+
+```java
+class Presenter {
+ void onUserAddsItemsToCart(String items) {
+ JSONObject items = new JSONObject();
+ items.put("items", items);
+
+ AddToCartEvent event = AddToCartEvent(items, System.currentTimeMillis());
+ event.addPredefinedParameter(PredefinedParameters.DESCRIPTION, "best before 2029");
+
+ Affise.sendEvent(event);
+ }
+}
+```
+In examples above `PredefinedParameters.DESCRIPTION` is used, but many others is available:
+- `ADREV_AD_TYPE`
+- `CITY`
+- `COUNTRY`
+- `REGION`
+- `CLASS`
+- `CONTENT`
+- `CONTENT_ID`
+- `CONTENT_LIST`
+- `CONTENT_TYPE`
+- `CURRENCY`
+- `CUSTOMER_USER_ID`
+- `DATE_A`
+- `DATE_B`
+- `DEPARTING_ARRIVAL_DATE`
+- `DEPARTING_DEPARTURE_DATE`
+- `DESCRIPTION`
+- `DESTINATION_A`
+- `DESTINATION_B`
+- `DESTINATION_LIST`
+- `HOTEL_SCORE`
+- `LEVEL`
+- `MAX_RATING_VALUE`
+- `NUM_ADULTS`
+- `NUM_CHILDREN`
+- `NUM_INFANTS`
+- `ORDER_ID`
+- `PAYMENT_INFO_AVAILABLE`
+- `PREFERRED_NEIGHBORHOODS`
+- `PREFERRED_NUM_STOPS`
+- `PREFERRED_PRICE_RANGE`
+- `PREFERRED_STAR_RATINGS`
+- `PRICE`
+- `PURCHASE_CURRENCY`
+- `QUANTITY`
+- `RATING_VALUE`
+- `RECEIPT_ID`
+- `REGISTRATION_METHOD`
+- `RETURNING_ARRIVAL_DATE`
+- `RETURNING_DEPARTURE_DATE`
+- `REVENUE`
+- `SCORE`
+- `SEARCH_STRING`
+- `SUBSCRIPTION_ID`
+- `SUCCESS`
+- `SUGGESTED_DESTINATIONS`
+- `SUGGESTED_HOTELS`
+- `TRAVEL_START`
+- `TRAVEL_END`
+- `USER_SCORE`
+- `VALIDATED`
+- `ACHIEVEMENT_ID`
+- `COUPON_CODE`
+- `CUSTOMER_SEGMENT`
+- `DEEP_LINK`
+- `EVENT_START`
+- `EVENT_END`
+- `LAT`
+- `LONG`
+- `NEW_VERSION`
+- `OLD_VERSION`
+- `REVIEW_TEXT`
+- `TUTORIAL_ID`
+- `VIRTUAL_CURRENCY_NAME`
+- `PARAM_01`
+- `PARAM_02`
+- `PARAM_03`
+- `PARAM_04`
+- `PARAM_05`
+- `PARAM_06`
+- `PARAM_07`
+- `PARAM_08`
+- `PARAM_09`
+
+### Events buffering
+
+Affise library will send any pending events with first opportunity,
+but if there is no network connection or device is disabled, events are kept locally for 7 days before deletion.
+
+
+### Advertising Identifier (google) tracking
+
+Advertising Identifier (google) tracking is supported automatically, no actions needed
+
+### Open Advertising Identifier (huawei) tracking
+
+Open Advertising Identifier is supported automatically, no actions needed
+
+### Install referrer tracking
+
+Install referrer tracking is supported automatically, no actions needed
+
+### Push token tracking
+
+To let affise track push token you need to receive it from your push service provider, and pass to Affise library.
+First add firebase integration to your app completing theese steps: https://firebase.google.com/docs/cloud-messaging/android/client
+
+After you have done with firebase inegration, add to your cloud messaging service `onNewToken` method `Affise.addPushToken(token)`
+
+```kotlin
+class FirebaseCloudMessagingService : FirebaseMessagingService() {
+ override fun onNewToken(token: String) {
+ // New token generated
+ Affise.addPushToken(token)
+ }
+}
+```
+### Reinstall Uninstall tracking
+
+Affise automaticly track reinstall events by using silent-push technology, to make this feature work, pass push token when it is recreated by user and on you application starts up
+```kotlin
+Affise.addPushToken(token)
+```
+
+### APK preinstall tracking
+
+SDK is also supports scenario when APK is installed not from one of application markets, such as google play, huawei appgallery or amazon appstore
+To use this feature, create file with name `partner_key` in your app assets directory, and write unique identifier inside, this key will be passed to our backend so you can track events by partner later in your Affise console.
+
+### Deeplinks
+
+To integrate applink support you need:
+
+- add intent filter to one of your activities, replacing YOUR_AFFISE_APP_ID with id from your affise personal cabinet
+
+```xml
+
+
+
+
+
+
+
+```
+- register applink callback right after Affise.init(..)
+
+for kotlin:
+```kotlin
+Affise.init(..)
+Affise.registerDeeplinkCallback { uri ->
+ val screen = uri.getQueryParameter("screen")
+ if(screen == "special_offer") {
+ // open special offer activity
+ } else {
+ // open another activity
+ }
+ // return true if deeplink is handled successfully
+ true
+}
+```
+
+for java:
+```java
+Affise.registerDeeplinkCallback(uri -> {
+ String screen = uri.getQueryParameter("screen");
+ if (screen.equals("special_offer")) {
+ // open special offer screen
+ } else {
+ // open another activity
+ }
+ // return true if deeplink is handled successfully
+ return true;
+});
+```
+
+### Offline mode
+
+In some scenarious you would want to limit Affise network usage, to pause that activity call anywhere in your application following code after Affise init:
+
+```kotlin
+Affise.init(..)
+Affise.setOfflineModeEnabled(enabled = true) // to enable offline mode
+Affise.setOfflineModeEnabled(enabled = false) // to disable offline mode
+```
+While offline mode is enabled, your metrics and other events are kept locally, and will be delivered once offline mode is disabled.
+Offline mode is persistent as Application lifecycle, and will be disabled with process termination automaticly.
+To check current offline mode status call:
+
+```kotlin
+Affise.isOfflineModeEnabled() // returns true or false describing current tracking state
+```
+
+### Disable tracking
+
+To disable any tracking activity, storing events and gathering device identifiers and metrics call anywhere in your application following code after Affise init:
+
+```kotlin
+Affise.init(..)
+Affise.setTrackingEnabled(enabled = true) // to enable tracking
+Affise.setTrackingEnabled(enabled = false) // to disable tracking
+```
+
+By default tracking is enabled.
+
+While tracking mode is disabled, metrics and other identifiers is not generated locally.
+Keep in mind that this flag is persistent until app reinstall, and don't forget to reactivate tracking when needed.
+To check current status of tracking call:
+
+```kotlin
+Affise.isTrackingEnabled() // returns true or false describing current tracking state
+```
+
+### Disable background tracking
+
+To disable any background tracking activity, storing events and gathering device identifiers and metrics call anywhere in your application following code after Affise init:
+
+```kotlin
+Affise.init(..)
+Affise.setBackgroundTrackingEnabled(enabled = true) // to enable background tracking
+Affise.setBackgroundTrackingEnabled(enabled = false) // to disable background tracking
+```
+
+By default background tracking is enabled.
+
+While background tracking mode is disabled, metrics and other identifiers is not generated locally.
+Background tracking mode is persistent as Application lifecycle, and will be re-enabled with process termination automatically.
+To check current status of background tracking call:
+
+```kotlin
+Affise.isBackgroundTrackingEnabled() // returns true or false describing current background tracking state
+```
+
+### GDPR right to be forgotten
+
+Under the EU's General Data Protection Regulation (GDPR): An individual has the right to have their personal data erased.
+To provide this functionality to user, as the app developer, you can call
+
+```kotlin
+Affise.init(..)
+Affise.forget() // to forget users data
+```
+After processing such request our backend servers will delete all users data.
+To prevent library from generating new events, disable tracking just before calling Affise.forget:
+
+```kotlin
+Affise.init(..)
+Affise.setTrackingEnabled(enabled = false)
+Affise.forget() // to forget users data
+```
+
+### Get referrer
+
+Use the next public method of SDK
+
+```kotlin
+Affise.getReferrer()
+```
+
+### Get referrer parameter
+
+Use the next public method of SDK to get referrer parameter by
+
+For kotlin:
+```kotlin
+Affise.getReferrerValue(ReferrerKey.CLICK_ID) { value ->
+
+}
+```
+
+For java:
+```java
+Affise.getReferrerValue(ReferrerKey.CLICK_ID, value -> {
+
+});
+```
+
+#### Referrer keys
+
+In examples above `ReferrerKey.CLICK_ID` is used, but many others is available:
+
+- `AD_ID`
+- `CAMPAIGN_ID`
+- `CLICK_ID`
+- `AFFISE_AD`
+- `AFFISE_AD_ID`
+- `AFFISE_AD_TYPE`
+- `AFFISE_ADSET`
+- `AFFISE_ADSET_ID`
+- `AFFISE_AFFC_ID`
+- `AFFISE_CHANNEL`
+- `AFFISE_CLICK_LOOK_BACK`
+- `AFFISE_COST_CURRENCY`
+- `AFFISE_COST_MODEL`
+- `AFFISE_COST_VALUE`
+- `AFFISE_DEEPLINK`
+- `AFFISE_KEYWORDS`
+- `AFFISE_MEDIA_TYPE`
+- `AFFISE_MODEL`
+- `AFFISE_OS`
+- `AFFISE_PARTNER`
+- `AFFISE_REF`
+- `AFFISE_SITE_ID`
+- `AFFISE_SUB_SITE_ID`
+- `AFFC`
+- `PID`
+- `SUB_1`
+- `SUB_2`
+- `SUB_3`
+- `SUB_4`
+- `SUB_5`
+
+### Webview tracking
+#### Initialize webview
+To integrate the library into the JavaScript environment, we added a bridge between JavaScript and the native SDK. Now you can send events and use the functionality of the native library directly from Webview.
+Here are step by step instructions:
+
+
+```kotlin
+// retreive webview from view hierarhy
+val webView = findViewById(R.Id.your_webview_id)
+// make sure javascript is enabled
+webView.javaScriptEnabled = true
+// initialize webview with Affise native library
+Affise.registerWebView(webView)
+
+```
+
+Other Javascript enviroment features is described below.
+
+#### Events tracking JS
+after webview is initialized you send events from JavaScript enviroment
+
+```javascript
+var event = new AddPaymentInfoEvent(
+ { card: 4138, type: 'phone' },
+ Date.now(),
+ 'taxi'
+);
+
+event.addPredefinedParameter('affise_p_purchase_currency', 'USD');
+
+Affise.sendEvent(event)
+
+});
+```
+
+Just like with native SDK, javascript enviroment also provides default events that can be passed from webview:
+- `AchieveLevelEvent`
+- `AddPaymentInfoEvent`
+- `AddToCartEvent`
+- `AddToWishlistEvent`
+- `ClickAdvEvent`
+- `CompleteRegistrationEvent`
+- `CompleteStreamEvent`
+- `CompleteTrialEvent`
+- `CompleteTutorialEvent`
+- `ContentItemsViewEvent`
+- `ConvertedOfferEvent`
+- `ConvertedOfferFromRetryEvent`
+- `ConvertedTrialEvent`
+- `ConvertedTrialFromRetryEvent`
+- `CustomId01Event`
+- `CustomId02Event`
+- `CustomId03Event`
+- `CustomId04Event`
+- `CustomId05Event`
+- `CustomId06Event`
+- `CustomId07Event`
+- `CustomId08Event`
+- `CustomId09Event`
+- `CustomId10Event`
+- `DeepLinkedEvent`
+- `FailedOfferFromRetryEvent`
+- `FailedOfferiseEvent`
+- `FailedSubscriptionEvent`
+- `FailedSubscriptionFromRetryEvent`
+- `FailedTrialEvent`
+- `FailedTrialFromRetryEvent`
+- `InitialOfferEvent`
+- `InitialSubscriptionEvent`
+- `InitialTrialEvent`
+- `InitiatePurchaseEvent`
+- `InitiateStreamEvent`
+- `InviteEvent`
+- `LastAttributedTouchEvent`
+- `ListViewEvent`
+- `LoginEvent`
+- `OfferInRetryEvent`
+- `OpenedFromPushNotificationEvent`
+- `PurchaseEvent`
+- `RateEvent`
+- `ReEngageEvent`
+- `ReactivatedSubscriptionEvent`
+- `RenewedSubscriptionEvent`
+- `RenewedSubscriptionFromRetryEvent`
+- `ReserveEvent`
+- `SalesEvent`
+- `SearchEvent`
+- `ShareEvent`
+- `SpendCreditsEvent`
+- `StartRegistrationEvent`
+- `StartTrialEvent`
+- `StartTutorialEvent`
+- `SubscribeEvent`
+- `SubscriptionEvent`
+- `SubscriptionInRetryEvent`
+- `TravelBookingEvent`
+- `TrialInRetryEvent`
+- `UnlockAchievementEvent`
+- `UnsubscribeEvent`
+- `UnsubscriptionEvent`
+- `UpdateEvent`
+- `ViewAdvEvent`
+- `ViewCartEvent`
+- `ViewItemEvent`
+- `ViewItemsEvent`
+
+#### Predefined event parameters JS
+Each event can be extended with custom event parameters. By calling `addPredefinedParameter` function you can pass predefined parameters name and value, for example:
+```javascript
+var event = ...
+
+event.addPredefinedParameter('affise_p_purchase_currency', 'USD');
+
+Affise.sendEvent(event)
+
+});
+```
+
+#### Custom events JS
+
+If above event functionality still limits your usecase, you can allways extend `Event` class to override fields you are missing
+
+```javascript
+class AchieveLevelEvent extends Event {
+ constructor(level, timeStampMillis, userData) {
+ super('AchieveLevel');
+
+ this.affise_event_first_for_user = false;
+ this.affise_event_user_data = userData;
+ this.affise_event_data = {
+ affise_event_achieve_level: level,
+ affise_event_achieve_level_timestamp: timeStampMillis
+ };
+ }
+}});
+```
+
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..a07d548
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,87 @@
+import java.text.SimpleDateFormat
+
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'com.google.gms.google-services'
+ id 'kotlin-kapt'
+}
+
+android {
+ compileSdk 33
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ defaultConfig {
+ applicationId "com.affise.app"
+ minSdk 21
+ targetSdk 33
+ versionCode 1
+ versionName "1.0 build (${buildTime()})"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'com.google.android.material:material:1.5.0'
+
+ implementation "com.google.firebase:firebase-core:20.0.2"
+ implementation 'com.google.firebase:firebase-messaging-ktx:23.0.0'
+
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
+
+ implementation "androidx.navigation:navigation-fragment-ktx:2.4.0"
+ implementation "androidx.navigation:navigation-ui-ktx:2.4.0"
+
+ implementation "com.google.dagger:dagger:2.37"
+ implementation 'com.google.firebase:firebase-firestore-ktx:24.0.1'
+ kapt "com.google.dagger:dagger-compiler:2.37"
+
+ implementation "com.google.dagger:dagger-android:2.37"
+ kapt "com.google.dagger:dagger-android-processor:2.37"
+ implementation "com.google.dagger:dagger-android-support:2.37"
+
+ implementation 'com.github.bumptech.glide:glide:4.13.0'
+ kapt 'com.github.bumptech.glide:compiler:4.13.0'
+
+ implementation(project("::attribution"))
+
+ implementation 'com.google.android.flexbox:flexbox:3.0.0'
+
+ implementation "com.fasterxml.jackson.core:jackson-core:2.11.1"
+ implementation "com.fasterxml.jackson.core:jackson-databind:2.11.1"
+
+ testImplementation "junit:junit:$testJunit"
+ androidTestImplementation "androidx.test.ext:junit:$testAndroidxJunit"
+ androidTestImplementation "androidx.test.espresso:espresso-core:$testEspressoCore"
+}
+
+static def buildTime() {
+ def df = new SimpleDateFormat("dd-MM-yyyy HH:mm")
+ df.setTimeZone(TimeZone.getTimeZone("UTC"))
+ return df.format(new Date())
+}
\ No newline at end of file
diff --git a/app/google-services.json b/app/google-services.json
new file mode 100644
index 0000000..946400f
--- /dev/null
+++ b/app/google-services.json
@@ -0,0 +1,39 @@
+{
+ "project_info": {
+ "project_number": "1067582769202",
+ "project_id": "ma-sdk",
+ "storage_bucket": "ma-sdk.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:1067582769202:android:dcd5253dc319029f47942b",
+ "android_client_info": {
+ "package_name": "com.affise.app"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "1067582769202-j4rppfc1of6ob0ogl8hsmk8mtssv107a.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCkjqprruzV3tj0Zp6KOh19F2shCOGTMMg"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "1067582769202-j4rppfc1of6ob0ogl8hsmk8mtssv107a.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..51acf94
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/img1.png b/app/src/main/assets/img1.png
new file mode 100644
index 0000000..e12846a
Binary files /dev/null and b/app/src/main/assets/img1.png differ
diff --git a/app/src/main/assets/img2.png b/app/src/main/assets/img2.png
new file mode 100644
index 0000000..cf25980
Binary files /dev/null and b/app/src/main/assets/img2.png differ
diff --git a/app/src/main/assets/img3.png b/app/src/main/assets/img3.png
new file mode 100644
index 0000000..d2d5cdb
Binary files /dev/null and b/app/src/main/assets/img3.png differ
diff --git a/app/src/main/assets/img4.png b/app/src/main/assets/img4.png
new file mode 100644
index 0000000..cee15f9
Binary files /dev/null and b/app/src/main/assets/img4.png differ
diff --git a/app/src/main/assets/img5.png b/app/src/main/assets/img5.png
new file mode 100644
index 0000000..775b1a4
Binary files /dev/null and b/app/src/main/assets/img5.png differ
diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html
new file mode 100644
index 0000000..f969ce8
--- /dev/null
+++ b/app/src/main/assets/index.html
@@ -0,0 +1,1001 @@
+
+
+ Test SDK
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/oaid.cert.pem b/app/src/main/assets/oaid.cert.pem
new file mode 100644
index 0000000..33f8111
--- /dev/null
+++ b/app/src/main/assets/oaid.cert.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFfDCCA2SgAwIBAgICA/EwDQYJKoZIhvcNAQELBQAwgYAxCzAJBgNVBAYTAkNO
+MRAwDgYDVQQIDAdCZWlqaW5nMQwwCgYDVQQKDANNU0ExETAPBgNVBAsMCE9BSURf
+U0RLMR4wHAYDVQQDDBVjb20uYnVuLm1paXRtZGlkLnNpZ24xHjAcBgkqhkiG9w0B
+CQEWD21zYUBjYWljdC5hYy5jbjAgFw0yMTA2MDgxODA2MDNaGA8yMTc2MDUwMjE4
+MDYwM1owbTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcM
+B0JlaWppbmcxCTAHBgNVBAoMADEeMBwGA1UEAwwVY29tLmV4YW1wbGUub2FpZHRl
+c3QyMQ8wDQYJKoZIhvcNAQkBFgAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQCyGC9T3GW4BSsvnVNqb8+HlnjJAQvTPd7H13m1f945uUsUYYMaaZuICYjb
+4qefZUyQgyxBUTYfBOTpE7ccdcNoZgCp05RTng30zvkc4pjawJIkNeIC2NGlN7Rq
+uO4Ka/06LSuq5N+bfL78E0MwZAinoxSswf/yNY5R1LJ2b3I7BnW7rVSb7KbB0Q4z
+5KmuJ04hPVl7BtSdfVnvnRVTBDRm/cMgyupEl+CCGW8HUejzPHdHeHRK1rx7fuwQ
+I+5jgMmDYRNXZD4AmJ0vClurQR5avN2xOuxB8HbtB+lQitsNnQCvYY1Xp4s7Ftfm
+T6JzDNQ//vRgXxYrOaVFfoY/ugrO+SfZtqej+mdKR3KgOaAA75t8zcJfM6WGrbPJ
+l3xHc2bhD4kGwRkTAcHJ7WQOIrWz85CPCfIoYh8nZlBlsCNamJJh6Pl2nAtmnfMt
+DuYZJo/2QK6J/KFtywsYIekU6fiRsJaaunBf5sD9IxbvJcRRB+HRCsySKYEg8z7X
+lh90fHimfUgT0Gt44/o+nzPh7Cq9Ay6dWtMxYFx78Z1epapOJdUNpZWIET8wQHri
+JmbJ18TlidGRAW85kW2ka2cr4u6Ed68JQa+nN+kbS0dDeuvOBGOsiqqCy5aeA0oz
+88HRWWIga02PLTZbEs4PdYrNzHWCISjnk34KpwwxYJUn6Zpy1wIDAQABoxAwDjAM
+BgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQCEhPUIc+6sNQkVFCA/+ZvO
+LO5P/DEJ/WVJODF+HIWiwcilwLUGJuWV3b4jO4VAJiDvrNJH60BVhmV/jt7eYOHF
+99hOucbZRo+q4TrloXVZN/rVXzKqfHjv6y8Ss/YN45KanE/IeOxByRsMpKYollcs
+DYOrF82uPHeurBuIqIVxYZO7qIM8y8G2TaMGL+//IpT8CnQBPCmI4OxHu3l0bnRp
+FBHQQQSrEZHhbD+sbKkTQL5BJJJkx4gnxB24t42wYdYgaluj76l6fvspveEiURVk
+J2Ag0oo8XWXC+kjK/+BedrLJ0b5fyGck/B+emgWPXNmkN26F3dgcDjx3ILpWGqI/
+amoyR4IA726IYXFQ1WBm/nOAiq7DZYbKhuNk2ZhS50+eLu030MqsuxMNeAjSXCvC
+AQowuHMANLgSxHWJDKuEIpQYErGHS2QmeOyEA7sdmemVhZ+EgiB2QzaP2E13LDfd
+xc0n+0IOYxdr3bT6HTvdhT02+H7Fl8aS3mp9adhAJ0IT1Xnh4o4rKuxlYH5fM+RG
+4R4RaAB52tYxF6gOsud2KHWdQrt3gewP76FTzfizwJ8jkqBsIMAncQUHMyJwkLeg
+KEDOuPr+54bK8jJbo+g3VBQ760f7WGRdNZYcb2ml6+rbVKb8twd52ZNLdM51CA0Z
+mW2UsdCR0FgdAloOLI3KHQ==
+-----END CERTIFICATE-----
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..053d148
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/affise/app/application/App.kt b/app/src/main/java/com/affise/app/application/App.kt
new file mode 100644
index 0000000..954b112
--- /dev/null
+++ b/app/src/main/java/com/affise/app/application/App.kt
@@ -0,0 +1,49 @@
+package com.affise.app.application
+
+import android.content.SharedPreferences
+import com.affise.app.dependencies.DaggerAppComponent
+import com.affise.attribution.Affise
+import com.affise.attribution.events.autoCatchingClick.AutoCatchingType
+import com.affise.attribution.init.AffiseInitProperties
+import com.google.firebase.FirebaseApp
+import dagger.android.AndroidInjector
+import dagger.android.DaggerApplication
+import javax.inject.Inject
+
+class App : DaggerApplication() {
+
+ @Inject
+ lateinit var preferences: SharedPreferences
+
+ override fun onCreate() {
+ super.onCreate()
+
+ FirebaseApp.initializeApp(this)
+
+ val props = AffiseInitProperties(
+ affiseAppId = "AffiseDemoApp",
+ partParamName = "partParamName",
+ partParamNameToken = "partParamNameToken",
+ appToken = "app-token",
+ isProduction = false,
+ secretId = preferences.getString(SECRET_ID_KEY, "be07d122-3f3c-11ec-9bbc-0242ac130002"),
+ autoCatchingClickEvents = preferences.getStringSet(App.AUTO_CATCHING_TYPES_KEY, null)
+ ?.map { AutoCatchingType.valueOf(it) }
+ ?: emptyList(),
+ enabledMetrics = preferences.getBoolean(ENABLED_METRICS_KEY, false)
+ )
+
+ Affise.init(this, props)
+ }
+
+ override fun applicationInjector(): AndroidInjector =
+ DaggerAppComponent.builder()
+ .application(this)
+ .build()
+
+ companion object {
+ const val SECRET_ID_KEY = "SECRET_ID_KEY"
+ const val AUTO_CATCHING_TYPES_KEY = "AUTO_CATCHING_TYPES_KEY"
+ const val ENABLED_METRICS_KEY = "ENABLED_METRICS_KEY"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/AppComponent.kt b/app/src/main/java/com/affise/app/dependencies/AppComponent.kt
new file mode 100644
index 0000000..992c1f4
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/AppComponent.kt
@@ -0,0 +1,22 @@
+package com.affise.app.dependencies
+
+import android.app.Application
+import com.affise.app.application.App
+import dagger.BindsInstance
+import dagger.Component
+import dagger.android.AndroidInjector
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [AppModule::class])
+interface AppComponent : AndroidInjector {
+
+ @Component.Builder
+ interface Builder {
+
+ @BindsInstance
+ fun application(application: Application): Builder
+
+ fun build(): AppComponent
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/AppModule.kt b/app/src/main/java/com/affise/app/dependencies/AppModule.kt
new file mode 100644
index 0000000..de71b38
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/AppModule.kt
@@ -0,0 +1,41 @@
+package com.affise.app.dependencies
+
+import android.app.Application
+import android.content.Context
+import android.content.SharedPreferences
+import com.affise.app.dependencies.activities.ActivityModule
+import com.affise.app.dependencies.fragments.FragmentModule
+import com.affise.app.dependencies.models.ViewModelModule
+import com.affise.app.dependencies.repositories.RepositoryModule
+import com.affise.app.dependencies.store.StoreModule
+import com.affise.app.dependencies.usecases.UseCaseModule
+import com.fasterxml.jackson.databind.ObjectMapper
+import dagger.Module
+import dagger.Provides
+import dagger.android.support.AndroidSupportInjectionModule
+import javax.inject.Singleton
+
+@Module(
+ includes = [
+ AndroidSupportInjectionModule::class,
+ ActivityModule::class,
+ FragmentModule::class,
+ ViewModelModule::class,
+ RepositoryModule::class,
+ UseCaseModule::class,
+ StoreModule::class
+ ]
+)
+class AppModule {
+
+ @Provides
+ @Singleton
+ fun mapper(): ObjectMapper = ObjectMapper()
+
+ @Provides
+ @Singleton
+ fun sharedPreferences(app: Application): SharedPreferences = app.getSharedPreferences(
+ "com.affise.attribution",
+ Context.MODE_PRIVATE
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/activities/ActivityModule.kt b/app/src/main/java/com/affise/app/dependencies/activities/ActivityModule.kt
new file mode 100644
index 0000000..d677d63
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/activities/ActivityModule.kt
@@ -0,0 +1,15 @@
+package com.affise.app.dependencies.activities
+
+import com.affise.app.dependencies.activities.modules.MainActivityModule
+import com.affise.app.dependencies.scope.ActivityScope
+import com.affise.app.ui.activity.main.MainActivity
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+
+@Module
+abstract class ActivityModule {
+
+ @ActivityScope
+ @ContributesAndroidInjector(modules = [MainActivityModule::class])
+ abstract fun mainActivity(): MainActivity
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/activities/modules/MainActivityModule.kt b/app/src/main/java/com/affise/app/dependencies/activities/modules/MainActivityModule.kt
new file mode 100644
index 0000000..5a7d84b
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/activities/modules/MainActivityModule.kt
@@ -0,0 +1,20 @@
+package com.affise.app.dependencies.activities.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MainActivity
+import com.affise.app.ui.activity.main.MainActivityContract
+import com.affise.app.ui.activity.main.MainActivityViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class MainActivityModule {
+
+ @Provides
+ fun provideViewModel(
+ activity: MainActivity,
+ viewModelFactory: DaggerViewModelFactory
+ ): MainActivityContract.ViewModel = ViewModelProvider(activity, viewModelFactory)
+ .get(MainActivityViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/FragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/FragmentModule.kt
new file mode 100644
index 0000000..3008046
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/FragmentModule.kt
@@ -0,0 +1,55 @@
+package com.affise.app.dependencies.fragments
+
+import com.affise.app.dependencies.fragments.modules.*
+import com.affise.app.dependencies.scope.FragmentScope
+import com.affise.app.ui.fragments.autoCatching.AutoCatchingFragment
+import com.affise.app.ui.fragments.buttons.ButtonsFragment
+import com.affise.app.ui.fragments.cart.CartFragment
+import com.affise.app.ui.fragments.details.ProductDetailsFragment
+import com.affise.app.ui.fragments.home.HomeFragment
+import com.affise.app.ui.fragments.likes.LikesFragment
+import com.affise.app.ui.fragments.menu.MenuFragment
+import com.affise.app.ui.fragments.metrics.MetricsFragment
+import com.affise.app.ui.fragments.settings.SettingsFragment
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+
+@Module
+abstract class FragmentModule {
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [MenuFragmentModule::class])
+ abstract fun menuFragment(): MenuFragment
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [HomeFragmentModule::class])
+ abstract fun homeFragment(): HomeFragment
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [ProductDetailsFragmentModule::class])
+ abstract fun productDetailsFragment(): ProductDetailsFragment
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [LikesFragmentModule::class])
+ abstract fun likesFragment(): LikesFragment
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [CartFragmentModule::class])
+ abstract fun cartFragment(): CartFragment
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [ButtonsFragmentModule::class])
+ abstract fun buttonsFragment(): ButtonsFragment
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [SettingsFragmentModule::class])
+ abstract fun settingsFragment(): SettingsFragment
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [AutoCatchingFragmentModule::class])
+ abstract fun autoCatchingFragment(): AutoCatchingFragment
+
+ @FragmentScope
+ @ContributesAndroidInjector(modules = [MetricsFragmentModule::class])
+ abstract fun metricsFragment(): MetricsFragment
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/AutoCatchingFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/AutoCatchingFragmentModule.kt
new file mode 100644
index 0000000..afdb36d
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/AutoCatchingFragmentModule.kt
@@ -0,0 +1,27 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.autoCatching.AutoCatchingContract
+import com.affise.app.ui.fragments.autoCatching.AutoCatchingFragment
+import com.affise.app.ui.fragments.autoCatching.AutoCatchingViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class AutoCatchingFragmentModule {
+
+ @Provides
+ fun provideMenuHolder(
+ fragment: AutoCatchingFragment
+ ): MenuHolder = fragment.requireActivity() as MenuHolder
+
+ @Provides
+ fun provideViewModel(
+ fragment: AutoCatchingFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): AutoCatchingContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(AutoCatchingViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/ButtonsFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/ButtonsFragmentModule.kt
new file mode 100644
index 0000000..b49dba8
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/ButtonsFragmentModule.kt
@@ -0,0 +1,27 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.buttons.ButtonsContract
+import com.affise.app.ui.fragments.buttons.ButtonsFragment
+import com.affise.app.ui.fragments.buttons.ButtonsViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class ButtonsFragmentModule {
+
+ @Provides
+ fun provideMenuHolder(
+ fragment: ButtonsFragment
+ ): MenuHolder = fragment.requireActivity() as MenuHolder
+
+ @Provides
+ fun provideViewModel(
+ fragment: ButtonsFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): ButtonsContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(ButtonsViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/CartFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/CartFragmentModule.kt
new file mode 100644
index 0000000..787fff7
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/CartFragmentModule.kt
@@ -0,0 +1,27 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.cart.CartContract
+import com.affise.app.ui.fragments.cart.CartFragment
+import com.affise.app.ui.fragments.cart.CartViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class CartFragmentModule {
+
+ @Provides
+ fun provideMenuHolder(
+ fragment: CartFragment
+ ): MenuHolder = fragment.requireActivity() as MenuHolder
+
+ @Provides
+ fun provideViewModel(
+ fragment: CartFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): CartContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(CartViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/HomeFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/HomeFragmentModule.kt
new file mode 100644
index 0000000..d55b491
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/HomeFragmentModule.kt
@@ -0,0 +1,27 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.home.HomeContract
+import com.affise.app.ui.fragments.home.HomeFragment
+import com.affise.app.ui.fragments.home.HomeViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class HomeFragmentModule {
+
+ @Provides
+ fun provideMenuHolder(
+ fragment: HomeFragment
+ ): MenuHolder = fragment.requireActivity() as MenuHolder
+
+ @Provides
+ fun provideViewModel(
+ fragment: HomeFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): HomeContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(HomeViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/LikesFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/LikesFragmentModule.kt
new file mode 100644
index 0000000..39891d5
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/LikesFragmentModule.kt
@@ -0,0 +1,27 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.likes.LikesContract
+import com.affise.app.ui.fragments.likes.LikesFragment
+import com.affise.app.ui.fragments.likes.LikesViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class LikesFragmentModule {
+
+ @Provides
+ fun provideMenuHolder(
+ fragment: LikesFragment
+ ): MenuHolder = fragment.requireActivity() as MenuHolder
+
+ @Provides
+ fun provideViewModel(
+ fragment: LikesFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): LikesContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(LikesViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/MenuFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/MenuFragmentModule.kt
new file mode 100644
index 0000000..0a87a79
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/MenuFragmentModule.kt
@@ -0,0 +1,27 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.menu.MenuContract
+import com.affise.app.ui.fragments.menu.MenuFragment
+import com.affise.app.ui.fragments.menu.MenuViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class MenuFragmentModule {
+
+ @Provides
+ fun provideMenuHolder(
+ fragment: MenuFragment
+ ): MenuHolder = fragment.requireActivity() as MenuHolder
+
+ @Provides
+ fun provideViewModel(
+ fragment: MenuFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): MenuContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(MenuViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/MetricsFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/MetricsFragmentModule.kt
new file mode 100644
index 0000000..e4bccbc
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/MetricsFragmentModule.kt
@@ -0,0 +1,27 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.metrics.MetricsContract
+import com.affise.app.ui.fragments.metrics.MetricsFragment
+import com.affise.app.ui.fragments.metrics.MetricsViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class MetricsFragmentModule {
+
+ @Provides
+ fun provideMenuHolder(
+ fragment: MetricsFragment
+ ): MenuHolder = fragment.requireActivity() as MenuHolder
+
+ @Provides
+ fun provideViewModel(
+ fragment: MetricsFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): MetricsContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(MetricsViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/ProductDetailsFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/ProductDetailsFragmentModule.kt
new file mode 100644
index 0000000..f7d71ac
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/ProductDetailsFragmentModule.kt
@@ -0,0 +1,21 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.fragments.details.ProductDetailsContract
+import com.affise.app.ui.fragments.details.ProductDetailsFragment
+import com.affise.app.ui.fragments.details.ProductDetailsViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class ProductDetailsFragmentModule {
+
+ @Provides
+ fun provideViewModel(
+ fragment: ProductDetailsFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): ProductDetailsContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(ProductDetailsViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/SettingsFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/SettingsFragmentModule.kt
new file mode 100644
index 0000000..ffe2076
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/SettingsFragmentModule.kt
@@ -0,0 +1,27 @@
+package com.affise.app.dependencies.fragments.modules
+
+import androidx.lifecycle.ViewModelProvider
+import com.affise.app.dependencies.models.DaggerViewModelFactory
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.settings.SettingsContract
+import com.affise.app.ui.fragments.settings.SettingsFragment
+import com.affise.app.ui.fragments.settings.SettingsViewModel
+import dagger.Module
+import dagger.Provides
+
+@Module
+class SettingsFragmentModule {
+
+ @Provides
+ fun provideMenuHolder(
+ fragment: SettingsFragment
+ ): MenuHolder = fragment.requireActivity() as MenuHolder
+
+ @Provides
+ fun provideViewModel(
+ fragment: SettingsFragment,
+ viewModelFactory: DaggerViewModelFactory
+ ): SettingsContract.ViewModel = ViewModelProvider(
+ fragment, viewModelFactory
+ ).get(SettingsViewModel::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/models/DaggerViewModelFactory.kt b/app/src/main/java/com/affise/app/dependencies/models/DaggerViewModelFactory.kt
new file mode 100644
index 0000000..8fe227c
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/models/DaggerViewModelFactory.kt
@@ -0,0 +1,26 @@
+package com.affise.app.dependencies.models
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import javax.inject.Inject
+import javax.inject.Provider
+import javax.inject.Singleton
+
+@Singleton
+class DaggerViewModelFactory @Inject constructor(
+ private val creators: Map, @JvmSuppressWildcards Provider>
+) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ val creator = creators[modelClass] ?: creators.asIterable()
+ .firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
+ ?: throw IllegalArgumentException("Unknown model class $modelClass")
+
+ return try {
+ creator.get() as T
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/models/ViewModelKey.kt b/app/src/main/java/com/affise/app/dependencies/models/ViewModelKey.kt
new file mode 100644
index 0000000..3633100
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/models/ViewModelKey.kt
@@ -0,0 +1,13 @@
+package com.affise.app.dependencies.models
+
+import androidx.lifecycle.ViewModel
+import dagger.MapKey
+import kotlin.reflect.KClass
+
+@Target(
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY_GETTER,
+ AnnotationTarget.PROPERTY_SETTER
+)
+@MapKey
+annotation class ViewModelKey(val value: KClass)
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/models/ViewModelModule.kt b/app/src/main/java/com/affise/app/dependencies/models/ViewModelModule.kt
new file mode 100644
index 0000000..5006f0a
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/models/ViewModelModule.kt
@@ -0,0 +1,70 @@
+package com.affise.app.dependencies.models
+
+import androidx.lifecycle.ViewModel
+import com.affise.app.ui.activity.main.MainActivityViewModel
+import com.affise.app.ui.fragments.autoCatching.AutoCatchingViewModel
+import com.affise.app.ui.fragments.buttons.ButtonsViewModel
+import com.affise.app.ui.fragments.cart.CartViewModel
+import com.affise.app.ui.fragments.details.ProductDetailsViewModel
+import com.affise.app.ui.fragments.home.HomeViewModel
+import com.affise.app.ui.fragments.likes.LikesViewModel
+import com.affise.app.ui.fragments.menu.MenuViewModel
+import com.affise.app.ui.fragments.metrics.MetricsViewModel
+import com.affise.app.ui.fragments.settings.SettingsViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+interface ViewModelModule {
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(MainActivityViewModel::class)
+ fun mainActivityViewModel(viewModel: MainActivityViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(MenuViewModel::class)
+ fun menuViewModel(viewModel: MenuViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(HomeViewModel::class)
+ fun homeViewModel(viewModel: HomeViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(ProductDetailsViewModel::class)
+ fun productDetailsViewModel(viewModel: ProductDetailsViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(ButtonsViewModel::class)
+ fun buttonsViewModel(viewModel: ButtonsViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(LikesViewModel::class)
+ fun likesViewModel(viewModel: LikesViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(CartViewModel::class)
+ fun cartViewModel(viewModel: CartViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(SettingsViewModel::class)
+ fun settingsViewModel(viewModel: SettingsViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(AutoCatchingViewModel::class)
+ fun autoCatchingViewModel(viewModel: AutoCatchingViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(MetricsViewModel::class)
+ fun metricsViewModel(viewModel: MetricsViewModel): ViewModel
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/repositories/RepositoryModule.kt b/app/src/main/java/com/affise/app/dependencies/repositories/RepositoryModule.kt
new file mode 100644
index 0000000..afceeb2
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/repositories/RepositoryModule.kt
@@ -0,0 +1,18 @@
+package com.affise.app.dependencies.repositories
+
+import com.affise.app.repositories.ProductRepository
+import com.affise.app.repositories.ProductRepositoryImpl
+import com.affise.app.storage.Storage
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+
+@Module
+class RepositoryModule {
+
+ @Reusable
+ @Provides
+ fun productRepository(
+ storage: Storage
+ ): ProductRepository = ProductRepositoryImpl(storage)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/scope/ActivityScope.kt b/app/src/main/java/com/affise/app/dependencies/scope/ActivityScope.kt
new file mode 100644
index 0000000..02568c9
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/scope/ActivityScope.kt
@@ -0,0 +1,7 @@
+package com.affise.app.dependencies.scope
+
+import javax.inject.Scope
+
+@Scope
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ActivityScope
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/scope/FragmentScope.kt b/app/src/main/java/com/affise/app/dependencies/scope/FragmentScope.kt
new file mode 100644
index 0000000..7711cf9
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/scope/FragmentScope.kt
@@ -0,0 +1,7 @@
+package com.affise.app.dependencies.scope
+
+import javax.inject.Scope
+
+@Scope
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FragmentScope
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/store/StoreModule.kt b/app/src/main/java/com/affise/app/dependencies/store/StoreModule.kt
new file mode 100644
index 0000000..d240385
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/store/StoreModule.kt
@@ -0,0 +1,15 @@
+package com.affise.app.dependencies.store
+
+import com.affise.app.storage.FakeStorageImpl
+import com.affise.app.storage.Storage
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+class StoreModule {
+
+ @Singleton
+ @Provides
+ fun productRepository(): Storage = FakeStorageImpl()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/dependencies/usecases/UseCaseModule.kt b/app/src/main/java/com/affise/app/dependencies/usecases/UseCaseModule.kt
new file mode 100644
index 0000000..194be8e
--- /dev/null
+++ b/app/src/main/java/com/affise/app/dependencies/usecases/UseCaseModule.kt
@@ -0,0 +1,18 @@
+package com.affise.app.dependencies.usecases
+
+import com.affise.app.repositories.ProductRepository
+import com.affise.app.usecase.ProductUseCase
+import com.affise.app.usecase.ProductUseCaseImpl
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+
+@Module
+class UseCaseModule {
+
+ @Reusable
+ @Provides
+ fun productRepository(
+ repository: ProductRepository
+ ): ProductUseCase = ProductUseCaseImpl(repository)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/entity/Colors.kt b/app/src/main/java/com/affise/app/entity/Colors.kt
new file mode 100644
index 0000000..f8d6c5e
--- /dev/null
+++ b/app/src/main/java/com/affise/app/entity/Colors.kt
@@ -0,0 +1,10 @@
+package com.affise.app.entity
+
+import com.affise.app.R
+
+enum class Colors(val colorRes: Int) {
+ WHITE(R.color.item_white),
+ BLACK(R.color.item_black),
+ BLUE(R.color.item_blue),
+ YELLOW(R.color.item_yellow)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/entity/ProductEntity.kt b/app/src/main/java/com/affise/app/entity/ProductEntity.kt
new file mode 100644
index 0000000..31fa44e
--- /dev/null
+++ b/app/src/main/java/com/affise/app/entity/ProductEntity.kt
@@ -0,0 +1,17 @@
+package com.affise.app.entity
+
+import com.affise.app.ui.fragments.details.adapters.SizeType
+import java.math.BigDecimal
+
+data class ProductEntity(
+ val id: Long,
+ val price: BigDecimal?,
+ val unit: String?,
+ val name: String,
+ val description: String,
+ val rating: Double,
+ val size_type: SizeType,
+ val size: Double,
+ val color: Colors,
+ val images: List
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/extensions/Context.kt b/app/src/main/java/com/affise/app/extensions/Context.kt
new file mode 100644
index 0000000..07c4bf2
--- /dev/null
+++ b/app/src/main/java/com/affise/app/extensions/Context.kt
@@ -0,0 +1,27 @@
+package com.affise.app.extensions
+
+import android.app.Activity
+import android.content.Context
+import android.util.TypedValue
+import android.view.inputmethod.InputMethodManager
+
+fun Context.convertDpToPixels(dp: Float): Int {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ dp,
+ resources.displayMetrics
+ ).toInt()
+}
+
+fun Activity.hideKeyboard() {
+ val viewFocus = currentFocus
+
+ val token = viewFocus?.windowToken
+ ?: window.decorView.rootView.windowToken
+ ?: return
+
+ val imm = (getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)
+ ?: return
+
+ imm.hideSoftInputFromWindow(token, 0)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/extensions/IOWithErrorHandling.kt b/app/src/main/java/com/affise/app/extensions/IOWithErrorHandling.kt
new file mode 100644
index 0000000..b7fec16
--- /dev/null
+++ b/app/src/main/java/com/affise/app/extensions/IOWithErrorHandling.kt
@@ -0,0 +1,12 @@
+package com.affise.app.extensions
+
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlin.coroutines.CoroutineContext
+
+fun Dispatchers.IOWithErrorHandling(handler: (Throwable) -> Unit): CoroutineContext {
+ return SupervisorJob() + IO + CoroutineExceptionHandler { _, throwable ->
+ handler.invoke(throwable)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/receivers/PushService.kt b/app/src/main/java/com/affise/app/receivers/PushService.kt
new file mode 100644
index 0000000..1ac68cc
--- /dev/null
+++ b/app/src/main/java/com/affise/app/receivers/PushService.kt
@@ -0,0 +1,11 @@
+package com.affise.app.receivers
+
+import com.affise.attribution.Affise
+import com.google.firebase.messaging.FirebaseMessagingService
+
+class PushService : FirebaseMessagingService() {
+
+ override fun onNewToken(token: String) {
+ Affise.addPushToken(token)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/repositories/ProductRepository.kt b/app/src/main/java/com/affise/app/repositories/ProductRepository.kt
new file mode 100644
index 0000000..42855bd
--- /dev/null
+++ b/app/src/main/java/com/affise/app/repositories/ProductRepository.kt
@@ -0,0 +1,25 @@
+package com.affise.app.repositories
+
+import com.affise.app.entity.ProductEntity
+import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity
+import com.affise.app.ui.fragments.home.adapters.ProductNewEntity
+import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity
+import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity
+
+interface ProductRepository {
+ suspend fun getPopularProducts(): List
+ suspend fun getNewProducts(): List
+ suspend fun getLikeProducts(): List
+
+ suspend fun getProductDetails(id: Long): ProductEntity?
+
+ suspend fun inLike(productId: Long): Boolean
+ suspend fun inCart(productId: Long): Boolean
+
+ suspend fun updateLike(productId: Long?): Boolean
+ suspend fun updateCart(productId: Long?): Boolean
+
+ suspend fun getCart(): List
+ suspend fun removeOnCart(productId: Long): List
+ suspend fun addToCart(productId: Long): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/repositories/ProductRepositoryImpl.kt b/app/src/main/java/com/affise/app/repositories/ProductRepositoryImpl.kt
new file mode 100644
index 0000000..d256600
--- /dev/null
+++ b/app/src/main/java/com/affise/app/repositories/ProductRepositoryImpl.kt
@@ -0,0 +1,41 @@
+package com.affise.app.repositories
+
+import com.affise.app.entity.ProductEntity
+import com.affise.app.storage.Storage
+import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity
+import com.affise.app.ui.fragments.home.adapters.ProductNewEntity
+import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity
+import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity
+import javax.inject.Inject
+
+class ProductRepositoryImpl @Inject constructor(
+ private val storage: Storage
+) : ProductRepository {
+
+ override suspend fun getPopularProducts(
+ ): List = storage.getPopularProducts()
+
+ override suspend fun getNewProducts(): List = storage.getNewProducts()
+
+ override suspend fun getLikeProducts(): List = storage.getLikeProducts()
+
+ override suspend fun getProductDetails(id: Long): ProductEntity? = storage.getProductDetails(id)
+
+ override suspend fun inLike(productId: Long): Boolean = storage.inLike(productId)
+
+ override suspend fun inCart(productId: Long): Boolean = storage.inCart(productId)
+
+ override suspend fun updateLike(productId: Long?): Boolean = storage.updateLike(productId)
+
+ override suspend fun updateCart(productId: Long?): Boolean = storage.updateCart(productId)
+
+ override suspend fun getCart(): List = storage.getCart()
+
+ override suspend fun removeOnCart(
+ productId: Long
+ ): List = storage.removeOnCart(productId)
+
+ override suspend fun addToCart(
+ productId: Long
+ ): List = storage.addToCart(productId)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/storage/FakeStorageImpl.kt b/app/src/main/java/com/affise/app/storage/FakeStorageImpl.kt
new file mode 100644
index 0000000..ae2600b
--- /dev/null
+++ b/app/src/main/java/com/affise/app/storage/FakeStorageImpl.kt
@@ -0,0 +1,174 @@
+package com.affise.app.storage
+
+import com.affise.app.entity.Colors
+import com.affise.app.entity.ProductEntity
+import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity
+import com.affise.app.ui.fragments.details.adapters.SizeType
+import com.affise.app.ui.fragments.home.adapters.ProductNewEntity
+import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity
+import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity
+import javax.inject.Inject
+
+class FakeStorageImpl @Inject constructor() : Storage {
+
+ private val product1 = ProductEntity(
+ 1,
+ 10.toBigDecimal(),
+ "$",
+ "Air Max 270 G",
+ "The Nike Air max 270 G Huarache Run DNA ch. 1 reimagines not only the original Air",
+ 3.8,
+ SizeType.US,
+ 9.0,
+ Colors.BLACK,
+ listOf("img1.png")
+ )
+
+ private val product2 = ProductEntity(
+ 2,
+ 20.toBigDecimal(),
+ "$",
+ "Nike Ultra Zoom",
+ "The Nike Ultra Zoom Huarache Run DNA ch. 1 reimagines not only the original Air",
+ 4.8,
+ SizeType.US,
+ 8.0,
+ Colors.WHITE,
+ listOf("img2.png", "img2.png")
+ )
+
+ private val product3 = ProductEntity(
+ 3,
+ 18.6.toBigDecimal(),
+ "$",
+ "Nike Power pro",
+ "The Nike Ultra Zoom Huarache Run DNA ch. 1 reimagines not only the original Air",
+ 4.1,
+ SizeType.US,
+ 7.0,
+ Colors.BLUE,
+ listOf("img3.png", "img3.png", "img3.png")
+ )
+
+ private val product4 = ProductEntity(
+ 4,
+ 999.toBigDecimal(),
+ "$",
+ "Nike Air",
+ "The Nike Air Huarache Run DNA ch. 1 reimagines not only the original Air",
+ 2.1,
+ SizeType.EURO,
+ 45.0,
+ Colors.YELLOW,
+ listOf("img4.png", "img4.png", "img4.png", "img4.png")
+ )
+
+ private val product5 = ProductEntity(
+ 5,
+ 202.22.toBigDecimal(),
+ "$",
+ "Nike Power",
+ "The Nike Power Huarache Run DNA ch. 1 reimagines not only the original Air",
+ 4.6,
+ SizeType.US,
+ 9.0,
+ Colors.BLACK,
+ listOf("img5.png", "img5.png", "img5.png", "img5.png", "img5.png")
+ )
+
+ private val allProducts = listOf(
+ product1, product2, product3, product4, product5
+ )
+
+ private val popularProducts = mutableListOf(
+ product1, product2, product3, product4, product5
+ )
+
+ private val newProducts = mutableListOf(
+ product1, product2, product3, product4, product5
+ )
+
+ private val likeProducts = mutableListOf(
+ product1, product3
+ )
+
+ private val cartProducts = mutableListOf(
+ ProductCartEntity(product4, 1)
+ )
+
+ override suspend fun getPopularProducts(): List = popularProducts
+ .map {
+ ProductPopularEntity(it, inLike(it.id), inCart(it.id))
+ }
+
+ override suspend fun getNewProducts(): List = newProducts
+ .map {
+ ProductNewEntity(it, inLike(it.id), inCart(it.id))
+ }
+
+ override suspend fun getLikeProducts(): List = likeProducts
+ .map {
+ ProductLikeEntity(it, true, inCart(it.id))
+ }
+
+ override suspend fun getProductDetails(
+ id: Long
+ ): ProductEntity? = allProducts.find { it.id == id }
+
+ override suspend fun inLike(productId: Long): Boolean = likeProducts
+ .any { it.id == productId }
+
+ override suspend fun inCart(productId: Long): Boolean = cartProducts
+ .any { it.product.id == productId }
+
+ override suspend fun updateLike(productId: Long?): Boolean {
+ productId ?: return false
+
+ likeProducts.find { it.id == productId }
+ ?.let {
+ likeProducts.remove(it)
+ }
+ ?: allProducts.find { it.id == productId }?.let {
+ likeProducts.add(it)
+ }
+
+ return inLike(productId)
+ }
+
+ override suspend fun updateCart(productId: Long?): Boolean {
+ productId ?: return false
+
+ cartProducts.find { it.product.id == productId }
+ ?.let {
+ cartProducts.remove(it)
+ }
+ ?: allProducts.find { it.id == productId }?.let {
+ cartProducts.add(ProductCartEntity(it, 1))
+ }
+
+ return inCart(productId)
+ }
+
+ override suspend fun getCart(): List = cartProducts
+ .map { ProductCartEntity(it.product, it.count) }
+
+ override suspend fun removeOnCart(productId: Long): List {
+ cartProducts.find { it.product.id == productId }?.let {
+ if (it.count > 1) {
+ it.count = it.count - 1
+ } else {
+ cartProducts.remove(it)
+ }
+ }
+
+ return getCart()
+ }
+
+ override suspend fun addToCart(productId: Long): List {
+ cartProducts.find { it.product.id == productId }?.let {
+ it.count = it.count + 1
+ }
+
+ return getCart()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/storage/Storage.kt b/app/src/main/java/com/affise/app/storage/Storage.kt
new file mode 100644
index 0000000..15e1944
--- /dev/null
+++ b/app/src/main/java/com/affise/app/storage/Storage.kt
@@ -0,0 +1,25 @@
+package com.affise.app.storage
+
+import com.affise.app.entity.ProductEntity
+import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity
+import com.affise.app.ui.fragments.home.adapters.ProductNewEntity
+import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity
+import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity
+
+interface Storage {
+ suspend fun getPopularProducts(): List
+ suspend fun getNewProducts(): List
+ suspend fun getLikeProducts(): List
+
+ suspend fun getProductDetails(id: Long): ProductEntity?
+
+ suspend fun inLike(productId: Long): Boolean
+ suspend fun inCart(productId: Long): Boolean
+
+ suspend fun updateLike(productId: Long?): Boolean
+ suspend fun updateCart(productId: Long?): Boolean
+
+ suspend fun getCart(): List
+ suspend fun removeOnCart(productId: Long): List
+ suspend fun addToCart(productId: Long): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/activity/main/MainActivity.kt b/app/src/main/java/com/affise/app/ui/activity/main/MainActivity.kt
new file mode 100644
index 0000000..7406ae7
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/activity/main/MainActivity.kt
@@ -0,0 +1,76 @@
+package com.affise.app.ui.activity.main
+
+import android.os.Bundle
+import androidx.appcompat.app.AlertDialog
+import androidx.navigation.Navigation
+import com.affise.app.R
+import com.affise.app.databinding.ActivityMainBinding
+import com.affise.app.ui.fragments.menu.adapters.Menu
+import com.affise.attribution.Affise
+import dagger.android.support.DaggerAppCompatActivity
+import javax.inject.Inject
+
+class MainActivity : DaggerAppCompatActivity(), MenuHolder {
+
+ @Inject
+ lateinit var viewModel: MainActivityViewModel
+
+ lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ Affise.registerDeeplinkCallback {
+ AlertDialog.Builder(this)
+ .setMessage(it.toString())
+ .create().show()
+ true
+ }
+
+ with(ActivityMainBinding.inflate(layoutInflater)) {
+ setContentView(root)
+
+ binding = this
+ }
+
+ Navigation.findNavController(this, R.id.general_menu_holder).apply {
+ graph = navInflater.inflate(R.navigation.menu)
+ }
+ }
+
+ override fun changeMenuSate() {
+ binding.generalMenuNavigation.let {
+ if (it.isShown) {
+ binding.generalDrawer.closeDrawers()
+ } else {
+ binding.generalDrawer.openDrawer(it)
+ }
+ }
+ }
+
+ override fun selectMenu(menuItem: Menu.MenuItem?) {
+ menuItem ?: return
+
+ binding.generalMenuNavigation.let {
+ if (it.isShown) {
+ binding.generalDrawer.closeDrawers()
+ }
+ }
+
+ Navigation.findNavController(findViewById(R.id.general_holder)).apply {
+ graph = navInflater.inflate(menuItem.graph)
+ }
+ }
+
+ override fun onBackPressed() {
+ binding.generalMenuNavigation.let {
+ if (it.isShown) {
+ binding.generalDrawer.closeDrawers()
+
+ return
+ }
+ }
+
+ super.onBackPressed()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/activity/main/MainActivityContract.kt b/app/src/main/java/com/affise/app/ui/activity/main/MainActivityContract.kt
new file mode 100644
index 0000000..53ef93f
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/activity/main/MainActivityContract.kt
@@ -0,0 +1,5 @@
+package com.affise.app.ui.activity.main
+
+interface MainActivityContract {
+ interface ViewModel
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/activity/main/MainActivityViewModel.kt b/app/src/main/java/com/affise/app/ui/activity/main/MainActivityViewModel.kt
new file mode 100644
index 0000000..d5b73f7
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/activity/main/MainActivityViewModel.kt
@@ -0,0 +1,21 @@
+package com.affise.app.ui.activity.main
+
+import androidx.lifecycle.ViewModel
+import com.affise.attribution.Affise
+import com.google.firebase.messaging.FirebaseMessaging
+import javax.inject.Inject
+
+class MainActivityViewModel @Inject constructor() : ViewModel(), MainActivityContract.ViewModel {
+
+ init {
+ FirebaseMessaging.getInstance().token.addOnCompleteListener {
+ try {
+ it.result?.toString()?.let { newToken ->
+ Affise.addPushToken(newToken)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/activity/main/MenuHolder.kt b/app/src/main/java/com/affise/app/ui/activity/main/MenuHolder.kt
new file mode 100644
index 0000000..ce338b1
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/activity/main/MenuHolder.kt
@@ -0,0 +1,10 @@
+package com.affise.app.ui.activity.main
+
+import com.affise.app.ui.fragments.menu.adapters.Menu
+
+interface MenuHolder {
+
+ fun changeMenuSate()
+
+ fun selectMenu(menuItem: Menu.MenuItem?)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/adapters/CenterDecoration.kt b/app/src/main/java/com/affise/app/ui/adapters/CenterDecoration.kt
new file mode 100644
index 0000000..6c482d7
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/adapters/CenterDecoration.kt
@@ -0,0 +1,52 @@
+package com.affise.app.ui.adapters
+
+import android.graphics.Rect
+import android.view.View
+import androidx.annotation.Px
+import androidx.core.view.doOnPreDraw
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+class CenterDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
+
+ private var firstViewWidth = -1
+
+ private var lastViewWidth = -1
+
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ val adapterPosition = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition
+ val lm = parent.layoutManager as LinearLayoutManager
+
+ if (adapterPosition == 0) {
+
+ if (view.width != firstViewWidth) {
+ view.doOnPreDraw { parent.invalidateItemDecorations() }
+ }
+ firstViewWidth = view.width
+ outRect.left = parent.width / 2 - view.width / 2
+
+ if (lm.itemCount > 1) {
+ outRect.right = spacing / 2
+ } else {
+ outRect.right = outRect.left
+ }
+ } else if (adapterPosition == lm.itemCount - 1) {
+ if (view.width != lastViewWidth) {
+ view.doOnPreDraw { parent.invalidateItemDecorations() }
+ }
+ lastViewWidth = view.width
+ outRect.right = parent.width / 2 - view.width / 2
+ outRect.left = spacing / 2
+ } else {
+ outRect.left = spacing / 2
+ outRect.right = spacing / 2
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/adapters/CenterSnapHelper.kt b/app/src/main/java/com/affise/app/ui/adapters/CenterSnapHelper.kt
new file mode 100644
index 0000000..588d097
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/adapters/CenterSnapHelper.kt
@@ -0,0 +1,180 @@
+package com.affise.app.ui.adapters
+
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import androidx.recyclerview.widget.LinearSnapHelper
+import androidx.recyclerview.widget.OrientationHelper
+import androidx.recyclerview.widget.RecyclerView
+
+class CenterSnapHelper : LinearSnapHelper() {
+
+ private var verticalHelper: OrientationHelper? = null
+
+ private var horizontalHelper: OrientationHelper? = null
+
+ private var scrolled = false
+
+ private var recyclerView: RecyclerView? = null
+
+ private val scrollListener = object : RecyclerView.OnScrollListener() {
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ super.onScrollStateChanged(recyclerView, newState)
+
+ if (newState == RecyclerView.SCROLL_STATE_IDLE && scrolled) {
+ if (recyclerView.layoutManager != null) {
+ val view = findSnapView(recyclerView.layoutManager)
+ if (view != null) {
+ val out = calculateDistanceToFinalSnap(recyclerView.layoutManager!!, view)
+ if (out != null) {
+ recyclerView.smoothScrollBy(out[0], out[1])
+ }
+ }
+ }
+ scrolled = false
+ } else {
+ scrolled = true
+ }
+ }
+ }
+
+ fun scrollTo(position: Int, smooth: Boolean, again: Boolean = false) {
+ if (recyclerView?.layoutManager != null) {
+ val viewHolder = recyclerView!!.findViewHolderForAdapterPosition(position)
+
+ if (viewHolder != null) {
+ val distances = calculateDistanceToFinalSnap(
+ recyclerView!!.layoutManager!!,
+ viewHolder.itemView
+ )
+ if (smooth) {
+ recyclerView!!.smoothScrollBy(distances!![0], distances[1])
+ } else {
+ recyclerView!!.scrollBy(distances!![0], distances[1])
+ }
+ } else {
+ Handler(Looper.getMainLooper()).postDelayed({
+ scrollTo(position, smooth, again = true)
+ }, 100)
+
+ if (!again) {
+ if (smooth) {
+ recyclerView!!.smoothScrollToPosition(position)
+ } else {
+ recyclerView!!.scrollToPosition(position)
+ }
+ }
+ }
+ }
+ }
+
+ override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
+ if (layoutManager == null) {
+ return null
+ }
+ if (layoutManager.canScrollVertically()) {
+ return findCenterView(layoutManager, getVerticalHelper(layoutManager))
+ } else if (layoutManager.canScrollHorizontally()) {
+ return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
+ }
+
+ return null
+ }
+
+ override fun attachToRecyclerView(recyclerView: RecyclerView?) {
+ this.recyclerView = recyclerView
+
+ recyclerView?.addOnScrollListener(scrollListener)
+ }
+
+ override fun calculateDistanceToFinalSnap(
+ layoutManager: RecyclerView.LayoutManager,
+ targetView: View
+ ): IntArray? {
+ val out = IntArray(2)
+
+ if (layoutManager.canScrollHorizontally()) {
+ out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager))
+ } else {
+ out[0] = 0
+ }
+ if (layoutManager.canScrollVertically()) {
+ out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager))
+ } else {
+ out[1] = 0
+ }
+
+ return out
+ }
+
+ private fun findCenterView(
+ layoutManager: RecyclerView.LayoutManager,
+ helper: OrientationHelper
+ ): View? {
+ val childCount = layoutManager.childCount
+ if (childCount == 0) {
+ return null
+ }
+ var closestChild: View? = null
+ val center: Int = if (layoutManager.clipToPadding) {
+ helper.startAfterPadding + helper.totalSpace / 2
+ } else {
+ helper.end / 2
+ }
+ var absClosest = Integer.MAX_VALUE
+
+ for (i in 0 until childCount) {
+ val child = layoutManager.getChildAt(i)
+ val childCenter = if (helper == horizontalHelper) {
+ (child!!.x + child.width / 2).toInt()
+ } else {
+ (child!!.y + child.height / 2).toInt()
+ }
+ val absDistance = Math.abs(childCenter - center)
+
+ if (absDistance < absClosest) {
+ absClosest = absDistance
+ closestChild = child
+ }
+ }
+
+ return closestChild
+ }
+
+ private fun distanceToCenter(
+ layoutManager: RecyclerView.LayoutManager,
+ targetView: View,
+ helper: OrientationHelper
+ ): Int {
+ val childCenter = if (helper == horizontalHelper) {
+ (targetView.x + targetView.width / 2).toInt()
+ } else {
+ (targetView.y + targetView.height / 2).toInt()
+ }
+ val containerCenter = if (layoutManager.clipToPadding) {
+ helper.startAfterPadding + helper.totalSpace / 2
+ } else {
+ helper.end / 2
+ }
+
+ return childCenter - containerCenter
+ }
+
+ private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
+ if (verticalHelper == null || verticalHelper!!.layoutManager !== layoutManager) {
+ verticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
+ }
+
+ return verticalHelper!!
+ }
+
+ private fun getHorizontalHelper(
+ layoutManager: RecyclerView.LayoutManager
+ ): OrientationHelper {
+ if (horizontalHelper == null || horizontalHelper!!.layoutManager !== layoutManager) {
+ horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
+ }
+
+ return horizontalHelper!!
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/adapters/SpaceDecoration.kt b/app/src/main/java/com/affise/app/ui/adapters/SpaceDecoration.kt
new file mode 100644
index 0000000..0b66f63
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/adapters/SpaceDecoration.kt
@@ -0,0 +1,58 @@
+package com.affise.app.ui.adapters
+
+import android.content.Context
+import android.graphics.Rect
+import android.view.View
+import androidx.annotation.IntRange
+import androidx.annotation.Px
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.extensions.convertDpToPixels
+
+class SpaceDecoration(
+ private val paddingTop: Float = 0f,
+ private val paddingEnd: Float = 0f,
+ private val paddingBottom: Float = 0f,
+ private val paddingStart: Float = 0f,
+) : RecyclerView.ItemDecoration() {
+
+ private lateinit var appearance: Appearance
+
+ override fun getItemOffsets(
+ outRect: Rect,
+ itemView: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ ensureAppearance(parent.context)
+
+ parent.findContainingViewHolder(itemView)?.let { viewHolder ->
+ val position = viewHolder.adapterPosition
+ val lastPosition = parent.adapter?.itemCount?.let { it - 1 }
+
+ val extraPaddingTop = if (position != 0) appearance.paddingTop else 0
+ val extraPaddingStart = if (position != 0) appearance.paddingStart else 0
+ val extraPaddingBottom = if (position != lastPosition) appearance.paddingBottom else 0
+ val extraPaddingEnd = if (position != lastPosition) appearance.paddingEnd else 0
+
+ outRect.set(extraPaddingStart, extraPaddingTop, extraPaddingEnd, extraPaddingBottom)
+ }
+ }
+
+ private fun ensureAppearance(context: Context) {
+ if (!this::appearance.isInitialized) {
+ appearance = Appearance(
+ paddingTop = context.convertDpToPixels(paddingTop),
+ paddingEnd = context.convertDpToPixels(paddingEnd),
+ paddingBottom = context.convertDpToPixels(paddingBottom),
+ paddingStart = context.convertDpToPixels(paddingStart),
+ )
+ }
+ }
+
+ private class Appearance(
+ @param:IntRange(from = 0) @param:Px @field:IntRange(from = 0) @field:Px val paddingTop: Int,
+ @param:IntRange(from = 0) @param:Px @field:IntRange(from = 0) @field:Px val paddingEnd: Int,
+ @param:IntRange(from = 0) @param:Px @field:IntRange(from = 0) @field:Px val paddingBottom: Int,
+ @param:IntRange(from = 0) @param:Px @field:IntRange(from = 0) @field:Px val paddingStart: Int
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingContract.kt b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingContract.kt
new file mode 100644
index 0000000..7f1fc17
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingContract.kt
@@ -0,0 +1,13 @@
+package com.affise.app.ui.fragments.autoCatching
+
+import androidx.lifecycle.LiveData
+import com.affise.attribution.events.autoCatchingClick.AutoCatchingType
+
+interface AutoCatchingContract {
+
+ interface ViewModel {
+ fun setSelected(position: Int, checked: Boolean)
+
+ val checkedItems: LiveData>
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingFragment.kt
new file mode 100644
index 0000000..2165305
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingFragment.kt
@@ -0,0 +1,64 @@
+package com.affise.app.ui.fragments.autoCatching
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import com.affise.app.R
+import com.affise.app.databinding.FragmentAutoCatchingBinding
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.attribution.events.autoCatchingClick.AutoCatchingType
+import dagger.android.support.DaggerFragment
+import javax.inject.Inject
+
+class AutoCatchingFragment : DaggerFragment() {
+
+ @Inject
+ lateinit var viewModel: AutoCatchingContract.ViewModel
+
+ @Inject
+ lateinit var menuHolder: MenuHolder
+
+ lateinit var binding: FragmentAutoCatchingBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = FragmentAutoCatchingBinding.inflate(layoutInflater).apply {
+ binding = this
+
+ menuSate.setOnClickListener {
+ menuHolder.changeMenuSate()
+ }
+
+ changeTypes.setOnClickListener {
+ val checkedItems = viewModel.checkedItems.value ?: emptyList()
+
+ val checked = AutoCatchingType.values().map {
+ checkedItems.contains(it)
+ }.toBooleanArray()
+
+ val items: Array = AutoCatchingType.values()
+ .map { it.name }
+ .toTypedArray()
+
+ AlertDialog.Builder(requireContext())
+ .setTitle(R.string.select_auto_catching_types_title)
+ .setPositiveButton(R.string.ok, null)
+ .setMultiChoiceItems(
+ items,
+ checked
+ ) { _, which, isChecked ->
+ viewModel.setSelected(which, isChecked)
+ }.show()
+ }
+
+ testButton.setOnClickListener { }
+ testText.setOnClickListener { }
+ testImageButton.setOnClickListener { }
+ testImage.setOnClickListener { }
+ testGroup.setOnClickListener { }
+ }.root
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingViewModel.kt
new file mode 100644
index 0000000..c186817
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingViewModel.kt
@@ -0,0 +1,34 @@
+package com.affise.app.ui.fragments.autoCatching
+
+import android.content.SharedPreferences
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.affise.app.application.App
+import com.affise.attribution.Affise
+import com.affise.attribution.events.autoCatchingClick.AutoCatchingType
+import javax.inject.Inject
+
+class AutoCatchingViewModel @Inject constructor(
+ private val preferences: SharedPreferences
+) : ViewModel(), AutoCatchingContract.ViewModel {
+
+ override val checkedItems = MutableLiveData(
+ preferences.getStringSet(App.AUTO_CATCHING_TYPES_KEY, null)
+ ?.map { AutoCatchingType.valueOf(it) }
+ ?: emptyList()
+ )
+
+ override fun setSelected(position: Int, checked: Boolean) {
+ checkedItems.value = checkedItems.value?.toMutableList()?.apply {
+ val item = AutoCatchingType.values()[position]
+
+ if (checked) add(item) else remove(item)
+
+ preferences.edit()
+ .putStringSet(App.AUTO_CATCHING_TYPES_KEY, this.map { it.name }.toSet())
+ .apply()
+
+ Affise.setAutoCatchingTypes(this)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsContract.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsContract.kt
new file mode 100644
index 0000000..e8ef5ed
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsContract.kt
@@ -0,0 +1,5 @@
+package com.affise.app.ui.fragments.buttons
+
+interface ButtonsContract {
+ interface ViewModel
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsFragment.kt
new file mode 100644
index 0000000..f53dcc8
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsFragment.kt
@@ -0,0 +1,104 @@
+package com.affise.app.ui.fragments.buttons
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.WebChromeClient
+import android.webkit.WebViewClient
+import androidx.core.view.isVisible
+import com.affise.app.databinding.FragmentMainButtonsBinding
+import com.affise.app.extensions.hideKeyboard
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.buttons.adapters.EventsAdapter
+import com.affise.app.ui.fragments.buttons.adapters.ItemCallback
+import com.affise.app.ui.fragments.buttons.factories.DefaultEventsFactory
+import com.affise.attribution.Affise
+import com.google.android.material.tabs.TabLayout
+import dagger.android.support.DaggerFragment
+import javax.inject.Inject
+
+class ButtonsFragment : DaggerFragment() {
+
+ @Inject
+ lateinit var viewModel: ButtonsContract.ViewModel
+
+ @Inject
+ lateinit var menuHolder: MenuHolder
+
+ lateinit var binding: FragmentMainButtonsBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = FragmentMainButtonsBinding.inflate(layoutInflater).apply {
+ binding = this
+
+ menuSate.setOnClickListener {
+ menuHolder.changeMenuSate()
+ }
+
+ with(binding.webView) {
+ settings.javaScriptEnabled = true
+ webChromeClient = WebChromeClient()
+ webViewClient = WebViewClient()
+ }
+
+ tabEvents.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabSelected(tab: TabLayout.Tab?) {
+ when (tab?.text) {
+ "events" -> {
+ binding.eventsRecyclerView.isVisible = true
+ binding.webView.isVisible = false
+ }
+ "Web events" -> {
+ binding.eventsRecyclerView.isVisible = false
+ binding.webView.isVisible = true
+ }
+ else -> Unit
+ }
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab?) {
+ }
+
+ override fun onTabReselected(tab: TabLayout.Tab?) {
+ }
+ })
+
+ binding.sendEventsButton.setOnClickListener {
+ Affise.sendEvents()
+ }
+
+ binding.eventsRecyclerView.adapter = EventsAdapter(ItemCallback()) {
+ Affise.sendEvent(it)
+ }.apply {
+ submitList(DefaultEventsFactory().createEvents())
+ }
+ }.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ Affise.registerWebView(binding.webView)
+
+ try {
+ binding.webView.loadUrl("file:///android_asset/index.html")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ requireActivity().hideKeyboard()
+ }
+
+ override fun onDestroyView() {
+ Affise.unregisterWebView()
+
+ super.onDestroyView()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsViewModel.kt
new file mode 100644
index 0000000..31258ff
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsViewModel.kt
@@ -0,0 +1,6 @@
+package com.affise.app.ui.fragments.buttons
+
+import androidx.lifecycle.ViewModel
+import javax.inject.Inject
+
+class ButtonsViewModel @Inject constructor() : ViewModel(), ButtonsContract.ViewModel
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventViewHolder.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventViewHolder.kt
new file mode 100644
index 0000000..80b893c
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventViewHolder.kt
@@ -0,0 +1,94 @@
+package com.affise.app.ui.fragments.buttons.adapters
+
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.databinding.ListItemEventBinding
+import com.affise.attribution.events.Event
+import com.affise.attribution.events.predefined.*
+import com.affise.attribution.events.subscription.*
+
+class EventViewHolder(
+ private val binding: ListItemEventBinding,
+ private val onClick: (Event) -> Unit
+) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: Event) {
+ when (item) {
+ is AchieveLevelEvent -> bind(item, item.getName())
+ is AddPaymentInfoEvent -> bind(item, item.getName())
+ is AddToCartEvent -> bind(item, item.getName())
+ is AddToWishlistEvent -> bind(item, item.getName())
+ is ClickAdvEvent -> bind(item, item.getName())
+ is CompleteRegistrationEvent -> bind(item, item.getName())
+ is CompleteStreamEvent -> bind(item, item.getName())
+ is CompleteTrialEvent -> bind(item, item.getName())
+ is CompleteTutorialEvent -> bind(item, item.getName())
+ is ContentItemsViewEvent -> bind(item, item.getName())
+ is CustomId01Event -> bind(item, item.getName())
+ is CustomId02Event -> bind(item, item.getName())
+ is CustomId03Event -> bind(item, item.getName())
+ is CustomId04Event -> bind(item, item.getName())
+ is CustomId05Event -> bind(item, item.getName())
+ is CustomId06Event -> bind(item, item.getName())
+ is CustomId07Event -> bind(item, item.getName())
+ is CustomId08Event -> bind(item, item.getName())
+ is CustomId09Event -> bind(item, item.getName())
+ is CustomId10Event -> bind(item, item.getName())
+ is DeepLinkedEvent -> bind(item, item.getName())
+ is InitiatePurchaseEvent -> bind(item, item.getName())
+ is InitiateStreamEvent -> bind(item, item.getName())
+ is InviteEvent -> bind(item, item.getName())
+ is LastAttributedTouchEvent -> bind(item, item.getName())
+ is ListViewEvent -> bind(item, item.getName())
+ is LoginEvent -> bind(item, item.getName())
+ is OpenedFromPushNotificationEvent -> bind(item, item.getName())
+ is PurchaseEvent -> bind(item, item.getName())
+ is RateEvent -> bind(item, item.getName())
+ is ReEngageEvent -> bind(item, item.getName())
+ is ReserveEvent -> bind(item, item.getName())
+ is SalesEvent -> bind(item, item.getName())
+ is SearchEvent -> bind(item, item.getName())
+ is ShareEvent -> bind(item, item.getName())
+ is SpendCreditsEvent -> bind(item, item.getName())
+ is StartRegistrationEvent -> bind(item, item.getName())
+ is StartTrialEvent -> bind(item, item.getName())
+ is StartTutorialEvent -> bind(item, item.getName())
+ is SubscribeEvent -> bind(item, item.getName())
+ is TravelBookingEvent -> bind(item, item.getName())
+ is UnlockAchievementEvent -> bind(item, item.getName())
+ is UnsubscribeEvent -> bind(item, item.getName())
+ is UpdateEvent -> bind(item, item.getName())
+ is ViewAdvEvent -> bind(item, item.getName())
+ is ViewCartEvent -> bind(item, item.getName())
+ is ViewItemEvent -> bind(item, item.getName())
+ is ViewItemsEvent -> bind(item, item.getName())
+
+ is InitialSubscriptionEvent -> bind(item, item.subtype)
+ is InitialTrialEvent -> bind(item, item.subtype)
+ is InitialOfferEvent -> bind(item, item.subtype)
+ is ConvertedTrialEvent -> bind(item, item.subtype)
+ is ConvertedOfferEvent -> bind(item, item.subtype)
+ is TrialInRetryEvent -> bind(item, item.subtype)
+ is OfferInRetryEvent -> bind(item, item.subtype)
+ is SubscriptionInRetryEvent -> bind(item, item.subtype)
+ is RenewedSubscriptionEvent -> bind(item, item.subtype)
+ is FailedSubscriptionFromRetryEvent -> bind(item, item.subtype)
+ is FailedOfferFromRetryEvent -> bind(item, item.subtype)
+ is FailedTrialFromRetryEvent -> bind(item, item.subtype)
+ is FailedSubscriptionEvent -> bind(item, item.subtype)
+ is FailedOfferiseEvent -> bind(item, item.subtype)
+ is FailedTrialEvent -> bind(item, item.subtype)
+ is ReactivatedSubscriptionEvent -> bind(item, item.subtype)
+ is RenewedSubscriptionFromRetryEvent -> bind(item, item.subtype)
+ is ConvertedOfferFromRetryEvent -> bind(item, item.subtype)
+ is ConvertedTrialFromRetryEvent -> bind(item, item.subtype)
+ is UnsubscriptionEvent -> bind(item, item.subtype)
+ }
+ }
+
+ private fun bind(item: Event, title: String) {
+ binding.eventButton.setOnClickListener {
+ onClick.invoke(item)
+ }
+
+ binding.eventButton.text = title
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventsAdapter.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventsAdapter.kt
new file mode 100644
index 0000000..57f3597
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventsAdapter.kt
@@ -0,0 +1,28 @@
+package com.affise.app.ui.fragments.buttons.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import com.affise.app.databinding.ListItemEventBinding
+import com.affise.attribution.events.Event
+
+class EventsAdapter(
+ itemCallback: DiffUtil.ItemCallback,
+ private val onClick: (Event) -> Unit
+) : ListAdapter(itemCallback) {
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup, viewType: Int
+ ): EventViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ListItemEventBinding.inflate(it, parent, false)
+ }
+ .let {
+ EventViewHolder(it, onClick)
+ }
+
+ override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/ItemCallback.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/ItemCallback.kt
new file mode 100644
index 0000000..2579076
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/ItemCallback.kt
@@ -0,0 +1,16 @@
+package com.affise.app.ui.fragments.buttons.adapters
+
+import androidx.recyclerview.widget.DiffUtil
+import com.affise.attribution.events.Event
+
+class ItemCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: Event,
+ newItem: Event
+ ): Boolean = oldItem::class == newItem::class
+
+ override fun areContentsTheSame(
+ oldItem: Event,
+ newItem: Event
+ ): Boolean = true
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/DefaultEventsFactory.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/DefaultEventsFactory.kt
new file mode 100644
index 0000000..c17e0a3
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/DefaultEventsFactory.kt
@@ -0,0 +1,832 @@
+package com.affise.app.ui.fragments.buttons.factories
+
+import com.affise.attribution.events.Event
+import com.affise.attribution.events.predefined.AchieveLevelEvent
+import com.affise.attribution.events.predefined.AddPaymentInfoEvent
+import com.affise.attribution.events.predefined.AddToCartEvent
+import com.affise.attribution.events.predefined.AddToWishlistEvent
+import com.affise.attribution.events.predefined.ClickAdvEvent
+import com.affise.attribution.events.predefined.CompleteRegistrationEvent
+import com.affise.attribution.events.predefined.CompleteStreamEvent
+import com.affise.attribution.events.predefined.CompleteTrialEvent
+import com.affise.attribution.events.predefined.CompleteTutorialEvent
+import com.affise.attribution.events.predefined.ContentItemsViewEvent
+import com.affise.attribution.events.predefined.CustomId01Event
+import com.affise.attribution.events.predefined.CustomId02Event
+import com.affise.attribution.events.predefined.CustomId03Event
+import com.affise.attribution.events.predefined.CustomId04Event
+import com.affise.attribution.events.predefined.CustomId05Event
+import com.affise.attribution.events.predefined.CustomId06Event
+import com.affise.attribution.events.predefined.CustomId07Event
+import com.affise.attribution.events.predefined.CustomId08Event
+import com.affise.attribution.events.predefined.CustomId09Event
+import com.affise.attribution.events.predefined.CustomId10Event
+import com.affise.attribution.events.predefined.DeepLinkedEvent
+import com.affise.attribution.events.predefined.InitiatePurchaseEvent
+import com.affise.attribution.events.predefined.InitiateStreamEvent
+import com.affise.attribution.events.predefined.InviteEvent
+import com.affise.attribution.events.predefined.LastAttributedTouchEvent
+import com.affise.attribution.events.predefined.ListViewEvent
+import com.affise.attribution.events.predefined.LoginEvent
+import com.affise.attribution.events.predefined.OpenedFromPushNotificationEvent
+import com.affise.attribution.events.predefined.PredefinedParameters
+import com.affise.attribution.events.predefined.PurchaseEvent
+import com.affise.attribution.events.predefined.RateEvent
+import com.affise.attribution.events.predefined.ReEngageEvent
+import com.affise.attribution.events.predefined.ReserveEvent
+import com.affise.attribution.events.predefined.SalesEvent
+import com.affise.attribution.events.predefined.SearchEvent
+import com.affise.attribution.events.predefined.ShareEvent
+import com.affise.attribution.events.predefined.SpendCreditsEvent
+import com.affise.attribution.events.predefined.StartRegistrationEvent
+import com.affise.attribution.events.predefined.StartTrialEvent
+import com.affise.attribution.events.predefined.StartTutorialEvent
+import com.affise.attribution.events.predefined.SubscribeEvent
+import com.affise.attribution.events.predefined.TouchType
+import com.affise.attribution.events.predefined.TravelBookingEvent
+import com.affise.attribution.events.predefined.UnlockAchievementEvent
+import com.affise.attribution.events.predefined.UnsubscribeEvent
+import com.affise.attribution.events.predefined.UpdateEvent
+import com.affise.attribution.events.predefined.ViewAdvEvent
+import com.affise.attribution.events.predefined.ViewCartEvent
+import com.affise.attribution.events.predefined.ViewItemEvent
+import com.affise.attribution.events.predefined.ViewItemsEvent
+import com.affise.attribution.events.subscription.*
+import org.json.JSONArray
+import org.json.JSONObject
+
+class DefaultEventsFactory : EventsFactory {
+ override fun createEvents(): List {
+ return listOf(
+ createAchieveLevelEvent(),
+ createAddPaymentInfoEvent(),
+ createAddToCartEvent(),
+ createAddToWishlistEvent(),
+ createClickAdvEvent(),
+ createCompleteRegistrationEvent(),
+ createCompleteStreamEvent(),
+ createCompleteTrialEvent(),
+ createCompleteTutorialEvent(),
+ createContentItemsViewEvent(),
+ createCustomId01Event(),
+ createCustomId02Event(),
+ createCustomId03Event(),
+ createCustomId04Event(),
+ createCustomId05Event(),
+ createCustomId06Event(),
+ createCustomId07Event(),
+ createCustomId08Event(),
+ createCustomId09Event(),
+ createCustomId10Event(),
+ createDeepLinkedEvent(),
+ createInitiatePurchaseEvent(),
+ createInitiateStreamEvent(),
+ createInviteEvent(),
+ createLastAttributedTouchEvent(),
+ createListViewEvent(),
+ createLoginEvent(),
+ createOpenedFromPushNotificationEvent(),
+ createPurchaseEvent(),
+ createRateEvent(),
+ createReEngageEvent(),
+ createReserveEvent(),
+ createSalesEvent(),
+ createSearchEvent(),
+ createShareEvent(),
+ createSpendCreditsEvent(),
+ createStartRegistrationEvent(),
+ createStartTrialEvent(),
+ createStartTutorialEvent(),
+ createSubscribeEvent(),
+ createTravelBookingEvent(),
+ createUnlockAchievementEvent(),
+ createUnsubscribeEvent(),
+ createUpdateEvent(),
+ createViewAdvEvent(),
+ createViewCartEvent(),
+ createViewItemEvent(),
+ createViewItemsEvent(),
+ createInitialSubscriptionEvent(),
+ createInitialTrialEvent(),
+ createInitialOfferEvent(),
+ createConvertedTrialEvent(),
+ createConvertedOfferEvent(),
+ createTrialInRetryEvent(),
+ createOfferInRetryEvent(),
+ createSubscriptionInRetryEvent(),
+ createRenewedSubscriptionEvent(),
+ createFailedSubscriptionFromRetryEvent(),
+ createFailedOfferFromRetryEvent(),
+ createFailedTrialFromRetryEvent(),
+ createFailedSubscriptionEvent(),
+ createFailedOfferiseEvent(),
+ createFailedTrialEvent(),
+ createReactivatedSubscriptionEvent(),
+ createRenewedSubscriptionFromRetryEvent(),
+ createConvertedOfferFromRetryEvent(),
+ createConvertedTrialFromRetryEvent(),
+ createUnsubscriptionEvent()
+ )
+ }
+
+ private fun createUnsubscriptionEvent() = UnsubscriptionEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Unsubscription"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createConvertedTrialFromRetryEvent() = ConvertedTrialFromRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createConvertedOfferFromRetryEvent() = ConvertedOfferFromRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createRenewedSubscriptionFromRetryEvent() = RenewedSubscriptionFromRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createReactivatedSubscriptionEvent() = ReactivatedSubscriptionEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createFailedTrialEvent() = FailedTrialEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createFailedOfferiseEvent() = FailedOfferiseEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createFailedSubscriptionEvent() = FailedSubscriptionEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createFailedTrialFromRetryEvent() = FailedTrialFromRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createFailedOfferFromRetryEvent() = FailedOfferFromRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createFailedSubscriptionFromRetryEvent() = FailedSubscriptionFromRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createRenewedSubscriptionEvent() = RenewedSubscriptionEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createTrialInRetryEvent() = TrialInRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createOfferInRetryEvent() = OfferInRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createSubscriptionInRetryEvent() = SubscriptionInRetryEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createInitialOfferEvent() = InitialOfferEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createInitialTrialEvent() = InitialTrialEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createInitialSubscriptionEvent() = InitialSubscriptionEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createConvertedTrialEvent() = ConvertedTrialEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createConvertedOfferEvent() = ConvertedOfferEvent(
+ JSONObject().apply {
+ put("affise_event_revenue", 2.99)
+ put("affise_event_currency", "USD")
+ put("affise_event_product_id", 278459628375)
+ },
+ "Subscription Plus"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+
+ private fun createAchieveLevelEvent(): Event {
+ val level = JSONObject().apply {
+ put("old_level", 69)
+ put("new_level", 70)
+ }
+ return AchieveLevelEvent(level, System.currentTimeMillis(), "warlock").apply {
+ addPredefinedParameter(PredefinedParameters.DEEP_LINK, "https://new-game.lt")
+ addPredefinedParameter(PredefinedParameters.SCORE, "25013")
+ addPredefinedParameter(PredefinedParameters.LEVEL, "70")
+ addPredefinedParameter(PredefinedParameters.SUCCESS, "true")
+ addPredefinedParameter(PredefinedParameters.TUTORIAL_ID, "12")
+ }
+ }
+
+ private fun createAddPaymentInfoEvent(): Event {
+ val data = JSONObject().apply {
+ put("card", 4138)
+ put("type", "phone")
+ }
+ return AddPaymentInfoEvent(data, System.currentTimeMillis(), "taxi").apply {
+ addPredefinedParameter(PredefinedParameters.PURCHASE_CURRENCY, "USD")
+ }
+ }
+
+ private fun createAddToCartEvent(): Event {
+ val products =
+ listOf("milk", "cookies", "meat", "vegetables").shuffled().take(1).joinToString()
+ val items = JSONObject().apply {
+ put("items", products)
+ }
+ return AddToCartEvent(items, System.currentTimeMillis()).apply {
+ addPredefinedParameter(PredefinedParameters.DESCRIPTION, "best before 2029")
+ }
+ }
+
+ private fun createAddToWishlistEvent(): Event {
+ val wishes = listOf("car", "house", "sdk").shuffled().take(1).joinToString()
+ val items = JSONObject().apply {
+ put("items", wishes)
+ }
+ return AddToWishlistEvent(items, System.currentTimeMillis(), "next year").apply {
+ addPredefinedParameter(PredefinedParameters.COUNTRY, "Russia")
+ addPredefinedParameter(PredefinedParameters.CITY, "Voronezh")
+ addPredefinedParameter(PredefinedParameters.LAT, "42")
+ addPredefinedParameter(PredefinedParameters.LONG, "24")
+ }
+ }
+
+ private fun createClickAdvEvent(): Event {
+ return ClickAdvEvent("facebook-meta", System.currentTimeMillis(), "header").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "PARAM_01")
+ addPredefinedParameter(PredefinedParameters.PARAM_02, "PARAM_02")
+ addPredefinedParameter(PredefinedParameters.PARAM_03, "PARAM_03")
+ addPredefinedParameter(PredefinedParameters.PARAM_04, "PARAM_04")
+ addPredefinedParameter(PredefinedParameters.PARAM_05, "PARAM_05")
+ addPredefinedParameter(PredefinedParameters.PARAM_06, "PARAM_06")
+ addPredefinedParameter(PredefinedParameters.PARAM_07, "PARAM_07")
+ addPredefinedParameter(PredefinedParameters.PARAM_08, "PARAM_08")
+ addPredefinedParameter(PredefinedParameters.PARAM_09, "PARAM_09")
+ addPredefinedParameter(PredefinedParameters.PARAM_10, "PARAM_10")
+ }
+ }
+
+ private fun createCompleteRegistrationEvent(): Event {
+ val data = JSONObject().apply {
+ put("email", "dog@gmail.com")
+ }
+ return CompleteRegistrationEvent(data, System.currentTimeMillis(), "promo").apply {
+ addPredefinedParameter(PredefinedParameters.VALIDATED, "02.11.2021")
+ addPredefinedParameter(PredefinedParameters.REVIEW_TEXT, "approve")
+ addPredefinedParameter(PredefinedParameters.CUSTOMER_SEGMENT, "DOG")
+ }
+ }
+
+ private fun createCompleteStreamEvent(): Event {
+ val data = JSONObject().apply {
+ put("streamer", "cat")
+ put("max_viewers", "29")
+ }
+ return CompleteStreamEvent(data, System.currentTimeMillis(), "23 hours").apply {
+ addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $")
+ }
+ }
+
+ private fun createCompleteTrialEvent(): Event {
+ val data = JSONObject().apply {
+ put("player", "ghost")
+ }
+ return CompleteTrialEvent(data, System.currentTimeMillis(), "time").apply {
+ addPredefinedParameter(PredefinedParameters.REGISTRATION_METHOD, "SMS")
+ }
+ }
+
+ private fun createCompleteTutorialEvent(): Event {
+ val data = JSONObject().apply {
+ put("name", "intro")
+ }
+ return CompleteTutorialEvent(data, System.currentTimeMillis(), "intro").apply {
+ addPredefinedParameter(PredefinedParameters.REGISTRATION_METHOD, "SMS")
+ }
+ }
+
+ private fun createContentItemsViewEvent(): Event {
+ val data = listOf(
+ JSONObject().apply {
+ put("item", "book")
+ },
+ JSONObject().apply {
+ put("item", "guitar")
+ }
+ )
+ return ContentItemsViewEvent(data, "personal").apply {
+ addPredefinedParameter(PredefinedParameters.CONTENT, "Greatest Hits")
+ addPredefinedParameter(PredefinedParameters.CONTENT_ID, "2561")
+ addPredefinedParameter(PredefinedParameters.CONTENT_LIST, "songs, videos")
+ addPredefinedParameter(PredefinedParameters.CONTENT_TYPE, "MATURE")
+ addPredefinedParameter(PredefinedParameters.CURRENCY, "USD")
+ }
+ }
+
+ private fun createCustomId01Event(): Event {
+ return CustomId01Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId02Event(): Event {
+ return CustomId02Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId03Event(): Event {
+ return CustomId03Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId04Event(): Event {
+ return CustomId04Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId05Event(): Event {
+ return CustomId05Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId06Event(): Event {
+ return CustomId06Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId07Event(): Event {
+ return CustomId07Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId08Event(): Event {
+ return CustomId08Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId09Event(): Event {
+ return CustomId09Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createCustomId10Event(): Event {
+ return CustomId10Event("custom", System.currentTimeMillis(), "custom").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createDeepLinkedEvent(): Event {
+ return DeepLinkedEvent(true, "referrer: google").apply {
+ addPredefinedParameter(PredefinedParameters.ADREV_AD_TYPE, "interstitial")
+ addPredefinedParameter(PredefinedParameters.REGION, "ASIA")
+ addPredefinedParameter(PredefinedParameters.CLASS, "student")
+ }
+ }
+
+ private fun createInitiatePurchaseEvent(): Event {
+ val data = JSONObject().apply {
+ put("payment", "card")
+ }
+ return InitiatePurchaseEvent(data, System.currentTimeMillis(), "best price").apply {
+ addPredefinedParameter(PredefinedParameters.ORDER_ID, "23123")
+ addPredefinedParameter(PredefinedParameters.PRICE, "2.19$")
+ addPredefinedParameter(PredefinedParameters.QUANTITY, "1")
+ }
+ }
+
+ private fun createInitiateStreamEvent(): Event {
+ val data = JSONObject().apply {
+ put("streamer", "car")
+ put("date", "02.03.2021")
+ }
+ return InitiateStreamEvent(data, System.currentTimeMillis(), "shooter").apply {
+ addPredefinedParameter(PredefinedParameters.COUPON_CODE, "25XLKM")
+ addPredefinedParameter(PredefinedParameters.VIRTUAL_CURRENCY_NAME, "BTC")
+ }
+ }
+
+ private fun createInviteEvent(): Event {
+ val data = JSONObject().apply {
+ put("invitation", "mail")
+ put("date", "02.03.2021")
+ }
+ return InviteEvent(data, System.currentTimeMillis(), "dancing").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createLastAttributedTouchEvent(): Event {
+ val touchData = JSONObject().apply {
+ put("header", 2)
+ }
+ return LastAttributedTouchEvent(
+ TouchType.CLICK,
+ System.currentTimeMillis(),
+ touchData,
+ "tablet"
+ ).apply {
+ addPredefinedParameter(PredefinedParameters.SUBSCRIPTION_ID, "lasAK22")
+ }
+ }
+
+ private fun createListViewEvent(): Event {
+ val data = JSONObject().apply {
+ put("clothes", 2)
+ }
+ return ListViewEvent(data, "items").apply {
+ addPredefinedParameter(PredefinedParameters.PAYMENT_INFO_AVAILABLE, "card")
+ addPredefinedParameter(PredefinedParameters.SEARCH_STRING, "best car wash")
+ addPredefinedParameter(PredefinedParameters.SUGGESTED_DESTINATIONS, "crete, spain")
+ addPredefinedParameter(
+ PredefinedParameters.SUGGESTED_HOTELS,
+ "beach resort, marina spa"
+ )
+ }
+ }
+
+ private fun createLoginEvent(): Event {
+ val data = JSONObject().apply {
+ put("email", "cat@gmail.com")
+ }
+ return LoginEvent(data, System.currentTimeMillis(), "web").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createOpenedFromPushNotificationEvent(): Event {
+ return OpenedFromPushNotificationEvent("silent", "active").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createPurchaseEvent(): Event {
+ val data = JSONObject().apply {
+ put("phone", 1)
+ put("case", 1)
+ }
+ return PurchaseEvent(data, System.currentTimeMillis(), "apple").apply {
+ addPredefinedParameter(PredefinedParameters.ORDER_ID, "23123")
+ addPredefinedParameter(PredefinedParameters.PRICE, "2.19$")
+ addPredefinedParameter(PredefinedParameters.QUANTITY, "1")
+ }
+ }
+
+ private fun createRateEvent(): Event {
+ val data = JSONObject().apply {
+ put("rating", 5)
+ }
+ return RateEvent(data, System.currentTimeMillis(), "no bugs").apply {
+ addPredefinedParameter(PredefinedParameters.PREFERRED_NEIGHBORHOODS, "2")
+ addPredefinedParameter(PredefinedParameters.PREFERRED_NUM_STOPS, "4")
+ addPredefinedParameter(PredefinedParameters.PREFERRED_PRICE_RANGE, "10-22")
+ addPredefinedParameter(PredefinedParameters.PREFERRED_STAR_RATINGS, "4.6")
+ }
+ }
+
+ private fun createReEngageEvent(): Event {
+ return ReEngageEvent("data", "web").apply {
+ addPredefinedParameter(PredefinedParameters.CUSTOMER_USER_ID, "4")
+ }
+ }
+
+ private fun createReserveEvent(): Event {
+ val data = JSONObject().apply {
+ put("ticket", "movie")
+ }
+ val data2 = JSONObject().apply {
+ put("food", "coke")
+ }
+ return ReserveEvent(listOf(data, data2), System.currentTimeMillis(), "discount").apply {
+ addPredefinedParameter(PredefinedParameters.ORDER_ID, "23123")
+ addPredefinedParameter(PredefinedParameters.PRICE, "2.19$")
+ addPredefinedParameter(PredefinedParameters.QUANTITY, "1")
+ }
+ }
+
+ private fun createSalesEvent(): Event {
+ val data = JSONObject().apply {
+ put("phone", 1)
+ put("case", 1)
+ }
+ return SalesEvent(data, System.currentTimeMillis(), "apple").apply {
+ addPredefinedParameter(PredefinedParameters.ORDER_ID, "23123")
+ addPredefinedParameter(PredefinedParameters.PRICE, "2.19$")
+ addPredefinedParameter(PredefinedParameters.QUANTITY, "1")
+ }
+ }
+
+ private fun createSearchEvent(): Event {
+ val data = JSONArray().apply {
+ put("eco milk")
+ put("grass")
+ }
+ return SearchEvent(data, System.currentTimeMillis(), "browser").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createShareEvent(): Event {
+ val data = JSONObject().apply {
+ put("post_id", "252242")
+ put("post_img", "img.webp")
+ }
+
+ return ShareEvent(data, System.currentTimeMillis(), "telegram").apply {
+ addPredefinedParameter(PredefinedParameters.RECEIPT_ID, "22")
+ }
+ }
+
+ private fun createSpendCreditsEvent(): Event {
+ return SpendCreditsEvent(2142, System.currentTimeMillis(), "boosters").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createStartRegistrationEvent(): Event {
+ val data = JSONObject().apply {
+ put("time", "19:22:11")
+ }
+ return StartRegistrationEvent(data, System.currentTimeMillis(), "referrer").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createStartTrialEvent(): Event {
+ val data = JSONObject().apply {
+ put("time", "19:22:11")
+ }
+ return StartTrialEvent(data, System.currentTimeMillis(), "30-days").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createStartTutorialEvent(): Event {
+ val data = JSONObject().apply {
+ put("time", "19:22:11")
+ put("reward", "22")
+ }
+
+ return StartTutorialEvent(data, System.currentTimeMillis(), "video-feature").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createSubscribeEvent(): Event {
+ val data = JSONObject().apply {
+ put("streamer", "cat")
+ }
+ return SubscribeEvent(data, System.currentTimeMillis(), "wire").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createTravelBookingEvent(): Event {
+ val data = JSONArray().apply {
+ put("may")
+ put("august")
+ }
+ return TravelBookingEvent(data, "booking").apply {
+ addPredefinedParameter(PredefinedParameters.NUM_ADULTS, "1")
+ addPredefinedParameter(PredefinedParameters.NUM_CHILDREN, "2")
+ addPredefinedParameter(PredefinedParameters.NUM_INFANTS, "1")
+ addPredefinedParameter(PredefinedParameters.DATE_A, "30.12.2020")
+ addPredefinedParameter(PredefinedParameters.DATE_B, "01.01.2021")
+ addPredefinedParameter(PredefinedParameters.DEPARTING_ARRIVAL_DATE, "01.01.2021")
+ addPredefinedParameter(PredefinedParameters.DEPARTING_DEPARTURE_DATE, "30.12.2020")
+ addPredefinedParameter(PredefinedParameters.DESTINATION_A, "usa")
+ addPredefinedParameter(PredefinedParameters.DESTINATION_B, "mexico")
+ addPredefinedParameter(PredefinedParameters.DESTINATION_LIST, "usa, mexico")
+ addPredefinedParameter(PredefinedParameters.HOTEL_SCORE, "5")
+ addPredefinedParameter(PredefinedParameters.TRAVEL_START, "01.12.2020")
+ addPredefinedParameter(PredefinedParameters.TRAVEL_END, "01.12.2021")
+ }
+ }
+
+ private fun createUnlockAchievementEvent(): Event {
+ val data = JSONObject().apply {
+ put("achievement", "new level")
+ }
+ return UnlockAchievementEvent(data, System.currentTimeMillis(), "best damage").apply {
+ addPredefinedParameter(PredefinedParameters.USER_SCORE, "12552")
+ addPredefinedParameter(PredefinedParameters.ACHIEVEMENT_ID, "1334-1225-ASDZ")
+ }
+ }
+
+ private fun createUnsubscribeEvent(): Event {
+ val data = JSONObject().apply {
+ put("reason", "span")
+ }
+ return UnsubscribeEvent(data, System.currentTimeMillis(), "02.01.2021").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createUpdateEvent(): Event {
+ val data = JSONArray().apply {
+ put("com.facebook")
+ }
+ return UpdateEvent(data, "firmware").apply {
+ addPredefinedParameter(PredefinedParameters.EVENT_START, "01.02.2021")
+ addPredefinedParameter(PredefinedParameters.EVENT_END, "01.01.2022")
+ addPredefinedParameter(PredefinedParameters.NEW_VERSION, "8")
+ addPredefinedParameter(PredefinedParameters.OLD_VERSION, "1.8")
+ }
+ }
+
+ private fun createViewAdvEvent(): Event {
+ val data = JSONObject().apply {
+ put("source", "amazon")
+ }
+ return ViewAdvEvent(data, System.currentTimeMillis(), "skip").apply {
+ addPredefinedParameter(PredefinedParameters.RETURNING_ARRIVAL_DATE, "01.12.2021")
+ addPredefinedParameter(PredefinedParameters.RETURNING_DEPARTURE_DATE, "01.12.2020")
+ }
+ }
+
+ private fun createViewCartEvent(): Event {
+ val data = JSONObject().apply {
+ put("cart_value", "25.22$")
+ put("cart_items", "2")
+ }
+ return ViewCartEvent(data, "main").apply {
+ addPredefinedParameter(PredefinedParameters.PARAM_01, "param1")
+ }
+ }
+
+ private fun createViewItemEvent(): Event {
+ val data = JSONObject().apply {
+ put("section_name", "header")
+ }
+ return ViewItemEvent(data, "main").apply {
+ addPredefinedParameter(PredefinedParameters.MAX_RATING_VALUE, "5")
+ addPredefinedParameter(PredefinedParameters.RATING_VALUE, "4.9")
+ }
+ }
+
+ private fun createViewItemsEvent(): Event {
+ val data = JSONArray().apply {
+ put(JSONObject().apply { put("section_name", "header") })
+ put(JSONObject().apply { put("section_name", "section-1") })
+ put(JSONObject().apply { put("section_name", "section-2") })
+ put(JSONObject().apply { put("section_name", "footer") })
+ }
+ return ViewItemsEvent(data, "main").apply {
+ addPredefinedParameter(PredefinedParameters.MAX_RATING_VALUE, "5")
+ addPredefinedParameter(PredefinedParameters.RATING_VALUE, "4.9")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/EventsFactory.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/EventsFactory.kt
new file mode 100644
index 0000000..5ab7106
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/EventsFactory.kt
@@ -0,0 +1,7 @@
+package com.affise.app.ui.fragments.buttons.factories
+
+import com.affise.attribution.events.Event
+
+interface EventsFactory {
+ fun createEvents(): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/CartContract.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/CartContract.kt
new file mode 100644
index 0000000..5bd89bd
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/cart/CartContract.kt
@@ -0,0 +1,14 @@
+package com.affise.app.ui.fragments.cart
+
+import androidx.lifecycle.LiveData
+import com.affise.app.entity.ProductEntity
+
+interface CartContract {
+ interface ViewModel {
+ val stateData: LiveData
+
+ fun update()
+ fun addProduct(product: ProductEntity)
+ fun removeProduct(product: ProductEntity)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/CartFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/CartFragment.kt
new file mode 100644
index 0000000..b610ebd
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/cart/CartFragment.kt
@@ -0,0 +1,116 @@
+package com.affise.app.ui.fragments.cart
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.navigation.fragment.findNavController
+import com.affise.app.R
+import com.affise.app.databinding.FragmentMainCartBinding
+import com.affise.app.entity.ProductEntity
+import com.affise.app.extensions.hideKeyboard
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.cart.adapters.AdapterCart
+import com.affise.app.ui.fragments.details.ProductDetailsFragment
+import dagger.android.support.DaggerFragment
+import java.math.BigDecimal
+import javax.inject.Inject
+
+class CartFragment : DaggerFragment() {
+
+ @Inject
+ lateinit var viewModel: CartContract.ViewModel
+
+ @Inject
+ lateinit var menuHolder: MenuHolder
+
+ @Inject
+ lateinit var adapter: AdapterCart
+
+ lateinit var binding: FragmentMainCartBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = FragmentMainCartBinding.inflate(layoutInflater).apply {
+ binding = this
+
+ recycles.adapter = adapter.apply {
+ onClickProduct = { clickProduct(it.product) }
+ onClickPlus = { viewModel.addProduct(it.product) }
+ onClickMinus = { viewModel.removeProduct(it.product) }
+ }
+
+ refresh.setOnRefreshListener {
+ viewModel.update()
+ }
+
+ menuSate.setOnClickListener {
+ menuHolder.changeMenuSate()
+ }
+
+ notification.setOnClickListener {
+
+ }
+
+ checkout.setOnClickListener {
+
+ }
+ }.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.stateData.observe(viewLifecycleOwner) {
+ when (it) {
+ is CartState.DataState -> {
+ binding.refresh.isRefreshing = false
+ adapter.submitList(it.cartProducts)
+
+ binding.totalPrice.text = it.cartProducts
+ .fold(BigDecimal.ZERO) { c, v ->
+ c + ((v.product.price ?: BigDecimal.ZERO) * v.count.toBigDecimal())
+ }
+ .takeIf { sum -> sum > BigDecimal.ZERO }
+ ?.toPlainString()
+ ?.let { price ->
+ it.cartProducts
+ .firstOrNull()
+ ?.product
+ ?.unit
+ ?.let { unit -> "$unit$price" }
+ ?: price
+ }
+ ?: ""
+ }
+ CartState.ErrorState -> {
+ binding.refresh.isRefreshing = false
+ }
+ CartState.LoadingState -> {
+ binding.refresh.isRefreshing = false
+ }
+ CartState.UpdateState -> {
+ binding.refresh.isRefreshing = true
+ }
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ viewModel.update()
+
+ requireActivity().hideKeyboard()
+ }
+
+ private fun clickProduct(productEntity: ProductEntity) {
+ findNavController().navigate(
+ R.id.action_cartFragment_to_productDetailsFragment,
+ Bundle().apply {
+ putLong(ProductDetailsFragment.PRODUCT_DETAILS_EXTRA_KEY, productEntity.id)
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/CartState.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/CartState.kt
new file mode 100644
index 0000000..9181ff8
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/cart/CartState.kt
@@ -0,0 +1,12 @@
+package com.affise.app.ui.fragments.cart
+
+import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity
+
+sealed class CartState {
+ object LoadingState : CartState()
+ object UpdateState : CartState()
+ object ErrorState : CartState()
+ data class DataState(
+ val cartProducts: List
+ ) : CartState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/CartViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/CartViewModel.kt
new file mode 100644
index 0000000..8a3a91c
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/cart/CartViewModel.kt
@@ -0,0 +1,90 @@
+package com.affise.app.ui.fragments.cart
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.affise.app.entity.ProductEntity
+import com.affise.app.extensions.IOWithErrorHandling
+import com.affise.app.usecase.ProductUseCase
+import com.affise.attribution.Affise
+import com.affise.attribution.events.predefined.ViewCartEvent
+import com.fasterxml.jackson.databind.ObjectMapper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.json.JSONArray
+import org.json.JSONObject
+import javax.inject.Inject
+
+class CartViewModel @Inject constructor(
+ private val useCase: ProductUseCase,
+ private val objectMapper: ObjectMapper
+) : ViewModel(), CartContract.ViewModel {
+
+ override val stateData = MutableLiveData()
+
+ init {
+ updateData()
+ }
+
+ override fun update() {
+ val currentState = stateData.value
+ if (currentState == CartState.LoadingState || currentState == CartState.UpdateState) {
+ return
+ }
+ stateData.value = CartState.UpdateState
+
+ updateData()
+ }
+
+ private fun updateData() {
+ stateData.value = CartState.UpdateState
+
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ stateData.postValue(CartState.ErrorState)
+ }) {
+ delay(300)
+
+ val items = useCase.getCart()
+
+ val itemsCart = items
+ .map { objectMapper.writeValueAsString(it) }
+ .map { JSONObject(it) }
+
+ Affise.sendEvent(
+ ViewCartEvent(
+ JSONObject().apply {
+ put("cart", JSONArray(itemsCart))
+ },
+ "Cart"
+ )
+ )
+
+ stateData.postValue(
+ CartState.DataState(items)
+ )
+ }
+ }
+
+ override fun addProduct(product: ProductEntity) {
+ stateData.value = CartState.UpdateState
+
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ }) {
+ stateData.postValue(
+ CartState.DataState(useCase.addToCart(product.id))
+ )
+ }
+ }
+
+ override fun removeProduct(product: ProductEntity) {
+ stateData.value = CartState.UpdateState
+
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ }) {
+ stateData.postValue(
+ CartState.DataState(useCase.removeOnCart(product.id))
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/adapters/AdapterCart.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/adapters/AdapterCart.kt
new file mode 100644
index 0000000..6592d22
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/cart/adapters/AdapterCart.kt
@@ -0,0 +1,96 @@
+package com.affise.app.ui.fragments.cart.adapters
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.R
+import com.affise.app.databinding.ItemRecyclerCartBinding
+import com.affise.app.entity.ProductEntity
+import com.bumptech.glide.Glide
+import javax.inject.Inject
+
+data class ProductCartEntity(
+ val product: ProductEntity,
+ var count: Int
+)
+
+class AdapterCart @Inject constructor(
+) : ListAdapter(ProductCartCallback()) {
+
+ var onClickProduct: (ProductCartEntity) -> Unit = {}
+
+ var onClickPlus: (ProductCartEntity) -> Unit = {}
+
+ var onClickMinus: (ProductCartEntity) -> Unit = {}
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ProductCartViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ItemRecyclerCartBinding.inflate(it, parent, false)
+ }
+ .let {
+ ProductCartViewHolder(it, onClickProduct, onClickPlus, onClickMinus)
+ }
+
+ override fun onBindViewHolder(holder: ProductCartViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+class ProductCartCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ProductCartEntity,
+ newItem: ProductCartEntity
+ ): Boolean = oldItem.product.id == newItem.product.id
+
+ override fun areContentsTheSame(
+ oldItem: ProductCartEntity,
+ newItem: ProductCartEntity
+ ): Boolean = oldItem.product.name == newItem.product.name &&
+ oldItem.product.price == newItem.product.price &&
+ oldItem.product.unit == newItem.product.unit &&
+ oldItem.product.rating == newItem.product.rating &&
+ oldItem.count == newItem.count
+}
+
+class ProductCartViewHolder(
+ private val binding: ItemRecyclerCartBinding,
+ private val onClickProduct: (ProductCartEntity) -> Unit,
+ private val onClickPlus: (ProductCartEntity) -> Unit,
+ private val onClickMinus: (ProductCartEntity) -> Unit
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(product: ProductCartEntity) = with(binding) {
+ Glide.with(imageView)
+ .load(Uri.parse("file:///android_asset/${product.product.images.firstOrNull()}"))
+ .error(R.drawable.img_product_test)
+ .into(imageView)
+
+ name.text = product.product.name
+
+ price.text = product.product.price?.toPlainString()?.let { price ->
+ product.product.unit?.let { unit ->
+ "$unit$price"
+ } ?: price
+ } ?: ""
+
+ count.text = product.count.toString()
+
+ main.setOnClickListener {
+ onClickProduct.invoke(product)
+ }
+
+ plus.setOnClickListener {
+ onClickPlus.invoke(product)
+ }
+
+ minus.setOnClickListener {
+ onClickMinus.invoke(product)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsContract.kt b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsContract.kt
new file mode 100644
index 0000000..3190be9
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsContract.kt
@@ -0,0 +1,16 @@
+package com.affise.app.ui.fragments.details
+
+import androidx.lifecycle.LiveData
+
+interface ProductDetailsContract {
+ interface ViewModel {
+ val stateData: LiveData
+ val currentGroup: LiveData
+
+ fun setItemId(id: Long?)
+ fun clickLike(id: Long?)
+ fun selectGroup(group: ProductDetailsActionGroup)
+ fun clickCart()
+ fun clickBuyNow()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsFragment.kt
new file mode 100644
index 0000000..191fb4d
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsFragment.kt
@@ -0,0 +1,364 @@
+package com.affise.app.ui.fragments.details
+
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import com.affise.app.R
+import com.affise.app.databinding.FragmentMainProductDetailsBinding
+import com.affise.app.entity.Colors
+import com.affise.app.entity.ProductEntity
+import com.affise.app.extensions.hideKeyboard
+import com.affise.app.ui.adapters.CenterDecoration
+import com.affise.app.ui.adapters.CenterSnapHelper
+import com.affise.app.ui.adapters.SpaceDecoration
+import com.affise.app.ui.fragments.details.adapters.*
+import com.google.android.material.card.MaterialCardView
+import com.google.android.material.textview.MaterialTextView
+import dagger.android.support.DaggerFragment
+import javax.inject.Inject
+
+class ProductDetailsFragment : DaggerFragment() {
+
+ @Inject
+ lateinit var viewModel: ProductDetailsContract.ViewModel
+
+ @Inject
+ lateinit var adapterImages: AdapterImages
+
+ @Inject
+ lateinit var adapterColors: AdapterColors
+
+ @Inject
+ lateinit var adapterSize: AdapterSize
+
+ @Inject
+ lateinit var adapterImagePages: AdapterImagePages
+
+ lateinit var binding: FragmentMainProductDetailsBinding
+
+ private val snapHelper = CenterSnapHelper()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = FragmentMainProductDetailsBinding.inflate(layoutInflater).apply {
+ binding = this
+
+ images.adapter = adapterImages
+
+ images.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ super.onPageSelected(position)
+
+ adapterImagePages.setSelected(position)
+ }
+ })
+
+ imagePages.adapter = adapterImagePages
+
+ imagePages.addItemDecoration(SpaceDecoration(paddingEnd = 5f))
+
+ colors.adapter = adapterColors
+
+ size.adapter = adapterSize.apply {
+ submitList(Size.getAllSize())
+ }
+
+ size.setHasFixedSize(true)
+
+ snapHelper.attachToRecyclerView(size)
+
+ size.addItemDecoration(
+ DividerItemDecoration(requireContext(), RecyclerView.HORIZONTAL).apply {
+ setDrawable(
+ ContextCompat.getDrawable(
+ requireContext(),
+ R.drawable.adapter_size_decoration
+ )!!
+ )
+ })
+
+ size.addItemDecoration(CenterDecoration(0))
+
+ productDetailsErrorBack.setOnClickListener {
+ requireActivity().onBackPressed()
+ }
+
+ productDetailsBack.setOnClickListener {
+ requireActivity().onBackPressed()
+ }
+
+ productDetailsLike.setOnClickListener {
+ viewModel.clickLike(getProductId())
+ }
+
+ productDetailsRefresh.setOnRefreshListener {
+ viewModel.setItemId(getProductId())
+ }
+
+ actionColor.setOnClickListener {
+ viewModel.selectGroup(ProductDetailsActionGroup.COLORS)
+ }
+
+ actionSize.setOnClickListener {
+ viewModel.selectGroup(ProductDetailsActionGroup.SIZE)
+ }
+
+ actionReviews.setOnClickListener {
+ viewModel.selectGroup(ProductDetailsActionGroup.REVIEWS)
+ }
+
+ actionSizeEuro.setOnClickListener {
+ adapterSize.currentType = SizeType.EURO
+
+ setSelectedSize(actionSizeEuro)
+ setUnSelectedSize(actionSizeUs)
+ setUnSelectedSize(actionSizeAsia)
+ }
+
+ actionSizeUs.setOnClickListener {
+ adapterSize.currentType = SizeType.US
+
+ setUnSelectedSize(actionSizeEuro)
+ setSelectedSize(actionSizeUs)
+ setUnSelectedSize(actionSizeAsia)
+ }
+
+ actionSizeAsia.setOnClickListener {
+ adapterSize.currentType = SizeType.ASIA
+
+ setUnSelectedSize(actionSizeEuro)
+ setUnSelectedSize(actionSizeUs)
+ setSelectedSize(actionSizeAsia)
+ }
+
+ cart.setOnClickListener {
+ viewModel.clickCart()
+ }
+
+ buyNow.setOnClickListener {
+ viewModel.clickBuyNow()
+ }
+ }.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.setItemId(getProductId())
+
+ viewModel.stateData.observe(viewLifecycleOwner) {
+ when (it) {
+ is ProductDetailsState.DataState -> {
+ binding.productDetailsLoading.isVisible = false
+ binding.productDetailsRefresh.isVisible = true
+ binding.productDetailsRefresh.isRefreshing = false
+ binding.productDetailsError.isVisible = false
+
+ val images = it.productsDetails.images
+
+ adapterImages.submitList(images)
+
+ if (images.size > 1) {
+ val pages = images.mapIndexed { index, image ->
+ ImagePagesItem(image, binding.images.currentItem == index)
+ }
+
+ adapterImagePages.submitList(pages)
+
+ binding.imagePages.isVisible = true
+ } else {
+ binding.imagePages.isVisible = false
+ }
+
+ adapterColors.submitList(
+ Colors.values().map { color ->
+ ColorItem(color, it.productsDetails.color == color)
+ }
+ )
+
+ val sizeValue = Size.getAllSize().first() { size ->
+ it.productsDetails.size == when (it.productsDetails.size_type) {
+ SizeType.EURO -> size.sizeEuro
+ SizeType.US -> size.sizeUs
+ SizeType.ASIA -> size.sizeAsia
+ }
+ }
+ adapterSize.currentType = it.productsDetails.size_type
+ adapterSize.currentSize = sizeValue
+ adapterSize.enabledSize = listOf(sizeValue)
+
+ when (it.productsDetails.size_type) {
+ SizeType.EURO -> {
+ setSelectedSize(binding.actionSizeEuro)
+ setUnSelectedSize(binding.actionSizeUs)
+ setUnSelectedSize(binding.actionSizeAsia)
+ }
+ SizeType.US -> {
+ setUnSelectedSize(binding.actionSizeEuro)
+ setSelectedSize(binding.actionSizeUs)
+ setUnSelectedSize(binding.actionSizeAsia)
+ }
+ SizeType.ASIA -> {
+ setUnSelectedSize(binding.actionSizeEuro)
+ setUnSelectedSize(binding.actionSizeUs)
+ setSelectedSize(binding.actionSizeAsia)
+ }
+ }
+
+ val position = Size.getAllSize().indexOf(sizeValue)
+ snapHelper.scrollTo(position, false)
+
+ binding.productDetailsLike.setImageResource(
+ if (it.inLike) R.drawable.ic_like_full else R.drawable.ic_like
+ )
+
+ binding.cartImage.setImageResource(
+ if (it.inCart) R.drawable.ic_cart_full else R.drawable.ic_cart
+ )
+
+ showDetails(it.productsDetails)
+ }
+ is ProductDetailsState.ErrorState -> {
+ binding.productDetailsLoading.isVisible = false
+ binding.productDetailsRefresh.isVisible = false
+ binding.productDetailsError.isVisible = true
+ binding.productDetailsErrorRefresh.isVisible = it.retry
+ }
+ ProductDetailsState.LoadingState -> {
+ binding.productDetailsLoading.isVisible = true
+ binding.productDetailsRefresh.isVisible = false
+ binding.productDetailsError.isVisible = false
+ }
+ ProductDetailsState.UpdateState -> {
+ binding.productDetailsLoading.isVisible = false
+ binding.productDetailsRefresh.isVisible = true
+ binding.productDetailsRefresh.isRefreshing = true
+ binding.productDetailsError.isVisible = false
+ }
+ }
+ }
+
+ viewModel.currentGroup.observe(viewLifecycleOwner) {
+ when (it) {
+ null -> Unit
+ ProductDetailsActionGroup.COLORS -> showColorsGroup()
+ ProductDetailsActionGroup.SIZE -> showSizeGroup()
+ ProductDetailsActionGroup.REVIEWS -> showReviewsGroup()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ requireActivity().hideKeyboard()
+ }
+
+ private fun showDetails(productsDetails: ProductEntity) = with(binding) {
+ price.text = productsDetails.price?.toPlainString()?.let { price ->
+ productsDetails.unit?.let { unit ->
+ "$unit$price"
+ } ?: price
+ } ?: ""
+
+ name.text = productsDetails.name
+ description.text = productsDetails.description
+ rating.text = getString(R.string.rating_with_value, productsDetails.rating.toString())
+ }
+
+ private fun showColorsGroup() = with(binding) {
+ setSelected(actionColorTitle, actionColor)
+ setUnSelected(actionSizeTitle, actionSize)
+ setUnSelected(actionReviewsTitle, actionReviews)
+
+ colors.isVisible = true
+ reviews.isVisible = false
+ sizeGroup.isVisible = false
+ }
+
+ private fun showSizeGroup() = with(binding) {
+ setUnSelected(actionColorTitle, actionColor)
+ setSelected(actionSizeTitle, actionSize)
+ setUnSelected(actionReviewsTitle, actionReviews)
+
+ colors.isVisible = false
+ reviews.isVisible = false
+ sizeGroup.isVisible = true
+ }
+
+ private fun showReviewsGroup() = with(binding) {
+ setUnSelected(actionColorTitle, actionColor)
+ setUnSelected(actionSizeTitle, actionSize)
+ setSelected(actionReviewsTitle, actionReviews)
+
+ colors.isVisible = false
+ reviews.isVisible = true
+ sizeGroup.isVisible = false
+ }
+
+ private fun setSelected(textView: MaterialTextView, cardView: MaterialCardView) {
+ with(textView) {
+ setBackgroundResource(R.color.ebony_clay)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ setTextAppearance(R.style.Selected)
+ } else {
+ @Suppress("DEPRECATION")
+ setTextAppearance(requireContext(), R.style.Selected)
+ }
+ }
+
+ cardView.alpha = 1f
+ }
+
+ private fun setUnSelected(textView: MaterialTextView, cardView: MaterialCardView) {
+ with(textView) {
+ setBackgroundResource(android.R.color.transparent)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ setTextAppearance(R.style.UnSelected)
+ } else {
+ @Suppress("DEPRECATION")
+ setTextAppearance(requireContext(), R.style.UnSelected)
+ }
+ }
+
+ cardView.alpha = 0.3f
+ }
+
+ private fun setSelectedSize(textView: MaterialTextView) {
+ with(textView) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ setTextAppearance(R.style.EnabledSize)
+ } else {
+ @Suppress("DEPRECATION")
+ setTextAppearance(requireContext(), R.style.EnabledSize)
+ }
+ }
+ }
+
+ private fun setUnSelectedSize(textView: MaterialTextView) {
+ with(textView) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ setTextAppearance(R.style.DisabledSize)
+ } else {
+ @Suppress("DEPRECATION")
+ setTextAppearance(requireContext(), R.style.DisabledSize)
+ }
+ }
+ }
+
+ private fun getProductId() = arguments?.getLong(PRODUCT_DETAILS_EXTRA_KEY)
+
+ companion object {
+ const val PRODUCT_DETAILS_EXTRA_KEY = "product_details"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsState.kt b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsState.kt
new file mode 100644
index 0000000..635670a
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsState.kt
@@ -0,0 +1,14 @@
+package com.affise.app.ui.fragments.details
+
+import com.affise.app.entity.ProductEntity
+
+sealed class ProductDetailsState {
+ object LoadingState : ProductDetailsState()
+ object UpdateState : ProductDetailsState()
+ data class ErrorState(val retry: Boolean) : ProductDetailsState()
+ data class DataState(
+ val productsDetails: ProductEntity,
+ val inLike: Boolean,
+ val inCart: Boolean
+ ) : ProductDetailsState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsViewModel.kt
new file mode 100644
index 0000000..c079e51
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsViewModel.kt
@@ -0,0 +1,140 @@
+package com.affise.app.ui.fragments.details
+
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.affise.app.entity.ProductEntity
+import com.affise.app.extensions.IOWithErrorHandling
+import com.affise.app.usecase.ProductUseCase
+import com.affise.attribution.Affise
+import com.affise.attribution.events.predefined.AddToCartEvent
+import com.affise.attribution.events.predefined.AddToWishlistEvent
+import com.affise.attribution.events.predefined.ViewItemEvent
+import com.fasterxml.jackson.databind.ObjectMapper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+import javax.inject.Inject
+
+enum class ProductDetailsActionGroup {
+ COLORS,
+ SIZE,
+ REVIEWS
+}
+
+class ProductDetailsViewModel @Inject constructor(
+ private val useCase: ProductUseCase,
+ private val objectMapper: ObjectMapper
+) : ViewModel(), ProductDetailsContract.ViewModel {
+
+ private var currentProductId: Long? = null
+
+ private val isLike = MutableLiveData()
+
+ private val isCart = MutableLiveData()
+
+ private val product = MutableLiveData()
+
+ override val stateData = MediatorLiveData().apply {
+ val observe: (Any) -> Unit = {
+ val inLike = isLike.value
+ val inCart = isCart.value
+ val product = product.value
+
+ if (inLike != null && inCart != null && product != null) {
+ postValue(ProductDetailsState.DataState(product, inLike, inCart))
+ }
+ }
+
+ addSource(isLike, observe)
+ addSource(isCart, observe)
+ addSource(product, observe)
+ }
+
+ override val currentGroup = MutableLiveData(ProductDetailsActionGroup.SIZE)
+
+ override fun setItemId(id: Long?) {
+ currentProductId = id
+
+ if (id == null) {
+ stateData.postValue(ProductDetailsState.ErrorState(false))
+
+ return
+ }
+
+ stateData.value = if (stateData.value is ProductDetailsState.DataState) {
+ ProductDetailsState.UpdateState
+ } else {
+ ProductDetailsState.LoadingState
+ }
+
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ stateData.postValue(ProductDetailsState.ErrorState(true))
+ }) {
+ useCase.getProductDetails(id)?.let {
+ val productData = it
+ val likeData = useCase.inLike(id)
+ val cartData = useCase.inCart(id)
+
+ Affise.sendEvent(
+ ViewItemEvent(
+ JSONObject().apply {
+ put("product", JSONObject(objectMapper.writeValueAsString(productData)))
+ put("isLike", likeData)
+ put("inCart", cartData)
+ },
+ "product details"
+ )
+ )
+
+ product.postValue(productData)
+ isLike.postValue(likeData)
+ isCart.postValue(cartData)
+ } ?: stateData.postValue(ProductDetailsState.ErrorState(false))
+ }
+ }
+
+ override fun selectGroup(group: ProductDetailsActionGroup) {
+ if (currentGroup.value != group) {
+ currentGroup.postValue(group)
+ }
+ }
+
+ override fun clickLike(id: Long?) {
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ }) {
+ (stateData.value as? ProductDetailsState.DataState)?.let {
+ Affise.sendEvent(
+ AddToWishlistEvent(
+ JSONObject(objectMapper.writeValueAsString(it.productsDetails)),
+ System.currentTimeMillis(),
+ "Add to wishlist in Product details"
+ )
+ )
+ }
+
+ isLike.postValue(useCase.updateLike(currentProductId))
+ }
+ }
+
+ override fun clickCart() {
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ }) {
+ (stateData.value as? ProductDetailsState.DataState)?.let {
+ Affise.sendEvent(
+ AddToCartEvent(
+ JSONObject(objectMapper.writeValueAsString(it.productsDetails)),
+ System.currentTimeMillis(),
+ "Add to cart in Product details"
+ )
+ )
+ }
+
+ isCart.postValue(useCase.updateCart(currentProductId))
+ }
+ }
+
+ override fun clickBuyNow() {
+ }
+}
diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterColors.kt b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterColors.kt
new file mode 100644
index 0000000..cfe1902
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterColors.kt
@@ -0,0 +1,62 @@
+package com.affise.app.ui.fragments.details.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.databinding.ItemRecyclerColorBinding
+import com.affise.app.entity.Colors
+import javax.inject.Inject
+
+data class ColorItem(val color: Colors, val enabled: Boolean)
+
+class AdapterColors @Inject constructor(
+) : ListAdapter(ColorCallback()) {
+
+ val onSelected: (ColorItem) -> Unit = {}
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ColorViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ItemRecyclerColorBinding.inflate(it, parent, false)
+ }
+ .let {
+ ColorViewHolder(it)
+ }
+
+ override fun onBindViewHolder(holder: ColorViewHolder, position: Int) {
+ holder.bind(getItem(position), onSelected)
+ }
+}
+
+class ColorCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ColorItem,
+ newItem: ColorItem
+ ): Boolean = oldItem.color == newItem.color
+
+ override fun areContentsTheSame(
+ oldItem: ColorItem,
+ newItem: ColorItem
+ ): Boolean = oldItem.enabled == newItem.enabled
+}
+
+class ColorViewHolder(
+ private val binding: ItemRecyclerColorBinding,
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(color: ColorItem, onSelected: (ColorItem) -> Unit) = with(binding) {
+ root.setOnClickListener {
+ onSelected.invoke(color)
+ }
+
+ root.isEnabled = color.enabled
+
+ root.alpha = if (color.enabled) 1f else 0.3f
+
+ image.setImageResource(color.color.colorRes)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImagePages.kt b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImagePages.kt
new file mode 100644
index 0000000..333101a
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImagePages.kt
@@ -0,0 +1,87 @@
+package com.affise.app.ui.fragments.details.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.R
+import com.affise.app.databinding.ItemRecyclerPageBinding
+import com.affise.app.extensions.convertDpToPixels
+import javax.inject.Inject
+
+class ImagePagesItem(val name: String, var isSelected: Boolean = false)
+
+class AdapterImagePages @Inject constructor(
+) : ListAdapter(PageCallback()) {
+
+ private var selection = -1
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ImagePageViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ItemRecyclerPageBinding.inflate(it, parent, false)
+ }
+ .let {
+ ImagePageViewHolder(it)
+ }
+
+ override fun onBindViewHolder(holder: ImagePageViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ fun getSelectedPosition(): Int = selection
+
+ fun setSelected(position: Int) {
+ if (selection == position) return
+
+ if (selection in 0 until itemCount) {
+ getItem(selection)?.isSelected = false
+
+ notifyItemChanged(selection)
+ }
+
+ if (position in 0 until itemCount) {
+ getItem(position)?.isSelected = true
+
+ notifyItemChanged(position)
+ }
+
+ selection = position
+ }
+}
+
+class PageCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ImagePagesItem,
+ newItem: ImagePagesItem
+ ): Boolean = oldItem.name == newItem.name
+
+ override fun areContentsTheSame(
+ oldItem: ImagePagesItem,
+ newItem: ImagePagesItem
+ ): Boolean = oldItem.isSelected == newItem.isSelected
+}
+
+class ImagePageViewHolder(
+ private val binding: ItemRecyclerPageBinding,
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(item: ImagePagesItem) = with(binding) {
+ root.setCardBackgroundColor(
+ ContextCompat.getColor(
+ root.context,
+ if (item.isSelected) R.color.ebony_clay else R.color.gray_c4
+ )
+ )
+
+ root.layoutParams = root.layoutParams.apply {
+ width = root.context.convertDpToPixels(
+ if (item.isSelected) 26f else 10f
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImages.kt b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImages.kt
new file mode 100644
index 0000000..c1ea1c5
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImages.kt
@@ -0,0 +1,55 @@
+package com.affise.app.ui.fragments.details.adapters
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.R
+import com.affise.app.databinding.ItemRecyclerImageBinding
+import com.bumptech.glide.Glide
+import javax.inject.Inject
+
+class AdapterImages @Inject constructor(
+) : ListAdapter(ImageCallback()) {
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ImageViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ItemRecyclerImageBinding.inflate(it, parent, false)
+ }
+ .let {
+ ImageViewHolder(it)
+ }
+
+ override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+class ImageCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: String,
+ newItem: String
+ ): Boolean = oldItem == newItem
+
+ override fun areContentsTheSame(
+ oldItem: String,
+ newItem: String
+ ): Boolean = true
+}
+
+class ImageViewHolder(
+ private val binding: ItemRecyclerImageBinding,
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(product: String) = with(binding) {
+ Glide.with(root)
+ .load(Uri.parse("file:///android_asset/${product}"))
+ .error(R.drawable.img_product_test)
+ .into(root)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterSize.kt b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterSize.kt
new file mode 100644
index 0000000..33ddd66
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterSize.kt
@@ -0,0 +1,117 @@
+package com.affise.app.ui.fragments.details.adapters
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.view.isInvisible
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.R
+import com.affise.app.databinding.ItemRecyclerSizeBinding
+import javax.inject.Inject
+
+enum class SizeType {
+ EURO,
+ US,
+ ASIA
+}
+
+data class Size(val sizeUs: Double, val sizeEuro: Double, val sizeAsia: Double) {
+ companion object {
+ fun getAllSize(): List = listOf(
+ Size(6.0, 38.0, 24.0),
+ Size(6.5, 38.5, 24.5),
+ Size(7.0, 39.0, 25.0),
+ Size(7.5, 40.0, 25.5),
+ Size(8.0, 41.0, 26.0),
+ Size(8.5, 42.0, 26.5),
+ Size(9.0, 43.0, 27.0),
+ Size(9.5, 43.5, 27.5),
+ Size(10.0, 44.0, 28.0),
+ Size(10.5, 44.5, 28.5),
+ Size(11.0, 45.0, 29.0),
+ Size(11.5, 45.5, 29.5),
+ Size(12.0, 46.0, 30.0),
+ )
+ }
+}
+
+@SuppressLint("NotifyDataSetChanged")
+class AdapterSize @Inject constructor(
+) : ListAdapter(SizeCallback()) {
+
+ var currentType: SizeType = SizeType.US
+ set(value) {
+ field = value
+
+ notifyDataSetChanged()
+ }
+
+ var enabledSize: List = emptyList()
+ set(value) {
+ field = value
+
+ notifyDataSetChanged()
+ }
+
+ var currentSize: Size = Size(8.0, 41.0, 26.0)
+ set(value) {
+ field = value
+
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): SizeViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ItemRecyclerSizeBinding.inflate(it, parent, false)
+ }
+ .let {
+ SizeViewHolder(it)
+ }
+
+ override fun onBindViewHolder(holder: SizeViewHolder, position: Int) {
+ val size = getItem(position)
+ holder.bind(size, currentType, size in enabledSize, size == currentSize)
+ }
+}
+
+class SizeCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: Size,
+ newItem: Size
+ ): Boolean = oldItem.sizeUs == newItem.sizeUs
+
+ override fun areContentsTheSame(
+ oldItem: Size,
+ newItem: Size
+ ): Boolean = true
+}
+
+class SizeViewHolder(
+ private val binding: ItemRecyclerSizeBinding,
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(size: Size, type: SizeType, enabled: Boolean, isCurrent: Boolean) = with(binding) {
+ current.isInvisible = !isCurrent
+
+ value.text = when (type) {
+ SizeType.EURO -> size.sizeEuro
+ SizeType.US -> size.sizeUs
+ SizeType.ASIA -> size.sizeAsia
+ }.toString()
+
+ val textAppearance = if (enabled) R.style.EnabledSize else R.style.DisabledSize
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ value.setTextAppearance(textAppearance)
+ } else {
+ @Suppress("DEPRECATION")
+ value.setTextAppearance(itemView.context, textAppearance)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/HomeContract.kt b/app/src/main/java/com/affise/app/ui/fragments/home/HomeContract.kt
new file mode 100644
index 0000000..5efdc21
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/home/HomeContract.kt
@@ -0,0 +1,16 @@
+package com.affise.app.ui.fragments.home
+
+import androidx.lifecycle.LiveData
+import com.affise.app.entity.ProductEntity
+
+interface HomeContract {
+ interface ViewModel {
+ val stateData: LiveData
+
+ fun setSearch(value: String)
+
+ fun update()
+ fun clickLike(productEntity: ProductEntity)
+ fun clickAdd(productEntity: ProductEntity)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/HomeFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/home/HomeFragment.kt
new file mode 100644
index 0000000..3470919
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/home/HomeFragment.kt
@@ -0,0 +1,137 @@
+package com.affise.app.ui.fragments.home
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.core.widget.addTextChangedListener
+import androidx.navigation.fragment.findNavController
+import com.affise.app.R
+import com.affise.app.databinding.FragmentMainHomeBinding
+import com.affise.app.entity.ProductEntity
+import com.affise.app.extensions.hideKeyboard
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.adapters.SpaceDecoration
+import com.affise.app.ui.fragments.details.ProductDetailsFragment
+import com.affise.app.ui.fragments.home.adapters.AdapterNew
+import com.affise.app.ui.fragments.home.adapters.AdapterPopular
+import dagger.android.support.DaggerFragment
+import javax.inject.Inject
+
+class HomeFragment : DaggerFragment() {
+
+ @Inject
+ lateinit var viewModel: HomeContract.ViewModel
+
+ @Inject
+ lateinit var menuHolder: MenuHolder
+
+ @Inject
+ lateinit var adapterPopular: AdapterPopular
+
+ @Inject
+ lateinit var adapterNew: AdapterNew
+
+ lateinit var binding: FragmentMainHomeBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = FragmentMainHomeBinding.inflate(layoutInflater).apply {
+ binding = this
+
+ homeMenu.setOnClickListener {
+ menuHolder.changeMenuSate()
+ }
+
+ homeNotification.setOnClickListener {
+
+ }
+
+ homeErrorRefresh.setOnClickListener {
+ viewModel.update()
+ }
+
+ homeRefresh.setOnRefreshListener {
+ viewModel.update()
+ }
+
+ homeSearchSetting.setOnClickListener {
+
+ }
+
+ popularMore.setOnClickListener {
+
+ }
+
+ homeSearchValue.addTextChangedListener {
+ viewModel.setSearch(it.toString())
+ }
+
+ recyclerPopular.adapter = adapterPopular.apply {
+ onClickProduct = { clickProduct(it.product) }
+ onClickLike = { viewModel.clickLike(it.product) }
+ onClickAdd = { viewModel.clickAdd(it.product) }
+ }
+
+ recyclerNew.adapter = adapterNew.apply {
+ onClickProduct = { clickProduct(it.product) }
+ onClickLike = { viewModel.clickLike(it.product) }
+ onClickAdd = { viewModel.clickAdd(it.product) }
+ }
+
+ recyclerPopular.addItemDecoration(SpaceDecoration(paddingEnd = 16f))
+ recyclerNew.addItemDecoration(SpaceDecoration(paddingBottom = 16f))
+ }.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.stateData.observe(viewLifecycleOwner) {
+ when (it) {
+ is HomeState.DataState -> {
+ binding.homeLoading.isVisible = false
+ binding.homeError.isVisible = false
+ binding.homeRefresh.isRefreshing = false
+
+ adapterPopular.submitList(it.popularProducts)
+ adapterNew.submitList(it.newProducts)
+ }
+ HomeState.ErrorState -> {
+ binding.homeLoading.isVisible = false
+ binding.homeError.isVisible = true
+ binding.homeRefresh.isRefreshing = false
+ }
+ HomeState.LoadingState -> {
+ binding.homeLoading.isVisible = true
+ binding.homeError.isVisible = false
+ binding.homeRefresh.isRefreshing = false
+ }
+ HomeState.UpdateState -> {
+ binding.homeLoading.isVisible = false
+ binding.homeError.isVisible = false
+ binding.homeRefresh.isRefreshing = true
+ }
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ requireActivity().hideKeyboard()
+
+ viewModel.update()
+ }
+
+ private fun clickProduct(productEntity: ProductEntity) {
+ findNavController().navigate(
+ R.id.action_homeFragment_to_productDetailsFragment,
+ Bundle().apply {
+ putLong(ProductDetailsFragment.PRODUCT_DETAILS_EXTRA_KEY, productEntity.id)
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/HomeState.kt b/app/src/main/java/com/affise/app/ui/fragments/home/HomeState.kt
new file mode 100644
index 0000000..eee2207
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/home/HomeState.kt
@@ -0,0 +1,14 @@
+package com.affise.app.ui.fragments.home
+
+import com.affise.app.ui.fragments.home.adapters.ProductNewEntity
+import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity
+
+sealed class HomeState {
+ object LoadingState : HomeState()
+ object UpdateState : HomeState()
+ object ErrorState : HomeState()
+ data class DataState(
+ val popularProducts: List,
+ val newProducts: List,
+ ) : HomeState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/HomeViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/home/HomeViewModel.kt
new file mode 100644
index 0000000..b2d0ace
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/home/HomeViewModel.kt
@@ -0,0 +1,117 @@
+package com.affise.app.ui.fragments.home
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.affise.app.entity.ProductEntity
+import com.affise.app.extensions.IOWithErrorHandling
+import com.affise.app.usecase.ProductUseCase
+import com.affise.attribution.Affise
+import com.affise.attribution.events.predefined.AddToCartEvent
+import com.affise.attribution.events.predefined.AddToWishlistEvent
+import com.affise.attribution.events.predefined.SearchEvent
+import com.affise.attribution.events.predefined.ViewItemsEvent
+import com.fasterxml.jackson.databind.ObjectMapper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.json.JSONArray
+import org.json.JSONObject
+import javax.inject.Inject
+
+class HomeViewModel @Inject constructor(
+ private val useCase: ProductUseCase,
+ private val objectMapper: ObjectMapper
+) : ViewModel(), HomeContract.ViewModel {
+
+ override val stateData = MutableLiveData(HomeState.LoadingState)
+
+ init {
+ updateData()
+ }
+
+ override fun update() {
+ val currentState = stateData.value
+ if (currentState == HomeState.LoadingState || currentState == HomeState.UpdateState) {
+ return
+ }
+
+ stateData.value = HomeState.UpdateState
+
+ updateData()
+ }
+
+ private fun updateData() {
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ stateData.postValue(HomeState.ErrorState)
+ }) {
+ delay(300)
+ updateProducts()
+ }
+ }
+
+ override fun setSearch(value: String) {
+ Affise.sendEvent(
+ SearchEvent(JSONArray(), System.currentTimeMillis(), "search by '$value'")
+ )
+ }
+
+ override fun clickLike(productEntity: ProductEntity) {
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ }) {
+ Affise.sendEvent(
+ AddToWishlistEvent(
+ JSONObject(objectMapper.writeValueAsString(productEntity)),
+ System.currentTimeMillis(),
+ "Add to wishlist on home"
+ )
+ )
+
+ useCase.updateLike(productEntity.id)
+ updateProducts()
+ }
+ }
+
+ override fun clickAdd(productEntity: ProductEntity) {
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ }) {
+ Affise.sendEvent(
+ AddToCartEvent(
+ JSONObject(objectMapper.writeValueAsString(productEntity)),
+ System.currentTimeMillis(),
+ "Add to cart on home"
+ )
+ )
+
+
+ useCase.updateCart(productEntity.id)
+ updateProducts()
+ }
+ }
+
+ private suspend fun updateProducts() {
+ val popularProducts = useCase.getPopularProducts()
+
+ val newProducts = useCase.getNewProducts()
+
+ val itemsPopular = popularProducts
+ .map { objectMapper.writeValueAsString(it) }
+ .map { JSONObject(it) }
+
+ val itemsNew = newProducts
+ .map { objectMapper.writeValueAsString(it) }
+ .map { JSONObject(it) }
+
+ Affise.sendEvent(
+ ViewItemsEvent(JSONArray(itemsPopular), "popularProducts")
+ )
+
+ Affise.sendEvent(
+ ViewItemsEvent(JSONArray(itemsNew), "newProducts")
+ )
+
+ stateData.postValue(
+ HomeState.DataState(popularProducts, newProducts)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterNew.kt b/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterNew.kt
new file mode 100644
index 0000000..83b7751
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterNew.kt
@@ -0,0 +1,103 @@
+package com.affise.app.ui.fragments.home.adapters
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.R
+import com.affise.app.databinding.ItemRecyclerNewBinding
+import com.affise.app.entity.ProductEntity
+import com.bumptech.glide.Glide
+import javax.inject.Inject
+
+data class ProductNewEntity(
+ val product: ProductEntity,
+ val inLike: Boolean,
+ val inCart: Boolean
+)
+
+class AdapterNew @Inject constructor(
+) : ListAdapter(ProductNewCallback()) {
+
+ var onClickProduct: (ProductNewEntity) -> Unit = {}
+
+ var onClickLike: (ProductNewEntity) -> Unit = {}
+
+ var onClickAdd: (ProductNewEntity) -> Unit = {}
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ProductNewViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ItemRecyclerNewBinding.inflate(it, parent, false)
+ }
+ .let {
+ ProductNewViewHolder(it, onClickProduct, onClickLike, onClickAdd)
+ }
+
+ override fun onBindViewHolder(holder: ProductNewViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+class ProductNewCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ProductNewEntity,
+ newItem: ProductNewEntity
+ ): Boolean = oldItem.product.id == newItem.product.id
+
+ override fun areContentsTheSame(
+ oldItem: ProductNewEntity,
+ newItem: ProductNewEntity
+ ): Boolean = oldItem.product.name == newItem.product.name &&
+ oldItem.product.price == newItem.product.price &&
+ oldItem.product.unit == newItem.product.unit &&
+ oldItem.inCart == newItem.inCart &&
+ oldItem.inLike == newItem.inLike
+}
+
+class ProductNewViewHolder(
+ private val binding: ItemRecyclerNewBinding,
+ private val onClickProduct: (ProductNewEntity) -> Unit,
+ private val onClickLike: (ProductNewEntity) -> Unit,
+ private val onClickAdd: (ProductNewEntity) -> Unit
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(product: ProductNewEntity) = with(binding) {
+ Glide.with(imageView)
+ .load(Uri.parse("file:///android_asset/${product.product.images.firstOrNull()}"))
+ .error(R.drawable.img_product_test)
+ .into(imageView)
+
+ name.text = product.product.name
+
+ price.text = product.product.price?.toPlainString()?.let { price ->
+ product.product.unit?.let { unit ->
+ "$unit$price"
+ } ?: price
+ } ?: ""
+
+ like.setImageResource(
+ if (product.inLike) R.drawable.ic_like_full else R.drawable.ic_like
+ )
+
+ add.setImageResource(
+ if (product.inCart) R.drawable.ic_cart_full else R.drawable.ic_cart
+ )
+
+ main.setOnClickListener {
+ onClickProduct.invoke(product)
+ }
+
+ like.setOnClickListener {
+ onClickLike.invoke(product)
+ }
+
+ add.setOnClickListener {
+ onClickAdd.invoke(product)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterPopular.kt b/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterPopular.kt
new file mode 100644
index 0000000..fe9aa51
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterPopular.kt
@@ -0,0 +1,103 @@
+package com.affise.app.ui.fragments.home.adapters
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.R
+import com.affise.app.databinding.ItemRecyclerPopularBinding
+import com.affise.app.entity.ProductEntity
+import com.bumptech.glide.Glide
+import javax.inject.Inject
+
+data class ProductPopularEntity(
+ val product: ProductEntity,
+ val inLike: Boolean,
+ val inCart: Boolean
+)
+
+class AdapterPopular @Inject constructor(
+) : ListAdapter(ProductPopularCallback()) {
+
+ var onClickProduct: (ProductPopularEntity) -> Unit = {}
+
+ var onClickLike: (ProductPopularEntity) -> Unit = {}
+
+ var onClickAdd: (ProductPopularEntity) -> Unit = {}
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ProductPopularViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ItemRecyclerPopularBinding.inflate(it, parent, false)
+ }
+ .let {
+ ProductPopularViewHolder(it, onClickProduct, onClickLike, onClickAdd)
+ }
+
+ override fun onBindViewHolder(holder: ProductPopularViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+class ProductPopularCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ProductPopularEntity,
+ newItem: ProductPopularEntity
+ ): Boolean = oldItem.product.id == newItem.product.id
+
+ override fun areContentsTheSame(
+ oldItem: ProductPopularEntity,
+ newItem: ProductPopularEntity
+ ): Boolean = oldItem.product.name == newItem.product.name &&
+ oldItem.product.price == newItem.product.price &&
+ oldItem.product.unit == newItem.product.unit &&
+ oldItem.inCart == newItem.inCart &&
+ oldItem.inLike == newItem.inLike
+}
+
+class ProductPopularViewHolder(
+ private val binding: ItemRecyclerPopularBinding,
+ private val onClickProduct: (ProductPopularEntity) -> Unit,
+ private val onClickLike: (ProductPopularEntity) -> Unit,
+ private val onClickAdd: (ProductPopularEntity) -> Unit
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(product: ProductPopularEntity) = with(binding) {
+ Glide.with(imageView)
+ .load(Uri.parse("file:///android_asset/${product.product.images.firstOrNull()}"))
+ .error(R.drawable.img_product_test)
+ .into(imageView)
+
+ name.text = product.product.name
+
+ price.text = product.product.price?.toPlainString()?.let { price ->
+ product.product.unit?.let { unit ->
+ "$unit$price"
+ } ?: price
+ } ?: ""
+
+ like.setImageResource(
+ if (product.inLike) R.drawable.ic_like_full else R.drawable.ic_like
+ )
+
+ add.setImageResource(
+ if (product.inCart) R.drawable.ic_cart_full else R.drawable.ic_cart
+ )
+
+ main.setOnClickListener {
+ onClickProduct.invoke(product)
+ }
+
+ like.setOnClickListener {
+ onClickLike.invoke(product)
+ }
+
+ add.setOnClickListener {
+ onClickAdd.invoke(product)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/LikesContract.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesContract.kt
new file mode 100644
index 0000000..d5063e1
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesContract.kt
@@ -0,0 +1,15 @@
+package com.affise.app.ui.fragments.likes
+
+import androidx.lifecycle.LiveData
+import com.affise.app.entity.ProductEntity
+
+interface LikesContract {
+
+ interface ViewModel {
+ val stateData: LiveData
+
+ fun update()
+ fun clickLike(product: ProductEntity)
+ fun clickAdd(product: ProductEntity)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/LikesFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesFragment.kt
new file mode 100644
index 0000000..aa32a98
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesFragment.kt
@@ -0,0 +1,103 @@
+package com.affise.app.ui.fragments.likes
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.navigation.fragment.findNavController
+import com.affise.app.R
+import com.affise.app.databinding.FragmentMainLikesBinding
+import com.affise.app.entity.ProductEntity
+import com.affise.app.extensions.hideKeyboard
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.details.ProductDetailsFragment
+import com.affise.app.ui.fragments.likes.adapters.AdapterLikes
+import dagger.android.support.DaggerFragment
+import javax.inject.Inject
+
+class LikesFragment : DaggerFragment() {
+
+ @Inject
+ lateinit var viewModel: LikesContract.ViewModel
+
+ @Inject
+ lateinit var menuHolder: MenuHolder
+
+ @Inject
+ lateinit var adapter: AdapterLikes
+
+ lateinit var binding: FragmentMainLikesBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = FragmentMainLikesBinding.inflate(layoutInflater).apply {
+ binding = this
+
+ menuSate.setOnClickListener {
+ menuHolder.changeMenuSate()
+ }
+
+ recycles.adapter = adapter.apply {
+ onClickProduct = { clickProduct(it.product) }
+ onClickLike = { viewModel.clickLike(it.product) }
+ onClickAdd = { viewModel.clickAdd(it.product) }
+ }
+
+ refresh.setOnRefreshListener {
+ viewModel.update()
+ }
+
+ notification.setOnClickListener {
+
+ }
+
+ searchSetting.setOnClickListener {
+
+ }
+
+ checkout.setOnClickListener {
+
+ }
+ }.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.stateData.observe(viewLifecycleOwner) {
+ when (it) {
+ is LikesState.DataState -> {
+ adapter.submitList(it.likeProducts)
+ binding.refresh.isRefreshing = false
+ }
+ LikesState.ErrorState -> {
+ binding.refresh.isRefreshing = false
+ }
+ LikesState.LoadingState -> {
+ binding.refresh.isRefreshing = true
+ }
+ LikesState.UpdateState -> {
+ binding.refresh.isRefreshing = false
+ }
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ viewModel.update()
+
+ requireActivity().hideKeyboard()
+ }
+
+ private fun clickProduct(productEntity: ProductEntity) {
+ findNavController().navigate(
+ R.id.action_likesFragment_to_productDetailsFragment,
+ Bundle().apply {
+ putLong(ProductDetailsFragment.PRODUCT_DETAILS_EXTRA_KEY, productEntity.id)
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/LikesState.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesState.kt
new file mode 100644
index 0000000..af7b121
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesState.kt
@@ -0,0 +1,12 @@
+package com.affise.app.ui.fragments.likes
+
+import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity
+
+sealed class LikesState {
+ object LoadingState : LikesState()
+ object UpdateState : LikesState()
+ object ErrorState : LikesState()
+ data class DataState(
+ val likeProducts: List
+ ) : LikesState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/LikesViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesViewModel.kt
new file mode 100644
index 0000000..557244f
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesViewModel.kt
@@ -0,0 +1,99 @@
+package com.affise.app.ui.fragments.likes
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.affise.app.entity.ProductEntity
+import com.affise.app.extensions.IOWithErrorHandling
+import com.affise.app.usecase.ProductUseCase
+import com.affise.attribution.Affise
+import com.affise.attribution.events.predefined.AddToCartEvent
+import com.affise.attribution.events.predefined.AddToWishlistEvent
+import com.affise.attribution.events.predefined.ViewItemsEvent
+import com.fasterxml.jackson.databind.ObjectMapper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.json.JSONArray
+import org.json.JSONObject
+import javax.inject.Inject
+
+class LikesViewModel @Inject constructor(
+ private val useCase: ProductUseCase,
+ private val objectMapper: ObjectMapper
+) : ViewModel(), LikesContract.ViewModel {
+
+ override val stateData = MutableLiveData(LikesState.LoadingState)
+
+ init {
+ updateData()
+ }
+
+ override fun update() {
+ val currentState = stateData.value
+ if (currentState == LikesState.LoadingState || currentState == LikesState.UpdateState) {
+ return
+ }
+ stateData.value = LikesState.UpdateState
+
+ updateData()
+ }
+
+ private fun updateData() {
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ stateData.postValue(LikesState.ErrorState)
+ }) {
+ delay(300)
+
+ updateProducts()
+ }
+ }
+
+ override fun clickLike(product: ProductEntity) {
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ }) {
+ Affise.sendEvent(
+ AddToWishlistEvent(
+ JSONObject(objectMapper.writeValueAsString(product)),
+ System.currentTimeMillis(),
+ "Add to wishlist in Product details"
+ )
+ )
+
+ useCase.updateLike(product.id)
+ updateProducts()
+ }
+ }
+
+ override fun clickAdd(product: ProductEntity) {
+ viewModelScope.launch(Dispatchers.IOWithErrorHandling {
+ }) {
+ Affise.sendEvent(
+ AddToCartEvent(
+ JSONObject(objectMapper.writeValueAsString(product)),
+ System.currentTimeMillis(),
+ "Add to cart in Product details"
+ )
+ )
+
+ useCase.updateCart(product.id)
+ updateProducts()
+ }
+ }
+
+ private suspend fun updateProducts() {
+ val likeProducts = useCase.getLikeProducts()
+
+ val itemsWishlist = likeProducts
+ .map { objectMapper.writeValueAsString(it) }
+ .map { JSONObject(it) }
+
+ Affise.sendEvent(
+ ViewItemsEvent(JSONArray(itemsWishlist), "itemsWishlist")
+ )
+
+ stateData.postValue(
+ LikesState.DataState(likeProducts)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/adapters/AdapterLikes.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/adapters/AdapterLikes.kt
new file mode 100644
index 0000000..66902b7
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/likes/adapters/AdapterLikes.kt
@@ -0,0 +1,103 @@
+package com.affise.app.ui.fragments.likes.adapters
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.R
+import com.affise.app.databinding.ItemRecyclerLikeBinding
+import com.affise.app.entity.ProductEntity
+import com.bumptech.glide.Glide
+import javax.inject.Inject
+
+data class ProductLikeEntity(
+ val product: ProductEntity,
+ val inLike: Boolean,
+ val inCart: Boolean
+)
+
+class AdapterLikes @Inject constructor(
+) : ListAdapter(ProductLikeCallback()) {
+
+ var onClickProduct: (ProductLikeEntity) -> Unit = {}
+
+ var onClickLike: (ProductLikeEntity) -> Unit = {}
+
+ var onClickAdd: (ProductLikeEntity) -> Unit = {}
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ProductLikeViewHolder = LayoutInflater.from(parent.context)
+ .let {
+ ItemRecyclerLikeBinding.inflate(it, parent, false)
+ }
+ .let {
+ ProductLikeViewHolder(it, onClickProduct, onClickLike, onClickAdd)
+ }
+
+ override fun onBindViewHolder(holder: ProductLikeViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+class ProductLikeCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ProductLikeEntity,
+ newItem: ProductLikeEntity
+ ): Boolean = oldItem.product.id == newItem.product.id
+
+ override fun areContentsTheSame(
+ oldItem: ProductLikeEntity,
+ newItem: ProductLikeEntity
+ ): Boolean = oldItem.product.name == newItem.product.name &&
+ oldItem.product.price == newItem.product.price &&
+ oldItem.product.unit == newItem.product.unit &&
+ oldItem.inCart == newItem.inCart &&
+ oldItem.inLike == newItem.inLike
+}
+
+class ProductLikeViewHolder(
+ private val binding: ItemRecyclerLikeBinding,
+ private val onClickProduct: (ProductLikeEntity) -> Unit,
+ private val onClickLike: (ProductLikeEntity) -> Unit,
+ private val onClickAdd: (ProductLikeEntity) -> Unit
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(product: ProductLikeEntity) = with(binding) {
+ Glide.with(imageView)
+ .load(Uri.parse("file:///android_asset/${product.product.images.firstOrNull()}"))
+ .error(R.drawable.img_product_test)
+ .into(imageView)
+
+ name.text = product.product.name
+
+ price.text = product.product.price?.toPlainString()?.let { price ->
+ product.product.unit?.let { unit ->
+ "$unit$price"
+ } ?: price
+ } ?: ""
+
+ like.setImageResource(
+ if (product.inLike) R.drawable.ic_like_full else R.drawable.ic_like
+ )
+
+ add.setImageResource(
+ if (product.inCart) R.drawable.ic_cart_full else R.drawable.ic_cart
+ )
+
+ main.setOnClickListener {
+ onClickProduct.invoke(product)
+ }
+
+ like.setOnClickListener {
+ onClickLike.invoke(product)
+ }
+
+ add.setOnClickListener {
+ onClickAdd.invoke(product)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/menu/MenuContract.kt b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuContract.kt
new file mode 100644
index 0000000..4ef300a
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuContract.kt
@@ -0,0 +1,13 @@
+package com.affise.app.ui.fragments.menu
+
+import androidx.lifecycle.LiveData
+import com.affise.app.ui.fragments.menu.adapters.Menu
+
+interface MenuContract {
+ interface ViewModel {
+ val menu: LiveData>
+ val currentMenu: LiveData
+
+ fun setCurrentMenu(item: Menu.MenuItem)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/menu/MenuFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuFragment.kt
new file mode 100644
index 0000000..8158edd
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuFragment.kt
@@ -0,0 +1,51 @@
+package com.affise.app.ui.fragments.menu
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.affise.app.databinding.FragmentMainMenuBinding
+import com.affise.app.ui.activity.main.MenuHolder
+import com.affise.app.ui.fragments.menu.adapters.AdapterMenu
+import dagger.android.support.DaggerFragment
+import javax.inject.Inject
+
+class MenuFragment : DaggerFragment() {
+
+ @Inject
+ lateinit var viewModel: MenuContract.ViewModel
+
+ @Inject
+ lateinit var menuHolder: MenuHolder
+
+ @Inject
+ lateinit var adapterMenu: AdapterMenu
+
+ lateinit var binding: FragmentMainMenuBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = FragmentMainMenuBinding.inflate(layoutInflater).apply {
+ binding = this
+
+ menu.adapter = adapterMenu.apply {
+ onClick = viewModel::setCurrentMenu
+ }
+ }.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.menu.observe(viewLifecycleOwner) {
+ adapterMenu.submitList(it)
+ }
+
+ viewModel.currentMenu.observe(viewLifecycleOwner) {
+ adapterMenu.current = it
+
+ menuHolder.selectMenu(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/menu/MenuViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuViewModel.kt
new file mode 100644
index 0000000..6570bce
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuViewModel.kt
@@ -0,0 +1,40 @@
+package com.affise.app.ui.fragments.menu
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.affise.app.R
+import com.affise.app.ui.fragments.menu.adapters.Menu
+import javax.inject.Inject
+
+class MenuViewModel @Inject constructor() : ViewModel(), MenuContract.ViewModel {
+
+ private val menuItems = listOf(
+ Menu.MenuItem(R.drawable.ic_menu_home, R.string.menu_home, R.navigation.main),
+ Menu.MenuItem(R.drawable.ic_menu_favorite, R.string.menu_wishlist, R.navigation.like),
+ Menu.MenuItem(R.drawable.ic_menu_shopping, R.string.menu_cart, R.navigation.cart),
+ Menu.MenuDivider,
+ Menu.MenuItem(
+ R.drawable.ic_menu_buttons,
+ R.string.menu_test_buttons,
+ R.navigation.test_buttons
+ ),
+ Menu.MenuItem(
+ R.drawable.ic_menu_auto_cathing,
+ R.string.menu_test_auto_cathing,
+ R.navigation.test_auto_cathing
+ ),
+ Menu.MenuItem(R.drawable.ic_menu_metrics, R.string.menu_test_metrics, R.navigation.metrics),
+ Menu.MenuDivider,
+ Menu.MenuItem(R.drawable.ic_munu_settings, R.string.menu_settings, R.navigation.settings),
+ )
+
+ override val menu = MutableLiveData(menuItems)
+
+ override val currentMenu = MutableLiveData(
+ menuItems.mapNotNull { it as? Menu.MenuItem }.first()
+ )
+
+ override fun setCurrentMenu(item: Menu.MenuItem) {
+ currentMenu.postValue(item)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/affise/app/ui/fragments/menu/adapters/AdapterMenu.kt b/app/src/main/java/com/affise/app/ui/fragments/menu/adapters/AdapterMenu.kt
new file mode 100644
index 0000000..0e32fbb
--- /dev/null
+++ b/app/src/main/java/com/affise/app/ui/fragments/menu/adapters/AdapterMenu.kt
@@ -0,0 +1,113 @@
+package com.affise.app.ui.fragments.menu.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.affise.app.databinding.ItemRecyclerMenuBinding
+import com.affise.app.databinding.ItemRecyclerMenuDividerBinding
+import javax.inject.Inject
+
+sealed class Menu {
+ data class MenuItem(val iconRes: Int, val nameRes: Int, val graph: Int) : Menu()
+ object MenuDivider : Menu()
+}
+
+class AdapterMenu @Inject constructor(
+) : ListAdapter