diff --git a/.idea/navEditor.xml b/.idea/navEditor.xml new file mode 100644 index 00000000..3c0c1fa7 --- /dev/null +++ b/.idea/navEditor.xml @@ -0,0 +1,164 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6f16ce4e..bddbd78f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,7 @@ apply plugin: 'io.objectbox' apply plugin: 'com.google.gms.google-services' apply plugin: 'io.fabric' apply from: '../versions.gradle' +apply plugin: "androidx.navigation.safeargs.kotlin" def keystoreProperties = new Properties() // Load your keystore.properties file into the keystoreProperties object. @@ -69,6 +70,9 @@ android { targetCompatibility 1.8 sourceCompatibility 1.8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } flavorDimensions "feature" productFlavors { extra { @@ -97,20 +101,20 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin" // Android KTX - implementation ( + implementation( "androidx.core:core-ktx:$versions.ktx", "androidx.fragment:fragment-ktx:$versions.ktx" ) // Lifecycle Android KTX - implementation ( + implementation( "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle", "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle", "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle", ) // AndroidX libraries - implementation ( + implementation( "androidx.legacy:legacy-support-v4:1.0.0", "androidx.core:core:1.1.0", "androidx.constraintlayout:constraintlayout:1.1.3", @@ -141,26 +145,26 @@ dependencies { // OkHttp3 //noinspection GradleDependency - implementation ( + implementation( "com.squareup.okhttp3:okhttp:$versions.okhttp", "com.squareup.okhttp3:logging-interceptor:$versions.okhttp" ) // Play Services - implementation ( + implementation( "com.google.android.gms:play-services-auth:$versions.playService", // Google Maps "com.google.android.gms:play-services-maps:$versions.playService", - "com.google.android.gms:play-services-location:$versions.playService" + "com.google.android.gms:play-services-location:$versions.playService", ) // Google Places API - implementation "com.google.android.libraries.places:places:2.1.0" + implementation "com.google.android.libraries.places:places:2.2.0" // Google Material - implementation "com.google.android.material:material:1.2.0-alpha04" + implementation "com.google.android.material:material:1.2.0-alpha05" // FireBase libraries - implementation ( + implementation( "com.google.firebase:firebase-common-ktx:19.3.0", "com.google.firebase:firebase-auth:19.2.0", "com.google.firebase:firebase-messaging:20.1.0", @@ -172,16 +176,24 @@ dependencies { implementation "com.crashlytics.sdk.android:crashlytics:2.10.1" // Facebook SDK - implementation ( + implementation( "com.facebook.android:facebook-android-sdk:$versions.facebook", "com.facebook.android:facebook-login:$versions.facebook" ) + // Navigation Component + implementation( + "androidx.navigation:navigation-fragment-ktx:$versions.navigation", + "androidx.navigation:navigation-ui-ktx:$versions.navigation" + ) + // Glide - implementation ( + implementation( "com.github.bumptech.glide:glide:$versions.glide", "jp.wasabeef:glide-transformations:4.1.0" ) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' kapt "com.github.bumptech.glide:compiler:$versions.glide" // QR Scanner @@ -212,16 +224,23 @@ dependencies { implementation "com.yarolegovich:discrete-scrollview:1.4.9" implementation "com.appyvet:materialrangebar:1.4.4" implementation "com.github.antonygolovin:fluentsnackbar:1.0.0" - implementation "com.github.adrielcafe:PageIndicatorView:1.0.6" // original library doesn't support ViewPager2 + // original PageIndicatorView library doesn't support ViewPager2 + implementation "com.github.adrielcafe:PageIndicatorView:1.0.6" implementation "com.lyft:scissors:1.1.1" implementation "com.borjabravo:readmoretextview:2.1.0" implementation "com.wdullaer:materialdatetimepicker:4.2.3" + implementation "com.redmadrobot:input-mask-android:6.0.0" // Onboarding Screens implementation "com.getkeepsafe.taptargetview:taptargetview:1.12.0" - // PayTM sdk - implementation ("com.paytm:pgplussdk:$versions.paytm") { transitive = true } + // Razorpay Payment SDK + implementation(name: "razorpay-android-3.8.8", ext: 'aar') + // Native Google Pay support + implementation(name: "tez-client-api-0.9.4", ext: 'aar') + implementation(name: "razorpay-googlepay-1.3.0", ext: 'aar') + // Legacy Paytm SDK + implementation("com.paytm:pgplussdk:$versions.paytm") { transitive = true } // Shimmering Effects implementation "com.facebook.shimmer:shimmer:0.5.0" @@ -234,6 +253,9 @@ dependencies { // Material Drawer implementation 'co.zsmb:materialdrawer-kt:3.0.0' + // Balloon + implementation "com.github.skydoves:balloon:1.1.2" + // Modified libraries implementation project(':library:MaterialSearchView') } diff --git a/app/libs/razorpay-android-3.8.8.aar b/app/libs/razorpay-android-3.8.8.aar new file mode 100644 index 00000000..64b116d3 Binary files /dev/null and b/app/libs/razorpay-android-3.8.8.aar differ diff --git a/app/libs/razorpay-googlepay-1.3.0.aar b/app/libs/razorpay-googlepay-1.3.0.aar new file mode 100644 index 00000000..69d12cc1 Binary files /dev/null and b/app/libs/razorpay-googlepay-1.3.0.aar differ diff --git a/app/libs/tez-client-api-0.9.4.aar b/app/libs/tez-client-api-0.9.4.aar new file mode 100644 index 00000000..84b46ce2 Binary files /dev/null and b/app/libs/tez-client-api-0.9.4.aar differ diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json index 0146537e..91354a95 100644 --- a/app/objectbox-models/default.json +++ b/app/objectbox-models/default.json @@ -326,10 +326,155 @@ } ], "relations": [] + }, + { + "id": "28:2637295725738034332", + "lastPropertyId": "5:2171973534084650197", + "name": "NetBankingPaymentOptionModel", + "properties": [ + { + "id": "1:906387674869218463", + "name": "bankCode", + "indexId": "21:8194089081142924871", + "type": 9, + "flags": 2080 + }, + { + "id": "2:7167637186813962386", + "name": "bankName", + "type": 9 + }, + { + "id": "4:7727107652406938795", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "5:2171973534084650197", + "name": "iconRes", + "type": 5, + "flags": 4 + } + ], + "relations": [] + }, + { + "id": "29:1777276744629765100", + "lastPropertyId": "2:8716906613208397282", + "name": "UPICollectPaymentOptionModel", + "properties": [ + { + "id": "1:1392579230187797382", + "name": "vpa", + "indexId": "22:5633125302271845147", + "type": 9, + "flags": 2080 + }, + { + "id": "2:8716906613208397282", + "name": "id", + "type": 6, + "flags": 1 + } + ], + "relations": [] + }, + { + "id": "30:4830555268212426831", + "lastPropertyId": "4:8218390875832245089", + "name": "UPIPushPaymentOptionModel", + "properties": [ + { + "id": "1:1423326543846927519", + "name": "appName", + "indexId": "23:2893800455538585981", + "type": 9, + "flags": 2080 + }, + { + "id": "2:4216217286675531178", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "3:6560148610545783217", + "name": "iconUrl", + "type": 9 + }, + { + "id": "4:8218390875832245089", + "name": "packageName", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "31:6910879426788537457", + "lastPropertyId": "11:8918262677824334159", + "name": "CardPaymentOptionModel", + "properties": [ + { + "id": "1:3435382428226039743", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:2273203262273331765", + "name": "formatNumber", + "type": 9 + }, + { + "id": "3:2880955115990580488", + "name": "cardNumber", + "indexId": "24:5078945634884132146", + "type": 9, + "flags": 2080 + }, + { + "id": "4:3076818549596311869", + "name": "expiryMonth", + "type": 9 + }, + { + "id": "5:3661628796394561983", + "name": "expiryYear", + "type": 9 + }, + { + "id": "6:8917065254115417992", + "name": "cvv", + "type": 9 + }, + { + "id": "7:7773644904251959063", + "name": "bankCode", + "type": 9 + }, + { + "id": "8:2386712652577379948", + "name": "channel", + "type": 9 + }, + { + "id": "10:8071455483819393748", + "name": "cardType", + "type": 9 + }, + { + "id": "11:8918262677824334159", + "name": "name", + "type": 9 + } + ], + "relations": [] } ], - "lastEntityId": "27:8576342329397953849", - "lastIndexId": "20:3529059293124018419", + "lastEntityId": "31:6910879426788537457", + "lastIndexId": "24:5078945634884132146", "lastRelationId": "11:5984758523878906706", "lastSequenceId": "0:0", "modelVersion": 5, @@ -513,7 +658,9 @@ 9101401991107749500, 3602570634923630077, 4572046133134441175, - 971978216354515742 + 971978216354515742, + 8268594737625530103, + 6541405607808058445 ], "retiredRelationUids": [ 1392243414364918450, diff --git a/app/objectbox-models/default.json.bak b/app/objectbox-models/default.json.bak index 7c1cd6bd..843a1403 100644 --- a/app/objectbox-models/default.json.bak +++ b/app/objectbox-models/default.json.bak @@ -282,7 +282,7 @@ }, { "id": "27:8576342329397953849", - "lastPropertyId": "7:230755440213239301", + "lastPropertyId": "8:597209738448975281", "name": "AccountModel", "properties": [ { @@ -290,12 +290,6 @@ "name": "formatAccountType", "type": 9 }, - { - "id": "2:971978216354515742", - "name": "pk", - "type": 6, - "flags": 4 - }, { "id": "3:2106734719002594427", "name": "targetPk", @@ -323,20 +317,166 @@ "name": "id", "type": 6, "flags": 1 + }, + { + "id": "8:597209738448975281", + "name": "pk", + "type": 6, + "flags": 4 + } + ], + "relations": [] + }, + { + "id": "28:2637295725738034332", + "lastPropertyId": "5:2171973534084650197", + "name": "NetBankingPaymentOptionModel", + "properties": [ + { + "id": "1:906387674869218463", + "name": "bankCode", + "indexId": "21:8194089081142924871", + "type": 9, + "flags": 2080 + }, + { + "id": "2:7167637186813962386", + "name": "bankName", + "type": 9 + }, + { + "id": "4:7727107652406938795", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "5:2171973534084650197", + "name": "iconRes", + "type": 5, + "flags": 4 + } + ], + "relations": [] + }, + { + "id": "29:1777276744629765100", + "lastPropertyId": "2:8716906613208397282", + "name": "UPICollectPaymentOptionModel", + "properties": [ + { + "id": "1:1392579230187797382", + "name": "vpa", + "indexId": "22:5633125302271845147", + "type": 9, + "flags": 2080 + }, + { + "id": "2:8716906613208397282", + "name": "id", + "type": 6, + "flags": 1 + } + ], + "relations": [] + }, + { + "id": "30:4830555268212426831", + "lastPropertyId": "4:8218390875832245089", + "name": "UPIPushPaymentOptionModel", + "properties": [ + { + "id": "1:1423326543846927519", + "name": "appName", + "indexId": "23:2893800455538585981", + "type": 9, + "flags": 2080 + }, + { + "id": "2:4216217286675531178", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "3:6560148610545783217", + "name": "iconUrl", + "type": 9 + }, + { + "id": "4:8218390875832245089", + "name": "packageName", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "31:6910879426788537457", + "lastPropertyId": "11:8918262677824334159", + "name": "CardPaymentOptionModel", + "properties": [ + { + "id": "1:3435382428226039743", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:2273203262273331765", + "name": "formatNumber", + "type": 9 + }, + { + "id": "3:2880955115990580488", + "name": "cardNumber", + "type": 9 + }, + { + "id": "4:3076818549596311869", + "name": "expiryMonth", + "type": 9 + }, + { + "id": "5:3661628796394561983", + "name": "expiryYear", + "type": 9 + }, + { + "id": "6:8917065254115417992", + "name": "cvv", + "type": 9 + }, + { + "id": "7:7773644904251959063", + "name": "bankCode", + "type": 9 + }, + { + "id": "8:2386712652577379948", + "name": "channel", + "type": 9 + }, + { + "id": "10:8071455483819393748", + "name": "cardType", + "type": 9 + }, + { + "id": "11:8918262677824334159", + "name": "name", + "type": 9 } ], "relations": [] } ], - "lastEntityId": "27:8576342329397953849", - "lastIndexId": "20:3529059293124018419", + "lastEntityId": "31:6910879426788537457", + "lastIndexId": "23:2893800455538585981", "lastRelationId": "11:5984758523878906706", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, - "newUidPool": [ - 597209738448975281 - ], "retiredEntityUids": [ 4431701432253560041, 8792488809676470805, @@ -515,7 +655,10 @@ 8850970574772370525, 9101401991107749500, 3602570634923630077, - 4572046133134441175 + 4572046133134441175, + 971978216354515742, + 8268594737625530103, + 6541405607808058445 ], "retiredRelationUids": [ 1392243414364918450, diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 84f30836..b7f2b66a 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -109,6 +109,17 @@ public *; } +########## +# Razorpay +########## +-keepclassmembers class * { @android.webkit.JavascriptInterface ;} +-keepattributes JavascriptInterface +-keepattributes *Annotation* +-dontwarn com.razorpay.** +-keep class com.razorpay.** {*;} +-optimizations !method/inlining/* +-keepclasseswithmembers class * { public void onPayment*(...);} + ########## # General ########## diff --git a/app/src/core/AndroidManifest.xml b/app/src/core/AndroidManifest.xml index 8b019a85..db508625 100644 --- a/app/src/core/AndroidManifest.xml +++ b/app/src/core/AndroidManifest.xml @@ -49,8 +49,8 @@ android:windowSoftInputMode="adjustNothing" /> + + + + + android:theme="@style/AppTheme" + android:windowSoftInputMode="adjustPan"> + android:theme="@style/AppThemeWhiteActionBar" + android:windowSoftInputMode="adjustPan" /> + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b84fdc71..9328f63f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,9 @@ + + + notificationUpdate = new MessageUtils.NotificationUpdate<>(context, builder); doUploadImage(pictureFile, notificationUpdate, index); } - private void doUploadImage(File pictureFile, ProgressRequestBody.UploadCallbacks listener, int index) { + private void doUploadImage(File pictureFile, ProgressRequestBody.UploadCallbacks listener, int index) { if (index == -1) - new RetrofitCallAsyncTask(listener).execute(mRepository.postRestaurantLogo(mShopPk, pictureFile, listener)); + new RetrofitCallAsyncTask(listener).execute(mRepository.postRestaurantLogo(mShopPk, pictureFile, listener)); else - new RetrofitCallAsyncTask(listener).execute(mRepository.postRestaurantCover(mShopPk, index, pictureFile, listener)); + new RetrofitCallAsyncTask(listener).execute(mRepository.postRestaurantCover(mShopPk, index, pictureFile, listener)); } } diff --git a/app/src/main/java/com/checkin/app/checkin/Shop/ShopRepository.kt b/app/src/main/java/com/checkin/app/checkin/Shop/ShopRepository.kt index d60e7643..bd24325a 100644 --- a/app/src/main/java/com/checkin/app/checkin/Shop/ShopRepository.kt +++ b/app/src/main/java/com/checkin/app/checkin/Shop/ShopRepository.kt @@ -27,16 +27,16 @@ import java.io.File class ShopRepository private constructor(context: Context) : BaseRepository() { private val mWebService: WebApiService = ApiClient.getApiService(context) - val nearbyRestaurants: LiveData>> - get() = object : NetworkBoundResource, List>() { - override fun shouldUseLocalDb(): Boolean { - return false - } + fun getNearbyRestaurants(id: Int): LiveData>> = + object : NetworkBoundResource, List>() { + override fun shouldUseLocalDb(): Boolean { + return false + } - override fun createCall(): LiveData>> { - return RetrofitLiveData(mWebService.nearbyRestaurants) - } - }.asLiveData + override fun createCall(): LiveData>> { + return RetrofitLiveData(mWebService.getNearbyRestaurants(if (id == 0) null else id)) + } + }.asLiveData fun registerShop(model: ShopJoinModel): LiveData> { return object : NetworkBoundResource() { @@ -160,14 +160,14 @@ class ShopRepository private constructor(context: Context) : BaseRepository() { }.asLiveData } - fun postRestaurantLogo(mShopPk: Long, pic: File, listener: ProgressRequestBody.UploadCallbacks): Call { + fun postRestaurantLogo(mShopPk: Long, pic: File, listener: ProgressRequestBody.UploadCallbacks): Call { val requestFile = RequestBody.create(MediaType.parse("image/jpeg"), pic) val requestBody = ProgressRequestBody(requestFile, listener) val body = MultipartBody.Part.createFormData("logo", "cover.jpg", requestBody) return mWebService.postRestaurantLogo(mShopPk, body) } - fun postRestaurantCover(mShopPk: Long, index: Int, pic: File, listener: ProgressRequestBody.UploadCallbacks): Call { + fun postRestaurantCover(mShopPk: Long, index: Int, pic: File, listener: ProgressRequestBody.UploadCallbacks): Call { val requestFile = RequestBody.create(MediaType.parse("image/jpeg"), pic) val requestBody = ProgressRequestBody(requestFile, listener) val body = MultipartBody.Part.createFormData("image", "cover.jpg", requestBody) diff --git a/app/src/main/java/com/checkin/app/checkin/accounts/AccountModel.kt b/app/src/main/java/com/checkin/app/checkin/accounts/AccountModel.kt index db8211c1..fd6d93a3 100644 --- a/app/src/main/java/com/checkin/app/checkin/accounts/AccountModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/accounts/AccountModel.kt @@ -40,7 +40,7 @@ enum class ACCOUNT_TYPE(override val value: Int) : EnumIntType { RESTAURANT_MANAGER(204), RESTAURANT_WAITER(205), RESTAURANT_COOK(206); companion object : EnumIntGetter() { - override fun getByValue(value: Int): ACCOUNT_TYPE = EnumIntType.getByValue(value) + override fun getByValue(value: Int?): ACCOUNT_TYPE? = value?.let { EnumIntType.getByValue(it) } class Deserializer : EnumDeserializer(this) class Serializer : EnumSerializer(this) diff --git a/app/src/main/java/com/checkin/app/checkin/accounts/AccountRepository.kt b/app/src/main/java/com/checkin/app/checkin/accounts/AccountRepository.kt index 6b0e7b9b..4b9c7357 100644 --- a/app/src/main/java/com/checkin/app/checkin/accounts/AccountRepository.kt +++ b/app/src/main/java/com/checkin/app/checkin/accounts/AccountRepository.kt @@ -4,7 +4,7 @@ import android.app.Application import android.content.Context import androidx.lifecycle.LiveData import com.checkin.app.checkin.data.BaseRepository -import com.checkin.app.checkin.data.db.AppDatabase +import com.checkin.app.checkin.data.db.dbStore import com.checkin.app.checkin.data.network.ApiClient.Companion.getApiService import com.checkin.app.checkin.data.network.ApiResponse import com.checkin.app.checkin.data.network.RetrofitLiveData @@ -16,7 +16,7 @@ import io.objectbox.android.ObjectBoxLiveData class AccountRepository private constructor(context: Context) : BaseRepository() { private val mWebService: WebApiService = getApiService(context) - private val boxAccount = AppDatabase.getAccountModel(context) + private val boxAccount by dbStore() val selfAccounts: LiveData>> get() = object : NetworkBoundResource, List>() { diff --git a/app/src/main/java/com/checkin/app/checkin/data/Converters.kt b/app/src/main/java/com/checkin/app/checkin/data/Converters.kt index 6fec09ea..02b77175 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/Converters.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/Converters.kt @@ -1,16 +1,16 @@ package com.checkin.app.checkin.data import android.util.Log +import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.JavaType -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.objectbox.converter.PropertyConverter +import org.json.JSONObject import java.io.IOException + object Converters { val objectMapper = jacksonObjectMapper().apply { disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // Serialize Date object to ISO-8601 standard @@ -58,4 +58,10 @@ object Converters { return "" } } + + class JsonObjectSerializer : JsonSerializer() { + override fun serialize(value: JSONObject?, gen: JsonGenerator?, serializers: SerializerProvider?) { + gen?.writeRawValue(value?.toString()) + } + } } diff --git a/app/src/main/java/com/checkin/app/checkin/data/config/AnalyticsUtil.kt b/app/src/main/java/com/checkin/app/checkin/data/config/AnalyticsUtil.kt new file mode 100644 index 00000000..a51b64bb --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/data/config/AnalyticsUtil.kt @@ -0,0 +1,15 @@ +package com.checkin.app.checkin.data.config + +import com.checkin.app.checkin.utility.Constants.IS_RELEASE_BUILD +import com.google.firebase.analytics.FirebaseAnalytics + +object AnalyticsUtil { + fun setAppBuild(analytics: FirebaseAnalytics) { + val type = if (IS_RELEASE_BUILD()) "PROD" else "STAGING" + analytics.setUserProperty(Constants.APP_BUILD_TYPE, type) + } + + object Constants { + const val APP_BUILD_TYPE = "checkin_build_type" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/data/config/RemoteConfig.kt b/app/src/main/java/com/checkin/app/checkin/data/config/RemoteConfig.kt index 72266596..9bb6b882 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/config/RemoteConfig.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/config/RemoteConfig.kt @@ -42,5 +42,6 @@ object RemoteConfig { const val SUPPORT_INSTAGRAM_PAGE_ID = "checkin_support_instagram_page_id" const val SUPPORT_YOUTUBE_CHANNEL_ID = "checkin_support_youtube_channel_id" const val HOME_TOP_BANNERS_AD = "checkin_home_banners_top" + const val KEY_RAZORPAY = "checkin_key_razorpay" } } \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/data/db/AppDatabase.kt b/app/src/main/java/com/checkin/app/checkin/data/db/AppDatabase.kt index 837af6ae..27e6b1d9 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/db/AppDatabase.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/db/AppDatabase.kt @@ -2,57 +2,40 @@ package com.checkin.app.checkin.data.db import android.content.Context import com.checkin.app.checkin.MyObjectBox -import com.checkin.app.checkin.accounts.AccountModel import com.checkin.app.checkin.menu.models.* import com.checkin.app.checkin.restaurant.models.RestaurantBriefModel import io.objectbox.Box import io.objectbox.BoxStore +import io.objectbox.kotlin.boxFor object AppDatabase { - private var mBoxStore: BoxStore? = null + lateinit var store: BoxStore + private set - private fun getBoxStore(context: Context?): BoxStore? { - if (mBoxStore == null) { - mBoxStore = MyObjectBox.builder().androidContext(context!!).buildDefault() - } - return mBoxStore + fun init(context: Context) { + if (!::store.isInitialized) + store = MyObjectBox.builder() + .androidContext(context.applicationContext) + .buildDefault() } - @JvmStatic - fun getAccountModel(context: Context?): Box = getBoxStore(context)!!.boxFor(AccountModel::class.java) - - @JvmStatic - fun getMenuItemCustomizationFieldModel(context: Context?): Box { - return getBoxStore(context)!!.boxFor(ItemCustomizationFieldModel::class.java) - } + inline fun boxFor() = store.boxFor() @JvmStatic - fun getMenuItemCustomizationGroupModel(context: Context?): Box { - return getBoxStore(context)!!.boxFor(ItemCustomizationGroupModel::class.java) - } + fun getMenuItemCustomizationFieldModel(): Box = store.boxFor() @JvmStatic - fun getMenuItemModel(context: Context?): Box { - return getBoxStore(context)!!.boxFor(MenuItemModel::class.java) - } + fun getMenuItemCustomizationGroupModel(): Box = store.boxFor() @JvmStatic - fun getMenuGroupModel(context: Context?): Box { - return getBoxStore(context)!!.boxFor(MenuGroupModel::class.java) - } + fun getMenuItemModel(): Box = store.boxFor() @JvmStatic - fun getMenuModel(context: Context?): Box { - return getBoxStore(context)!!.boxFor(MenuModel::class.java) - } + fun getMenuGroupModel(): Box = store.boxFor() @JvmStatic - fun getRestaurantBriefModel(context: Context?): Box = getBoxStore(context)!!.boxFor(RestaurantBriefModel::class.java) + fun getMenuModel(): Box = store.boxFor() @JvmStatic - fun getCartStatusModel(context: Context?): Box = getBoxStore(context)!!.boxFor(CartStatusModel::class.java) - - fun init(context: Context) { - getBoxStore(context) - } + fun getRestaurantBriefModel(): Box = store.boxFor() } diff --git a/app/src/main/java/com/checkin/app/checkin/data/db/DatabaseDelegate.kt b/app/src/main/java/com/checkin/app/checkin/data/db/DatabaseDelegate.kt new file mode 100644 index 00000000..2a770291 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/data/db/DatabaseDelegate.kt @@ -0,0 +1,19 @@ +package com.checkin.app.checkin.data.db + +import com.checkin.app.checkin.data.BaseRepository +import io.objectbox.Box +import io.objectbox.kotlin.boxFor +import kotlin.reflect.KClass + +class DatabaseDelegate(private val boxClass: KClass) : Lazy> { + private var cached: Box? = null + + override val value: Box + get() { + return cached ?: AppDatabase.store.boxFor(boxClass).also { cached = it } + } + + override fun isInitialized(): Boolean = cached != null +} + +inline fun BaseRepository.dbStore(): Lazy> = DatabaseDelegate(T::class) diff --git a/app/src/main/java/com/checkin/app/checkin/data/network/RetrofitCallAsyncTask.kt b/app/src/main/java/com/checkin/app/checkin/data/network/RetrofitCallAsyncTask.kt index f153cd4d..ddeea677 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/network/RetrofitCallAsyncTask.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/network/RetrofitCallAsyncTask.kt @@ -5,16 +5,17 @@ import com.checkin.app.checkin.utility.ProgressRequestBody.UploadCallbacks import retrofit2.Call import java.io.IOException -class RetrofitCallAsyncTask(private val mListener: UploadCallbacks?) : AsyncTask, Void?, Void?>() { +class RetrofitCallAsyncTask(private val mListener: UploadCallbacks?) : AsyncTask, Void?, Void?>() { @SafeVarargs override fun doInBackground(vararg calls: Call): Void? { for (call in calls) { try { val response = call.execute() - if (response.isSuccessful) mListener?.onSuccess() else mListener?.onFailure() + if (response.isSuccessful) mListener?.onSuccess(ApiResponse(response)) + else mListener?.onFailure(ApiResponse(response)) } catch (e: IOException) { e.printStackTrace() - mListener?.onFailure() + mListener?.onFailure(ApiResponse(e)) } } return null diff --git a/app/src/main/java/com/checkin/app/checkin/data/network/WebApiService.kt b/app/src/main/java/com/checkin/app/checkin/data/network/WebApiService.kt index 328132cf..561fd1cb 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/network/WebApiService.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/network/WebApiService.kt @@ -17,13 +17,14 @@ import com.checkin.app.checkin.Shop.ShopJoin.ShopJoinModel import com.checkin.app.checkin.Waiter.Model.* import com.checkin.app.checkin.accounts.AccountModel import com.checkin.app.checkin.auth.AuthResultModel -import com.checkin.app.checkin.home.model.ActiveLiveSessionDetailModel -import com.checkin.app.checkin.home.model.NearbyRestaurantModel -import com.checkin.app.checkin.home.model.ScheduledLiveSessionDetailModel +import com.checkin.app.checkin.home.model.* import com.checkin.app.checkin.manager.models.* import com.checkin.app.checkin.menu.models.* import com.checkin.app.checkin.misc.models.GenericDetailModel import com.checkin.app.checkin.misc.paytm.PaytmModel +import com.checkin.app.checkin.payment.models.NewPaytmTransactionModel +import com.checkin.app.checkin.payment.models.NewRazorpayTransactionModel +import com.checkin.app.checkin.payment.models.RazorpayTxnResponseModel import com.checkin.app.checkin.restaurant.models.RestaurantModel import com.checkin.app.checkin.restaurant.models.RestaurantServiceModel import com.checkin.app.checkin.session.activesession.chat.SessionChatModel @@ -123,6 +124,11 @@ interface WebApiService { @POST("sessions/active/pay/paytm/") fun postPaytmRequest(): Call + @get:GET("/sessions/customer/closed/") + val customerClosedTransactions: Call> + + @GET("/sessions/customer/closed/{session_id}") + fun getCustomerClosedSessionDetails(@Path("session_id") sessionId: Long): Call // endregion // region SCHEDULED_SESSION @@ -197,8 +203,8 @@ interface WebApiService { @GET("/restaurants/{restaurant_id}/profile/") fun getRestaurantProfile(@Path("restaurant_id") restaurantId: Long): Call - @get:GET("restaurants/nearby/") - val nearbyRestaurants: Call> + @GET("restaurants/nearby/") + fun getNearbyRestaurants(@Query("city_id") cityId: Int?): Call> @GET("restaurants/{restaurant_id}/brief/") fun getRestaurantBriefDetail(@Path("restaurant_id") restaurantId: Long): Call @@ -415,9 +421,18 @@ interface WebApiService { // endregion - // region PAYTM + // region PAYMENT @POST("payments/callback/paytm/") fun postPaytmCallback(@Body data: ObjectNode): Call + + @POST("payments/callback/razorpay/") + fun postRazorpayCallback(@Body data: RazorpayTxnResponseModel): Call + + @POST("payments/pay/paytm/sessions/{session_id}/") + fun postNewPaytmTransaction(@Path("session_id") sessionId: Long): Call + + @POST("payments/pay/razorpay/sessions/{session_id}/") + fun postNewRazorpayTransaction(@Path("session_id") sessionId: Long): Call //endregion //region REVIEW @@ -429,4 +444,11 @@ interface WebApiService { fun postCustomerReview(@Path("session_id") sessionId: Long, @Body review: NewReviewModel): Call // endregion + + //region LOCATION + + @get:GET("location/cities/") + val getCities: Call> + + //endregion } diff --git a/app/src/main/java/com/checkin/app/checkin/data/notifications/MessageUtils.kt b/app/src/main/java/com/checkin/app/checkin/data/notifications/MessageUtils.kt index 9ad1da0a..d9683875 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/notifications/MessageUtils.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/notifications/MessageUtils.kt @@ -17,6 +17,7 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.checkin.app.checkin.R +import com.checkin.app.checkin.data.network.ApiResponse import com.checkin.app.checkin.data.notifications.Constants.CHANNEL import com.checkin.app.checkin.data.notifications.Constants.CHANNEL_GROUP import com.checkin.app.checkin.data.notifications.Constants.FORMAT_SP_KEY_NOTIFICATION_CHANNEL @@ -236,7 +237,7 @@ object MessageUtils { } } - open class NotificationUpdate(context: Context, val builder: NotificationCompat.Builder) : UploadCallbacks { + open class NotificationUpdate(context: Context, val builder: NotificationCompat.Builder) : UploadCallbacks { private val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val notificationId: Int = notificationID @@ -246,7 +247,7 @@ object MessageUtils { notificationManager.notify(notificationId, builder.build()) } - override fun onSuccess() { + override fun onSuccess(response: ApiResponse) { builder.setContentText("Upload completed.") .setProgress(0, 0, false) .setAutoCancel(true) @@ -254,7 +255,7 @@ object MessageUtils { notificationManager.notify(notificationId, builder.build()) } - override fun onFailure() { + override fun onFailure(response: ApiResponse) { builder.setContentText("Upload error.") .setProgress(0, 0, false) .setAutoCancel(true) diff --git a/app/src/main/java/com/checkin/app/checkin/data/resource/NetworkBoundResource.kt b/app/src/main/java/com/checkin/app/checkin/data/resource/NetworkBoundResource.kt index 31c36fc8..68a95b90 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/resource/NetworkBoundResource.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/resource/NetworkBoundResource.kt @@ -22,7 +22,7 @@ abstract class NetworkBoundResource @MainThread constru protected var `val`: Any? = null private fun fetchFromNetwork(dbSource: LiveData?) { - val apiResponse = createCall() + val apiResponse = createCall()!! val useDb = shouldUseLocalDb() && dbSource != null if (useDb) { // we re-attach dbSource as a new source, it will dispatch its latest value quickly @@ -88,7 +88,7 @@ abstract class NetworkBoundResource @MainThread constru // Called to create the API call. @MainThread - protected abstract fun createCall(): LiveData> + protected abstract fun createCall(): LiveData>? // Called to get the cached data from the database @MainThread diff --git a/app/src/main/java/com/checkin/app/checkin/data/resource/ProblemModel.kt b/app/src/main/java/com/checkin/app/checkin/data/resource/ProblemModel.kt index b40db0fd..130f53d9 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/resource/ProblemModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/resource/ProblemModel.kt @@ -35,7 +35,8 @@ class ProblemModel(@JsonProperty("type") val type: String, @JsonProperty("status USER_MISSING_PHONE("user__missing_phone"), ACCOUNT_ALREADY_REGISTERED("account__already_registered"), SESSION_SCHEDULED_CBYG_INVALID_PLANNED_TIME("session__scheduled_invalid_time"), SESSION_SCHEDULED_CBYG_SHOP_CLOSED("session__scheduled_shop_invalid_time_shop_closed"), - SESSION_SCHEDULED_CBYG_PAST_PLANNED_TIME("session__scheduled__plan_time_elapsed"); + SESSION_SCHEDULED_CBYG_PAST_PLANNED_TIME("session__scheduled__plan_time_elapsed"), + SESSION_PAYMENT_ALREADY_DONE("session__payment_done"); companion object { internal fun findByTag(tag: String): ERROR_CODE { diff --git a/app/src/main/java/com/checkin/app/checkin/data/resource/Resource.kt b/app/src/main/java/com/checkin/app/checkin/data/resource/Resource.kt index b096fc47..a9df1ba6 100644 --- a/app/src/main/java/com/checkin/app/checkin/data/resource/Resource.kt +++ b/app/src/main/java/com/checkin/app/checkin/data/resource/Resource.kt @@ -2,6 +2,7 @@ package com.checkin.app.checkin.data.resource import android.util.Log import com.checkin.app.checkin.data.network.ApiResponse +import com.checkin.app.checkin.misc.exceptions.NetworkIssueException import com.checkin.app.checkin.misc.exceptions.NoConnectivityException import com.checkin.app.checkin.misc.exceptions.RequestCanceledException import com.checkin.app.checkin.utility.Utils @@ -30,6 +31,8 @@ class Resource private constructor(val status: Status, val data: T?, val @get:JvmName("isInError") val inError: Boolean = status != Status.NO_REQUEST && !isSuccess && status != Status.LOADING && status != Status.ERROR_CANCELLED + val mayLoad: Boolean = isSuccess || status == Status.LOADING + enum class Status { NO_REQUEST, SUCCESS, @@ -72,17 +75,21 @@ class Resource private constructor(val status: Status, val data: T?, val fun noRequest(): Resource = Resource(Status.NO_REQUEST, null, null, null) + fun error(errorThrowable: Throwable, errorMessage: String?, data: T?, errorData: JsonNode?): Resource = when (errorThrowable) { + is RequestCanceledException -> error(Status.ERROR_CANCELLED, null, null, null) + is NoConnectivityException, is NetworkIssueException -> error(Status.ERROR_DISCONNECTED, errorMessage, data, null) + else -> { + Utils.logErrors(TAG, errorThrowable, errorMessage) + error(Status.ERROR_UNKNOWN, errorMessage, data, errorData) + } + } + + fun error(errorThrowable: Throwable): Resource = error(errorThrowable, null, null, null) + fun createResource(apiResponse: ApiResponse): Resource { return when { apiResponse.isSuccessful -> success(apiResponse.data) - apiResponse.errorThrowable != null -> when (apiResponse.errorThrowable) { - is RequestCanceledException -> error(Status.ERROR_CANCELLED, null, null, null) - is NoConnectivityException -> error(Status.ERROR_DISCONNECTED, apiResponse.errorMessage, apiResponse.data, null) - else -> { - Utils.logErrors(TAG, apiResponse.errorThrowable, apiResponse.errorMessage) - error(Status.ERROR_UNKNOWN, apiResponse.errorMessage, apiResponse.data, apiResponse.errorData) - } - } + apiResponse.errorThrowable != null -> error(apiResponse.errorThrowable, apiResponse.errorMessage, apiResponse.data, apiResponse.errorData) apiResponse.hasStatus(HTTP_NOT_FOUND) -> error(Status.ERROR_NOT_FOUND, apiResponse.errorMessage, apiResponse.data, apiResponse.errorData) apiResponse.hasStatus(HTTP_BAD_REQUEST) -> error(Status.ERROR_INVALID_REQUEST, apiResponse.errorMessage, apiResponse.data, apiResponse.errorData) apiResponse.hasStatus(HTTP_UNAUTHORIZED) -> error(Status.ERROR_UNAUTHORIZED, apiResponse.errorMessage, apiResponse.data, apiResponse.errorData) diff --git a/app/src/main/java/com/checkin/app/checkin/home/HomeRepository.kt b/app/src/main/java/com/checkin/app/checkin/home/HomeRepository.kt new file mode 100644 index 00000000..53000cc2 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/HomeRepository.kt @@ -0,0 +1,31 @@ +package com.checkin.app.checkin.home + +import android.app.Application +import android.content.Context +import androidx.lifecycle.LiveData +import com.checkin.app.checkin.data.BaseRepository +import com.checkin.app.checkin.data.network.ApiClient +import com.checkin.app.checkin.data.network.ApiResponse +import com.checkin.app.checkin.data.network.RetrofitLiveData +import com.checkin.app.checkin.data.network.WebApiService +import com.checkin.app.checkin.data.resource.NetworkBoundResource +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.model.CityLocationModel +import com.checkin.app.checkin.utility.SingletonHolder + +class HomeRepository private constructor(context: Context) : BaseRepository() { + private val mWebService: WebApiService = ApiClient.getApiService(context) + + val getAllCities: LiveData>> + get() { + return object : NetworkBoundResource, List>() { + override fun shouldUseLocalDb(): Boolean = false + + override fun createCall(): LiveData>> { + return RetrofitLiveData(mWebService.getCities) + } + }.asLiveData + } + + companion object : SingletonHolder({ HomeRepository(it.applicationContext) }) +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/activities/AboutUsActivity.kt b/app/src/main/java/com/checkin/app/checkin/home/activities/AboutUsActivity.kt index b0d5aeda..38764ba5 100644 --- a/app/src/main/java/com/checkin/app/checkin/home/activities/AboutUsActivity.kt +++ b/app/src/main/java/com/checkin/app/checkin/home/activities/AboutUsActivity.kt @@ -20,7 +20,6 @@ import com.checkin.app.checkin.data.config.RemoteConfig import com.checkin.app.checkin.utility.Constants.PLAY_STORE_URI import com.checkin.app.checkin.utility.Utils - class AboutUsActivity : AppCompatActivity() { @BindView(R.id.im_aboutus_googleplay) lateinit var imGooglePlay: ImageView diff --git a/app/src/main/java/com/checkin/app/checkin/home/activities/ClosedSessionDetailsActivity.kt b/app/src/main/java/com/checkin/app/checkin/home/activities/ClosedSessionDetailsActivity.kt new file mode 100644 index 00000000..170f7c45 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/activities/ClosedSessionDetailsActivity.kt @@ -0,0 +1,80 @@ +package com.checkin.app.checkin.home.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.TextView +import androidx.activity.viewModels +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.Observer +import butterknife.BindView +import butterknife.ButterKnife +import com.checkin.app.checkin.R +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.fragments.ClosedSessionDetailFragment +import com.checkin.app.checkin.home.model.ClosedSessionDetailsModel +import com.checkin.app.checkin.home.viewmodels.ClosedSessionViewModel +import com.checkin.app.checkin.misc.BlockingNetworkViewModel +import com.checkin.app.checkin.misc.activities.BaseActivity +import com.checkin.app.checkin.misc.fragments.NetworkBlockingFragment +import com.checkin.app.checkin.utility.inTransaction + +class ClosedSessionDetailsActivity : BaseActivity() { + @BindView(R.id.toolbar_closed_session_details) + internal lateinit var toolbar: Toolbar + @BindView(R.id.tv_closed_session_restaurant) + internal lateinit var tvRestaurantName: TextView + @BindView(R.id.tv_closed_session_address) + internal lateinit var tvAddress: TextView + + private val networkFragment = NetworkBlockingFragment() + private val mainFragment = ClosedSessionDetailFragment() + + val networkViewModel: BlockingNetworkViewModel by viewModels() + val viewModel: ClosedSessionViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_closed_session_details) + ButterKnife.bind(this) + + initUI() + } + + private fun initUI() { + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + supportFragmentManager.inTransaction { + add(android.R.id.content, networkFragment, NetworkBlockingFragment.FRAGMENT_TAG) + add(R.id.frg_container_activity, mainFragment, "main") + } + + val sessionId = intent.getLongExtra(KEY_SESSION_ID, 0L) + viewModel.fetchSessionData(sessionId) + + viewModel.sessionData.observe(this, Observer { + networkViewModel.updateStatus(it) + if (it.status == Resource.Status.SUCCESS && it.data != null) setupHeader(it.data) + else if (it.status == Resource.Status.ERROR_NOT_FOUND) finish() + }) + } + + override fun onSupportNavigateUp(): Boolean { + finish() + return true + } + + private fun setupHeader(data: ClosedSessionDetailsModel) { + tvRestaurantName.text = data.restaurant.name + tvAddress.text = data.restaurant.formatAddress + } + + companion object { + private const val KEY_SESSION_ID = "payment.details.session.id" + + fun withSessionIntent(context: Context, sessionId: Long) = Intent(context, ClosedSessionDetailsActivity::class.java).apply { + putExtra(KEY_SESSION_ID, sessionId) + } + } +} diff --git a/app/src/main/java/com/checkin/app/checkin/home/activities/ClosedTransactionsActivity.kt b/app/src/main/java/com/checkin/app/checkin/home/activities/ClosedTransactionsActivity.kt new file mode 100644 index 00000000..3d8151ad --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/activities/ClosedTransactionsActivity.kt @@ -0,0 +1,80 @@ +package com.checkin.app.checkin.home.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.lifecycle.Observer +import butterknife.BindView +import butterknife.ButterKnife +import com.airbnb.epoxy.EpoxyRecyclerView +import com.checkin.app.checkin.R +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.epoxy.closedTransactionModelHolder +import com.checkin.app.checkin.home.viewmodels.ClosedSessionViewModel +import com.checkin.app.checkin.misc.activities.BaseActivity + +class ClosedTransactionsActivity : BaseActivity() { + @BindView(R.id.epoxy_closed_transaction) + internal lateinit var epoxyTransactionDetails: EpoxyRecyclerView + + val viewModel: ClosedSessionViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_closed_transaction) + ButterKnife.bind(this) + + initUI() + setupObservers() + } + + private fun initUI() { + initRefreshScreen(R.id.sr_closed_sessions) + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(true) + } + + epoxyTransactionDetails.withModels { + viewModel.customerClosedList.value?.data?.forEachIndexed { index, item -> + closedTransactionModelHolder { + id(index) + data(item) + } + } + } + } + + private fun setupObservers() { + viewModel.customerClosedList.observe(this, Observer { + it?.let { resource -> + handleLoadingRefresh(resource) + when (resource.status) { + Resource.Status.SUCCESS -> { + epoxyTransactionDetails.requestModelBuild() + } + } + } + }) + viewModel.fetchClosedSessions() + } + + override fun updateScreen() { + super.updateScreen() + viewModel.fetchClosedSessions() + } + + override fun onResume() { + super.onResume() + viewModel.fetchMissing() + } + + override fun onSupportNavigateUp(): Boolean { + finish() + return true + } + + companion object { + fun withIntent(context: Context) = Intent(context, ClosedTransactionsActivity::class.java) + } +} diff --git a/app/src/main/java/com/checkin/app/checkin/home/activities/HomeActivity.kt b/app/src/main/java/com/checkin/app/checkin/home/activities/HomeActivity.kt index 36be1d09..bf83183f 100644 --- a/app/src/main/java/com/checkin/app/checkin/home/activities/HomeActivity.kt +++ b/app/src/main/java/com/checkin/app/checkin/home/activities/HomeActivity.kt @@ -10,6 +10,7 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle +import android.preference.PreferenceManager import android.provider.Settings import android.view.View import android.view.ViewGroup @@ -27,6 +28,7 @@ import butterknife.BindView import butterknife.ButterKnife import butterknife.OnClick import co.zsmb.materialdrawerkt.builders.DrawerBuilderKt +import co.zsmb.materialdrawerkt.draweritems.badgeable.primaryItem import com.checkin.app.checkin.BuildConfig import com.checkin.app.checkin.R import com.checkin.app.checkin.accounts.ACCOUNT_TYPE @@ -39,6 +41,7 @@ import com.checkin.app.checkin.data.notifications.MessageUtils import com.checkin.app.checkin.data.resource.ProblemModel import com.checkin.app.checkin.data.resource.Resource import com.checkin.app.checkin.home.fragments.UserHomeFragment +import com.checkin.app.checkin.home.fragments.UserLocationFragment import com.checkin.app.checkin.home.viewmodels.HomeViewModel import com.checkin.app.checkin.home.viewmodels.LiveSessionViewModel import com.checkin.app.checkin.location.UserCurrentLocationService @@ -54,6 +57,8 @@ import com.checkin.app.checkin.user.fragments.UserPrivateProfileFragment import com.checkin.app.checkin.user.models.UserModel import com.checkin.app.checkin.user.viewmodels.UserViewModel import com.checkin.app.checkin.utility.* +import com.checkin.app.checkin.utility.Constants.LOCATION_CITY_ID +import com.checkin.app.checkin.utility.Constants.LOCATION_CITY_NAME import com.checkin.app.checkin.utility.OnBoardingUtils.OnBoardingModel import com.golovin.fluentstackbar.FluentSnackbar import com.google.android.material.snackbar.Snackbar @@ -72,12 +77,15 @@ class HomeActivity : BaseAccountActivity() { internal lateinit var tvCartRestaurant: TextView @BindView(R.id.im_home_cart_restaurant) internal lateinit var imCartRestaurant: ImageView + @BindView(R.id.tv_checkin_location) + internal lateinit var tvCityLocation: TextView var imTabUserIcon: ImageView? = null private val mViewModel: HomeViewModel by viewModels() private val userViewModel: UserViewModel by viewModels() private val liveViewModel: LiveSessionViewModel by viewModels() private val networkFragment = NetworkBlockingFragment() + private val locationFragment by lazy { UserLocationFragment() } private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -137,7 +145,7 @@ class HomeActivity : BaseAccountActivity() { } }) - setupUserLocationTracker() + setupObserver() explainQr() @@ -170,7 +178,6 @@ class HomeActivity : BaseAccountActivity() { .show() } } - } override fun updateScreen() { @@ -242,12 +249,22 @@ class HomeActivity : BaseAccountActivity() { containerCart.visibility = View.GONE } }) + + mViewModel.cityId.observe(this, Observer { + tvCityLocation.text = PreferenceManager.getDefaultSharedPreferences(this).getString(LOCATION_CITY_NAME, "Current Location") + if (it == 0) setupUserLocationTracker() + }) liveViewModel.clearCartData.observe(this, Observer { }) + + mViewModel.setCityId(getCityId()) mViewModel.fetchSessionStatus() - mViewModel.fetchNearbyRestaurants() liveViewModel.fetchCartStatus() } + private fun getCityId(): Int { + return PreferenceManager.getDefaultSharedPreferences(this).getInt(LOCATION_CITY_ID, 0) + } + private fun resetUserIcon() { imTabUserIcon!!.background = null imTabUserIcon!!.setPadding(0, 0, 0, 0) @@ -263,6 +280,18 @@ class HomeActivity : BaseAccountActivity() { override fun setupDrawerItems(builder: DrawerBuilderKt) = builder.run { selectedItem = -1 + primaryItem { + name = "My Transactions" + enabled = true + icon = R.drawable.ic_order_summary + textColorRes = R.color.brownish_grey + onClick { _ -> + val intent = ClosedTransactionsActivity.withIntent(this@HomeActivity) + startActivity(intent) + false + } + } + pass } @@ -371,6 +400,14 @@ class HomeActivity : BaseAccountActivity() { liveViewModel.cartStatus.value?.data?.let { openPublicRestaurantProfile(it.restaurant.targetId, it.pk, true) } } + @OnClick(R.id.cl_checkin_location) + fun onCLickLocation() { + supportFragmentManager.inTransaction { + replace(R.id.frg_container_activity, locationFragment, UserLocationFragment.TAG) + addToBackStack(null) + } + } + private inner class HomeFragmentAdapter(fm: FragmentManager) : BaseFragmentAdapterBottomNav(fm) { override fun getTabDrawable(position: Int): Int = when (position) { 0 -> R.drawable.ic_home_toggle diff --git a/app/src/main/java/com/checkin/app/checkin/home/activities/SplashActivity.kt b/app/src/main/java/com/checkin/app/checkin/home/activities/SplashActivity.kt index bbcde944..76d3d8c1 100644 --- a/app/src/main/java/com/checkin/app/checkin/home/activities/SplashActivity.kt +++ b/app/src/main/java/com/checkin/app/checkin/home/activities/SplashActivity.kt @@ -13,11 +13,14 @@ import com.checkin.app.checkin.accounts.ACCOUNT_TYPE import com.checkin.app.checkin.auth.AuthPreferences import com.checkin.app.checkin.auth.activities.AuthActivity import com.checkin.app.checkin.auth.services.DeviceTokenService +import com.checkin.app.checkin.data.config.AnalyticsUtil import com.checkin.app.checkin.data.config.RemoteConfig +import com.checkin.app.checkin.data.db.AppDatabase import com.checkin.app.checkin.manager.activities.ManagerWorkActivity import com.checkin.app.checkin.utility.Constants import com.checkin.app.checkin.utility.Utils import com.checkin.app.checkin.utility.coroutineLifecycleScope +import com.checkin.app.checkin.utility.firebaseAnalytics import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -35,6 +38,12 @@ class SplashActivity : AppCompatActivity() { // Activates the fetched config from Firebase Remote Config RemoteConfig.activate() + // Initializing the DB + AppDatabase.init(applicationContext) + + // Setting App ENVIRONMENT User Property + AnalyticsUtil.setAppBuild(firebaseAnalytics) + goForScreens() // lottieSplash.addAnimatorListener(this) } diff --git a/app/src/main/java/com/checkin/app/checkin/home/epoxy/CityLocationModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/home/epoxy/CityLocationModelHolder.kt new file mode 100644 index 00000000..9434b0ef --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/epoxy/CityLocationModelHolder.kt @@ -0,0 +1,45 @@ +package com.checkin.app.checkin.home.epoxy + +import android.view.View +import android.widget.TextView +import butterknife.BindView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.checkin.app.checkin.R +import com.checkin.app.checkin.home.listeners.LocationSelectedListener +import com.checkin.app.checkin.home.model.CityLocationModel +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_user_location_city) +abstract class CityLocationModelHolder : EpoxyModelWithHolder() { + @EpoxyAttribute + internal lateinit var data: CityLocationModel + + @EpoxyAttribute + internal lateinit var listener: LocationSelectedListener + + override fun createNewHolder() = Holder(listener) + + override fun bind(holder: Holder) = holder.bindData(data) + + class Holder(val listener: LocationSelectedListener) : BaseEpoxyHolder() { + @BindView(R.id.tv_user_location_city) + internal lateinit var tvCityLocation: TextView + @BindView(R.id.tv_user_location_state) + internal lateinit var tvStateLocation: TextView + + private lateinit var mData: CityLocationModel + + override fun bindView(itemView: View) { + super.bindView(itemView) + itemView.setOnClickListener { listener.onLocationSelected(mData) } + } + + override fun bindData(data: CityLocationModel) { + mData = data + tvCityLocation.text = data.name + tvStateLocation.text = "${data.state} ${data.country}" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/epoxy/ClosedTransactionModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/home/epoxy/ClosedTransactionModelHolder.kt new file mode 100644 index 00000000..bc59e55b --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/epoxy/ClosedTransactionModelHolder.kt @@ -0,0 +1,57 @@ +package com.checkin.app.checkin.home.epoxy + +import android.widget.TextView +import butterknife.BindView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.checkin.app.checkin.R +import com.checkin.app.checkin.home.activities.ClosedSessionDetailsActivity +import com.checkin.app.checkin.home.model.ClosedSessionBriefModel +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_transaction_details) +abstract class ClosedTransactionModelHolder : EpoxyModelWithHolder() { + + @EpoxyAttribute + internal lateinit var data: ClosedSessionBriefModel + + + override fun bind(holder: Holder) { + holder.bindData(data) + } + + class Holder : BaseEpoxyHolder() { + @BindView(R.id.tv_transaction_restaurant) + internal lateinit var tvTransactionRestaurant: TextView + + @BindView(R.id.tv_transaction_id) + internal lateinit var tvTransactionId: TextView + + @BindView(R.id.tv_transaction_timings) + internal lateinit var tvTransactionTimings: TextView + + @BindView(R.id.tv_transaction_count) + internal lateinit var tvTransactionCount: TextView + + @BindView(R.id.tv_transaction_amount) + internal lateinit var tvTransactionAmount: TextView + + + override fun bindData(data: ClosedSessionBriefModel) { + tvTransactionRestaurant.text = data.restaurant.displayName + tvTransactionId.text = data.formatId() + tvTransactionTimings.text = data.formatTimings() + tvTransactionCount.text = data.countCustomers.toString() + tvTransactionAmount.text = data.formatAmount() + + itemView.setOnClickListener { _ -> + val intent = ClosedSessionDetailsActivity.withSessionIntent(context, data.pk) + context.startActivity(intent) + } + + + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/epoxy/CurrentLocationModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/home/epoxy/CurrentLocationModelHolder.kt new file mode 100644 index 00000000..09063b47 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/epoxy/CurrentLocationModelHolder.kt @@ -0,0 +1,28 @@ +package com.checkin.app.checkin.home.epoxy + +import android.view.View +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.checkin.app.checkin.R +import com.checkin.app.checkin.home.listeners.LocationSelectedListener +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_user_location_current) +abstract class CurrentLocationModelHolder : EpoxyModelWithHolder() { + @EpoxyAttribute + internal lateinit var listener: LocationSelectedListener + + override fun createNewHolder() = Holder(listener) + + override fun bind(holder: Holder) = holder.bindData("Current Location") + + class Holder(val listener: LocationSelectedListener) : BaseEpoxyHolder() { + override fun bindView(itemView: View) { + super.bindView(itemView) + itemView.setOnClickListener { listener.onLocationSelected(null) } + } + + override fun bindData(data: String) {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/epoxy/InvoiceClosedOrderModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/home/epoxy/InvoiceClosedOrderModelHolder.kt new file mode 100644 index 00000000..10443d31 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/epoxy/InvoiceClosedOrderModelHolder.kt @@ -0,0 +1,41 @@ +package com.checkin.app.checkin.home.epoxy + +import android.view.View +import android.widget.TextView +import butterknife.BindView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.checkin.app.checkin.R +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder +import com.checkin.app.checkin.session.models.SessionOrderedItemModel +import com.checkin.app.checkin.utility.Utils + + +@EpoxyModelClass(layout = R.layout.item_invoice_order_without_customizations) +abstract class InvoiceClosedOrderModelHolder : EpoxyModelWithHolder() { + @EpoxyAttribute + lateinit var orderData: SessionOrderedItemModel + + override fun bind(holder: Holder) = holder.bindData(orderData) + + class Holder : BaseEpoxyHolder() { + @BindView(R.id.tv_invoice_order_item_name) + internal lateinit var tvItemName: TextView + @BindView(R.id.tv_invoice_order_item_price) + internal lateinit var tvItemPrice: TextView + @BindView(R.id.tv_invoice_order_customized) + internal lateinit var tvCustomized: TextView + + override fun bindData(data: SessionOrderedItemModel) { + tvItemName.text = "${data.item.name} x ${data.quantity}" + tvItemPrice.text = Utils.formatCurrencyAmount(itemView.context, data.cost) + tvCustomized.visibility = if (data.isCustomized) View.VISIBLE else View.GONE + data.item.isVegetarian.let { + if (it != null) + tvItemName.setCompoundDrawablesWithIntrinsicBounds(if (it) R.drawable.ic_veg else R.drawable.ic_non_veg, 0, 0, 0) + else tvItemName.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/epoxy/NearbyRestaurantModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/home/epoxy/NearbyRestaurantModelHolder.kt new file mode 100644 index 00000000..4fd83756 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/epoxy/NearbyRestaurantModelHolder.kt @@ -0,0 +1,105 @@ +package com.checkin.app.checkin.home.epoxy + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import butterknife.BindView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.checkin.app.checkin.R +import com.checkin.app.checkin.home.model.NearbyRestaurantModel +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder +import com.checkin.app.checkin.restaurant.activities.openPublicRestaurantProfile +import com.checkin.app.checkin.utility.Utils +import com.checkin.app.checkin.utility.blackAndWhite + +@EpoxyModelClass(layout = R.layout.item_home_restaurant_banner) +abstract class NearbyRestaurantModelHolder : EpoxyModelWithHolder() { + + @EpoxyAttribute + internal lateinit var restaurantModel: NearbyRestaurantModel + + override fun bind(holder: Holder) = holder.bindData(restaurantModel) + + inner class Holder : BaseEpoxyHolder() { + @BindView(R.id.im_restaurant_banner_cover) + internal lateinit var imRestaurantBanner: ImageView + @BindView(R.id.tv_restaurant_banner_cuisines) + internal lateinit var tvCuisines: TextView + @BindView(R.id.tv_restaurant_banner_rating) + internal lateinit var tvRating: TextView + @BindView(R.id.tv_restaurant_banner_name_locality) + internal lateinit var tvRestaurantName: TextView + @BindView(R.id.tv_restaurant_banner_distance) + internal lateinit var tvDistance: TextView + @BindView(R.id.tv_restaurant_banner_offer_code) + internal lateinit var tvCouponCode: TextView + @BindView(R.id.tv_restaurant_banner_count_checkins) + internal lateinit var tvCountCheckins: TextView + @BindView(R.id.tv_restaurant_banner_offer_special) + internal lateinit var tvExclusiveOffer: TextView + @BindView(R.id.tv_restaurant_banner_offer_summary) + internal lateinit var tvOfferSummary: TextView + @BindView(R.id.im_restaurant_banner_distance) + internal lateinit var imDistance: ImageView + @BindView(R.id.container_restaurant_banner_offer) + internal lateinit var containerOffer: ViewGroup + @BindView(R.id.cl_restaurant_banner_location) + internal lateinit var clLocation: ConstraintLayout + @BindView(R.id.tv_restaurant_banner_opening_timings) + internal lateinit var tvOpenTimings: TextView + + override fun bindData(data: NearbyRestaurantModel) { + itemView.setOnClickListener { + data.let { itemView.context.openPublicRestaurantProfile(it.pk) } + } + + tvCuisines.text = data.cuisines.let { + if (it.isNotEmpty()) { + val maxLen = it.size.coerceAtMost(3) + it.slice(0 until maxLen).joinToString(" • ") + } else "-" + } + + tvRating.text = data.formatRating + tvDistance.text = data.formatDistance + + imDistance.setImageResource(if (data.distance > 1.5) R.drawable.ic_distance_vehicle else R.drawable.ic_distance_walking) + + tvRestaurantName.text = data.locality.let { + if (it != null) data.name + " - " + data.locality + else data.name + } + + tvCountCheckins.text = data.formatCheckins + + data.offer.let { + if (it == null) containerOffer.visibility = View.GONE + else { + containerOffer.visibility = View.VISIBLE + tvCouponCode.text = Utils.fromHtml(itemView.context.getString(R.string.discount_code, it.code)) + tvExclusiveOffer.visibility = if (it.isGlobal) View.GONE else View.VISIBLE + tvOfferSummary.text = Utils.fromHtml(it.name) + } + } + + data.covers.let { + if (it.getOrNull(0) != null) Utils.loadImageOrDefault(imRestaurantBanner, it[0], R.drawable.cover_restaurant_unknown) + else imRestaurantBanner.setImageResource(R.drawable.cover_restaurant_unknown) + } + if (data.isOpen) { + imRestaurantBanner.colorFilter = null + clLocation.visibility = View.VISIBLE + tvOpenTimings.visibility = View.GONE + } else { + imRestaurantBanner.blackAndWhite() + clLocation.visibility = View.GONE + tvOpenTimings.visibility = View.VISIBLE + tvOpenTimings.text = data.timings.formatDescription(itemView.context) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/fragments/ClosedOrderDetailsModel.kt b/app/src/main/java/com/checkin/app/checkin/home/fragments/ClosedOrderDetailsModel.kt new file mode 100644 index 00000000..b654c3ad --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/fragments/ClosedOrderDetailsModel.kt @@ -0,0 +1,40 @@ +package com.checkin.app.checkin.home.fragments + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import butterknife.BindView +import com.checkin.app.checkin.R +import com.checkin.app.checkin.accounts.AccountUtil +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.viewmodels.ClosedSessionViewModel +import com.checkin.app.checkin.misc.fragments.BaseFragment + +class ClosedOrderDetailsModel : BaseFragment() { + override val rootLayout: Int = R.layout.fragment_closed_order_details + + @BindView(R.id.tv_closed_order_confirmation_guest_name) + internal lateinit var tvGuestName: TextView + @BindView(R.id.tv_closed_order_confirmation_due) + internal lateinit var tvConfirmationDate: TextView + @BindView(R.id.tv_closed_order_confirmation_order_id) + internal lateinit var tvOrderId: TextView + + val model: ClosedSessionViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + model.sessionData.observe(viewLifecycleOwner, Observer { + when (it.status) { + Resource.Status.SUCCESS -> { + tvGuestName.text = AccountUtil.getUsername(requireContext()) + it.data.let { + tvConfirmationDate.text = it?.formatPlannedDate + tvOrderId.text = it?.formatId + } + } + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/fragments/ClosedSessionDetailFragment.kt b/app/src/main/java/com/checkin/app/checkin/home/fragments/ClosedSessionDetailFragment.kt new file mode 100644 index 00000000..14b272b1 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/fragments/ClosedSessionDetailFragment.kt @@ -0,0 +1,49 @@ +package com.checkin.app.checkin.home.fragments + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import butterknife.BindView +import com.checkin.app.checkin.R +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.model.ClosedSessionDetailsModel +import com.checkin.app.checkin.home.viewmodels.ClosedSessionViewModel +import com.checkin.app.checkin.misc.fragments.BaseFragment +import com.checkin.app.checkin.misc.views.CollapsibleSectionView + +class ClosedSessionDetailFragment : BaseFragment() { + override val rootLayout: Int = R.layout.fragment_closed_session + + @BindView(R.id.csv_closed_session_billing) + internal lateinit var csvBilling: CollapsibleSectionView + @BindView(R.id.csv_closed_session_order) + internal lateinit var csvOrders: CollapsibleSectionView + @BindView(R.id.tv_serving_time) + internal lateinit var tvServingTime: TextView + @BindView(R.id.tv_session_time) + internal lateinit var tvSessionTime: TextView + + val viewModel: ClosedSessionViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + csvBilling.attachFragment(childFragmentManager, ClosedOrderDetailsModel()) + csvOrders.attachFragment(childFragmentManager, CommonClosedOrderDetailsFragment()) + + viewModel.sessionData.observe(viewLifecycleOwner, Observer { + if (it.status == Resource.Status.SUCCESS && it.data != null) { + setupData(it.data) + } + }) + } + + private fun setupData(data: ClosedSessionDetailsModel) { + tvServingTime.text = data.formatServingTIme + } + + override fun onResume() { + super.onResume() + viewModel.fetchMissing() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/fragments/CommonClosedOrderDetailsFragment.kt b/app/src/main/java/com/checkin/app/checkin/home/fragments/CommonClosedOrderDetailsFragment.kt new file mode 100644 index 00000000..63b8b45e --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/fragments/CommonClosedOrderDetailsFragment.kt @@ -0,0 +1,48 @@ +package com.checkin.app.checkin.home.fragments + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.epoxy.invoiceClosedOrderModelHolder +import com.checkin.app.checkin.home.model.ClosedSessionDetailsModel +import com.checkin.app.checkin.home.viewmodels.ClosedSessionViewModel +import com.checkin.app.checkin.misc.fragments.BaseOrderDetailFragment +import com.checkin.app.checkin.utility.Utils +import com.checkin.app.checkin.utility.isNotEmpty + + +class CommonClosedOrderDetailsFragment : BaseOrderDetailFragment() { + + val viewModel: ClosedSessionViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + epoxyRvOrders.withModels { + viewModel.ordersData.value?.data.takeIf { it.isNotEmpty() }?.forEach { + invoiceClosedOrderModelHolder { + id(it.pk) + orderData(it) + } + } + } + + viewModel.ordersData.observe(viewLifecycleOwner, Observer { + if (it.status == Resource.Status.SUCCESS && it.data != null) epoxyRvOrders.requestModelBuild() + }) + viewModel.sessionData.observe(viewLifecycleOwner, Observer { + if (it.status == Resource.Status.SUCCESS && it.data != null) { + setupData(it.data) + } else if (it.status == Resource.Status.LOADING) { + billHolder.showLoading() + } + }) + } + + private fun setupData(data: ClosedSessionDetailsModel) { + billHolder.bind(data.bill) + tvTotal.text = Utils.formatCurrencyAmount(requireContext(), data.bill.total) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/fragments/UserHomeFragment.kt b/app/src/main/java/com/checkin/app/checkin/home/fragments/UserHomeFragment.kt index 6fff1307..9363ee81 100644 --- a/app/src/main/java/com/checkin/app/checkin/home/fragments/UserHomeFragment.kt +++ b/app/src/main/java/com/checkin/app/checkin/home/fragments/UserHomeFragment.kt @@ -8,8 +8,6 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import butterknife.BindView import com.airbnb.epoxy.EpoxyRecyclerView @@ -18,9 +16,9 @@ import com.checkin.app.checkin.data.config.RemoteConfig import com.checkin.app.checkin.data.notifications.MESSAGE_TYPE import com.checkin.app.checkin.data.notifications.MessageUtils import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.epoxy.nearbyRestaurantModelHolder import com.checkin.app.checkin.home.holders.LiveSessionTrackerAdapter import com.checkin.app.checkin.home.holders.LiveSessionTrackerInteraction -import com.checkin.app.checkin.home.holders.NearbyRestaurantAdapter import com.checkin.app.checkin.home.model.LiveSessionDetailModel import com.checkin.app.checkin.home.model.SessionType import com.checkin.app.checkin.home.model.TopAdBannerModel @@ -29,6 +27,7 @@ import com.checkin.app.checkin.home.viewmodels.LiveSessionViewModel import com.checkin.app.checkin.menu.activities.ActiveSessionMenuActivity import com.checkin.app.checkin.misc.fragments.BaseFragment import com.checkin.app.checkin.misc.holders.adBannerModelHolder +import com.checkin.app.checkin.misc.holders.shimmerModelHolder import com.checkin.app.checkin.restaurant.activities.openPublicRestaurantProfile import com.checkin.app.checkin.restaurant.models.RestaurantLocationModel import com.checkin.app.checkin.session.activesession.ActiveSessionActivity @@ -43,14 +42,13 @@ class UserHomeFragment : BaseFragment(), LiveSessionTrackerInteraction { @BindView(R.id.epoxy_rv_home_banner) internal lateinit var epoxyRvHomeBanner: EpoxyRecyclerView - @BindView(R.id.rv_home_nearby_restaurants) - internal lateinit var rvNearbyRestaurants: RecyclerView + @BindView(R.id.epoxy_rv_home_nearby_restaurants) + internal lateinit var epoxyRvNearbyRestaurants: EpoxyRecyclerView @BindView(R.id.container_home_live_session) internal lateinit var containerLiveSession: ViewGroup @BindView(R.id.vp_home_session_live) internal lateinit var vpLiveSession: ViewPager2 - private lateinit var mRestAdapter: NearbyRestaurantAdapter private lateinit var mLiveSessionAdapter: LiveSessionTrackerAdapter private val mViewModel: HomeViewModel by activityViewModels() @@ -89,24 +87,28 @@ class UserHomeFragment : BaseFragment(), LiveSessionTrackerInteraction { } } } - mRestAdapter = NearbyRestaurantAdapter() mLiveSessionAdapter = LiveSessionTrackerAdapter(this) vpLiveSession.adapter = mLiveSessionAdapter - rvNearbyRestaurants.layoutManager = LinearLayoutManager(context) - rvNearbyRestaurants.adapter = mRestAdapter - - rvNearbyRestaurants.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - enableDisableSwipeRefresh(newState == RecyclerView.SCROLL_STATE_IDLE) + epoxyRvNearbyRestaurants.withModels { + mViewModel.nearbyRestaurants.value?.data?.forEachIndexed { it, data -> + nearbyRestaurantModelHolder { + id(it) + restaurantModel(data) + } + } ?: (0 until 3L).map { + shimmerModelHolder { + id("shimmer.rest", it) + withHomeRestaurantBannerLayout() + } } - }) + } mViewModel.nearbyRestaurants.observe(this, Observer { it?.let { listResource -> when (listResource.status) { - Resource.Status.SUCCESS -> listResource.data?.let(mRestAdapter::updateData) + Resource.Status.SUCCESS -> epoxyRvNearbyRestaurants.requestModelBuild() else -> pass } } @@ -131,6 +133,7 @@ class UserHomeFragment : BaseFragment(), LiveSessionTrackerInteraction { mLiveSessionViewModel.fetchScheduledSessions() mLiveSessionViewModel.fetchLiveActiveSession() + epoxyRvNearbyRestaurants.setHasFixedSize(false) } override fun updateScreen() { @@ -173,6 +176,7 @@ class UserHomeFragment : BaseFragment(), LiveSessionTrackerInteraction { MESSAGE_TYPE.USER_SCHEDULED_QSR_DONE ) MessageUtils.registerLocalReceiver(requireContext(), receiver, *types) + mViewModel.fetchMissing() } override fun onPause() { diff --git a/app/src/main/java/com/checkin/app/checkin/home/fragments/UserLocationFragment.kt b/app/src/main/java/com/checkin/app/checkin/home/fragments/UserLocationFragment.kt new file mode 100644 index 00000000..86fa446d --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/fragments/UserLocationFragment.kt @@ -0,0 +1,102 @@ +package com.checkin.app.checkin.home.fragments + +import android.os.Bundle +import android.preference.PreferenceManager +import android.text.Editable +import android.view.View +import android.widget.EditText +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import butterknife.BindView +import butterknife.OnClick +import butterknife.OnTextChanged +import com.airbnb.epoxy.EpoxyRecyclerView +import com.checkin.app.checkin.R +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.epoxy.cityLocationModelHolder +import com.checkin.app.checkin.home.epoxy.currentLocationModelHolder +import com.checkin.app.checkin.home.listeners.LocationSelectedListener +import com.checkin.app.checkin.home.model.CityLocationModel +import com.checkin.app.checkin.home.viewmodels.HomeViewModel +import com.checkin.app.checkin.home.viewmodels.UserLocationViewModel +import com.checkin.app.checkin.misc.fragments.BaseFragment +import com.checkin.app.checkin.utility.Constants + +class UserLocationFragment : BaseFragment(), LocationSelectedListener { + override val rootLayout = R.layout.fragment_user_location_switch + + @BindView(R.id.et_user_location) + internal lateinit var etUserLocation: EditText + @BindView(R.id.epoxy_rv_user_location) + internal lateinit var epoxyUserLocation: EpoxyRecyclerView + + val viewModel: UserLocationViewModel by activityViewModels() + val homeViewModel: HomeViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + epoxyUserLocation.withModels { + if (etUserLocation.text.isEmpty() || viewModel.locationData.value?.inError == true) { + currentLocationModelHolder { + id("present.location") + listener(this@UserLocationFragment) + } + } + + viewModel.locationData.value?.data?.forEachIndexed { _, model -> + cityLocationModelHolder { + id(model.id) + data(model) + listener(this@UserLocationFragment) + } + } + } + + viewModel.locationData.observe(viewLifecycleOwner, Observer { + when (it.status) { + Resource.Status.SUCCESS, Resource.Status.ERROR_NOT_FOUND -> { + epoxyUserLocation.requestModelBuild() + } + } + }) + viewModel.fetchData() + } + + @OnTextChanged(R.id.et_user_location, callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED) + fun onTextChanged(et: Editable?) { + if (et != null) viewModel.searchCities(et.toString()) + } + + @OnClick(R.id.im_user_location_back) + fun onClickBack() { + onBackPressed() + } + + override fun onBackPressed(): Boolean { + viewModel.resetResults() + parentFragmentManager.popBackStack() + return true + } + + override fun onLocationSelected(data: CityLocationModel?) { + var id = 0 + var name = "Current Location" + + if (data != null) { + id = data.id + name = data.name + } + + with(PreferenceManager.getDefaultSharedPreferences(context).edit()) { + putInt(Constants.LOCATION_CITY_ID, id) + putString(Constants.LOCATION_CITY_NAME, name) + apply() + } + + homeViewModel.setCityId(id) + onBackPressed() + } + + companion object { + val TAG = UserLocationFragment::class.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/holders/NearbyRestaurantAdapter.kt b/app/src/main/java/com/checkin/app/checkin/home/holders/NearbyRestaurantAdapter.kt deleted file mode 100644 index 5be54db4..00000000 --- a/app/src/main/java/com/checkin/app/checkin/home/holders/NearbyRestaurantAdapter.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.checkin.app.checkin.home.holders - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager.widget.PagerAdapter -import androidx.viewpager.widget.ViewPager -import butterknife.BindView -import butterknife.ButterKnife -import com.checkin.app.checkin.R -import com.checkin.app.checkin.home.model.NearbyRestaurantModel -import com.checkin.app.checkin.misc.holders.BaseViewHolder -import com.checkin.app.checkin.restaurant.activities.openPublicRestaurantProfile -import com.checkin.app.checkin.utility.Utils -import com.checkin.app.checkin.utility.navigateToLocation -import com.rd.PageIndicatorView -import com.rd.animation.type.AnimationType - - -class NearbyRestaurantAdapter : RecyclerView.Adapter>() { - var data: List = emptyList() - - fun updateData(newData: List) { - data = newData - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = LayoutInflater.from(parent.context).inflate(viewType, parent, false).run { - if (viewType == R.layout.item_home_restaurant_banner) NearbyRestaurantViewHolder(this) as BaseViewHolder else AdvertisementViewHolder(this) - } - - private val adPosition: Int - get() = data.count().coerceAtMost(1) - - override fun getItemCount(): Int = data.count() //+1 - - override fun getItemViewType(position: Int): Int = R.layout.item_home_restaurant_banner - - override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { - /*when { - position < adPosition -> holder.bindData(data[position]) - position > adPosition -> holder.bindData(data[position - 1]) - else -> holder.bindData(pass) - }*/ - holder.bindData(data[position]) - } - - inner class NearbyRestaurantViewHolder(itemView: View) : BaseViewHolder(itemView) { - @BindView(R.id.im_restaurant_banner_cover) - internal lateinit var imRestaurantBanner: ImageView - @BindView(R.id.tv_restaurant_banner_cuisines) - internal lateinit var tvCuisines: TextView - @BindView(R.id.tv_restaurant_banner_rating) - internal lateinit var tvRating: TextView - @BindView(R.id.tv_restaurant_banner_name_locality) - internal lateinit var tvRestaurantName: TextView - @BindView(R.id.tv_restaurant_banner_distance) - internal lateinit var tvDistance: TextView - @BindView(R.id.tv_restaurant_banner_offer_code) - internal lateinit var tvCouponCode: TextView - @BindView(R.id.tv_restaurant_banner_count_checkins) - internal lateinit var tvCountCheckins: TextView - @BindView(R.id.tv_restaurant_banner_offer_special) - internal lateinit var tvExclusiveOffer: TextView - @BindView(R.id.tv_restaurant_banner_offer_summary) - internal lateinit var tvOfferSummary: TextView - @BindView(R.id.im_restaurant_banner_distance) - internal lateinit var imDistance: ImageView - @BindView(R.id.container_restaurant_banner_offer) - internal lateinit var containerOffer: ViewGroup - @BindView(R.id.tv_restaurant_banner_navigate) - internal lateinit var tvNavigateBtn: TextView - - private var mRestaurantData: NearbyRestaurantModel? = null - - init { - ButterKnife.bind(this, itemView) - - tvNavigateBtn.setOnClickListener { - mRestaurantData?.geolocation?.run { navigateToLocation(itemView.context) } - } - itemView.setOnClickListener { - mRestaurantData?.let { itemView.context.openPublicRestaurantProfile(it.pk) } - } - } - - override fun bindData(data: NearbyRestaurantModel) { - mRestaurantData = data - - tvCuisines.text = data.cuisines.let { - if (it.isNotEmpty()) { - val maxLen = it.size.coerceAtMost(3) - it.slice(0 until maxLen).joinToString(" ") - } else "-" - } - - tvRating.text = data.formatRating - tvDistance.text = data.formatDistance - - imDistance.setImageResource(if (data.distance > 1.5) R.drawable.ic_distance_vehicle else R.drawable.ic_distance_walking) - - tvRestaurantName.text = data.locality.let { - if (it != null) data.name + " - " + data.locality - else data.name - } - - tvCountCheckins.text = data.formatCheckins - - data.offer.let { - if (it == null) containerOffer.visibility = View.GONE - else { - containerOffer.visibility = View.VISIBLE - tvCouponCode.text = Utils.fromHtml(itemView.context.getString(R.string.discount_code, it.code)) - tvExclusiveOffer.visibility = if (it.isGlobal) View.GONE else View.VISIBLE - tvOfferSummary.text = Utils.fromHtml(it.name) - } - } - - data.covers.let { - if (it.getOrNull(0) != null) Utils.loadImageOrDefault(imRestaurantBanner, it[0], R.drawable.cover_restaurant_unknown) - else imRestaurantBanner.setImageResource(R.drawable.cover_restaurant_unknown) - } - } - } - - class AdvertisementViewHolder(itemView: View) : BaseViewHolder(itemView) { - @BindView(R.id.vp_home_banner) - internal lateinit var vpBanner: ViewPager - @BindView(R.id.indicator_home_banner) - internal lateinit var indicatorView: PageIndicatorView - - private val mPagerAdapter: BannerPagerAdapter = BannerPagerAdapter(itemView.context) - - init { - ButterKnife.bind(this, itemView) - - vpBanner.adapter = mPagerAdapter - indicatorView.setViewPager(vpBanner) - indicatorView.setAnimationType(AnimationType.FILL) - indicatorView.setClickListener { position -> vpBanner.currentItem = position } - } - - override fun bindData(data: Any) { - } - } - - class BannerPagerAdapter(context: Context) : PagerAdapter() { - private val mResources = intArrayOf()//R.drawable.first_banner, R.drawable.second_banner, R.drawable.third_banner) - private val mLayoutInflater: LayoutInflater = LayoutInflater.from(context) - - override fun getCount(): Int { - return mResources.size - } - - override fun isViewFromObject(view: View, `object`: Any): Boolean { - return `object` === view - } - - override fun instantiateItem(container: ViewGroup, position: Int): Any { - val itemView = mLayoutInflater.inflate(R.layout.item_ad_banner, container, false) - val imageView = itemView.findViewById(R.id.im_item_banner) as ImageView - imageView.scaleType = ImageView.ScaleType.FIT_CENTER - imageView.setImageResource(mResources[position]) - container.addView(itemView) - - return itemView - } - - override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { - container.removeView(`object` as View) - } - } -} diff --git a/app/src/main/java/com/checkin/app/checkin/home/listeners/LocationSelectedListener.kt b/app/src/main/java/com/checkin/app/checkin/home/listeners/LocationSelectedListener.kt new file mode 100644 index 00000000..31abcdc8 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/listeners/LocationSelectedListener.kt @@ -0,0 +1,7 @@ +package com.checkin.app.checkin.home.listeners + +import com.checkin.app.checkin.home.model.CityLocationModel + +interface LocationSelectedListener { + fun onLocationSelected(data: CityLocationModel?) +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/model/CityLocationModel.kt b/app/src/main/java/com/checkin/app/checkin/home/model/CityLocationModel.kt new file mode 100644 index 00000000..678636b9 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/model/CityLocationModel.kt @@ -0,0 +1,11 @@ +package com.checkin.app.checkin.home.model + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class CityLocationModel( + val id: Int, + val name: String, + val state: String, + val country: String +) \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/model/ClosedSessionBriefModel.kt b/app/src/main/java/com/checkin/app/checkin/home/model/ClosedSessionBriefModel.kt new file mode 100644 index 00000000..3aa4425d --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/model/ClosedSessionBriefModel.kt @@ -0,0 +1,29 @@ +package com.checkin.app.checkin.home.model + +import com.checkin.app.checkin.misc.models.BriefModel +import com.fasterxml.jackson.annotation.JsonProperty +import java.text.SimpleDateFormat +import java.util.* + +data class ClosedSessionBriefModel( + val pk: Long, + @JsonProperty("hash_id") val hashId: String, + @JsonProperty("checkedin_time") val checkinTime: Date, + @JsonProperty("count_orders") val countOrders: Int, + @JsonProperty("count_customers") val countCustomers: Int, + val total: Double, + @JsonProperty("session_type") val sessionType: Int, + val restaurant: BriefModel +) { + + fun formatId(): String = "Order ID: #${hashId}" + + fun formatTimings(): String { + val format = SimpleDateFormat("MMM dd, YYYY hh:mm a", Locale.getDefault()) + return "${format.format(checkinTime)} | ${formatCountOrder()}" + } + + fun formatAmount() = "₹${String.format("%.2f", total)}" + + fun formatCountOrder() = if (countOrders == 1) "$countOrders item" else "$countOrders items" +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/model/ClosedSessionDetailsModel.kt b/app/src/main/java/com/checkin/app/checkin/home/model/ClosedSessionDetailsModel.kt new file mode 100644 index 00000000..386735fb --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/model/ClosedSessionDetailsModel.kt @@ -0,0 +1,28 @@ +package com.checkin.app.checkin.home.model + +import com.checkin.app.checkin.restaurant.models.RestaurantLocationModel +import com.checkin.app.checkin.session.models.SessionBillModel +import com.checkin.app.checkin.session.models.SessionOrderedItemModel +import com.checkin.app.checkin.utility.Utils +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.* + +@JsonIgnoreProperties(ignoreUnknown = true) +data class ClosedSessionDetailsModel( + val pk: Int, + @JsonProperty("hash_id") val hashId: String, + @JsonProperty("checkedin_time") val checkedinTime: Date, + @JsonProperty("checked_out") val checkoutTime: Date, + @JsonProperty("ordered_items") val orderedItems: List, + val restaurant: RestaurantLocationModel, + val bill: SessionBillModel, + @JsonProperty("serving_time") val servingTime: Long, + @JsonProperty("session_time") val sessionTime: Long +) { + val formatPlannedDate: String = Utils.formatDate(checkedinTime, "hh:mm a | MMM dd, YYYY ") + + val formatId = "#$hashId" + + val formatServingTIme: String = "Serving Time: ${if (servingTime >= 0) Utils.formatTimeDuration(servingTime) else "Before Time"}" +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/home/model/NearbyRestaurantModel.kt b/app/src/main/java/com/checkin/app/checkin/home/model/NearbyRestaurantModel.kt index fd5768ae..b20cd266 100644 --- a/app/src/main/java/com/checkin/app/checkin/home/model/NearbyRestaurantModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/home/model/NearbyRestaurantModel.kt @@ -1,6 +1,7 @@ package com.checkin.app.checkin.home.model import com.checkin.app.checkin.misc.models.GeolocationModel +import com.checkin.app.checkin.restaurant.models.TimingModel import com.checkin.app.checkin.utility.Utils import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty @@ -19,7 +20,9 @@ data class NearbyRestaurantModel( val distance: Double, val ratings: Double, val cuisines: List, - val offer: RestaurantListingOfferModel? + val offer: RestaurantListingOfferModel?, + @JsonProperty("is_open") val isOpen: Boolean, + val timings: TimingModel ) { val formatDistance: String get() = "$distance ${if (distance <= 1.0) "km" else "kms"}" @@ -29,5 +32,4 @@ data class NearbyRestaurantModel( val formatRating: String get() = if (ratings < 1.0) "---" else ratings.toString() - } diff --git a/app/src/main/java/com/checkin/app/checkin/home/viewmodels/ClosedSessionViewModel.kt b/app/src/main/java/com/checkin/app/checkin/home/viewmodels/ClosedSessionViewModel.kt new file mode 100644 index 00000000..6d0fd635 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/viewmodels/ClosedSessionViewModel.kt @@ -0,0 +1,48 @@ +package com.checkin.app.checkin.home.viewmodels + +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.checkin.app.checkin.data.BaseViewModel +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.model.ClosedSessionBriefModel +import com.checkin.app.checkin.home.model.ClosedSessionDetailsModel +import com.checkin.app.checkin.session.SessionRepository +import com.checkin.app.checkin.session.models.SessionOrderedItemModel + +class ClosedSessionViewModel(application: Application) : BaseViewModel(application) { + private val sessionRepository = SessionRepository.getInstance(application) + + private var sessionId: Long = 0 + + private val mSessionData = createNetworkLiveData() + private val mClosedSessions = createNetworkLiveData>() + + val customerClosedList: LiveData>> = mClosedSessions + val sessionData: LiveData> = mSessionData + val ordersData: LiveData>> = Transformations.map(mSessionData) { + it?.data?.let { data -> + Resource.cloneResource(it, data.orderedItems) + } ?: Resource.cloneResource(it, emptyList()) + } + + fun fetchClosedSessions() { + mClosedSessions.addSource(sessionRepository.customerClosedSessionList, mClosedSessions::setValue) + } + + fun fetchSessionData(sessionId: Long) { + this.sessionId = sessionId + mSessionData.addSource(sessionRepository.getCustomerClosedSessionDetails(sessionId), mSessionData::setValue) + } + + override fun fetchMissing() { + super.fetchMissing() + if (sessionId != 0L && mSessionData.value?.mayLoad == false) fetchSessionData(sessionId) + else if (mClosedSessions.value?.mayLoad == false) fetchClosedSessions() + } + + override fun updateResults() { + if (sessionId != 0L && mSessionData.value?.mayLoad == false) fetchSessionData(sessionId) + else fetchClosedSessions() + } +} diff --git a/app/src/main/java/com/checkin/app/checkin/home/viewmodels/HomeViewModel.kt b/app/src/main/java/com/checkin/app/checkin/home/viewmodels/HomeViewModel.kt index 6c3b47ff..3b460caa 100644 --- a/app/src/main/java/com/checkin/app/checkin/home/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/home/viewmodels/HomeViewModel.kt @@ -2,10 +2,12 @@ package com.checkin.app.checkin.home.viewmodels import android.app.Application import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import com.checkin.app.checkin.Shop.ShopRepository import com.checkin.app.checkin.data.BaseViewModel import com.checkin.app.checkin.data.Converters import com.checkin.app.checkin.data.resource.Resource -import com.checkin.app.checkin.Shop.ShopRepository import com.checkin.app.checkin.home.model.NearbyRestaurantModel import com.checkin.app.checkin.session.SessionRepository import com.checkin.app.checkin.session.models.QRResultModel @@ -16,46 +18,55 @@ class HomeViewModel(application: Application) : BaseViewModel(application) { private val mSessionRepository: SessionRepository = SessionRepository.getInstance(application) private val mShopRepository: ShopRepository = ShopRepository.getInstance(application) + private val mCityId = MutableLiveData() private val mQrResult = createNetworkLiveData() private val mSessionStatus = createNetworkLiveData() private val mCancelDineInRequest = createNetworkLiveData() private val mNearbyRestaurants = createNetworkLiveData>() - val sessionStatus: LiveData> - get() = mSessionStatus - - val qrResult: LiveData> - get() = mQrResult - - val cancelDineInData: LiveData> - get() = mCancelDineInRequest - - val nearbyRestaurants: LiveData>> - get() = mNearbyRestaurants + val sessionStatus: LiveData> = mSessionStatus + val qrResult: LiveData> = mQrResult + val cancelDineInData: LiveData> = mCancelDineInRequest + val cityId: LiveData = mCityId + val nearbyRestaurants: LiveData>> = Transformations.map(mNearbyRestaurants) { + it?.data?.let { list -> + Resource.cloneResource(it, list.sortedBy { !it.isOpen }) + } ?: it + } override fun updateResults() { fetchSessionStatus() } override fun fetchMissing() { - if (mNearbyRestaurants.value?.status != Resource.Status.SUCCESS) fetchNearbyRestaurants() + if (mNearbyRestaurants.value?.mayLoad == false) fetchNearbyRestaurants() } fun processQr(data: String) { val requestJson = Converters.objectMapper.createObjectNode() requestJson.put("data", data) - mQrResult.addSource(mSessionRepository.newCustomerSession(requestJson)) { mQrResult.setValue(it) } + mQrResult.addSource(mSessionRepository.newCustomerSession(requestJson), mQrResult::setValue) } fun fetchSessionStatus() { - mSessionStatus.addSource(mSessionRepository.activeSessionCheck) { mSessionStatus.setValue(it) } + mSessionStatus.addSource(mSessionRepository.activeSessionCheck, mSessionStatus::setValue) } fun cancelUserWaitingDineIn() { - mCancelDineInRequest.addSource(mSessionRepository.cancelSessionJoinRequest()) { mCancelDineInRequest.setValue(it) } + mCancelDineInRequest.addSource(mSessionRepository.cancelSessionJoinRequest(), mCancelDineInRequest::setValue) + } + + fun setCityId(id: Int) { + if (mCityId.value == id) return + + mCityId.value = id + if (mCityId.value != 0) + // Fetch restaurants if not current location + fetchNearbyRestaurants() } fun fetchNearbyRestaurants() { - mNearbyRestaurants.addSource(mShopRepository.nearbyRestaurants) { mNearbyRestaurants.setValue(it) } + mNearbyRestaurants.addSource(mShopRepository.getNearbyRestaurants(cityId.value + ?: 0), mNearbyRestaurants::setValue) } } diff --git a/app/src/main/java/com/checkin/app/checkin/home/viewmodels/UserLocationViewModel.kt b/app/src/main/java/com/checkin/app/checkin/home/viewmodels/UserLocationViewModel.kt new file mode 100644 index 00000000..13390853 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/home/viewmodels/UserLocationViewModel.kt @@ -0,0 +1,49 @@ +package com.checkin.app.checkin.home.viewmodels + +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.HomeRepository +import com.checkin.app.checkin.home.model.CityLocationModel +import com.checkin.app.checkin.menu.viewmodels.BaseMenuViewModel + +class UserLocationViewModel(application: Application) : BaseMenuViewModel(application) { + val repository = HomeRepository.getInstance(application) + + private val mAllLocationsData = createNetworkLiveData>() + private val mLocationData = createNetworkLiveData>() + + val locationData: LiveData>> = mLocationData + + init { + mLocationData.addSource(mAllLocationsData) { resource -> + mLocationData.value = resource.data?.let { + Resource.cloneResource(resource, it.slice(0 until it.size.coerceAtMost(5))) + } ?: resource + } + } + + fun fetchData() { + mAllLocationsData.addSource(repository.getAllCities, mAllLocationsData::setValue) + } + + fun searchCities(query: String) = internalSearchCities(query) + + private fun internalSearchCities(query: String) { + val resourceLiveData = Transformations.map(mAllLocationsData) { input -> + val items = input.data?.filter { + it.name.contains(query, ignoreCase = true) || it.state.contains(query, ignoreCase = true) + } + if (items?.isEmpty() == true) return@map Resource.errorNotFound>("No Such City Found.") + Resource.cloneResource(input, items) + } + mLocationData.addSource(resourceLiveData, mLocationData::setValue) + } + + fun resetResults() { + mLocationData.value = mAllLocationsData.value?.data?.let { + Resource.cloneResource(mAllLocationsData.value, it.slice(0 until it.size.coerceAtMost(5))) + } ?: mAllLocationsData.value + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/manager/activities/ManagerSessionActivity.java b/app/src/main/java/com/checkin/app/checkin/manager/activities/ManagerSessionActivity.java index 3824ad84..3a0a2c8d 100644 --- a/app/src/main/java/com/checkin/app/checkin/manager/activities/ManagerSessionActivity.java +++ b/app/src/main/java/com/checkin/app/checkin/manager/activities/ManagerSessionActivity.java @@ -116,7 +116,8 @@ public void onReceive(Context context, Intent intent) { ManagerSessionActivity.this.updateSessionHost(user); break; case MANAGER_SESSION_BILL_CHANGE: - ManagerSessionActivity.this.updateBill(message.getRawData().getSessionBillTotal()); + Double total = message.getRawData().getSessionBillTotal(); + if (total != null) ManagerSessionActivity.this.updateBill(total); break; case MANAGER_SESSION_MEMBER_CHANGE: ManagerSessionActivity.this.updateMemberCount(message.getRawData().getSessionCustomerCount()); diff --git a/app/src/main/java/com/checkin/app/checkin/manager/fragments/ManagerInvoiceFragment.java b/app/src/main/java/com/checkin/app/checkin/manager/fragments/ManagerInvoiceFragment.java deleted file mode 100644 index 15911139..00000000 --- a/app/src/main/java/com/checkin/app/checkin/manager/fragments/ManagerInvoiceFragment.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.checkin.app.checkin.manager.fragments; - -import android.content.Intent; -import android.os.Bundle; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.ViewModelProviders; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.checkin.app.checkin.R; -import com.checkin.app.checkin.Shop.Private.Invoice.RestaurantSessionModel; -import com.checkin.app.checkin.Shop.Private.Invoice.ShopInvoiceDetailActivity; -import com.checkin.app.checkin.Shop.Private.Invoice.ShopInvoiceSessionAdapter; -import com.checkin.app.checkin.Shop.Private.Invoice.ShopInvoiceViewModel; -import com.checkin.app.checkin.data.resource.Resource; -import com.checkin.app.checkin.misc.fragments.BaseFragment; -import com.checkin.app.checkin.utility.Utils; - -import butterknife.BindView; - -public class ManagerInvoiceFragment extends BaseFragment implements ShopInvoiceSessionAdapter.ShopInvoiceInteraction { - - @BindView(R.id.rv_shop_invoice_sessions) - RecyclerView rvSessions; - - private ShopInvoiceSessionAdapter mAdapter; - private ShopInvoiceViewModel mViewModel; - - public ManagerInvoiceFragment() { - } - - public static ManagerInvoiceFragment newInstance() { - return new ManagerInvoiceFragment(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - setupUi(); - mViewModel = ViewModelProviders.of(requireActivity()).get(ShopInvoiceViewModel.class); - - mViewModel.getRestaurantSessions().observe(this, input -> { - if (input == null) - return; - if (input.getStatus() == Resource.Status.SUCCESS && input.getData() != null) { - mAdapter.setSessionData(input.getData()); - } else if (input.getStatus() != Resource.Status.LOADING) { - Utils.toast(requireContext(), input.getMessage()); - } - }); - } - - private void setupUi() { - rvSessions.setLayoutManager(new LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)); - mAdapter = new ShopInvoiceSessionAdapter(this); - rvSessions.setAdapter(mAdapter); - } - - @Override - protected int getRootLayout() { - return R.layout.fragment_manager_invoice; - } - - @Override - public void onClickSession(RestaurantSessionModel data) { - Intent intent = new Intent(requireContext(), ShopInvoiceDetailActivity.class); - intent.putExtra(ShopInvoiceDetailActivity.KEY_SESSION_DATA, data); - startActivity(intent); - } -} diff --git a/app/src/main/java/com/checkin/app/checkin/manager/fragments/ManagerInvoiceFragment.kt b/app/src/main/java/com/checkin/app/checkin/manager/fragments/ManagerInvoiceFragment.kt new file mode 100644 index 00000000..6d1a3488 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/manager/fragments/ManagerInvoiceFragment.kt @@ -0,0 +1,68 @@ +package com.checkin.app.checkin.manager.fragments + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import com.checkin.app.checkin.R +import com.checkin.app.checkin.Shop.Private.Invoice.RestaurantSessionModel +import com.checkin.app.checkin.Shop.Private.Invoice.ShopInvoiceDetailActivity +import com.checkin.app.checkin.Shop.Private.Invoice.ShopInvoiceSessionAdapter +import com.checkin.app.checkin.Shop.Private.Invoice.ShopInvoiceSessionAdapter.ShopInvoiceInteraction +import com.checkin.app.checkin.Shop.Private.Invoice.ShopInvoiceViewModel +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.misc.fragments.BaseFragment +import com.checkin.app.checkin.utility.Utils + +class ManagerInvoiceFragment : BaseFragment(), ShopInvoiceInteraction { + override val rootLayout = R.layout.fragment_manager_invoice + + @BindView(R.id.rv_shop_invoice_sessions) + internal lateinit var rvSessions: RecyclerView + + + private val mAdapter: ShopInvoiceSessionAdapter by lazy { + ShopInvoiceSessionAdapter(this) + } + private val mViewModel: ShopInvoiceViewModel by activityViewModels() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + initRefreshScreen(R.id.sr_manager_invoice) + setupUi() + mViewModel.restaurantSessions.observe(viewLifecycleOwner, Observer { + it?.let { input -> + handleLoadingRefresh(input) + if (input.status === Resource.Status.SUCCESS && input.data != null) { + mAdapter.setSessionData(input.data) + } + } + }) + } + + private fun setupUi() { + rvSessions.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + rvSessions.adapter = mAdapter + } + + override fun updateScreen() { + super.updateScreen() + mViewModel.filterFrom(Utils.getCurrentFormattedDateInvoice()) + mViewModel.filterTo(Utils.getCurrentFormattedDateInvoice()) + mViewModel.updateResults() + } + + override fun onClickSession(data: RestaurantSessionModel?) { + context?.let { ctx -> + Intent(ctx, ShopInvoiceDetailActivity::class.java).apply { + putExtra(ShopInvoiceDetailActivity.KEY_SESSION_DATA, data) + }.also { startActivity(it) } + } + } + + companion object { + fun newInstance() = ManagerInvoiceFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/menu/MenuRepository.kt b/app/src/main/java/com/checkin/app/checkin/menu/MenuRepository.kt index b7372223..29901f01 100644 --- a/app/src/main/java/com/checkin/app/checkin/menu/MenuRepository.kt +++ b/app/src/main/java/com/checkin/app/checkin/menu/MenuRepository.kt @@ -3,9 +3,10 @@ package com.checkin.app.checkin.menu import android.app.Application import android.content.Context import androidx.lifecycle.LiveData +import com.checkin.app.checkin.data.BaseRepository import com.checkin.app.checkin.data.Converters -import com.checkin.app.checkin.data.db.AppDatabase import com.checkin.app.checkin.data.db.ObjectBoxInstanceLiveData +import com.checkin.app.checkin.data.db.dbStore import com.checkin.app.checkin.data.network.ApiClient.Companion.getApiService import com.checkin.app.checkin.data.network.ApiResponse import com.checkin.app.checkin.data.network.RetrofitLiveData @@ -16,11 +17,10 @@ import com.checkin.app.checkin.menu.models.* import com.checkin.app.checkin.session.models.TrendingDishModel import com.checkin.app.checkin.utility.SingletonHolder import com.fasterxml.jackson.databind.node.ObjectNode -import io.objectbox.Box -class MenuRepository private constructor(context: Context) { - private val mMenuBox: Box = AppDatabase.getMenuModel(context) - private val mCartBox: Box = AppDatabase.getCartStatusModel(context) +class MenuRepository private constructor(context: Context) : BaseRepository() { + private val mMenuBox by dbStore() + private val mCartBox by dbStore() private val mWebService: WebApiService = getApiService(context) fun getAvailableMenu(shopId: Long): LiveData> { diff --git a/app/src/main/java/com/checkin/app/checkin/menu/models/CartStatusModel.kt b/app/src/main/java/com/checkin/app/checkin/menu/models/CartStatusModel.kt index 0294776a..ff41de5f 100644 --- a/app/src/main/java/com/checkin/app/checkin/menu/models/CartStatusModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/menu/models/CartStatusModel.kt @@ -17,8 +17,8 @@ data class CartStatusModel( @JsonProperty("restaurant") fun setRestaurantInfo(data: RestaurantBriefModel) { - AppDatabase.getCartStatusModel(null).attach(this) - AppDatabase.getRestaurantBriefModel(null).put(data) + AppDatabase.boxFor().attach(this) + AppDatabase.getRestaurantBriefModel().put(data) this.restaurant.target = data } diff --git a/app/src/main/java/com/checkin/app/checkin/menu/models/ItemCustomizationGroupModel.java b/app/src/main/java/com/checkin/app/checkin/menu/models/ItemCustomizationGroupModel.java index b871e813..bab48044 100644 --- a/app/src/main/java/com/checkin/app/checkin/menu/models/ItemCustomizationGroupModel.java +++ b/app/src/main/java/com/checkin/app/checkin/menu/models/ItemCustomizationGroupModel.java @@ -77,10 +77,10 @@ public List getCustomizationFields() { @JsonProperty("fields") public void setCustomizationFields(List customizationFields) { - AppDatabase.getMenuItemCustomizationGroupModel(null).attach(this); - AppDatabase.getMenuItemCustomizationFieldModel(null).remove(this.customizationFields); - AppDatabase.getMenuItemCustomizationFieldModel(null).put(customizationFields); - AppDatabase.getMenuItemCustomizationGroupModel(null).put(this); + AppDatabase.getMenuItemCustomizationGroupModel().attach(this); + AppDatabase.getMenuItemCustomizationFieldModel().remove(this.customizationFields); + AppDatabase.getMenuItemCustomizationFieldModel().put(customizationFields); + AppDatabase.getMenuItemCustomizationGroupModel().put(this); this.customizationFields.addAll(customizationFields); } diff --git a/app/src/main/java/com/checkin/app/checkin/menu/models/MenuGroupModel.java b/app/src/main/java/com/checkin/app/checkin/menu/models/MenuGroupModel.java index 6ae648df..7b3f95be 100644 --- a/app/src/main/java/com/checkin/app/checkin/menu/models/MenuGroupModel.java +++ b/app/src/main/java/com/checkin/app/checkin/menu/models/MenuGroupModel.java @@ -61,11 +61,11 @@ public List getItems() { public void setItems(List items) { vegItems = null; nonVegItems = null; - AppDatabase.getMenuGroupModel(null).attach(this); - AppDatabase.getMenuItemModel(null).remove(this.items); - AppDatabase.getMenuItemModel(null).put(items); + AppDatabase.getMenuGroupModel().attach(this); + AppDatabase.getMenuItemModel().remove(this.items); + AppDatabase.getMenuItemModel().put(items); this.items.addAll(items); - AppDatabase.getMenuGroupModel(null).put(this); + AppDatabase.getMenuGroupModel().put(this); } public void setCacheItems(List items) { diff --git a/app/src/main/java/com/checkin/app/checkin/menu/models/MenuItemModel.java b/app/src/main/java/com/checkin/app/checkin/menu/models/MenuItemModel.java index 534f54a6..d2d5bb9a 100644 --- a/app/src/main/java/com/checkin/app/checkin/menu/models/MenuItemModel.java +++ b/app/src/main/java/com/checkin/app/checkin/menu/models/MenuItemModel.java @@ -177,11 +177,11 @@ public ToMany getCustomizationGroups() { @JsonProperty("customizations") public void setCustomizationGroups(List customizationGroups) { - AppDatabase.getMenuItemModel(null).attach(this); - AppDatabase.getMenuItemCustomizationGroupModel(null).remove(this.customizationGroups); - AppDatabase.getMenuItemCustomizationGroupModel(null).put(customizationGroups); + AppDatabase.getMenuItemModel().attach(this); + AppDatabase.getMenuItemCustomizationGroupModel().remove(this.customizationGroups); + AppDatabase.getMenuItemCustomizationGroupModel().put(customizationGroups); this.customizationGroups.addAll(customizationGroups); - AppDatabase.getMenuItemModel(null).put(this); + AppDatabase.getMenuItemModel().put(this); } public List getAvailableMeals() { diff --git a/app/src/main/java/com/checkin/app/checkin/menu/models/MenuModel.java b/app/src/main/java/com/checkin/app/checkin/menu/models/MenuModel.java index 20317ccf..f6b87b84 100644 --- a/app/src/main/java/com/checkin/app/checkin/menu/models/MenuModel.java +++ b/app/src/main/java/com/checkin/app/checkin/menu/models/MenuModel.java @@ -43,11 +43,11 @@ public List getGroups() { @JsonProperty("groups") public void setGroups(List groups) { - AppDatabase.getMenuModel(null).attach(this); - AppDatabase.getMenuGroupModel(null).remove(this.groups); - AppDatabase.getMenuGroupModel(null).put(groups); + AppDatabase.getMenuModel().attach(this); + AppDatabase.getMenuGroupModel().remove(this.groups); + AppDatabase.getMenuGroupModel().put(groups); this.groups.addAll(groups); - AppDatabase.getMenuModel(null).put(this); + AppDatabase.getMenuModel().put(this); } public long getPk() { diff --git a/app/src/main/java/com/checkin/app/checkin/menu/viewmodels/ScheduledCartViewModel.kt b/app/src/main/java/com/checkin/app/checkin/menu/viewmodels/ScheduledCartViewModel.kt index 5f3e3021..5d8504ca 100644 --- a/app/src/main/java/com/checkin/app/checkin/menu/viewmodels/ScheduledCartViewModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/menu/viewmodels/ScheduledCartViewModel.kt @@ -59,7 +59,7 @@ class ScheduledCartViewModel(application: Application) : BaseCartViewModel(appli it?.let { resource -> if (resource.status == Resource.Status.SUCCESS && resource.data != null) { mOrderedItems.value = resource.data.orderedItems.map { - val menuItem = AppDatabase.getMenuItemModel(null).get(it.item.pk) + val menuItem = AppDatabase.getMenuItemModel().get(it.item.pk) ?: return@addSource OrderedItemModel( it.longPk, menuItem, it.cost, it.quantity, it.typeIndex, diff --git a/app/src/main/java/com/checkin/app/checkin/misc/BillHolder.kt b/app/src/main/java/com/checkin/app/checkin/misc/BillHolder.kt index 7097e543..c0f1a576 100644 --- a/app/src/main/java/com/checkin/app/checkin/misc/BillHolder.kt +++ b/app/src/main/java/com/checkin/app/checkin/misc/BillHolder.kt @@ -3,14 +3,22 @@ package com.checkin.app.checkin.misc import android.content.Context import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import android.widget.TextView +import androidx.core.content.ContextCompat import butterknife.BindView import butterknife.BindViews import butterknife.ButterKnife import com.checkin.app.checkin.R import com.checkin.app.checkin.session.models.SessionBillModel +import com.checkin.app.checkin.session.models.TaxDetailModel import com.checkin.app.checkin.utility.Utils +import com.checkin.app.checkin.utility.toCurrency import com.facebook.shimmer.ShimmerFrameLayout +import com.skydoves.balloon.ArrowOrientation +import com.skydoves.balloon.BalloonAnimation +import com.skydoves.balloon.createBalloon +import kotlinx.android.synthetic.main.tooltip_tax_details.view.* class BillHolder(itemView: View) { @BindView(R.id.shimmer_invoice_holder) @@ -39,6 +47,8 @@ class BillHolder(itemView: View) { internal lateinit var containerInvoicePromo: ViewGroup @BindView(R.id.container_invoice_discount) internal lateinit var containerInvoiceDiscount: ViewGroup + @BindView(R.id.container_invoice_tax_details) + internal lateinit var llTaxDetails: LinearLayout @BindViews(R.id.shimmer_bill_subtotal, R.id.shimmer_bill_charge, R.id.shimmer_bill_discount, R.id.shimmer_bill_tax, R.id.shimmer_bill_brownie, R.id.shimmer_bill_promo, R.id.shimmer_bill_tip) internal lateinit var shimmerViews: List<@JvmSuppressWildcards View> @@ -48,11 +58,33 @@ class BillHolder(itemView: View) { private val mContext: Context + private val balloonTaxDetails by lazy { + createBalloon(itemView.context) { + layout = R.layout.tooltip_tax_details + arrowOrientation = ArrowOrientation.BOTTOM + backgroundColor = ContextCompat.getColor(itemView.context, R.color.pinkish_grey) + balloonAnimation = BalloonAnimation.FADE + padding = 1 + setBackgroundDrawableResource(R.drawable.layout_border_grey) + dismissWhenClicked = true + dismissWhenShowAgain = true + dismissWhenTouchOutside = true + } + } + init { ButterKnife.bind(this, itemView) mContext = itemView.context containerInvoicePromo.visibility = View.GONE containerInvoiceDiscount.visibility = View.GONE + + llTaxDetails.setOnClickListener { + if (balloonTaxDetails.isShowing) { + balloonTaxDetails.dismiss() + } else { + balloonTaxDetails.showAlignTop(it) + } + } } fun bind(bill: SessionBillModel) { @@ -64,6 +96,7 @@ class BillHolder(itemView: View) { if (it != null) tvInvoiceTax.text = Utils.formatCurrencyAmount(mContext, bill.tax) else containerInvoiceTax.visibility = View.GONE } + setupTaxDetails(bill.taxDetail) // Charges bill.charges.let { if (it != null && it > 0) { @@ -104,4 +137,25 @@ class BillHolder(itemView: View) { shimmerViews.forEach { it.visibility = View.VISIBLE } shimmerHolder.startShimmer() } + + private fun setupTaxDetails(taxDetailModel: TaxDetailModel) { + balloonTaxDetails.getContentView().apply { + tv_tax_cgst.text = taxDetailModel.cgst.toCurrency(context) + tv_tax_sgst.text = taxDetailModel.sgst.toCurrency(context) + if (taxDetailModel.igst > 0) { + tv_tax_igst.visibility = View.VISIBLE + tv_tax_igst_heading.visibility = View.VISIBLE + tv_tax_igst.text = taxDetailModel.igst.toCurrency(context) + } else { + tv_tax_igst.visibility = View.GONE + tv_tax_igst_heading.visibility = View.GONE + } + } + } + + fun resetUi() { + if (balloonTaxDetails.isShowing) { + balloonTaxDetails.dismiss() + } + } } diff --git a/app/src/main/java/com/checkin/app/checkin/misc/activities/BaseActivity.kt b/app/src/main/java/com/checkin/app/checkin/misc/activities/BaseActivity.kt index 2b3211d2..bfd18f60 100644 --- a/app/src/main/java/com/checkin/app/checkin/misc/activities/BaseActivity.kt +++ b/app/src/main/java/com/checkin/app/checkin/misc/activities/BaseActivity.kt @@ -10,6 +10,10 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.checkin.app.checkin.data.resource.Resource import com.checkin.app.checkin.data.resource.Resource.Status import com.checkin.app.checkin.misc.fragments.DataStatusFragment +import com.checkin.app.checkin.utility.coroutineLifecycleScope +import com.checkin.app.checkin.utility.toast +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @SuppressLint("Registered") open class BaseActivity : AppCompatActivity() { @@ -22,6 +26,8 @@ open class BaseActivity : AppCompatActivity() { private var swipeRefreshLayout: SwipeRefreshLayout? = null private var progressBar: ProgressBar? = null + private var doublePressedBackToExit: Boolean = false + fun init(@IdRes groupId: Int, isNetworkRequired: Boolean) { if (mFragment != null) { Log.e(TAG, "init called with existing fragment!") @@ -96,6 +102,19 @@ open class BaseActivity : AppCompatActivity() { } } + protected fun onDoubleBackPressed(): Boolean { + return if (doublePressedBackToExit) true + else { + doublePressedBackToExit = true + toast("Press back again to exit") + coroutineLifecycleScope.launch { + delay(2000) + doublePressedBackToExit = false + } + false + } + } + companion object { private val TAG = BaseActivity::class.java.simpleName } diff --git a/app/src/main/java/com/checkin/app/checkin/misc/fragments/BaseOrderDetailFragment.kt b/app/src/main/java/com/checkin/app/checkin/misc/fragments/BaseOrderDetailFragment.kt new file mode 100644 index 00000000..37cefc70 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/misc/fragments/BaseOrderDetailFragment.kt @@ -0,0 +1,26 @@ +package com.checkin.app.checkin.misc.fragments + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import butterknife.BindView +import com.airbnb.epoxy.EpoxyRecyclerView +import com.checkin.app.checkin.R +import com.checkin.app.checkin.misc.BillHolder + +abstract class BaseOrderDetailFragment : BaseFragment() { + override val rootLayout: Int = R.layout.fragment_user_scheduled_detail_orders_common + + @BindView(R.id.epoxy_rv_user_scheduled_orders) + internal lateinit var epoxyRvOrders: EpoxyRecyclerView + @BindView(R.id.tv_user_scheduled_session_total) + internal lateinit var tvTotal: TextView + @BindView(R.id.tv_user_scheduled_remarks) + internal lateinit var tvRemarks: TextView + + lateinit var billHolder: BillHolder + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + billHolder = BillHolder(view) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/misc/holders/TextSectionModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/misc/holders/TextSectionModelHolder.kt index 21169a2f..5789dc8c 100644 --- a/app/src/main/java/com/checkin/app/checkin/misc/holders/TextSectionModelHolder.kt +++ b/app/src/main/java/com/checkin/app/checkin/misc/holders/TextSectionModelHolder.kt @@ -8,7 +8,7 @@ import com.airbnb.epoxy.EpoxyModelWithHolder import com.checkin.app.checkin.R import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder -@EpoxyModelClass(layout = R.layout.item_section_heading) +@EpoxyModelClass(layout = R.layout.item_section_heading, useLayoutOverloads = true) abstract class TextSectionModelHolder : EpoxyModelWithHolder() { @EpoxyAttribute lateinit var heading: String diff --git a/app/src/main/java/com/checkin/app/checkin/payment/PaymentRepository.kt b/app/src/main/java/com/checkin/app/checkin/payment/PaymentRepository.kt new file mode 100644 index 00000000..559d0218 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/PaymentRepository.kt @@ -0,0 +1,139 @@ +package com.checkin.app.checkin.payment + +import android.app.Application +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import com.checkin.app.checkin.R +import com.checkin.app.checkin.data.BaseRepository +import com.checkin.app.checkin.data.db.dbStore +import com.checkin.app.checkin.data.network.ApiClient +import com.checkin.app.checkin.data.network.ApiResponse +import com.checkin.app.checkin.data.network.RetrofitLiveData +import com.checkin.app.checkin.data.network.WebApiService +import com.checkin.app.checkin.data.resource.NetworkBoundResource +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.payment.models.* +import com.checkin.app.checkin.payment.services.PaymentServiceLocator +import com.checkin.app.checkin.utility.SingletonHolder +import com.checkin.app.checkin.utility.pass +import com.fasterxml.jackson.databind.node.ObjectNode +import io.objectbox.android.ObjectBoxLiveData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.* + +class PaymentRepository private constructor(val context: Context) : BaseRepository() { + private val webService: WebApiService = ApiClient.getApiService(context) + private val boxUpiCollectOption by dbStore() + private val boxUpiPushOption by dbStore() + private val boxCardOption by dbStore() + private val paymentService by lazy { + PaymentServiceLocator.getInstance().paymentService + } + + private lateinit var cachedPaymentMethods: LiveData> + + fun getPaymentMethods(): LiveData> { + if (!::cachedPaymentMethods.isInitialized) + cachedPaymentMethods = liveData { + kotlin.runCatching { + paymentService.getPaymentMethods() + }.onFailure { emit(Resource.error(it)) }.onSuccess { emit(Resource.success(it)) } + } + return cachedPaymentMethods + } + + val netBankingOptions: LiveData>> + get() = liveData { + kotlin.runCatching { + paymentService.getNetBankingOptions().map { + val iconRes = when (it.bankCode) { + "SBIN" -> R.drawable.ic_payment_netbanking_sbi + "UTIB" -> R.drawable.ic_payment_netbanking_axis + "KKBK" -> R.drawable.ic_payment_netbanking_kotak + else -> 0 + } + it.copy(iconRes = iconRes) + } + }.onFailure { emit(Resource.error(it)) }.onSuccess { emit(Resource.success(it)) } + } + + val UPIAppOptions: LiveData>> + get() = liveData { + kotlin.runCatching { + paymentService.getUPIAppOptions(context) + }.onFailure { emit(Resource.error(it)) }.onSuccess { emit(Resource.success(it)) } + } + + val UPIIdOptions: LiveData>> + get() = object : NetworkBoundResource, List>() { + override fun shouldFetch(data: List?): Boolean = false + + override fun shouldUseLocalDb(): Boolean = true + + override fun createCall(): LiveData>>? = null + + override fun loadFromDb(): LiveData>? = ObjectBoxLiveData(boxUpiCollectOption.query().build()) + }.asLiveData + + val cardOptions: LiveData>> + get() = object : NetworkBoundResource, List>() { + override fun shouldFetch(data: List?): Boolean = false + + override fun shouldUseLocalDb(): Boolean = true + + override fun createCall(): LiveData>>? = null + + override fun loadFromDb(): LiveData>? = ObjectBoxLiveData(boxCardOption.query().build()) + }.asLiveData + + fun createNewTransaction(sessionId: Long): LiveData> { + return object : NetworkBoundResource() { + override fun createCall(): LiveData>? { + return RetrofitLiveData(webService.postNewRazorpayTransaction(sessionId)) + } + }.asLiveData + } + + fun postTransactionResponse(data: TransactionResponseModel): LiveData> { + return object : NetworkBoundResource() { + override fun createCall(): LiveData>? { + return RetrofitLiveData(webService.postRazorpayCallback(data as RazorpayTxnResponseModel)) + } + }.asLiveData + } + + suspend fun savePaymentOption(paymentOptionModel: PaymentOptionModel) = withContext(Dispatchers.IO) { + val currentTime = Calendar.getInstance().time + when (paymentOptionModel) { + is UPIPushPaymentOptionModel -> { + val data = boxUpiPushOption.getByAppName(paymentOptionModel.appName) + ?: paymentOptionModel + data.lastUsed = currentTime + boxUpiPushOption.put(data) + } + is UPICollectPaymentOptionModel -> { + val data = boxUpiCollectOption.getByVpa(paymentOptionModel.vpa) + ?: paymentOptionModel + data.lastUsed = currentTime + boxUpiCollectOption.put(data) + } + is CardPaymentOptionModel -> { + val data = boxCardOption.getByCardNumber(paymentOptionModel.cardNumber) + ?: paymentOptionModel + data.lastUsed = currentTime + boxCardOption.put(data) + } + else -> pass + } + } + + suspend fun isValidVpa(vpa: String) = paymentService.isValidVpa(vpa) + + suspend fun isValidCard(cardNumber: String) = paymentService.isValidCardNumber(cardNumber) + + suspend fun getCardNetwork(cardNumber: String) = paymentService.getCardNetwork(cardNumber) + + companion object : SingletonHolder({ PaymentRepository(it.applicationContext) }) +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/PaymentViewModel.kt b/app/src/main/java/com/checkin/app/checkin/payment/PaymentViewModel.kt new file mode 100644 index 00000000..f60ae7a8 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/PaymentViewModel.kt @@ -0,0 +1,145 @@ +package com.checkin.app.checkin.payment + +import android.app.Application +import androidx.lifecycle.* +import com.checkin.app.checkin.data.BaseViewModel +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.payment.models.* +import com.fasterxml.jackson.databind.node.ObjectNode +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +@UseExperimental(ExperimentalCoroutinesApi::class) +class PaymentViewModel(application: Application) : BaseViewModel(application) { + private val repository = PaymentRepository.getInstance(application) + + lateinit var transactionData: NewTransactionModel + private set + + private val payRequestChannel = ConflatedBroadcastChannel() + private val mPaymentMethods = createNetworkLiveData() + private val mUPIPushOptions = createNetworkLiveData>() + private val mUPICollectOptions = createNetworkLiveData>() + private val mNetBankingOptions = createNetworkLiveData>() + private val mCardOptions = createNetworkLiveData>() + private val mAllOptions = MutableLiveData>() + private val mPayCallback = createNetworkLiveData() + + private val paymentCallback = mPayCallback.asFlow() + + val upiOptions: LiveData>> = MediatorLiveData>>().apply { + addSource(mUPIPushOptions) { + it?.let { listResource -> + if (listResource.status == Resource.Status.SUCCESS && listResource.data != null) + value = value?.data?.filterIsInstance()?.run { Resource.success(listResource.data + this) } + ?: listResource + } + } + addSource(mUPICollectOptions) { + it?.let { listResource -> + if (listResource.status == Resource.Status.SUCCESS && listResource.data != null) + value = value?.data?.filterIsInstance()?.run { Resource.success(this + listResource.data) } + ?: listResource + } + } + } + val netBankingOptions: LiveData>> = Transformations.map(mNetBankingOptions) { + it?.data?.filter { it.iconRes != 0 }?.let { data -> + Resource.cloneResource(it, data) + } ?: it + } + val cardOptions: LiveData>> = mCardOptions + val recentlyUsedOptions: LiveData> = Transformations.map(mAllOptions) { + it?.distinct()?.sortedByDescending { it.lastUsed } + } + + init { + mNetBankingOptions.addSource(mPaymentMethods) { + if (it?.status == Resource.Status.SUCCESS) fetchNetBankingOptions() + } + } + + fun init(transactionModel: NewTransactionModel) { + transactionData = transactionModel + } + + fun fetchUPIOptions() { + mUPIPushOptions.addSource(repository.UPIAppOptions) { + mUPIPushOptions.value = it + it?.data?.also { + val savedList = it.filter { it.lastUsed != null } + mAllOptions.value = mAllOptions.value?.run { this + savedList } ?: savedList + } + } + mUPICollectOptions.addSource(repository.UPIIdOptions) { + mUPICollectOptions.value = it + it?.data?.also { + mAllOptions.value = mAllOptions.value?.run { this + it } ?: it + } + } + } + + private fun fetchNetBankingOptions() { + mNetBankingOptions.addSource(repository.netBankingOptions, mNetBankingOptions::setValue) + } + + fun fetchPaymentMethods() { + if (mPaymentMethods.value?.status == Resource.Status.SUCCESS) return + + mPaymentMethods.addSource(repository.getPaymentMethods(), mPaymentMethods::setValue) + } + + fun fetchCardOptions() { + mCardOptions.addSource(repository.cardOptions) { + mCardOptions.value = it + it?.data?.also { + mAllOptions.value = mAllOptions.value?.run { this + it } ?: it + } + } + } + + fun payUsing(paymentOptionModel: PaymentOptionModel, saveOption: Boolean = true) { + payRequestChannel.offer(PaymentRequest(paymentOptionModel, saveOption)) + } + + fun onRequestPay(action: (PaymentOptionModel) -> Unit) = viewModelScope.launch { + payRequestChannel.openSubscription().consumeEach { action(it.optionModel) } + } + + fun onPaymentCallback(action: (Resource) -> Unit) = viewModelScope.launch { + paymentCallback.collect { if (it != null) action(it) } + } + + fun callPaymentCallback(data: TransactionResponseModel) { + mPayCallback.addSource(repository.postTransactionResponse(data), mPayCallback::setValue) + } + + fun savePaymentOption() { + val lastRequest = payRequestChannel.valueOrNull + if (lastRequest?.saveOption == true) { + viewModelScope.launch { + repository.savePaymentOption(lastRequest.optionModel) + } + } + } + + suspend fun isValidVpa(vpa: String): Boolean = repository.isValidVpa(vpa) + + fun isValidCard(cardNumber: String): Boolean = runBlocking { + repository.isValidCard(cardNumber) + } + + fun getCardNetwork(cardNumber: String): CardPaymentOptionModel.CARD_PROVIDER? = runBlocking { + repository.getCardNetwork(cardNumber) + } + + override fun updateResults() { + + } +} + +class PaymentRequest(val optionModel: PaymentOptionModel, val saveOption: Boolean) \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/activities/PaymentActivity.kt b/app/src/main/java/com/checkin/app/checkin/payment/activities/PaymentActivity.kt new file mode 100644 index 00000000..d483b180 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/activities/PaymentActivity.kt @@ -0,0 +1,132 @@ +package com.checkin.app.checkin.payment.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.webkit.WebView +import android.widget.TextView +import androidx.activity.viewModels +import androidx.navigation.findNavController +import butterknife.BindView +import butterknife.ButterKnife +import com.checkin.app.checkin.R +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.misc.BlockingNetworkViewModel +import com.checkin.app.checkin.misc.activities.BaseActivity +import com.checkin.app.checkin.misc.fragments.NetworkBlockingFragment +import com.checkin.app.checkin.payment.PaymentViewModel +import com.checkin.app.checkin.payment.listeners.TransactionListener +import com.checkin.app.checkin.payment.models.* +import com.checkin.app.checkin.payment.services.TransactionException +import com.checkin.app.checkin.payment.services.paymentLocator +import com.checkin.app.checkin.utility.inTransaction +import com.checkin.app.checkin.utility.toCurrency +import com.checkin.app.checkin.utility.toast + +class PaymentActivity : BaseActivity(), TransactionListener { + @BindView(R.id.tv_payment_toolbar_amount) + internal lateinit var tvAmountToolbar: TextView + @BindView(R.id.wv_payment_process) + internal lateinit var wvPaymentProcess: WebView + + private val viewModel: PaymentViewModel by viewModels() + private val txnService by lazy { paymentLocator.getTransactionService(this) } + private val networkViewModel: BlockingNetworkViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_payment) + ButterKnife.bind(this) + + setSupportActionBar(findViewById(R.id.toolbar_payment)) + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + val txnData = intent.getSerializableExtra(KEY_TRANSACTION_MODEL) as NewTransactionModel + viewModel.init(txnData) + + tvAmountToolbar.text = txnData.amount.toCurrency(this) + txnService.initialize(wvPaymentProcess, txnData) + findNavController(R.id.nav_host_payment).setGraph(R.navigation.nav_payment) + + setupObserver() + supportFragmentManager.inTransaction { + add(R.id.frg_container_activity, NetworkBlockingFragment.withBlockingLoader(), NetworkBlockingFragment.FRAGMENT_TAG) + } + } + + private fun setupObserver() { + viewModel.fetchPaymentMethods() + viewModel.onRequestPay { + wvPaymentProcess.visibility = View.VISIBLE + when (it) { + is UPICollectPaymentOptionModel -> txnService.payByUPICollect(it) + is UPIPushPaymentOptionModel -> txnService.payByUPIPush(it) + is NetBankingPaymentOptionModel -> txnService.payByNetBanking(it) + is CardPaymentOptionModel -> txnService.payByCard(it) + } + } + viewModel.onPaymentCallback { + networkViewModel.updateStatus(it) + if (it.status == Resource.Status.SUCCESS) { + setResult(RESULT_PAID) + finish() + } else if (it.status != Resource.Status.LOADING) { + toast(it.message) + } + } + } + + override fun onTransactionResponse(data: TransactionResponseModel) { + toast("Success Payment!") + viewModel.savePaymentOption() + viewModel.callPaymentCallback(data) + resetWebView() + } + + override fun onTransactionCancel(msg: String?) { + resetWebView() + } + + override fun onTransactionError(error: TransactionException) { + toast(error.localizedMessage) + resetWebView() + } + + private fun resetWebView() { + wvPaymentProcess.visibility = View.GONE + } + + override fun onBackPressed() { + if (wvPaymentProcess.visibility != View.VISIBLE || onDoubleBackPressed()) { + setResult(RESULT_CANCELED) + super.onBackPressed() + } + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + kotlin.runCatching { + paymentLocator.paymentProvider.onActivityResult(requestCode, resultCode, data) + } + } + + companion object { + const val RESULT_CANCELED = 0 + const val RESULT_PAID = 1 + + const val KEY_TRANSACTION_MODEL = "payment.transaction" + + fun withTransactionIntent(context: Context, txnModel: NewTransactionModel) = Intent(context, PaymentActivity::class.java).apply { + putExtra(KEY_TRANSACTION_MODEL, txnModel) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/controllers/PaymentOptionsController.kt b/app/src/main/java/com/checkin/app/checkin/payment/controllers/PaymentOptionsController.kt new file mode 100644 index 00000000..b8ce5d07 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/controllers/PaymentOptionsController.kt @@ -0,0 +1,139 @@ +package com.checkin.app.checkin.payment.controllers + +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyController +import com.checkin.app.checkin.misc.holders.textSectionModelHolder +import com.checkin.app.checkin.payment.holders.PaymentNetBankingModelHolder_ +import com.checkin.app.checkin.payment.holders.PaymentOptionInteraction +import com.checkin.app.checkin.payment.holders.paymentAddOptionModelHolder +import com.checkin.app.checkin.payment.holders.paymentOptionModelHolder +import com.checkin.app.checkin.payment.listeners.PaymentOptionSelectListener +import com.checkin.app.checkin.payment.models.* +import com.checkin.app.checkin.utility.carousel +import com.checkin.app.checkin.utility.isNotEmpty + +class PaymentOptionsController(val listener: PaymentOptionSelectListener) : BaseEpoxyController(), PaymentOptionInteraction { + private var selectedOptionID: Long = 0 + set(value) { + field = value + requestModelBuild() + } + + var lastUsedOptions: List? = null + set(value) { + field = value + requestModelBuild() + } + + var cardOptions: List? = null + set(value) { + field = value + requestModelBuild() + } + + var upiOptions: List? = null + set(value) { + field = value + requestModelBuild() + } + + var netBankingOptions: List? = null + set(value) { + field = value + requestModelBuild() + } + + override fun buildModels() { + // SECTION: Recently Used + lastUsedOptions.takeIf { it.isNotEmpty() }?.also { + textSectionModelHolder { + withBackgroundLayout() + .id("recently used") + .heading("Recently Used") + } + it.forEachIndexed { index, data -> + paymentOptionModelHolder { + id("recently used", index.toLong()) + optionData(data) + selectedId(selectedOptionID) + selectListener(this@PaymentOptionsController) + listener(this@PaymentOptionsController.listener) + } + } + } + + // SECTION: Cards + textSectionModelHolder { + withBackgroundLayout() + .id("card") + .heading("Debit/Credit Cards") + } + cardOptions.takeIf { it.isNotEmpty() }?.also { + it.forEachIndexed { index, data -> + paymentOptionModelHolder { + id("card", index.toLong()) + optionData(data) + selectedId(selectedOptionID) + selectListener(this@PaymentOptionsController) + listener(this@PaymentOptionsController.listener) + } + } + } + paymentAddOptionModelHolder { + withCardLayout() + .id("card add") + .content("Add Card") + .optionType(PAYMENT_TYPE.CARD) + .listener(this@PaymentOptionsController.listener) + } + + // SECTION: UPI + textSectionModelHolder { + withBackgroundLayout() + .id("upi") + .heading("UPI Options") + } + upiOptions.takeIf { it.isNotEmpty() }?.also { + it.forEachIndexed { index, data -> + paymentOptionModelHolder { + id("upi ${if (data is UPIPushPaymentOptionModel) "app" else "id"}", index.toLong()) + optionData(data) + selectedId(selectedOptionID) + selectListener(this@PaymentOptionsController) + listener(this@PaymentOptionsController.listener) + } + } + } + paymentAddOptionModelHolder { + id("upi id add") + content("Add UPI ID") + optionType(PAYMENT_TYPE.UPI) + listener(this@PaymentOptionsController.listener) + } + + // SECTION: NetBanking + netBankingOptions.takeIf { it.isNotEmpty() }?.also { + val models = it.mapIndexed { index, data -> + PaymentNetBankingModelHolder_().apply { + id("netbanking", index.toLong()) + data(data) + listener(this@PaymentOptionsController.listener) + } + } + carousel { + id("netbanking") + models(models) + } + } + paymentAddOptionModelHolder { + id("netbanking add") + content("Other Bank") + optionType(PAYMENT_TYPE.NET_BANKING) + listener(this@PaymentOptionsController.listener) + } + } + + override fun onSelectOption(id: Long) { + if (selectedOptionID != id) + selectedOptionID = id + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionAddCardFragment.kt b/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionAddCardFragment.kt new file mode 100644 index 00000000..77afb27b --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionAddCardFragment.kt @@ -0,0 +1,172 @@ +package com.checkin.app.checkin.payment.fragments + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.Button +import android.widget.CheckBox +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.activityViewModels +import butterknife.BindView +import butterknife.OnClick +import com.checkin.app.checkin.R +import com.checkin.app.checkin.misc.fragments.BaseBottomSheetFragment +import com.checkin.app.checkin.payment.PaymentViewModel +import com.checkin.app.checkin.payment.models.CardPaymentOptionModel +import com.checkin.app.checkin.utility.addError +import com.checkin.app.checkin.utility.isValidForm +import com.checkin.app.checkin.utility.validateField +import com.checkin.app.checkin.utility.validateForm +import com.google.android.material.textfield.TextInputEditText +import com.redmadrobot.inputmask.MaskedTextChangedListener +import com.redmadrobot.inputmask.helper.AffinityCalculationStrategy + +class PaymentOptionAddCardFragment : BaseBottomSheetFragment() { + override val rootLayout = R.layout.fragment_payment_option_add_card + + @BindView(R.id.tet_payment_option_card_name) + internal lateinit var tetCardName: TextInputEditText + @BindView(R.id.tet_payment_option_card_number) + internal lateinit var tetCardNumber: TextInputEditText + @BindView(R.id.tet_payment_option_card_expiry) + internal lateinit var tetExpiry: TextInputEditText + @BindView(R.id.tet_payment_option_card_cvv) + internal lateinit var tetCvv: TextInputEditText + @BindView(R.id.cb_payment_option_save_card) + internal lateinit var cbSaveCard: CheckBox + @BindView(R.id.btn_payment_option_pay_card) + internal lateinit var btnPay: Button + + private val maskCardNumber by lazy { + MaskedTextChangedListener( + field = tetCardNumber, primaryFormat = "[0000] [0000] [0000] [0000]", + affineFormats = listOf("[0000] [000000] [00000]"), + affinityCalculationStrategy = AffinityCalculationStrategy.CAPACITY, + autoskip = true, + valueListener = object : MaskedTextChangedListener.ValueListener { + override fun onTextChanged(maskFilled: Boolean, extractedValue: String, formattedValue: String) { + validateCardNumber() + if (tetCardNumber.error == null && extractedValue.length >= 6) { + val cardNetwork = viewModel.getCardNetwork(extractedValue) + tetCardNumber.tag = cardNetwork + tetCardNumber.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, cardNetwork?.drawable + ?: 0, 0) + } else tetCardNumber.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) + } + } + ) + } + private val maskCardExpiry by lazy { + MaskedTextChangedListener( + field = tetExpiry, primaryFormat = "[00]{/}[00]", autoskip = true, + valueListener = object : MaskedTextChangedListener.ValueListener { + override fun onTextChanged(maskFilled: Boolean, extractedValue: String, formattedValue: String) { + validateCardExpiry() + } + } + ) + } + + private val cardNumber: String + get() = tetCardNumber.text!!.replace("\\s".toRegex(), "") + + private val viewModel: PaymentViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // Run validations initially to fill errors + validateData() + + setupInputMasks() + } + + private fun setupInputMasks() { + // FIXME: An hack to fix keyboard not opening the first time on focusing the below edittext's + tetCardNumber.hint = "" + tetExpiry.hint = "" + + tetCardNumber.addTextChangedListener(maskCardNumber) + tetCardNumber.setOnFocusChangeListener { v, hasFocus -> + tetCardNumber.hint = if (hasFocus) maskCardNumber.placeholder() else "" + maskCardNumber.onFocusChange(v, hasFocus) + btnPay.validateField(tetCardNumber) + } + tetExpiry.addTextChangedListener(maskCardExpiry) + tetExpiry.setOnFocusChangeListener { v, hasFocus -> + tetExpiry.hint = if (hasFocus) maskCardExpiry.placeholder() else "" + maskCardExpiry.onFocusChange(v, hasFocus) + btnPay.validateField(tetExpiry) + } + tetCardName.addTextChangedListener { validateCardName() } + tetCvv.addTextChangedListener { validateCvv() } + tetCvv.setOnEditorActionListener { _, actionId, _ -> + var handled = false + if (actionId == EditorInfo.IME_ACTION_DONE) { + btnPay.performClick() + handled = true + } + return@setOnEditorActionListener handled + } + } + + private fun validateCardNumber() { + val errCard = cardNumber.let { + when { + it.length !in 15..16 -> "Input 15/16 digit Card Number" + !viewModel.isValidCard(it) -> "Entered Card is Invalid" + else -> null + } + } + btnPay.addError(view = tetCardNumber, msg = errCard) + updateButtonState() + } + + private fun validateCardExpiry() { + val errExpiry = tetExpiry.text?.split('/')?.let { + when { + it.size < 2 -> "Input Expiry Year" + it[0].toInt() !in 1..12 -> "Input correct Expiry Month" + else -> null + } + } + btnPay.addError(view = tetExpiry, msg = errExpiry) + updateButtonState() + } + + private fun validateCardName() { + val errName = if (tetCardName.text?.isBlank() == true) "Input Name on Card" else null + btnPay.addError(view = tetCardName, msg = errName) + updateButtonState() + } + + private fun validateCvv() { + val errCvv = if (tetCvv.text?.length != 3) "Input the CVV written on back of card" else null + btnPay.addError(view = tetCvv, msg = errCvv) + updateButtonState() + } + + private fun updateButtonState() { + btnPay.isActivated = btnPay.isValidForm + } + + private fun validateData() { + validateCardNumber() + validateCvv() + validateCardName() + validateCardExpiry() + updateButtonState() + } + + @OnClick(R.id.btn_payment_option_pay_card) + fun onPayClick() { + if (!validateForm(btnPay)) return + val name = tetCardName.text.toString() + val number = cardNumber + val expirySplit = tetExpiry.text!!.split('/') + val cvv = tetCvv.text.toString() + val network = (tetCardNumber.tag as? CardPaymentOptionModel.CARD_PROVIDER) + ?: viewModel.getCardNetwork(number) + val data = CardPaymentOptionModel(name, number, expirySplit[0], expirySplit[1], cvv, channel = network) + viewModel.payUsing(data, saveOption = cbSaveCard.isChecked) + dismiss() + } +} diff --git a/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionAddUpiFragment.kt b/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionAddUpiFragment.kt new file mode 100644 index 00000000..3205adc8 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionAddUpiFragment.kt @@ -0,0 +1,69 @@ +package com.checkin.app.checkin.payment.fragments + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.Button +import android.widget.CheckBox +import android.widget.EditText +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.activityViewModels +import butterknife.BindView +import butterknife.OnClick +import com.checkin.app.checkin.R +import com.checkin.app.checkin.misc.fragments.BaseBottomSheetFragment +import com.checkin.app.checkin.payment.PaymentViewModel +import com.checkin.app.checkin.payment.models.UPICollectPaymentOptionModel +import com.checkin.app.checkin.utility.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +class PaymentOptionAddUpiFragment : BaseBottomSheetFragment() { + override val rootLayout = R.layout.fragment_payment_option_add_upi + + @BindView(R.id.et_payment_option_upi_vpa) + internal lateinit var etVpa: EditText + @BindView(R.id.cb_payment_option_save_upi) + internal lateinit var cbSaveUpi: CheckBox + @BindView(R.id.btn_payment_option_pay_upi) + internal lateinit var btnPay: Button + + private val viewModel: PaymentViewModel by activityViewModels() + private var validationJob: Job? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + btnPay.isActivated = false + etVpa.addTextChangedListener { + validationJob?.also { + it.cancel("Enqueued new job") + validationJob = null + } + validationJob = coroutineLifecycleScope.launch { + var errMsg = if (it != null && it.isNotBlank() && it.split('@').size == 2) null else "Input correct UPI ID" + if (errMsg == null) + errMsg = if (!viewModel.isValidVpa(it.toString())) "Entered UPI is invalid!" else null + btnPay.addError(view = etVpa, msg = errMsg) + btnPay.isActivated = btnPay.isValidForm + } + } + etVpa.setOnEditorActionListener { _, actionId, _ -> + var handled = false + if (actionId == EditorInfo.IME_ACTION_DONE) { + btnPay.performClick() + handled = true + } + return@setOnEditorActionListener handled + } + } + + @OnClick(R.id.btn_payment_option_pay_upi) + fun onPayClick() { + if (validationJob?.isActive == true) + toast("Validating input...") + if (!validateForm(btnPay)) return + val vpa = etVpa.text.toString() + viewModel.payUsing(UPICollectPaymentOptionModel(vpa), saveOption = cbSaveUpi.isChecked) + dismiss() + } +} diff --git a/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionsFragment.kt b/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionsFragment.kt new file mode 100644 index 00000000..d2fe75f8 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/fragments/PaymentOptionsFragment.kt @@ -0,0 +1,79 @@ +package com.checkin.app.checkin.payment.fragments + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import butterknife.BindView +import com.airbnb.epoxy.EpoxyRecyclerView +import com.checkin.app.checkin.R +import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.misc.fragments.BaseFragment +import com.checkin.app.checkin.payment.PaymentViewModel +import com.checkin.app.checkin.payment.controllers.PaymentOptionsController +import com.checkin.app.checkin.payment.listeners.PaymentOptionSelectListener +import com.checkin.app.checkin.payment.models.PAYMENT_TYPE +import com.checkin.app.checkin.payment.models.PaymentOptionModel +import com.checkin.app.checkin.utility.toast + +class PaymentOptionsFragment : BaseFragment(), PaymentOptionSelectListener { + override val rootLayout: Int = R.layout.fragment_payment_options + + @BindView(R.id.epoxy_rv_payment_options) + lateinit var epoxyRvOptions: EpoxyRecyclerView + + private val viewModel: PaymentViewModel by activityViewModels() + private val controller by lazy { PaymentOptionsController(this) } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + epoxyRvOptions.setHasFixedSize(false) + epoxyRvOptions.setControllerAndBuildModels(controller) + + setupObserver() + viewModel.fetchUPIOptions() + viewModel.fetchCardOptions() + } + + private fun setupObserver() { + viewModel.upiOptions.observe(this, Observer { + it?.let { resource -> + if (resource.status == Resource.Status.SUCCESS && resource.data != null) + controller.upiOptions = resource.data + } + }) + viewModel.netBankingOptions.observe(this, Observer { + it?.let { resource -> + if (resource.status == Resource.Status.SUCCESS && resource.data != null) + controller.netBankingOptions = resource.data.slice(0 until resource.data.size.coerceAtMost(5)) + } + }) + viewModel.cardOptions.observe(this, Observer { + it?.let { resource -> + if (resource.status == Resource.Status.SUCCESS && resource.data != null) + controller.cardOptions = resource.data + } + }) + viewModel.recentlyUsedOptions.observe(this, Observer { + controller.lastUsedOptions = it?.run { subList(0, size.coerceAtMost(5)) } + }) + } + + override fun onAddPaymentOption(pmtType: PAYMENT_TYPE) { + val action = when (pmtType) { + PAYMENT_TYPE.UPI -> PaymentOptionsFragmentDirections.actionPaymentOptionsFragmentToPaymentUpiFragment() + PAYMENT_TYPE.CARD -> PaymentOptionsFragmentDirections.actionPaymentOptionsFragmentToPaymentCardFragment() + else -> { + toast("Not yet added") + return + } + } + kotlin.runCatching { + findNavController().navigate(action) + } + } + + override fun onPayPaymentOption(paymentOption: PaymentOptionModel) { + viewModel.payUsing(paymentOption, saveOption = true) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentAddOptionModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentAddOptionModelHolder.kt new file mode 100644 index 00000000..c9488be0 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentAddOptionModelHolder.kt @@ -0,0 +1,47 @@ +package com.checkin.app.checkin.payment.holders + +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import butterknife.BindView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.checkin.app.checkin.R +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder +import com.checkin.app.checkin.payment.listeners.PaymentOptionSelectListener +import com.checkin.app.checkin.payment.models.PAYMENT_TYPE + +@EpoxyModelClass(layout = R.layout.item_payment_add_option, useLayoutOverloads = true) +abstract class PaymentAddOptionModelHolder : EpoxyModelWithHolder() { + @EpoxyAttribute + lateinit var content: String + + @EpoxyAttribute + lateinit var optionType: PAYMENT_TYPE + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var listener: PaymentOptionSelectListener + + override fun bind(holder: AddItemHolder) = holder.bindData(content) + + inner class AddItemHolder : BaseEpoxyHolder() { + @BindView(R.id.container_payment_add_option) + internal lateinit var containerAddOption: ViewGroup + @BindView(R.id.btn_payment_add_item) + internal lateinit var btnAddItem: Button + @BindView(R.id.tv_payment_add_content) + internal lateinit var tvAddContent: TextView + + override fun bindView(itemView: View) { + super.bindView(itemView) + btnAddItem.setOnClickListener { listener.onAddPaymentOption(optionType) } + containerAddOption.setOnClickListener { listener.onAddPaymentOption(optionType) } + } + + override fun bindData(data: String) { + tvAddContent.text = data + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentNetBankingModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentNetBankingModelHolder.kt new file mode 100644 index 00000000..3bdd78af --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentNetBankingModelHolder.kt @@ -0,0 +1,43 @@ +package com.checkin.app.checkin.payment.holders + +import android.view.View +import android.widget.ImageView +import butterknife.BindView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.checkin.app.checkin.R +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder +import com.checkin.app.checkin.payment.listeners.PaymentOptionSelectListener +import com.checkin.app.checkin.payment.models.NetBankingPaymentOptionModel + +@EpoxyModelClass(layout = R.layout.item_payment_netbanking_option) +abstract class PaymentNetBankingModelHolder : EpoxyModelWithHolder() { + @EpoxyAttribute + lateinit var listener: PaymentOptionSelectListener + + @EpoxyAttribute + lateinit var data: NetBankingPaymentOptionModel + + override fun createNewHolder() = OptionHolder(listener) + + override fun bind(holder: OptionHolder) = holder.bindData(data) + + class OptionHolder(val listener: PaymentOptionSelectListener) : BaseEpoxyHolder() { + @BindView(R.id.im_payment_netbanking_option) + internal lateinit var imOption: ImageView + + private lateinit var mData: NetBankingPaymentOptionModel + + override fun bindView(itemView: View) { + super.bindView(itemView) + imOption.setOnClickListener { listener.onPayPaymentOption(mData) } + } + + override fun bindData(data: NetBankingPaymentOptionModel) { + mData = data + if (mData.iconRes != 0) + imOption.setImageResource(mData.iconRes) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentOptionModelHolder.kt b/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentOptionModelHolder.kt new file mode 100644 index 00000000..9cf5c668 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/holders/PaymentOptionModelHolder.kt @@ -0,0 +1,126 @@ +package com.checkin.app.checkin.payment.holders + +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.Button +import android.widget.EditText +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.widget.AppCompatRadioButton +import androidx.core.widget.addTextChangedListener +import butterknife.BindView +import butterknife.OnClick +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.checkin.app.checkin.R +import com.checkin.app.checkin.misc.epoxy.BaseEpoxyHolder +import com.checkin.app.checkin.payment.listeners.PaymentOptionSelectListener +import com.checkin.app.checkin.payment.models.CardPaymentOptionModel +import com.checkin.app.checkin.payment.models.PaymentOptionModel +import com.checkin.app.checkin.payment.models.UPICollectPaymentOptionModel +import com.checkin.app.checkin.payment.models.UPIPushPaymentOptionModel +import com.checkin.app.checkin.utility.Utils + +@EpoxyModelClass(layout = R.layout.item_payment_option) +abstract class PaymentOptionModelHolder : EpoxyModelWithHolder() { + @EpoxyAttribute + var selectedId: Long = 0 + + @EpoxyAttribute + lateinit var optionData: PaymentOptionModel + + @EpoxyAttribute + lateinit var listener: PaymentOptionSelectListener + + @EpoxyAttribute + lateinit var selectListener: PaymentOptionInteraction + + override fun bind(holder: OptionHolder) { + holder.bindData(optionData) + holder.bindShowPay(selectedId == id()) + } + + inner class OptionHolder : BaseEpoxyHolder() { + @BindView(R.id.tv_payment_option_name) + internal lateinit var tvName: TextView + @BindView(R.id.cl_payment_option_pay) + internal lateinit var containerPay: ViewGroup + @BindView(R.id.rb_payment_option_select) + internal lateinit var rbSelect: AppCompatRadioButton + @BindView(R.id.et_payment_option_card_cvv) + internal lateinit var etCardCvv: EditText + @BindView(R.id.im_payment_option_icon) + internal lateinit var imIcon: ImageView + @BindView(R.id.cl_payment_option_select) + internal lateinit var containerSelect: ViewGroup + @BindView(R.id.btn_payment_option_pay) + internal lateinit var btnPay: Button + + private lateinit var mData: PaymentOptionModel + private var mShowPay: Boolean = false + + override fun bindView(itemView: View) { + super.bindView(itemView) + rbSelect.setOnCheckedChangeListener { _, isChecked -> if (isChecked && !mShowPay) selectListener.onSelectOption(id()) } + containerSelect.setOnClickListener { rbSelect.isChecked = true } + etCardCvv.addTextChangedListener { + btnPay.isActivated = it?.length == 3 + } + etCardCvv.setOnEditorActionListener { _, actionId, _ -> + var handled = false + if (actionId == EditorInfo.IME_ACTION_DONE) { + btnPay.performClick() + handled = true + } + return@setOnEditorActionListener handled + } + } + + override fun bindData(data: PaymentOptionModel) { + mData = data + + when (data) { + is UPIPushPaymentOptionModel -> { + tvName.text = data.appName + Utils.loadImageOrDefault(imIcon, data.iconUrl, 0) + etCardCvv.visibility = GONE + btnPay.isActivated = true + } + is CardPaymentOptionModel -> { + tvName.text = data.formatNumber + data.channel?.also { imIcon.setImageResource(it.drawable) } + etCardCvv.visibility = VISIBLE + btnPay.isActivated = false + } + is UPICollectPaymentOptionModel -> { + tvName.text = data.vpa + imIcon.setImageResource(R.drawable.ic_payment_upi) + etCardCvv.visibility = GONE + btnPay.isActivated = true + } + } + } + + fun bindShowPay(showPay: Boolean) { + rbSelect.isChecked = showPay + mShowPay = showPay + containerPay.visibility = if (showPay) VISIBLE else GONE + } + + @OnClick(R.id.btn_payment_option_pay) + fun onClickPay() { + if (!btnPay.isActivated) { + etCardCvv.error = "Input CVV to pay" + } + listener.onPayPaymentOption(mData) + } + } +} + +interface PaymentOptionInteraction { + fun onSelectOption(id: Long) +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/listeners/PaymentOptionSelectListener.kt b/app/src/main/java/com/checkin/app/checkin/payment/listeners/PaymentOptionSelectListener.kt new file mode 100644 index 00000000..31fc1f37 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/listeners/PaymentOptionSelectListener.kt @@ -0,0 +1,9 @@ +package com.checkin.app.checkin.payment.listeners + +import com.checkin.app.checkin.payment.models.PAYMENT_TYPE +import com.checkin.app.checkin.payment.models.PaymentOptionModel + +interface PaymentOptionSelectListener { + fun onAddPaymentOption(pmtType: PAYMENT_TYPE) + fun onPayPaymentOption(paymentOption: PaymentOptionModel) +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/listeners/TransactionListener.kt b/app/src/main/java/com/checkin/app/checkin/payment/listeners/TransactionListener.kt new file mode 100644 index 00000000..5a5a259b --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/listeners/TransactionListener.kt @@ -0,0 +1,10 @@ +package com.checkin.app.checkin.payment.listeners + +import com.checkin.app.checkin.payment.models.TransactionResponseModel +import com.checkin.app.checkin.payment.services.TransactionException + +interface TransactionListener { + fun onTransactionResponse(data: TransactionResponseModel) + fun onTransactionCancel(msg: String?) + fun onTransactionError(error: TransactionException) +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/models/NewTransactionModels.kt b/app/src/main/java/com/checkin/app/checkin/payment/models/NewTransactionModels.kt new file mode 100644 index 00000000..30a78be7 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/models/NewTransactionModels.kt @@ -0,0 +1,29 @@ +package com.checkin.app.checkin.payment.models + +import com.fasterxml.jackson.annotation.JsonProperty +import java.io.Serializable + +sealed class NewTransactionModel( + open val amount: Double, + open val customerPhone: String, + open val customerEmail: String? +) : Serializable + +data class NewPaytmTransactionModel( + @JsonProperty("amount_paid") override val amount: Double, + @JsonProperty("customer_id") val customerId: Long, + @JsonProperty("phone") override val customerPhone: String, + @JsonProperty("email") override val customerEmail: String?, + @JsonProperty("merchant_id") val mid: String, + @JsonProperty("order_id") val orderId: String, + @JsonProperty("txn_token") val txnToken: String +) : NewTransactionModel(amount, customerPhone, customerEmail) + +data class NewRazorpayTransactionModel( + @JsonProperty("amount_paid") override val amount: Double, + @JsonProperty("phone") override val customerPhone: String, + @JsonProperty("email") override val customerEmail: String?, + val currency: String, + val receipt: String, + @JsonProperty("order_id") val orderId: String +) : NewTransactionModel(amount, customerPhone, customerEmail) diff --git a/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentMethods.kt b/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentMethods.kt new file mode 100644 index 00000000..98033b5f --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentMethods.kt @@ -0,0 +1,3 @@ +package com.checkin.app.checkin.payment.models + +abstract class PaymentMethods \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentOptionDao.kt b/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentOptionDao.kt new file mode 100644 index 00000000..d05886da --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentOptionDao.kt @@ -0,0 +1,18 @@ +package com.checkin.app.checkin.payment.models + +import io.objectbox.Box + +fun Box.getByVpa(vpa: String): UPICollectPaymentOptionModel? = query() + .equal(UPICollectPaymentOptionModel_.vpa, vpa) + .build() + .findUnique() + +fun Box.getByAppName(appName: String): UPIPushPaymentOptionModel? = query() + .equal(UPIPushPaymentOptionModel_.appName, appName) + .build() + .findUnique() + +fun Box.getByCardNumber(cardNumber: String): CardPaymentOptionModel? = query() + .equal(CardPaymentOptionModel_.cardNumber, cardNumber) + .build() + .findUnique() diff --git a/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentOptionModels.kt b/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentOptionModels.kt new file mode 100644 index 00000000..e12b73af --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/models/PaymentOptionModels.kt @@ -0,0 +1,95 @@ +package com.checkin.app.checkin.payment.models + +import androidx.annotation.DrawableRes +import com.checkin.app.checkin.R +import com.checkin.app.checkin.utility.EnumConverter +import com.checkin.app.checkin.utility.EnumStringGetter +import com.checkin.app.checkin.utility.EnumStringType +import io.objectbox.annotation.Convert +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.Unique +import java.io.Serializable +import java.util.* + +sealed class PaymentOptionModel(val type: PAYMENT_TYPE) : Serializable { + abstract var id: Long + var lastUsed: Date? = null +} + +@Entity +data class CardPaymentOptionModel( + val name: String, + @Unique val cardNumber: String, + val expiryMonth: String, + val expiryYear: String, + val cvv: String, + var bankCode: String? = null, + @Convert(converter = CARD_PROVIDER.Companion.Converter::class, dbType = String::class) + var channel: CARD_PROVIDER? = null, + @Convert(converter = CARD_TYPE.Companion.TypeConverter::class, dbType = String::class) + var cardType: CARD_TYPE? = null +) : PaymentOptionModel(PAYMENT_TYPE.CARD) { + @Id + override var id: Long = 0 + + val formatNumber: String = cardNumber + + enum class CARD_PROVIDER(override val value: String, @DrawableRes val drawable: Int) : EnumStringType { + VISA("visa", R.drawable.ic_payment_card_visa), + MASTER_CARD("mastercard", R.drawable.ic_payment_card_mastercard), + AMEX("amex", R.drawable.ic_payment_card_amex), + MAESTRO("maestro", R.drawable.ic_payment_card_maestro), + RUPAY("rupay", R.drawable.ic_payment_card_rupay); + + companion object : EnumStringGetter() { + override fun getByValue(value: String?): CARD_PROVIDER? = value?.let { EnumStringType.getByValue(it) } + + class Converter : EnumConverter(this) + } + } + + enum class CARD_TYPE(override val value: String) : EnumStringType { + DEBIT("DEBIT_CARD"), CREDIT("CREDIT_CARD"); + + companion object : EnumStringGetter() { + override fun getByValue(value: String?): CARD_TYPE? = value?.let { EnumStringType.getByValue(it) } + + class TypeConverter : EnumConverter(this) + } + } +} + +sealed class UPIPaymentOptionModel : PaymentOptionModel(PAYMENT_TYPE.UPI) + +@Entity +data class UPIPushPaymentOptionModel( + @Unique val appName: String, + val iconUrl: String, + val packageName: String +) : UPIPaymentOptionModel() { + @Id + override var id: Long = 0 +} + +@Entity +data class UPICollectPaymentOptionModel( + @Unique val vpa: String +) : UPIPaymentOptionModel() { + @Id + override var id: Long = 0 +} + +@Entity +data class NetBankingPaymentOptionModel( + @Unique val bankCode: String, + val bankName: String, + @DrawableRes val iconRes: Int = 0 +) : PaymentOptionModel(PAYMENT_TYPE.NET_BANKING) { + @Id + override var id: Long = 0 +} + +enum class PAYMENT_TYPE { + CARD, WALLET, UPI, NET_BANKING +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/models/TransactionResponseModel.kt b/app/src/main/java/com/checkin/app/checkin/payment/models/TransactionResponseModel.kt new file mode 100644 index 00000000..7c7a265a --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/models/TransactionResponseModel.kt @@ -0,0 +1,17 @@ +package com.checkin.app.checkin.payment.models + +import com.checkin.app.checkin.data.Converters +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import org.json.JSONObject + +sealed class TransactionResponseModel(open val orderId: String) + +data class RazorpayTxnResponseModel( + @JsonProperty("payment_id") val paymentId: String, + @JsonProperty("order_id") override val orderId: String, + val signature: String, + @JsonProperty("extra_data") + @JsonSerialize(using = Converters.JsonObjectSerializer::class) + val data: JSONObject? +) : TransactionResponseModel(orderId) diff --git a/app/src/main/java/com/checkin/app/checkin/payment/models/razorpay/RazorpayPaymentMethod.kt b/app/src/main/java/com/checkin/app/checkin/payment/models/razorpay/RazorpayPaymentMethod.kt new file mode 100644 index 00000000..29c4c0f7 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/models/razorpay/RazorpayPaymentMethod.kt @@ -0,0 +1,15 @@ +package com.checkin.app.checkin.payment.models.razorpay + +import com.checkin.app.checkin.payment.models.PaymentMethods +import com.fasterxml.jackson.annotation.JsonProperty + +data class RazorpayPaymentMethod( + val card: Boolean, + @JsonProperty("debit_card") val debitCard: Boolean, + @JsonProperty("credit_card") val creditCard: Boolean, + @JsonProperty("card_networks") val cardNetworks: Map, + val netbanking: Map, + val wallet: Map, + val upi: Boolean, + @JsonProperty("upi_intent") val upiIntent: Boolean +) : PaymentMethods() diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/IPaymentService.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/IPaymentService.kt new file mode 100644 index 00000000..8b9bea81 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/IPaymentService.kt @@ -0,0 +1,17 @@ +package com.checkin.app.checkin.payment.services + +import android.content.Context +import com.checkin.app.checkin.payment.models.CardPaymentOptionModel +import com.checkin.app.checkin.payment.models.NetBankingPaymentOptionModel +import com.checkin.app.checkin.payment.models.PaymentMethods +import com.checkin.app.checkin.payment.models.UPIPushPaymentOptionModel + +interface IPaymentService { + suspend fun getPaymentMethods(): PaymentMethods + suspend fun getNetBankingOptions(): List + suspend fun getUPIAppOptions(context: Context): List + suspend fun isValidVpa(vpa: String): Boolean + suspend fun getCardNetwork(cardNumber: String): CardPaymentOptionModel.CARD_PROVIDER? + suspend fun isValidCardNumber(cardNumber: String): Boolean + +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/ITransactionService.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/ITransactionService.kt new file mode 100644 index 00000000..f8432bab --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/ITransactionService.kt @@ -0,0 +1,13 @@ +package com.checkin.app.checkin.payment.services + +import android.webkit.WebView +import com.checkin.app.checkin.payment.models.* + +interface ITransactionService { + fun initialize(txnToken: String, amount: Double, merchantId: String, orderId: String) {} + fun initialize(webView: WebView, txnData: NewTransactionModel) {} + fun payByUPICollect(data: UPICollectPaymentOptionModel) + fun payByUPIPush(data: UPIPushPaymentOptionModel) + fun payByNetBanking(data: NetBankingPaymentOptionModel) + fun payByCard(data: CardPaymentOptionModel) +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/PaymentServiceLocator.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/PaymentServiceLocator.kt new file mode 100644 index 00000000..a54ac158 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/PaymentServiceLocator.kt @@ -0,0 +1,28 @@ +package com.checkin.app.checkin.payment.services + +import androidx.appcompat.app.AppCompatActivity +import com.checkin.app.checkin.data.config.RemoteConfig +import com.checkin.app.checkin.payment.listeners.TransactionListener +import com.checkin.app.checkin.payment.services.razorpay.RazorpayPaymentService +import com.checkin.app.checkin.payment.services.razorpay.RazorpayTransactionService +import com.checkin.app.checkin.utility.ConflatedSingletonHolder +import com.razorpay.Razorpay + +class PaymentServiceLocator private constructor(val activity: AppCompatActivity) { + val paymentProvider: PaymentProvider by lazy { + Razorpay(activity, RemoteConfig[RemoteConfig.Constants.KEY_RAZORPAY].asString()) + } + + fun getTransactionService(listener: TransactionListener): ITransactionService { + return RazorpayTransactionService(paymentProvider, listener) + } + + val paymentService: IPaymentService = RazorpayPaymentService + + companion object : ConflatedSingletonHolder({ PaymentServiceLocator(it) }) +} + +typealias PaymentProvider = Razorpay + +val AppCompatActivity.paymentLocator + get() = PaymentServiceLocator.getInstance(this) diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/TransactionException.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/TransactionException.kt new file mode 100644 index 00000000..8e87e4b0 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/TransactionException.kt @@ -0,0 +1,7 @@ +package com.checkin.app.checkin.payment.services + +open class TransactionException(val errCode: Int, val errMsg: String?) : Exception("Unable to perform the transaction") { + override val message: String = "${super.message}\n[errorCode=$errCode, errorMsg='$errMsg']" + + override fun getLocalizedMessage(): String = errMsg ?: message +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/paytm/PaytmDataException.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/paytm/PaytmDataException.kt new file mode 100644 index 00000000..18939ac2 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/paytm/PaytmDataException.kt @@ -0,0 +1,5 @@ +package com.checkin.app.checkin.payment.services.paytm + +class PaytmDataException(resultCode: String?, resultMsg: String?, resultStatus: String?) : Exception("Unable to get the requested data") { + override val message: String = "${super.message}\n[resultCode=$resultCode, resultStatus=$resultStatus, resultMsg='$resultMsg']" +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/paytm/PaytmTransactionException.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/paytm/PaytmTransactionException.kt new file mode 100644 index 00000000..988cf039 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/paytm/PaytmTransactionException.kt @@ -0,0 +1,5 @@ +package com.checkin.app.checkin.payment.services.paytm + +class PaytmTransactionException(errCode: Int, errMsg: String?) : Exception("Unable to perform the transaction") { + override val message: String = "${super.message}\n[errorCode=$errCode, errorMsg='$errMsg']" +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayDataException.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayDataException.kt new file mode 100644 index 00000000..e232bb1a --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayDataException.kt @@ -0,0 +1,3 @@ +package com.checkin.app.checkin.payment.services.razorpay + +class RazorpayDataException(msg: String?) : Exception(msg ?: "Some error occurred using Razorpay") \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayPaymentService.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayPaymentService.kt new file mode 100644 index 00000000..11fe0d51 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayPaymentService.kt @@ -0,0 +1,91 @@ +package com.checkin.app.checkin.payment.services.razorpay + +import android.content.Context +import com.checkin.app.checkin.data.Converters +import com.checkin.app.checkin.payment.models.CardPaymentOptionModel +import com.checkin.app.checkin.payment.models.NetBankingPaymentOptionModel +import com.checkin.app.checkin.payment.models.UPIPushPaymentOptionModel +import com.checkin.app.checkin.payment.models.razorpay.RazorpayPaymentMethod +import com.checkin.app.checkin.payment.services.IPaymentService +import com.checkin.app.checkin.payment.services.PaymentProvider +import com.checkin.app.checkin.payment.services.PaymentServiceLocator +import com.fasterxml.jackson.core.type.TypeReference +import com.razorpay.BaseRazorpay +import com.razorpay.Razorpay +import com.razorpay.RzpUpiSupportedAppsCallback +import com.razorpay.ValidateVpaCallback +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +object RazorpayPaymentService : IPaymentService { + private var paymentMethods: RazorpayPaymentMethod? = null + private val provider: PaymentProvider + get() = PaymentServiceLocator.getInstance().paymentProvider + + override suspend fun getNetBankingOptions(): List { + return paymentMethods?.netbanking?.map { + NetBankingPaymentOptionModel(it.key, it.value, 0) + } ?: emptyList() + } + + override suspend fun getUPIAppOptions(context: Context): List = suspendCoroutine { continuation -> + val callback = RzpUpiSupportedAppsCallback { + val data = it.map { + UPIPushPaymentOptionModel(it.appName, it.iconBase64, it.packageName) + } + continuation.resume(data) + } + Razorpay.getAppsWhichSupportUpi(context, callback) + } + + override suspend fun getPaymentMethods(): RazorpayPaymentMethod = suspendCoroutine { continuation -> + paymentMethods?.also { + continuation.resume(it) + return@suspendCoroutine + } + val callback = object : BaseRazorpay.PaymentMethodsCallback { + override fun onPaymentMethodsReceived(result: String?) { + if (result != null) + kotlin.runCatching { + Converters.getObjectFromJson(result, object : TypeReference() {}) + }.onFailure { continuation.resumeWithException(it) }.onSuccess { + if (it != null) { + paymentMethods = it + continuation.resume(it) + } + } + } + + override fun onError(error: String?) { + continuation.resumeWithException(RazorpayDataException(error)) + } + } + provider.getPaymentMethods(callback) + } + + override suspend fun isValidVpa(vpa: String): Boolean = suspendCoroutine { continuation -> + val callback = object : ValidateVpaCallback { + override fun onFailure() { + // By default, if call fails resume execution assuming valid VPA + continuation.resume(false) + } + + override fun onResponse(isValid: Boolean) { + continuation.resume(isValid) + } + } + provider.isValidVpa(vpa, callback) + } + + override suspend fun getCardNetwork(cardNumber: String): CardPaymentOptionModel.CARD_PROVIDER? = when (provider.getCardNetwork(cardNumber)) { + "visa" -> CardPaymentOptionModel.CARD_PROVIDER.VISA + "mastercard" -> CardPaymentOptionModel.CARD_PROVIDER.MASTER_CARD + "maestro", "maestro16" -> CardPaymentOptionModel.CARD_PROVIDER.MAESTRO + "rupay" -> CardPaymentOptionModel.CARD_PROVIDER.RUPAY + "amex" -> CardPaymentOptionModel.CARD_PROVIDER.AMEX + else -> null + } + + override suspend fun isValidCardNumber(cardNumber: String): Boolean = provider.isValidCardNumber(cardNumber) +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayTransactionException.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayTransactionException.kt new file mode 100644 index 00000000..4937619e --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayTransactionException.kt @@ -0,0 +1,5 @@ +package com.checkin.app.checkin.payment.services.razorpay + +import com.checkin.app.checkin.payment.services.TransactionException + +class RazorpayTransactionException(errCode: Int, errMsg: String?) : TransactionException(errCode, errMsg) \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayTransactionService.kt b/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayTransactionService.kt new file mode 100644 index 00000000..26ebe6e6 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/payment/services/razorpay/RazorpayTransactionService.kt @@ -0,0 +1,118 @@ +package com.checkin.app.checkin.payment.services.razorpay + +import android.webkit.WebView +import com.checkin.app.checkin.data.Converters +import com.checkin.app.checkin.payment.listeners.TransactionListener +import com.checkin.app.checkin.payment.models.* +import com.checkin.app.checkin.payment.services.ITransactionService +import com.checkin.app.checkin.utility.Utils +import com.checkin.app.checkin.utility.log +import com.checkin.app.checkin.utility.putAll +import com.razorpay.BaseRazorpay +import com.razorpay.PaymentData +import com.razorpay.PaymentResultWithDataListener +import com.razorpay.Razorpay +import org.json.JSONObject + +class RazorpayTransactionService(private val razorpay: Razorpay, private val listener: TransactionListener) : ITransactionService, PaymentResultWithDataListener { + private lateinit var mTransactionData: NewRazorpayTransactionModel + private val baseJsonData: MutableMap + get() = mutableMapOf( + "amount" to mTransactionData.amount.toInt() * 100, + "currency" to mTransactionData.currency, + "order_id" to mTransactionData.orderId, + "contact" to mTransactionData.customerPhone, + "email" to (mTransactionData.customerEmail?.takeIf { it.isNotBlank() } + ?: "missing@email.com") + ) + + override fun initialize(webView: WebView, txnData: NewTransactionModel) { + razorpay.setWebView(webView) + mTransactionData = txnData as NewRazorpayTransactionModel + } + + private fun initiateTransaction(data: Map) { + val payload = JSONObject(data) + razorpay.validateFields(payload, object : BaseRazorpay.ValidationListener { + override fun onValidationError(errorData: MutableMap?) { + val msg = errorData?.let { + "Validation: ${it["field"]} - ${it["description"]}" + } ?: "Validation failed during payment" + onPaymentError(ERROR_CODE_INVALID_PAYLOAD, msg, null) + } + + override fun onValidationSuccess() { + kotlin.runCatching { + razorpay.submit(payload, this@RazorpayTransactionService) + }.onFailure { it.log(TAG) } + } + }) + } + + override fun payByUPICollect(data: UPICollectPaymentOptionModel) { + initiateTransaction(baseJsonData.apply { + putAll( + "method" to "upi", + "vpa" to data.vpa + ) + }) + } + + override fun payByUPIPush(data: UPIPushPaymentOptionModel) { + initiateTransaction(baseJsonData.apply { + putAll( + "method" to "upi", + "_[flow]" to "intent", + "upi_app_package_name" to data.packageName + ) + }) + } + + override fun payByNetBanking(data: NetBankingPaymentOptionModel) { + initiateTransaction(baseJsonData.apply { + putAll( + "method" to "netbanking", + "bank" to data.bankCode + ) + }) + } + + override fun payByCard(data: CardPaymentOptionModel) { + initiateTransaction(baseJsonData.apply { + putAll( + "method" to "card", + "card[name]" to data.name, + "card[number]" to data.cardNumber, + "card[expiry_month]" to data.expiryMonth, + "card[expiry_year]" to data.expiryYear, + "card[cvv]" to data.cvv + ) + }) + } + + override fun onPaymentError(errCode: Int, errorMsg: String?, data: PaymentData?) { + val msg = errorMsg?.let { + runCatching { Converters.objectMapper.readTree(it)["error"]["description"].asText() } + .onFailure { it.log(TAG) } + .getOrNull() + } ?: errorMsg + val exc = RazorpayTransactionException(errCode, msg) + Utils.logErrors(TAG, exc, errorMsg) + listener.onTransactionError(exc) + } + + override fun onPaymentSuccess(paymentId: String, data: PaymentData) { + val extraData = data.data ?: JSONObject() + extraData.put("user_email", data.userEmail) + extraData.put("user_phone", data.userContact) + listener.onTransactionResponse(RazorpayTxnResponseModel( + paymentId, data.orderId, data.signature, extraData + )) + } + + companion object { + private val TAG: String = RazorpayTransactionService::class.simpleName!! + + const val ERROR_CODE_INVALID_PAYLOAD = 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/restaurant/activities/PublicRestaurantProfileActivity.kt b/app/src/main/java/com/checkin/app/checkin/restaurant/activities/PublicRestaurantProfileActivity.kt index 9d8b37eb..62dbbaf5 100644 --- a/app/src/main/java/com/checkin/app/checkin/restaurant/activities/PublicRestaurantProfileActivity.kt +++ b/app/src/main/java/com/checkin/app/checkin/restaurant/activities/PublicRestaurantProfileActivity.kt @@ -44,7 +44,8 @@ import com.checkin.app.checkin.misc.fragments.BlankFragment import com.checkin.app.checkin.misc.fragments.NetworkBlockingFragment import com.checkin.app.checkin.misc.fragments.QRScannerWrapperFragment import com.checkin.app.checkin.misc.fragments.QRScannerWrapperInteraction -import com.checkin.app.checkin.misc.paytm.PaytmPayment +import com.checkin.app.checkin.payment.activities.PaymentActivity +import com.checkin.app.checkin.payment.models.NewTransactionModel import com.checkin.app.checkin.restaurant.fragments.PublicRestaurantInfoFragment import com.checkin.app.checkin.restaurant.models.RestaurantBriefModel import com.checkin.app.checkin.restaurant.models.RestaurantModel @@ -125,21 +126,6 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha .setAuthCallback(this) .build() } - private val paytmPayment: PaytmPayment by lazy { - object : PaytmPayment() { - override fun onPaytmTransactionResponse(inResponse: Bundle) { - scheduledSessionViewModel.postPaytmCallback(inResponse) - } - - override fun onPaytmError(inErrorMessage: String?) { - Utils.toast(this@PublicRestaurantProfileActivity, inErrorMessage) - } - - override fun onPaytmTransactionCancel(inResponse: Bundle?, msg: String?) { - Utils.toast(this@PublicRestaurantProfileActivity, msg) - } - } - } private val childSizeUtil by lazy { ChildSizeMeasureViewPager2(vpFragment) } @@ -150,7 +136,7 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha setupObservers() initUi() - setupPaytm() + setupPayment() } private fun initUi() { @@ -251,7 +237,10 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha toast(resource.message) } } else if (resource.errorBody?.has("planned_datetime") == true) { - toast(resource.errorBody.get("planned_datetime").get(0).asText()) + val msg = resource.errorBody.get("planned_datetime")?.let { + it.get("detail") ?: it.get(0) + }?.asText() + toast(msg) } else toast(resource.message) Resource.Status.LOADING -> pass else -> toast(resource.message) @@ -267,8 +256,7 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha else if (isMasterQr) { wrongRestaurantQrScanned() scheduledSessionViewModel.clearCart() - } - else startActivity(Intent(this@PublicRestaurantProfileActivity, ActiveSessionActivity::class.java)) + } else startActivity(Intent(this@PublicRestaurantProfileActivity, ActiveSessionActivity::class.java)) } Resource.Status.LOADING -> pass else -> toast(resource.message) @@ -290,11 +278,11 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha scheduledSessionViewModel.retrySessionCreation() } resource.problem?.getErrorCode() == ProblemModel.ERROR_CODE.ACCOUNT_ALREADY_REGISTERED -> { - Utils.toast(this, "This number already exists.") + toast("This number already exists.") onVerifyPhoneOfUser() } resource.status != Resource.Status.LOADING -> { - Utils.toast(this, resource.message) + toast(resource.message) } } } @@ -303,46 +291,36 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha networkViewModel.shouldTryAgain { when (it) { LOAD_DATA_RESTAURANT -> restaurantViewModel.fetchMissing() - LOAD_SYNC_PAY_REQUEST -> scheduledSessionViewModel.requestPaytmDetails() - LOAD_SYNC_PAYTM_CALLBACK -> scheduledSessionViewModel.retryPostPaytmCallback() + LOAD_SYNC_PAY_REQUEST -> scheduledSessionViewModel.initiateNewTransaction() LOAD_SYNC_USER_DETAILS -> userViewModel.retryUpdateProfile() } } } - private fun setupPaytm() { - scheduledSessionViewModel.paytmData.observe(this, Observer { - it?.let { paytmModelResource -> - networkViewModel.updateStatus(paytmModelResource, LOAD_SYNC_PAY_REQUEST) - if (paytmModelResource.status === Resource.Status.SUCCESS && paytmModelResource.data != null) { - paytmPayment.initializePayment(paytmModelResource.data, this) - } else if (paytmModelResource.status !== Resource.Status.LOADING) { - when (paytmModelResource.problem?.getErrorCode()) { + private fun setupPayment() { + scheduledSessionViewModel.newTransactionData.observe(this, Observer { + it?.let { txnResource -> + networkViewModel.updateStatus(txnResource, LOAD_SYNC_PAY_REQUEST) + if (txnResource.status === Resource.Status.SUCCESS && txnResource.data != null) { + goToPayment(txnResource.data) + } else if (txnResource.status !== Resource.Status.LOADING) { + toast(txnResource.message) + when (txnResource.problem?.getErrorCode()) { ProblemModel.ERROR_CODE.USER_MISSING_PHONE -> { scheduledSessionViewModel.isPhoneVerified = false onVerifyPhoneOfUser() } ProblemModel.ERROR_CODE.SESSION_SCHEDULED_CBYG_INVALID_PLANNED_TIME -> scheduledCartView.switchTime() + ProblemModel.ERROR_CODE.SESSION_PAYMENT_ALREADY_DONE -> navigateBackToHome() } - Utils.toast(this, paytmModelResource.message) - } - } - }) - - scheduledSessionViewModel.paytmCallbackData.observe(this, Observer { - it?.let { objectNodeResource -> - networkViewModel.updateStatus(objectNodeResource, LOAD_SYNC_PAYTM_CALLBACK) - if (objectNodeResource.status === Resource.Status.SUCCESS) { - Utils.navigateBackToHome(this) - } - if (objectNodeResource.status !== Resource.Status.LOADING) { - Utils.toast(this, objectNodeResource.message) - hideProgressBar() } } }) } + private fun goToPayment(data: NewTransactionModel) { + startActivityForResult(PaymentActivity.withTransactionIntent(this, data), REQUEST_PAYMENT_CODE) + } private fun handleErrorCartExists(cartRestaurant: RestaurantBriefModel) { AlertDialog.Builder(this) @@ -415,6 +393,28 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha } else "-" } ?: "-" tvRating.text = restaurantModel.formatRating() + + val ratingtext: String = tvRating.text.toString(); + + val ratenumber: Float = ratingtext.toFloat() + + if(ratenumber in 4.0..5.0){ + tvRating.setBackgroundColor(ContextCompat.getColor(this,R.color.md_green_400)); + + + } + if(ratenumber in 3.0..4.0){ + tvRating.setBackgroundColor(ContextCompat.getColor(this,R.color.md_deep_orange_300)); + + + } + if(ratenumber in 1.0..3.0){ + tvRating.setBackgroundColor(ContextCompat.getColor(this,R.color.red_500)); + + + + } + tvDistance.text = restaurantModel.formatDistance imDistance.setImageResource(if (restaurantModel.distance > 1.5) R.drawable.ic_distance_vehicle else R.drawable.ic_distance_walking) @@ -456,12 +456,12 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha override fun onStartPayment() { cartViewModel.cartDetailData.value?.data?.scheduled?.plannedDatetime?.let { if (it.time < Calendar.getInstance().time.time) { - Utils.toast(this, "Booking time must be set in future.") + toast("Booking time must be set in future.") scheduledCartView.switchTime() return } } - scheduledSessionViewModel.requestPaytmDetails() + scheduledSessionViewModel.initiateNewTransaction() } override fun onCartClose() { @@ -571,16 +571,27 @@ class PublicRestaurantProfileActivity : BaseActivity(), AppBarLayout.OnOffsetCha if (callSuper) super.onBackPressed() } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_PAYMENT_CODE) { + when (resultCode) { + PaymentActivity.RESULT_PAID -> navigateBackToHome() + else -> hideProgressBar() + } + } + } + companion object { const val KEY_RESTAURANT_ID = "restaurant_profile.public.id" const val KEY_SESSION_ID = "session.new.id" const val KEY_OPEN_CART = "cart.open" - private const val LOAD_SYNC_PAYTM_CALLBACK = "load.sync.paytm_callback" private const val LOAD_SYNC_PAY_REQUEST = "load.sync.pay.request" private const val LOAD_DATA_RESTAURANT = "load.data.restaurant" private const val LOAD_SYNC_USER_DETAILS = "load.sync.user_detail" + private const val REQUEST_PAYMENT_CODE = 151 + private val DEEP_LINK_RESTAURANT_PROFILE = Regex("/restaurants/(\\d+)/profile/") private val TAG: String = PublicRestaurantProfileActivity::class.java.simpleName diff --git a/app/src/main/java/com/checkin/app/checkin/restaurant/models/TimingModel.kt b/app/src/main/java/com/checkin/app/checkin/restaurant/models/TimingModel.kt new file mode 100644 index 00000000..df11d815 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/restaurant/models/TimingModel.kt @@ -0,0 +1,29 @@ +package com.checkin.app.checkin.restaurant.models + +import android.content.Context +import com.checkin.app.checkin.R +import com.checkin.app.checkin.utility.Utils +import com.checkin.app.checkin.utility.add +import com.checkin.app.checkin.utility.toHtml +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.* + +data class TimingModel( + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm:ss") + val open: Date, + var close: Date? +) { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm:ss") + @JsonProperty("close") + fun setCloseDate(closeDate: Date) { + close = if (closeDate.before(open)) closeDate.add(Calendar.DATE, 1) else closeDate + } + + fun formatDescription(context: Context): CharSequence { + val current = Calendar.getInstance().time + val repDay = if (current.after(close)) "tomorrow" else "today" + val repOpen = Utils.formatDateTo12HoursTime(open) + return context.getString(R.string.description_restaurant_closed_open_timing, repDay, repOpen).toHtml() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/session/SessionRepository.kt b/app/src/main/java/com/checkin/app/checkin/session/SessionRepository.kt index 15e640b2..d4e26619 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/SessionRepository.kt +++ b/app/src/main/java/com/checkin/app/checkin/session/SessionRepository.kt @@ -11,6 +11,8 @@ import com.checkin.app.checkin.data.network.WebApiService import com.checkin.app.checkin.data.resource.NetworkBoundResource import com.checkin.app.checkin.data.resource.Resource import com.checkin.app.checkin.home.model.ActiveLiveSessionDetailModel +import com.checkin.app.checkin.home.model.ClosedSessionBriefModel +import com.checkin.app.checkin.home.model.ClosedSessionDetailsModel import com.checkin.app.checkin.home.model.ScheduledLiveSessionDetailModel import com.checkin.app.checkin.manager.models.ManagerSessionEventModel import com.checkin.app.checkin.session.models.* @@ -45,7 +47,7 @@ class SessionRepository private constructor(context: Context) : BaseRepository() }.asLiveData val allPromoCodes: LiveData>> - get() = object : NetworkBoundResource, List>() { + get() = object : NetworkBoundResource, List>() { override fun shouldUseLocalDb(): Boolean { return false } @@ -186,5 +188,26 @@ class SessionRepository private constructor(context: Context) : BaseRepository() }.asLiveData } + val customerClosedSessionList: LiveData>> + get() = object : NetworkBoundResource, List>() { + override fun shouldUseLocalDb(): Boolean = false + + override fun createCall(): LiveData>> { + return RetrofitLiveData(mWebService.customerClosedTransactions) + } + + }.asLiveData + + fun getCustomerClosedSessionDetails(sessionId: Long): LiveData> = + object : NetworkBoundResource() { + override fun shouldUseLocalDb(): Boolean = false + + override fun createCall(): LiveData> { + return RetrofitLiveData(mWebService.getCustomerClosedSessionDetails(sessionId)) + } + + }.asLiveData + + companion object : SingletonHolder({ SessionRepository(it.applicationContext) }) } diff --git a/app/src/main/java/com/checkin/app/checkin/session/activesession/ActiveSessionInvoiceViewModel.kt b/app/src/main/java/com/checkin/app/checkin/session/activesession/ActiveSessionInvoiceViewModel.kt index d917e505..241301c0 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/activesession/ActiveSessionInvoiceViewModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/session/activesession/ActiveSessionInvoiceViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.Transformations import com.checkin.app.checkin.Shop.ShopModel.PAYMENT_MODE import com.checkin.app.checkin.data.BaseViewModel import com.checkin.app.checkin.data.Converters.objectMapper +import com.checkin.app.checkin.data.network.ApiResponse import com.checkin.app.checkin.data.network.RetrofitCallAsyncTask import com.checkin.app.checkin.data.resource.Resource import com.checkin.app.checkin.data.resource.Resource.Companion.cloneResource @@ -65,26 +66,26 @@ class ActiveSessionInvoiceViewModel(application: Application) : BaseViewModel(ap val data = objectMapper.createObjectNode() val keys = bundle.keySet() for (key in keys) { - data.put(key, bundle[key].toString()) + data.put(key, bundle[key]?.toString()) } - val listener: UploadCallbacks = object : UploadCallbacks { + val listener = object : UploadCallbacks { override fun onProgressUpdate(percentage: Int) { mPaytmCallbackData.postValue(loading(null)) } - override fun onSuccess() { + override fun onSuccess(response: ApiResponse) { mPaytmCallbackData.postValue(success(null)) } - override fun onFailure() { + override fun onFailure(response: ApiResponse) { mPaytmCallbackData.postValue(error("Sorry, but PayTM transaction failed", null)) } } doPostPaytmCallback(data, listener) } - private fun doPostPaytmCallback(data: ObjectNode, listener: UploadCallbacks) { - RetrofitCallAsyncTask(listener).execute(mRepository.syncPostPaytmCallback(data)) + private fun doPostPaytmCallback(data: ObjectNode, listener: UploadCallbacks) { + RetrofitCallAsyncTask(listener).execute(mRepository.syncPostPaytmCallback(data)) } val paytmCallbackData: LiveData> = mPaytmCallbackData diff --git a/app/src/main/java/com/checkin/app/checkin/session/models/SessionBillModel.java b/app/src/main/java/com/checkin/app/checkin/session/models/SessionBillModel.java index 3d8187ca..cac1da48 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/models/SessionBillModel.java +++ b/app/src/main/java/com/checkin/app/checkin/session/models/SessionBillModel.java @@ -35,6 +35,9 @@ public class SessionBillModel { private Double discountPercentage; + @JsonProperty("tax_detail") + private TaxDetailModel taxDetail; + public SessionBillModel() { } @@ -54,6 +57,10 @@ public Double getDiscount() { return discount; } + public TaxDetailModel getTaxDetail() { + return taxDetail; + } + @JsonProperty("discount") public void setDiscount(Double discount) { this.discount = (discount > 0) ? discount : null; diff --git a/app/src/main/java/com/checkin/app/checkin/session/models/TaxDetailModel.kt b/app/src/main/java/com/checkin/app/checkin/session/models/TaxDetailModel.kt new file mode 100644 index 00000000..60ffa138 --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/session/models/TaxDetailModel.kt @@ -0,0 +1,10 @@ +package com.checkin.app.checkin.session.models + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class TaxDetailModel( + val cgst: Double, + val sgst: Double, + val igst: Double +) diff --git a/app/src/main/java/com/checkin/app/checkin/session/scheduled/ScheduledSessionCartView.kt b/app/src/main/java/com/checkin/app/checkin/session/scheduled/ScheduledSessionCartView.kt index 685322c7..1ebf8bae 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/scheduled/ScheduledSessionCartView.kt +++ b/app/src/main/java/com/checkin/app/checkin/session/scheduled/ScheduledSessionCartView.kt @@ -76,6 +76,8 @@ class ScheduledSessionCartView @JvmOverloads constructor( internal lateinit var tvHeaderAmount: TextView @BindView(R.id.tv_cart_header_restaurant_name) internal lateinit var tvRestaurantName: TextView + @BindView(R.id.tv_cart_header_restaurant_locality) + internal lateinit var tvRestaurantLocality: TextView lateinit var activity: FragmentActivity lateinit var viewModel: ScheduledCartViewModel @@ -113,7 +115,10 @@ class ScheduledSessionCartView @JvmOverloads constructor( fun isExpanded() = bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED fun dismiss() { - if (isExpanded()) bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + if (isExpanded()) { + billHolder.resetUi() + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } } fun show() { @@ -280,6 +285,7 @@ class ScheduledSessionCartView @JvmOverloads constructor( setTotal(data.bill.total) } tvRestaurantName.text = data.restaurant.name + tvRestaurantLocality.text = data.restaurant.location?.locality } private fun setTotal(total: Double) { diff --git a/app/src/main/java/com/checkin/app/checkin/session/scheduled/ScheduledSessionRepository.kt b/app/src/main/java/com/checkin/app/checkin/session/scheduled/ScheduledSessionRepository.kt index f4d9dae1..1e13e47b 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/scheduled/ScheduledSessionRepository.kt +++ b/app/src/main/java/com/checkin/app/checkin/session/scheduled/ScheduledSessionRepository.kt @@ -65,8 +65,8 @@ class ScheduledSessionRepository private constructor(context: Context) : BaseRep }.asLiveData } - fun syncPostPaytmCallback(data: ObjectNode?): Call { - return mWebService.postPaytmCallback(data!!) + fun syncPostPaytmCallback(data: ObjectNode): Call { + return mWebService.postPaytmCallback(data) } fun postPaytmDetailRequest(sessionId: Long): LiveData> { diff --git a/app/src/main/java/com/checkin/app/checkin/session/scheduled/fragments/CommonOrderDetailFragment.kt b/app/src/main/java/com/checkin/app/checkin/session/scheduled/fragments/CommonOrderDetailFragment.kt index 501c222f..af1481c6 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/scheduled/fragments/CommonOrderDetailFragment.kt +++ b/app/src/main/java/com/checkin/app/checkin/session/scheduled/fragments/CommonOrderDetailFragment.kt @@ -2,36 +2,23 @@ package com.checkin.app.checkin.session.scheduled.fragments import android.os.Bundle import android.view.View -import android.widget.TextView import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer -import butterknife.BindView -import com.airbnb.epoxy.EpoxyRecyclerView -import com.checkin.app.checkin.R import com.checkin.app.checkin.data.resource.Resource import com.checkin.app.checkin.menu.holders.invoiceOrderModelHolder -import com.checkin.app.checkin.misc.BillHolder -import com.checkin.app.checkin.misc.fragments.BaseFragment +import com.checkin.app.checkin.misc.fragments.BaseOrderDetailFragment import com.checkin.app.checkin.session.models.CustomerScheduledSessionDetailModel import com.checkin.app.checkin.session.scheduled.viewmodels.ScheduledSessionDetailViewModel import com.checkin.app.checkin.utility.Utils import com.checkin.app.checkin.utility.isNotEmpty -class CommonOrderDetailFragment : BaseFragment() { - override val rootLayout: Int = R.layout.fragment_user_scheduled_detail_orders_common +class CommonOrderDetailFragment : BaseOrderDetailFragment() { - @BindView(R.id.epoxy_rv_user_scheduled_orders) - internal lateinit var epoxyRvOrders: EpoxyRecyclerView - @BindView(R.id.tv_user_scheduled_session_total) - internal lateinit var tvTotal: TextView - @BindView(R.id.tv_user_scheduled_remarks) - internal lateinit var tvRemarks: TextView - lateinit var billHolder: BillHolder val viewModel: ScheduledSessionDetailViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - billHolder = BillHolder(view) + super.onViewCreated(view, savedInstanceState) epoxyRvOrders.withModels { viewModel.ordersData.value?.data?.takeIf { it.isNotEmpty() }?.forEach { @@ -41,10 +28,10 @@ class CommonOrderDetailFragment : BaseFragment() { } } } - viewModel.ordersData.observe(this, Observer { + viewModel.ordersData.observe(viewLifecycleOwner, Observer { if (it.status == Resource.Status.SUCCESS && it.data != null) epoxyRvOrders.requestModelBuild() }) - viewModel.sessionData.observe(this, Observer { + viewModel.sessionData.observe(viewLifecycleOwner, Observer { if (it.status == Resource.Status.SUCCESS && it.data != null) { setupData(it.data) } else if (it.status == Resource.Status.LOADING) { diff --git a/app/src/main/java/com/checkin/app/checkin/session/scheduled/fragments/QSRFoodReadyFragment.kt b/app/src/main/java/com/checkin/app/checkin/session/scheduled/fragments/QSRFoodReadyFragment.kt index f22e7443..32c2542e 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/scheduled/fragments/QSRFoodReadyFragment.kt +++ b/app/src/main/java/com/checkin/app/checkin/session/scheduled/fragments/QSRFoodReadyFragment.kt @@ -12,11 +12,11 @@ import butterknife.OnClick import com.airbnb.epoxy.EpoxyRecyclerView import com.checkin.app.checkin.R import com.checkin.app.checkin.data.resource.Resource +import com.checkin.app.checkin.home.model.ClosedSessionDetailsModel +import com.checkin.app.checkin.home.viewmodels.ClosedSessionViewModel import com.checkin.app.checkin.misc.BillHolder import com.checkin.app.checkin.misc.fragments.BaseFragment import com.checkin.app.checkin.misc.holders.textModelHolder -import com.checkin.app.checkin.session.models.CustomerScheduledSessionDetailModel -import com.checkin.app.checkin.session.scheduled.viewmodels.ScheduledSessionDetailViewModel import com.checkin.app.checkin.utility.Utils import com.checkin.app.checkin.utility.isNotEmpty import com.checkin.app.checkin.utility.navigateBackToHome @@ -38,7 +38,7 @@ class QSRFoodReadyFragment : BaseFragment() { @BindView(R.id.epoxy_rv_qsr_ready_orders_summary) internal lateinit var epoxyRvOrders: EpoxyRecyclerView - val viewModel: ScheduledSessionDetailViewModel by viewModels() + val viewModel: ClosedSessionViewModel by viewModels() lateinit var billHolder: BillHolder override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -71,11 +71,11 @@ class QSRFoodReadyFragment : BaseFragment() { }) } - private fun setupData(data: CustomerScheduledSessionDetailModel) { + private fun setupData(data: ClosedSessionDetailsModel) { tvRestaurantName.text = data.restaurant.name tvAmount.text = Utils.formatCurrencyAmount(requireContext(), data.bill.total) tvTotal.text = tvAmount.text - tvOrderId.text = data.hashId + tvOrderId.text = data.formatId tvLocation.text = data.restaurant.formatAddress billHolder.bind(data.bill) } diff --git a/app/src/main/java/com/checkin/app/checkin/session/scheduled/viewmodels/NewScheduledSessionViewModel.kt b/app/src/main/java/com/checkin/app/checkin/session/scheduled/viewmodels/NewScheduledSessionViewModel.kt index 5fa8be9b..b239aab6 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/scheduled/viewmodels/NewScheduledSessionViewModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/session/scheduled/viewmodels/NewScheduledSessionViewModel.kt @@ -7,21 +7,17 @@ import androidx.lifecycle.Transformations import androidx.lifecycle.viewModelScope import com.checkin.app.checkin.data.BaseViewModel import com.checkin.app.checkin.data.Converters.objectMapper -import com.checkin.app.checkin.data.network.RetrofitCallAsyncTask import com.checkin.app.checkin.data.resource.Resource import com.checkin.app.checkin.data.resource.Resource.Companion.cloneResource -import com.checkin.app.checkin.data.resource.Resource.Companion.error import com.checkin.app.checkin.data.resource.Resource.Companion.errorNotFound -import com.checkin.app.checkin.data.resource.Resource.Companion.loading -import com.checkin.app.checkin.data.resource.Resource.Companion.success -import com.checkin.app.checkin.misc.paytm.PaytmModel +import com.checkin.app.checkin.payment.PaymentRepository +import com.checkin.app.checkin.payment.models.NewRazorpayTransactionModel import com.checkin.app.checkin.session.SessionRepository import com.checkin.app.checkin.session.models.NewScheduledSessionModel import com.checkin.app.checkin.session.models.PromoDetailModel import com.checkin.app.checkin.session.models.QRResultModel import com.checkin.app.checkin.session.models.SessionPromoModel import com.checkin.app.checkin.session.scheduled.ScheduledSessionRepository -import com.checkin.app.checkin.utility.ProgressRequestBody.UploadCallbacks import com.fasterxml.jackson.databind.node.ObjectNode import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -32,13 +28,14 @@ import java.util.* class NewScheduledSessionViewModel(application: Application) : BaseViewModel(application) { private val sessionRepository = SessionRepository.getInstance(application) private val scheduledSessionRepository = ScheduledSessionRepository.getInstance(application) + private val paymentRepository = PaymentRepository.getInstance(application) private val mNewSessionData = createNetworkLiveData() private val mClearCart = createNetworkLiveData() private val mQrResult = createNetworkLiveData() private val mPromoRemove = createNetworkLiveData() private val mPaytmCallbackData = createNetworkLiveData() - private val mPaytmData = createNetworkLiveData() + private val mTransactionData = createNetworkLiveData() private val mPromoData = createNetworkLiveData>() private val mSessionPromo = createNetworkLiveData() @@ -54,7 +51,7 @@ class NewScheduledSessionViewModel(application: Application) : BaseViewModel(app val newQrSessionData: LiveData> = mQrResult val clearCartData: LiveData> = mClearCart val promoCodes: LiveData>> = mPromoData - val paytmData: LiveData> = mPaytmData + val newTransactionData: LiveData> = mTransactionData val paytmCallbackData: LiveData> = mPaytmCallbackData val promoDeletedData: LiveData> = Transformations.map(mPromoRemove) { input -> @@ -67,6 +64,8 @@ class NewScheduledSessionViewModel(application: Application) : BaseViewModel(app private var lastPaymentBundle: Bundle? = null val sessionAppliedPromo: LiveData> = mSessionPromo + val isOfferApplied: Boolean = mSessionPromo.value?.data != null + private fun createNewScheduledSession(countPeople: Int, plannedTime: Date, restaurantId: Long, remarks: String?) { val body = NewScheduledSessionModel(countPeople = countPeople, plannedDatetime = plannedTime, remarks = remarks).apply { this.restaurantId = restaurantId @@ -94,29 +93,6 @@ class NewScheduledSessionViewModel(application: Application) : BaseViewModel(app } } - fun postPaytmCallback(bundle: Bundle) { - lastPaymentBundle = bundle - val data = objectMapper.createObjectNode() - val keys = bundle.keySet() - for (key in keys) { - data.put(key, bundle[key]?.toString()) - } - val listener: UploadCallbacks = object : UploadCallbacks { - override fun onProgressUpdate(percentage: Int) { - mPaytmCallbackData.postValue(loading(null)) - } - - override fun onSuccess() { - mPaytmCallbackData.postValue(success(null)) - } - - override fun onFailure() { - mPaytmCallbackData.postValue(error("Sorry, but PayTM transaction failed", null)) - } - } - doPostPaytmCallback(data, listener) - } - fun retrySessionCreation() { if (::retryQrData.isInitialized) { createNewQrSession(retryQrData) @@ -126,14 +102,8 @@ class NewScheduledSessionViewModel(application: Application) : BaseViewModel(app } } - fun retryPostPaytmCallback() = lastPaymentBundle?.let { postPaytmCallback(it) } - - fun requestPaytmDetails() { - mPaytmData.addSource(scheduledSessionRepository.postPaytmDetailRequest(sessionPk), mPaytmData::setValue) - } - - private fun doPostPaytmCallback(data: ObjectNode, listener: UploadCallbacks) { - RetrofitCallAsyncTask(listener).execute(scheduledSessionRepository.syncPostPaytmCallback(data)) + fun initiateNewTransaction() { + mTransactionData.addSource(paymentRepository.createNewTransaction(sessionPk), mTransactionData::setValue) } fun createNewQrSession(data: String) { @@ -173,8 +143,6 @@ class NewScheduledSessionViewModel(application: Application) : BaseViewModel(app mPromoData.addSource(liveData, mPromoData::setValue) } - val isOfferApplied: Boolean = mSessionPromo.value?.data != null - fun fetchSessionAppliedPromo() { mSessionPromo.addSource(scheduledSessionRepository.getSessionAppliedPromo(sessionPk), mSessionPromo::setValue) } diff --git a/app/src/main/java/com/checkin/app/checkin/session/scheduled/viewmodels/ScheduledSessionDetailViewModel.kt b/app/src/main/java/com/checkin/app/checkin/session/scheduled/viewmodels/ScheduledSessionDetailViewModel.kt index d20d9b7d..3481eac4 100644 --- a/app/src/main/java/com/checkin/app/checkin/session/scheduled/viewmodels/ScheduledSessionDetailViewModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/session/scheduled/viewmodels/ScheduledSessionDetailViewModel.kt @@ -28,7 +28,6 @@ class ScheduledSessionDetailViewModel(application: Application) : BaseViewModel( } ?: Resource.cloneResource(it, emptyList()) } - fun fetchSessionData(sessionId: Long) { this.sessionId = sessionId mSessionData.addSource(sessionRepository.getCustomerScheduledSessionDetail(sessionId), mSessionData::setValue) diff --git a/app/src/main/java/com/checkin/app/checkin/user/UserRepository.kt b/app/src/main/java/com/checkin/app/checkin/user/UserRepository.kt index 5ec161ab..8fb15d84 100644 --- a/app/src/main/java/com/checkin/app/checkin/user/UserRepository.kt +++ b/app/src/main/java/com/checkin/app/checkin/user/UserRepository.kt @@ -63,6 +63,7 @@ class UserRepository private constructor(context: Context) : BaseRepository() { }.asLiveData } + fun postUserData(objectNode: ObjectNode): LiveData> { return object : NetworkBoundResource() { override fun shouldUseLocalDb(): Boolean = false @@ -73,7 +74,7 @@ class UserRepository private constructor(context: Context) : BaseRepository() { }.asLiveData } - fun postUserProfilePic(pic: File, listener: UploadCallbacks): Call { + fun postUserProfilePic(pic: File, listener: UploadCallbacks): Call { val requestFile = RequestBody.create(MediaType.parse("image/jpeg"), pic) val requestBody = ProgressRequestBody(requestFile, listener) val body = MultipartBody.Part.createFormData("profile_pic", "profile.jpg", requestBody) diff --git a/app/src/main/java/com/checkin/app/checkin/user/viewmodels/UserViewModel.kt b/app/src/main/java/com/checkin/app/checkin/user/viewmodels/UserViewModel.kt index 34512c0f..1c0a8a36 100644 --- a/app/src/main/java/com/checkin/app/checkin/user/viewmodels/UserViewModel.kt +++ b/app/src/main/java/com/checkin/app/checkin/user/viewmodels/UserViewModel.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.lifecycle.LiveData import com.checkin.app.checkin.data.BaseViewModel import com.checkin.app.checkin.data.Converters.objectMapper +import com.checkin.app.checkin.data.network.ApiResponse import com.checkin.app.checkin.data.network.RetrofitCallAsyncTask import com.checkin.app.checkin.data.notifications.MessageUtils import com.checkin.app.checkin.data.notifications.MessageUtils.NotificationUpdate @@ -52,27 +53,28 @@ class UserViewModel(application: Application) : BaseViewModel(application) { fun updateProfilePic(pictureFile: File, context: Context) { val builder = MessageUtils.createUploadNotification(context) - val notificationUpdate: NotificationUpdate = object : NotificationUpdate(context, builder) { + val notificationUpdate = object : NotificationUpdate(context, builder) { override fun onProgressUpdate(percentage: Int) { super.onProgressUpdate(percentage) mImageUploadResult.postValue(loading(null)) } - override fun onSuccess() { - super.onSuccess() + override fun onSuccess(response: ApiResponse) { + super.onSuccess(response) mImageUploadResult.postValue(success(null)) } - override fun onFailure() { - super.onFailure() + override fun onFailure(response: ApiResponse) { + super.onFailure(response) mImageUploadResult.postValue(error("Unable to upload image", null)) } } doUploadImage(pictureFile, notificationUpdate) } - private fun doUploadImage(pictureFile: File, listener: UploadCallbacks) { - RetrofitCallAsyncTask(listener).execute(mRepository.postUserProfilePic(pictureFile, listener)) + private fun doUploadImage(pictureFile: File, listener: UploadCallbacks) { + RetrofitCallAsyncTask(listener) + .execute(mRepository.postUserProfilePic(pictureFile, listener)) } fun postUserData(firstName: String?, lastName: String?, phoneToken: String, bio: String?) { diff --git a/app/src/main/java/com/checkin/app/checkin/utility/Constants.kt b/app/src/main/java/com/checkin/app/checkin/utility/Constants.kt index 6f42d667..68fe36ee 100644 --- a/app/src/main/java/com/checkin/app/checkin/utility/Constants.kt +++ b/app/src/main/java/com/checkin/app/checkin/utility/Constants.kt @@ -30,6 +30,9 @@ object Constants { const val SP_MANAGER_LIVE_ORDER_ACTIVE_KEY = "com.checkin.app.checkin.sp.manager.live.order.key" const val SP_SHOP_PREFERENCES_TABLE = "com.checkin.app.checkin.sp.shop.preferences" + const val LOCATION_CITY_ID = "home.user.location.id" + const val LOCATION_CITY_NAME = "home.user.location.name" + val DEFAULT_ORDER_CANCEL_DURATION = MILLISECONDS.convert(5, MINUTES) val DEFAULT_OTP_AUTO_RETRIEVAL_TIMEOUT = MILLISECONDS.convert(1, MINUTES) diff --git a/app/src/main/java/com/checkin/app/checkin/utility/EnumType.kt b/app/src/main/java/com/checkin/app/checkin/utility/EnumType.kt index 6a4be4f8..22e32871 100644 --- a/app/src/main/java/com/checkin/app/checkin/utility/EnumType.kt +++ b/app/src/main/java/com/checkin/app/checkin/utility/EnumType.kt @@ -34,7 +34,7 @@ interface EnumStringType : EnumType { } sealed class EnumGetter, V> { - abstract fun getByValue(value: V): T + abstract fun getByValue(value: V?): T? abstract fun parseJson(p: JsonParser): V @@ -47,8 +47,14 @@ abstract class EnumIntGetter : EnumGetter() { override fun writeJson(gen: JsonGenerator, value: Int) = gen.writeNumber(value) } +abstract class EnumStringGetter : EnumGetter() { + override fun parseJson(p: JsonParser): String = p.text + + override fun writeJson(gen: JsonGenerator, value: String) = gen.writeString(value) +} + open class EnumDeserializer, V>(private val getter: EnumGetter) : JsonDeserializer() { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): T = getter.getByValue(getter.parseJson(p)) + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): T? = getter.getByValue(getter.parseJson(p)) } open class EnumSerializer, V>(private val getter: EnumGetter) : JsonSerializer() { @@ -58,7 +64,7 @@ open class EnumSerializer, V>(private val getter: EnumGetter, V>(private val getter: EnumGetter) : PropertyConverter { - override fun convertToDatabaseValue(entityProperty: T): V = entityProperty.value + override fun convertToDatabaseValue(entityProperty: T?): V? = entityProperty?.value - override fun convertToEntityProperty(databaseValue: V): T = getter.getByValue(databaseValue) + override fun convertToEntityProperty(databaseValue: V?): T? = getter.getByValue(databaseValue) } \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/utility/FormValidation.kt b/app/src/main/java/com/checkin/app/checkin/utility/FormValidation.kt new file mode 100644 index 00000000..8ed21c3b --- /dev/null +++ b/app/src/main/java/com/checkin/app/checkin/utility/FormValidation.kt @@ -0,0 +1,82 @@ +package com.checkin.app.checkin.utility + +import android.widget.Button +import android.widget.EditText +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment + +fun Button.resetErrors() { + tag = null +} + +fun Button.addError(msg: String?) { + tag = msg?.let { ToastError(it) } +} + +fun Button.addError(view: EditText? = null, @IdRes viewId: Int = 0, msg: String?) { + val key = view?.id ?: viewId + val map = (tag as? MutableMap) ?: mutableMapOf() + msg?.also { + map[key] = InputFieldError(view = view, viewId = viewId, msg = it) + } ?: also { + view?.error = null + map.remove(key) + } + tag = map +} + +val Button.isValidForm: Boolean + get() = tag?.let { !(it is FormError || (it is Map<*, *> && it.isNotEmpty())) } ?: true + +fun Button.validateField(view: EditText): Boolean { + (tag as? Map<*, *>)?.also { + val error = (it[view.id] as? InputFieldError) + view.error = error?.msg ?: return true + return false + } + return true +} + +fun Fragment.validateForm(button: Button): Boolean = kotlin.runCatching { + if (button.isValidForm) return true + button.tag.let { + when (it) { + is ToastError -> { + toast(it.msg) + false + } + is Map<*, *> -> { + it.forEach { + val error = it.value as InputFieldError + if (error.view != null) error.view.error = error.msg + else if (error.viewId != 0) view?.findViewById(error.viewId)?.error = error.msg + } + false + } + else -> true + } + } +}.getOrNull() ?: true + +fun AppCompatActivity.validateForm(button: Button): Boolean = button.tag?.let { + when (it) { + is ToastError -> { + toast(it.msg) + false + } + is Map<*, *> -> { + it.forEach { + val error = it.value as InputFieldError + if (error.view != null) error.view.error = error.msg + else if (error.viewId != 0) findViewById(error.viewId)?.error = error.msg + } + false + } + else -> true + } +} ?: false + +sealed class FormError +data class ToastError(val msg: String) : FormError() +data class InputFieldError(@IdRes val viewId: Int = 0, val view: EditText? = null, val msg: String) : FormError() diff --git a/app/src/main/java/com/checkin/app/checkin/utility/KExtensions.kt b/app/src/main/java/com/checkin/app/checkin/utility/KExtensions.kt index 7cf9303c..afd6b310 100644 --- a/app/src/main/java/com/checkin/app/checkin/utility/KExtensions.kt +++ b/app/src/main/java/com/checkin/app/checkin/utility/KExtensions.kt @@ -5,24 +5,25 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter import android.location.LocationManager import android.net.Uri import android.view.View +import android.widget.ImageView import androidx.annotation.ColorInt import androidx.annotation.StringRes import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.location.LocationManagerCompat +import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.checkin.app.checkin.misc.models.GeolocationModel import com.checkin.app.checkin.misc.models.LocationModel import com.google.android.material.tabs.TabLayout -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.channelFlow +import com.google.firebase.analytics.FirebaseAnalytics import java.util.* import java.util.concurrent.TimeUnit import kotlin.math.abs @@ -42,15 +43,25 @@ fun TabLayout.setTabBackground(@ColorInt color: Int) = (0 until tabCount).forEac getTabAt(it)?.view?.setBackgroundColor(color) } +fun ImageView.blackAndWhite() { + colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0.0f) }) +} + fun Context.toast(msg: String?) = Utils.toast(this, msg) fun Context.toast(@StringRes msgRes: Int) = Utils.toast(this, msgRes) +fun Fragment.toast(msg: String?) = context?.run { toast(msg) } +fun Fragment.toast(@StringRes msgRes: Int) = context?.run { toast(msgRes) } + fun Activity.snack(msg: String) = Utils.snack(this, msg) fun View.snack(@StringRes msgRes: Int) = Utils.snack(this, msgRes) fun Activity.errorSnack(msg: String) = Utils.errorSnack(this, msg) fun Context.navigateBackToHome() = Utils.navigateBackToHome(this) +val Activity.firebaseAnalytics + get() = FirebaseAnalytics.getInstance(this) + /* Data Utils */ @@ -71,6 +82,16 @@ fun String.callPhoneNumber(context: Context) { ContextCompat.startActivity(context, Intent(Intent.ACTION_DIAL, phoneIntent), null) } +fun String.toHtml() = Utils.fromHtml(this) + +fun Double.toCurrency(context: Context) = Utils.formatCurrencyAmount(context, this) + +fun MutableMap.putAll(vararg pairs: Pair) { + for ((key, value) in pairs) { + put(key, value) + } +} + /* Datetime */ @@ -97,22 +118,18 @@ operator fun Calendar.minus(other: Calendar): Int { fun Date.toCalendar(): Calendar = Calendar.getInstance().apply { time = this@toCalendar } +fun Date.add(unit: Int, amount: Int): Date = toCalendar() + .apply { add(unit, amount) } + .time + +fun Throwable.log(tag: String, errMsg: String? = null) = Utils.logErrors(tag, this, errMsg) + /* Lifecycle */ val LifecycleOwner.coroutineLifecycleScope: LifecycleCoroutineScope get() = lifecycleScope -@ExperimentalCoroutinesApi -fun LiveData.asFlow() = channelFlow { - offer(value) - val observer = Observer { t -> offer(t) } - observeForever(observer) - invokeOnClose { - removeObserver(observer) - } -} - /* Permissions */ diff --git a/app/src/main/java/com/checkin/app/checkin/utility/ProgressRequestBody.kt b/app/src/main/java/com/checkin/app/checkin/utility/ProgressRequestBody.kt index 58d23bb9..0c25f821 100644 --- a/app/src/main/java/com/checkin/app/checkin/utility/ProgressRequestBody.kt +++ b/app/src/main/java/com/checkin/app/checkin/utility/ProgressRequestBody.kt @@ -1,12 +1,13 @@ package com.checkin.app.checkin.utility import com.checkin.app.checkin.BuildConfig +import com.checkin.app.checkin.data.network.ApiResponse import okhttp3.MediaType import okhttp3.RequestBody import okio.* import java.io.IOException -class ProgressRequestBody(private val mDelegate: RequestBody, private val mListener: UploadCallbacks) : RequestBody() { +class ProgressRequestBody(private val mDelegate: RequestBody, private val mListener: UploadCallbacks<*>) : RequestBody() { private var mUpdateProgressFlag = !BuildConfig.DEBUG override fun contentType(): MediaType? = mDelegate.contentType() @@ -31,12 +32,12 @@ class ProgressRequestBody(private val mDelegate: RequestBody, private val mListe bufferedSink.flush() } - interface UploadCallbacks { + interface UploadCallbacks { fun onProgressUpdate(percentage: Int) - fun onSuccess() + fun onSuccess(response: ApiResponse) - fun onFailure() + fun onFailure(response: ApiResponse) } internal inner class ProgressSink(delegate: Sink) : ForwardingSink(delegate) { diff --git a/app/src/main/java/com/checkin/app/checkin/utility/SingletonHolder.kt b/app/src/main/java/com/checkin/app/checkin/utility/SingletonHolder.kt index 1813e677..ab26aeb9 100644 --- a/app/src/main/java/com/checkin/app/checkin/utility/SingletonHolder.kt +++ b/app/src/main/java/com/checkin/app/checkin/utility/SingletonHolder.kt @@ -1,26 +1,35 @@ package com.checkin.app.checkin.utility -open class SingletonHolder(creator: (A) -> T) { - private var creator: ((A) -> T)? = creator +import androidx.annotation.CallSuper +import java.lang.ref.WeakReference + +open class SingletonHolder(val creator: (A) -> T) { @Volatile private var instance: T? = null - fun getInstance(arg: A): T { - val i = instance - if (i != null) { - return i - } - - return synchronized(this) { - val i2 = instance - if (i2 != null) { - i2 - } else { - val created = creator!!(arg) - instance = created - creator = null - created - } - } + protected open fun createInstance(arg: A) = creator(arg) + + protected fun resetInstance() { + instance = null + } + + @CallSuper + open fun getInstance(arg: A? = null): T = instance ?: synchronized(this) { + instance ?: createInstance(arg!!).also { instance = it } + } +} + +open class ConflatedSingletonHolder(creator: (A) -> T) : SingletonHolder(creator) { + @Volatile + private var lastArg: WeakReference? = null + + override fun createInstance(arg: A): T { + lastArg = WeakReference(arg) + return super.createInstance(arg) + } + + override fun getInstance(arg: A?): T { + if (arg != null && lastArg != arg) resetInstance() + return super.getInstance(arg) } } \ No newline at end of file diff --git a/app/src/main/java/com/checkin/app/checkin/utility/Utils.java b/app/src/main/java/com/checkin/app/checkin/utility/Utils.java index 5d1d7a73..c988dd0f 100644 --- a/app/src/main/java/com/checkin/app/checkin/utility/Utils.java +++ b/app/src/main/java/com/checkin/app/checkin/utility/Utils.java @@ -243,7 +243,7 @@ public static String formatDateTo24HoursTime(@NonNull Date dateTime) { } public static String formatDateTo12HoursTime(@NonNull Date dateTime) { - return new SimpleDateFormat("HH:mm a", Locale.ENGLISH).format(dateTime); + return new SimpleDateFormat("hh:mm a", Locale.ENGLISH).format(dateTime); } public static String getCurrentFormattedDateInvoice() { @@ -278,24 +278,7 @@ public static String formatDate(@NonNull Date date, String dateFormat) { } public static String formatTimeDuration(long milliSec) { - long secondsInMilli = 1000; - long minutesInMilli = secondsInMilli * 60; - long hoursInMilli = minutesInMilli * 60; - - long elapsedHours = milliSec / hoursInMilli; - milliSec = milliSec % hoursInMilli; - long elapsedMinutes = milliSec / minutesInMilli; - milliSec = milliSec % minutesInMilli; - long elapsedSeconds = milliSec / secondsInMilli; - milliSec = milliSec % secondsInMilli; - - if (elapsedHours > 0) - return String.format(Locale.ENGLISH, "%d Hours", elapsedHours); - if (elapsedMinutes > 0) - return String.format(Locale.ENGLISH, "%d Mins", elapsedMinutes); - if (elapsedSeconds > 0) - return String.format(Locale.ENGLISH, "%d Sec", elapsedSeconds); - return "0 Sec"; + return formatDueTime(milliSec); } public static String formatElapsedTime(@NonNull Date eventTime) { @@ -318,7 +301,11 @@ public static String formatElapsedTime(@NonNull Date eventTime, @NonNull Date cu } public static String formatDueTime(@NonNull Date startTime, @NonNull Date endTime) { - Pair pair = getTimeDifference(startTime, endTime); + return formatDueTime(endTime.getTime() - startTime.getTime()); + } + + public static String formatDueTime(long diffTime) { + Pair pair = getTimeDifference(diffTime); long value = pair.getSecond(); String suffix = value > 1 ? "s " : " "; switch (pair.getFirst()) { @@ -334,6 +321,10 @@ public static String formatDueTime(@NonNull Date startTime, @NonNull Date endTim public static Pair getTimeDifference(@NonNull Date start, @NonNull Date end) { long diffTime = end.getTime() - start.getTime(); + return getTimeDifference(diffTime); + } + + public static Pair getTimeDifference(long diffTime) { long secondsInMilli = 1000; long minutesInMilli = secondsInMilli * 60; long hoursInMilli = minutesInMilli * 60; diff --git a/app/src/main/res/drawable-mdpi/ic_google_play.png b/app/src/main/res/drawable-mdpi/ic_google_play.png deleted file mode 100644 index b39a6eed..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_google_play.png and /dev/null differ diff --git a/app/src/main/res/drawable/background_rounded_borders.xml b/app/src/main/res/drawable/background_rounded_borders.xml new file mode 100644 index 00000000..74d787cb --- /dev/null +++ b/app/src/main/res/drawable/background_rounded_borders.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_background_pinkish_grey.xml b/app/src/main/res/drawable/button_background_pinkish_grey.xml new file mode 100644 index 00000000..faa3497f --- /dev/null +++ b/app/src/main/res/drawable/button_background_pinkish_grey.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_green_action_selector.xml b/app/src/main/res/drawable/button_green_action_selector.xml new file mode 100644 index 00000000..0a3302d0 --- /dev/null +++ b/app/src/main/res/drawable/button_green_action_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient_hourglass.xml b/app/src/main/res/drawable/gradient_hourglass.xml new file mode 100644 index 00000000..d19a320d --- /dev/null +++ b/app/src/main/res/drawable/gradient_hourglass.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 00000000..6083a649 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_cross_black.xml b/app/src/main/res/drawable/ic_cross_black.xml new file mode 100644 index 00000000..cf0fa90f --- /dev/null +++ b/app/src/main/res/drawable/ic_cross_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_google_play.png b/app/src/main/res/drawable/ic_google_play.png new file mode 100644 index 00000000..61c2e590 Binary files /dev/null and b/app/src/main/res/drawable/ic_google_play.png differ diff --git a/app/src/main/res/drawable/ic_hourglass_transparent.xml b/app/src/main/res/drawable/ic_hourglass_transparent.xml new file mode 100644 index 00000000..a91797de --- /dev/null +++ b/app/src/main/res/drawable/ic_hourglass_transparent.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_human.xml b/app/src/main/res/drawable/ic_human.xml new file mode 100644 index 00000000..8425a45f --- /dev/null +++ b/app/src/main/res/drawable/ic_human.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000..c4c7f309 --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_location_marker_grey.xml b/app/src/main/res/drawable/ic_location_marker_grey.xml new file mode 100644 index 00000000..971a2056 --- /dev/null +++ b/app/src/main/res/drawable/ic_location_marker_grey.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_order_summary.xml b/app/src/main/res/drawable/ic_order_summary.xml new file mode 100644 index 00000000..11eb8da3 --- /dev/null +++ b/app/src/main/res/drawable/ic_order_summary.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_payment_card_amex.xml b/app/src/main/res/drawable/ic_payment_card_amex.xml new file mode 100644 index 00000000..e1603e40 --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_card_amex.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_payment_card_maestro.xml b/app/src/main/res/drawable/ic_payment_card_maestro.xml new file mode 100644 index 00000000..31de3f2e --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_card_maestro.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_payment_card_mastercard.xml b/app/src/main/res/drawable/ic_payment_card_mastercard.xml new file mode 100644 index 00000000..e59706fb --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_card_mastercard.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_payment_card_rupay.xml b/app/src/main/res/drawable/ic_payment_card_rupay.xml new file mode 100644 index 00000000..a1b6d41c --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_card_rupay.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_payment_card_visa.xml b/app/src/main/res/drawable/ic_payment_card_visa.xml new file mode 100644 index 00000000..d73eeccf --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_card_visa.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_payment_netbanking_axis.xml b/app/src/main/res/drawable/ic_payment_netbanking_axis.xml new file mode 100644 index 00000000..b1543958 --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_netbanking_axis.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_payment_netbanking_kotak.xml b/app/src/main/res/drawable/ic_payment_netbanking_kotak.xml new file mode 100644 index 00000000..1f552108 --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_netbanking_kotak.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_payment_netbanking_sbi.xml b/app/src/main/res/drawable/ic_payment_netbanking_sbi.xml new file mode 100644 index 00000000..56502f04 --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_netbanking_sbi.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_payment_upi.xml b/app/src/main/res/drawable/ic_payment_upi.xml new file mode 100644 index 00000000..d5548c33 --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_upi.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_round_chip_orange.xml b/app/src/main/res/drawable/ic_round_chip_orange.xml new file mode 100644 index 00000000..5422e09e --- /dev/null +++ b/app/src/main/res/drawable/ic_round_chip_orange.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_grey.xml b/app/src/main/res/drawable/ic_search_grey.xml index a5b31a6e..4aa954c1 100644 --- a/app/src/main/res/drawable/ic_search_grey.xml +++ b/app/src/main/res/drawable/ic_search_grey.xml @@ -1,5 +1,10 @@ - - - + + diff --git a/app/src/main/res/drawable/ic_user_location.xml b/app/src/main/res/drawable/ic_user_location.xml new file mode 100644 index 00000000..0197b031 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_location.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/layout_border_grey.xml b/app/src/main/res/drawable/layout_border_grey.xml new file mode 100644 index 00000000..fc71c9f2 --- /dev/null +++ b/app/src/main/res/drawable/layout_border_grey.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_bar_states.xml b/app/src/main/res/drawable/progress_bar_states.xml new file mode 100644 index 00000000..d4e27183 --- /dev/null +++ b/app/src/main/res/drawable/progress_bar_states.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/round_border_button_grey.xml b/app/src/main/res/drawable/round_border_button_grey.xml new file mode 100644 index 00000000..c469c51d --- /dev/null +++ b/app/src/main/res/drawable/round_border_button_grey.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/round_border_button_orange.xml b/app/src/main/res/drawable/round_border_button_orange.xml new file mode 100644 index 00000000..d9821262 --- /dev/null +++ b/app/src/main/res/drawable/round_border_button_orange.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_closed_session_details.xml b/app/src/main/res/layout/activity_closed_session_details.xml new file mode 100644 index 00000000..9e19dc59 --- /dev/null +++ b/app/src/main/res/layout/activity_closed_session_details.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_closed_transaction.xml b/app/src/main/res/layout/activity_closed_transaction.xml new file mode 100644 index 00000000..7f46e4ee --- /dev/null +++ b/app/src/main/res/layout/activity_closed_transaction.xml @@ -0,0 +1,22 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 44221cbe..a73e3d71 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -22,15 +22,52 @@ android:id="@+id/toolbar_home" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize"> - - - + + + + + + + + + diff --git a/app/src/main/res/layout/activity_manager_preorder_session_detail.xml b/app/src/main/res/layout/activity_manager_preorder_session_detail.xml index 74aec386..00d4d8c1 100644 --- a/app/src/main/res/layout/activity_manager_preorder_session_detail.xml +++ b/app/src/main/res/layout/activity_manager_preorder_session_detail.xml @@ -10,7 +10,7 @@ @@ -22,9 +22,6 @@ + app:layout_constraintVertical_bias="0.0" /> diff --git a/app/src/main/res/layout/activity_manager_session_invoice.xml b/app/src/main/res/layout/activity_manager_session_invoice.xml index 377eab2e..54ee7d9f 100644 --- a/app/src/main/res/layout/activity_manager_session_invoice.xml +++ b/app/src/main/res/layout/activity_manager_session_invoice.xml @@ -64,7 +64,7 @@ android:id="@+id/ed_ms_invoice_discount" style="@style/GreyTextStyle" android:layout_width="@dimen/width_input_large" - android:layout_height="@dimen/height_input_large" + android:layout_height="@dimen/height_input_normal" android:layout_marginEnd="@dimen/spacing_huge" android:background="@drawable/bordered_text_light_grey" android:enabled="false" @@ -150,7 +150,7 @@ android:id="@+id/ed_ms_invoice_contact" style="@style/GreyTextStyle" android:layout_width="@dimen/button_width_wide" - android:layout_height="@dimen/height_input_large" + android:layout_height="@dimen/height_input_normal" android:layout_marginTop="@dimen/spacing_small" android:layout_marginEnd="@dimen/spacing_large" android:background="@drawable/bordered_text_light_grey" diff --git a/app/src/main/res/layout/activity_payment.xml b/app/src/main/res/layout/activity_payment.xml new file mode 100644 index 00000000..71516914 --- /dev/null +++ b/app/src/main/res/layout/activity_payment.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_auth_options.xml b/app/src/main/res/layout/fragment_auth_options.xml index 306ab250..bb70f722 100644 --- a/app/src/main/res/layout/fragment_auth_options.xml +++ b/app/src/main/res/layout/fragment_auth_options.xml @@ -11,7 +11,7 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_closed_session.xml b/app/src/main/res/layout/fragment_closed_session.xml new file mode 100644 index 00000000..a153287f --- /dev/null +++ b/app/src/main/res/layout/fragment_closed_session.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_manager_invoice.xml b/app/src/main/res/layout/fragment_manager_invoice.xml index c2863f4b..74d07158 100644 --- a/app/src/main/res/layout/fragment_manager_invoice.xml +++ b/app/src/main/res/layout/fragment_manager_invoice.xml @@ -1,14 +1,18 @@ - + android:layout_height="match_parent"> - + android:layout_height="match_parent" + android:orientation="vertical"> - \ No newline at end of file + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_manager_session_order.xml b/app/src/main/res/layout/fragment_manager_session_order.xml index 18e6e478..f212590c 100644 --- a/app/src/main/res/layout/fragment_manager_session_order.xml +++ b/app/src/main/res/layout/fragment_manager_session_order.xml @@ -8,7 +8,10 @@ + android:layout_height="match_parent" + android:layout_alignParentBottom="true" + + android:layout_marginBottom="0dp"> @@ -100,10 +105,10 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:background="@color/brownish_grey" - android:paddingTop="@dimen/spacing_small" - android:paddingBottom="@dimen/spacing_small" android:paddingStart="@dimen/spacing_large" - android:paddingEnd="@dimen/spacing_large"> + android:paddingTop="@dimen/spacing_small" + android:paddingEnd="@dimen/spacing_large" + android:paddingBottom="@dimen/spacing_small"> + android:text="Generate Bill" + android:textSize="@dimen/font_normal" /> + + + + + + + + + + + + + + + + + + + + + + + + + + +