Repository: i-vishi/shopping-android-app Branch: master Commit: 1768ebf52bb9 Files: 223 Total size: 593.6 KB Directory structure: gitextract_6suav1uc/ ├── .gitignore ├── .idea/ │ ├── .gitignore │ ├── .name │ ├── codeStyles/ │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── compiler.xml │ ├── gradle.xml │ ├── jarRepositories.xml │ ├── misc.xml │ ├── render.experimental.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── vishalgaur/ │ │ └── shoppingapp/ │ │ ├── AppDatabaseTest.kt │ │ ├── ClickClickableSpan.kt │ │ ├── ExampleInstrumentedTest.kt │ │ ├── LiveDataTestUtil.kt │ │ ├── MainCoroutineRule.kt │ │ ├── RecyclerViewMatcherUtils.kt │ │ ├── data/ │ │ │ └── source/ │ │ │ ├── FakeAuthRepository.kt │ │ │ ├── FakeProductsDataSource.kt │ │ │ ├── FakeProductsRepository.kt │ │ │ ├── FakeUserDataSource.kt │ │ │ └── repository/ │ │ │ ├── AuthRepositoryTest.kt │ │ │ └── ProductsRepositoryTest.kt │ │ ├── ui/ │ │ │ ├── home/ │ │ │ │ ├── HomeFragmentTest.kt │ │ │ │ └── ProductDetailsFragmentTest.kt │ │ │ └── loginSignup/ │ │ │ ├── LoginFragmentTest.kt │ │ │ └── SignupFragmentTest.kt │ │ └── viewModels/ │ │ ├── AddEditAddressViewModelTest.kt │ │ ├── AddEditProductViewModelTest.kt │ │ ├── AuthViewModelTest.kt │ │ ├── HomeViewModelTest.kt │ │ ├── OrderViewModelTest.kt │ │ └── ProductViewModelTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── vishalgaur/ │ │ │ └── shoppingapp/ │ │ │ ├── ServiceLocator.kt │ │ │ ├── ShoppingApplication.kt │ │ │ ├── Utils.kt │ │ │ ├── data/ │ │ │ │ ├── Product.kt │ │ │ │ ├── Result.kt │ │ │ │ ├── ShoppingAppSessionManager.kt │ │ │ │ ├── UserData.kt │ │ │ │ ├── source/ │ │ │ │ │ ├── ProductDataSource.kt │ │ │ │ │ ├── UserDataSource.kt │ │ │ │ │ ├── local/ │ │ │ │ │ │ ├── ProductsDao.kt │ │ │ │ │ │ ├── ProductsLocalDataSource.kt │ │ │ │ │ │ ├── ShoppingAppDatabase.kt │ │ │ │ │ │ ├── UserDao.kt │ │ │ │ │ │ └── UserLocalDataSource.kt │ │ │ │ │ ├── remote/ │ │ │ │ │ │ ├── AuthRemoteDataSource.kt │ │ │ │ │ │ └── ProductsRemoteDataSource.kt │ │ │ │ │ └── repository/ │ │ │ │ │ ├── AuthRepoInterface.kt │ │ │ │ │ ├── AuthRepository.kt │ │ │ │ │ ├── ProductsRepoInterface.kt │ │ │ │ │ └── ProductsRepository.kt │ │ │ │ └── utils/ │ │ │ │ ├── DateTypeConvertors.kt │ │ │ │ ├── EmailMobileData.kt │ │ │ │ ├── ListTypeConverter.kt │ │ │ │ ├── ObjectListTypeConvertor.kt │ │ │ │ ├── ProductUtils.kt │ │ │ │ └── Utils.kt │ │ │ ├── ui/ │ │ │ │ ├── LaunchActivity.kt │ │ │ │ ├── RecyclerViewPaddingItemDecoration.kt │ │ │ │ ├── UiUtils.kt │ │ │ │ ├── home/ │ │ │ │ │ ├── AccountFragment.kt │ │ │ │ │ ├── AddEditAddressFragment.kt │ │ │ │ │ ├── AddEditProductFragment.kt │ │ │ │ │ ├── AddProductImagesAdapter.kt │ │ │ │ │ ├── AddressAdapter.kt │ │ │ │ │ ├── AddressFragment.kt │ │ │ │ │ ├── CartFragment.kt │ │ │ │ │ ├── CartItemAdapter.kt │ │ │ │ │ ├── FavoritesFragment.kt │ │ │ │ │ ├── HomeFragment.kt │ │ │ │ │ ├── LikedProductAdapter.kt │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ ├── OrderDetailsFragment.kt │ │ │ │ │ ├── OrderProductsAdapter.kt │ │ │ │ │ ├── OrderSuccessFragment.kt │ │ │ │ │ ├── OrdersAdapter.kt │ │ │ │ │ ├── OrdersFragment.kt │ │ │ │ │ ├── PayByAdapter.kt │ │ │ │ │ ├── ProductAdapter.kt │ │ │ │ │ ├── ProductDetailsFragment.kt │ │ │ │ │ ├── ProductImagesAdapter.kt │ │ │ │ │ ├── ProfileFragment.kt │ │ │ │ │ ├── SelectAddressFragment.kt │ │ │ │ │ └── SelectPaymentFragment.kt │ │ │ │ └── loginSignup/ │ │ │ │ ├── LoginFragment.kt │ │ │ │ ├── LoginSignupActivity.kt │ │ │ │ ├── LoginSignupBaseFragment.kt │ │ │ │ ├── OtpActivity.kt │ │ │ │ └── SignupFragment.kt │ │ │ └── viewModels/ │ │ │ ├── AddEditAddressViewModel.kt │ │ │ ├── AddEditProductViewModel.kt │ │ │ ├── AuthViewModel.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── OrderViewModel.kt │ │ │ ├── OtpViewModel.kt │ │ │ └── ProductViewModel.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── address_account_drawable.xml │ │ │ ├── avatar_background.xml │ │ │ ├── bottom_nav_selector.xml │ │ │ ├── btn_gradient.xml │ │ │ ├── card_item_selector.xml │ │ │ ├── color_radio_normal.xml │ │ │ ├── color_radio_selected.xml │ │ │ ├── color_radio_selector.xml │ │ │ ├── dotted_line_drawable.xml │ │ │ ├── heart_icon_drawable.xml │ │ │ ├── ic_add_24.xml │ │ │ ├── ic_add_48.xml │ │ │ ├── ic_add_shopping_cart_24.xml │ │ │ ├── ic_baseline_person_24.xml │ │ │ ├── ic_baseline_shopping_cart_24.xml │ │ │ ├── ic_cancel_24.xml │ │ │ ├── ic_chevron_left_48.xml │ │ │ ├── ic_delete_24.xml │ │ │ ├── ic_edit_24.xml │ │ │ ├── ic_favorite_filled_24.xml │ │ │ ├── ic_favorite_outlined_24.xml │ │ │ ├── ic_filled_check_circle_24.xml │ │ │ ├── ic_filled_library_books_24.xml │ │ │ ├── ic_filled_location_on_24.xml │ │ │ ├── ic_filled_logout_24.xml │ │ │ ├── ic_filled_person_24.xml │ │ │ ├── ic_filter_alt_24.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_menu_24.xml │ │ │ ├── ic_outline_arrow_back_24.xml │ │ │ ├── ic_outline_email_24.xml │ │ │ ├── ic_outline_library_books_24.xml │ │ │ ├── ic_outline_phone_android_24.xml │ │ │ ├── ic_outlined_home_24.xml │ │ │ ├── ic_outlined_person_24.xml │ │ │ ├── ic_outlined_shopping_cart_24.xml │ │ │ ├── ic_remove_24.xml │ │ │ ├── ic_remove_shopping_cart_24.xml │ │ │ ├── ic_search_24.xml │ │ │ ├── layout_background_rounded_corners.xml │ │ │ ├── liked_heart_drawable.xml │ │ │ ├── login_bg_img.xml │ │ │ ├── orders_account_drawable.xml │ │ │ ├── person_account_drawable.xml │ │ │ ├── radio_normal.xml │ │ │ ├── radio_selected.xml │ │ │ ├── radio_selector.xml │ │ │ ├── round_button.xml │ │ │ ├── round_outline_rect.xml │ │ │ ├── signout_account_drawable.xml │ │ │ └── sl_favourite_24dp.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── font/ │ │ │ ├── nunito_sans.xml │ │ │ └── nunito_sans_extrabold.xml │ │ ├── layout/ │ │ │ ├── activity_launch.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_otp.xml │ │ │ ├── activity_signup.xml │ │ │ ├── add_images_item.xml │ │ │ ├── cart_list_item.xml │ │ │ ├── country_list_item.xml │ │ │ ├── fragment_account.xml │ │ │ ├── fragment_add_edit_address.xml │ │ │ ├── fragment_add_edit_product.xml │ │ │ ├── fragment_address.xml │ │ │ ├── fragment_cart.xml │ │ │ ├── fragment_favorites.xml │ │ │ ├── fragment_home.xml │ │ │ ├── fragment_login.xml │ │ │ ├── fragment_order_details.xml │ │ │ ├── fragment_order_success.xml │ │ │ ├── fragment_orders.xml │ │ │ ├── fragment_product_details.xml │ │ │ ├── fragment_profile.xml │ │ │ ├── fragment_select_address.xml │ │ │ ├── fragment_select_payment.xml │ │ │ ├── fragment_signup.xml │ │ │ ├── images_item.xml │ │ │ ├── layout_address_card.xml │ │ │ ├── layout_circular_loader.xml │ │ │ ├── layout_home_ad.xml │ │ │ ├── layout_home_top_app_bar.xml │ │ │ ├── layout_list_item.xml │ │ │ ├── layout_loader_card.xml │ │ │ ├── layout_no_icon_app_bar.xml │ │ │ ├── layout_order_summary_card.xml │ │ │ ├── layout_price_card.xml │ │ │ ├── layout_shipping_card.xml │ │ │ ├── layout_top_bar.xml │ │ │ └── products_list_item.xml │ │ ├── menu/ │ │ │ ├── app_bar_menu.xml │ │ │ ├── bottom_navigation_menu.xml │ │ │ ├── home_app_bar_menu.xml │ │ │ ├── menu_main.xml │ │ │ └── menu_with_add_only.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── navigation/ │ │ │ ├── home_nav_graph.xml │ │ │ └── signup_nav_graph.xml │ │ ├── values/ │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── font_certs.xml │ │ │ ├── preloaded_fonts.xml │ │ │ ├── shapes.xml │ │ │ ├── strings.xml │ │ │ ├── styles.xml │ │ │ ├── themes.xml │ │ │ └── type.xml │ │ └── values-night/ │ │ └── themes.xml │ └── test/ │ └── java/ │ └── com/ │ └── vishalgaur/ │ └── shoppingapp/ │ └── UtilsTest.kt ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx local.properties ================================================ FILE: .idea/.gitignore ================================================ ================================================ FILE: .idea/.name ================================================ Shopping App ================================================ FILE: .idea/codeStyles/Project.xml ================================================ ================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/jarRepositories.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/render.experimental.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Vishal Gaur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Shopping Android App An e-commerce android application written in Kotlin where users can sell and buy products. ## Overview The application contains list of products such as shoes, slippers on which user can click to view its details and then, add them to cart. User can like and dislike the product as well. Also, User can sell products, if he/she signed up as a Seller. Some other features are as following: - Login / Signup with OTP Verification. - Recyclerview with variable span size to show products. - Search Bar and filtering - Product detail screen with image carousel and custom Radio Buttons. - Add/Edit Product for Sellers - See all orders placed. - Increase/Decrease quantity of product in cart. - Place Order. - Modify status of order for Seller. - Add/Edit Address - Tested using Espresso. Written unit, instrumentation and UI tests. ## Some Screenshots | Splash Screen | Application Home | Product Detail | | :----------------------------------: | :---------------------------------------: | :----------------------------------:| | ![](snapshots/shopping-launcher.png) | ![](snapshots/shopping-home-customer.png) | ![](snapshots/shopping-product.png) | | Signup | Login | OTP Verification | | :---------------------------------: | :-------------------------------: | :------------------------------:| | ![](snapshots/shopping-sign-up.png) | ![](snapshots/shopping-login.png) | ![](snapshots/shopping-otp.png) | | Shopping Cart | Address Selection | Payment Method | Order Success | | :------------------------------: | :----------------------------------------: | :-------------------------------------:| :---------------------------------------: | | ![](snapshots/shopping-cart.png) | ![](snapshots/shopping-select-address.png) | ![](snapshots/shopping-choose-pay.png) | ![](snapshots/shopping-order-success.png) | | Add Product | All Orders | Order Detail | Sign Out | | :-------------------------------------: | :--------------------------------: | :---------------------------------------:| :----------------------------------: | | ![](snapshots/shopping-add-product.png) | ![](snapshots/shopping-orders.png) | ![](snapshots/shopping-order-detail.png) | ![](snapshots/shopping-sign-out.png) | ## Project Setup ### Clone and install Clone this repository and import into Android Studio ``` git clone https://github.com/i-vishi/shopping-android-app.git ``` ### Configuration - The project requires Firebase. So follow the steps given [here (Add Firebase to Android Project)](https://firebase.google.com/docs/android/setup) to add firebase to your android project. - Download the firebase config file `google-services.json` - Move the config file to `(app)` module of the project. - Also, add Cloud Firestore for storing users, products, orders and addresses data. Follow instructions [here (Add Cloud Firestore to your app)](https://firebase.google.com/docs/firestore/quickstart) to add Cloud Firestore to your app. - There need to be two collections - `users` for Users data and `products` for Products Data. - This project also, requires OTP based authentication. So, you just need to enable Phone Number sign-in in your firebase project. Follow instructions [here (Enable Phone Number sign-in)](https://firebase.google.com/docs/auth/android/phone-auth) to enable Phone Number sign-in. - Do not forget to enable app verification for your firebase project. Follow instructions [here (Enable app verification)](https://firebase.google.com/docs/auth/android/phone-auth#enable-app-verification) to enable app verification. Add both SHA-1 and SHA-256 fingerprints. Tried everything but still not able to explore the app due to OTP errors? Don't worry, you can by-pass the OTP screen and explore the app. - Go to `app/src/main/java/com/vishalgaur/shoppingapp/Utils.kt` file. - Change the return value for function `shouldBypassOTPValidation()` to `true`. - You are good to go now. Just run the app and explore. - And take your time to setup the OTP verification. :wink: ## Star History Star History Chart ## Built With - Kotlin - Firebase - Room - Material - Glide ---

Made with :blue_heart: by Vishal Gaur

================================================ FILE: app/.gitignore ================================================ /build /google-services.json ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' id 'com.google.gms.google-services' id 'kotlin-parcelize' } android { compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.vishalgaur.shoppingapp" minSdkVersion 23 targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildFeatures { viewBinding true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } packagingOptions { exclude 'META-INF/AL2.0' exclude 'META-INF/LGPL2.1' } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // Source: https://github.com/Kotlin/kotlinx.coroutines/tree/master/integration/kotlinx-coroutines-play-services implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1' implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' // RecyclerView - beta with ConcatAdapter (previously MergeAdapter) implementation "androidx.recyclerview:recyclerview:1.2.0" // Room implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" implementation 'androidx.legacy:legacy-support-v4:1.0.0' kapt "androidx.room:room-compiler:$room_version" // ViewModel and LiveData implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-auth-ktx' // Glide implementation "com.github.bumptech.glide:glide:$glide_version" //Firebase implementation platform('com.google.firebase:firebase-bom:27.0.0') implementation 'com.google.firebase:firebase-firestore-ktx' implementation 'com.google.firebase:firebase-storage-ktx' // Testing testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation "androidx.test.ext:junit-ktx:1.1.2" androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' // AndroidJUnitRunner and JUnit Rules androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0' // espresso for intents androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0' //espresso for recyclerview androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0' // AndroidX Test - JVM testing testImplementation "androidx.test.ext:junit-ktx:1.1.2" testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "androidx.test:core-ktx:1.3.0" testImplementation "org.robolectric:robolectric:4.3.1" androidTestImplementation "androidx.arch.core:core-testing:2.1.0" // testing coroutines androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3' //fragment testing debugImplementation "androidx.fragment:fragment-testing:1.3.3" //navigation testing androidTestImplementation "androidx.navigation:navigation-testing:2.3.5" } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/AppDatabaseTest.kt ================================================ package com.vishalgaur.shoppingapp import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.local.ProductsDao import com.vishalgaur.shoppingapp.data.source.local.ShoppingAppDatabase import com.vishalgaur.shoppingapp.data.source.local.UserDao import kotlinx.coroutines.runBlocking import org.hamcrest.Matchers.* import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AppDatabaseTest { private val pro1 = Product( "pro-owner1-shoe-101", "Shoe Name 101", "owner1", "some description", "Shoes", 250.0, 300.0, listOf(5, 6, 7, 8), listOf("Red", "Blue"), listOf("http://image-ref-uri/shoe-101-01.jpg", "http://image-ref-uri/-shoe-101-02.jpg"), 2.5 ) private val pro2 = Product( "pro-owner1-slipper-101", "Slipper Name 101", "owner1", "some description", "Slippers", 50.0, 80.0, listOf(6, 7, 8), listOf("Black", "Blue"), listOf( "http://image-ref-uri/-slipper-101-01.jpg", "http://image-ref-uri/-slipper-101-02.jpg" ), 4.0 ) private lateinit var userDao: UserDao private lateinit var productsDao: ProductsDao private lateinit var appDb: ShoppingAppDatabase @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun createDb() { val context = InstrumentationRegistry.getInstrumentation().targetContext appDb = Room.inMemoryDatabaseBuilder(context, ShoppingAppDatabase::class.java) .allowMainThreadQueries() .build() userDao = appDb.userDao() productsDao = appDb.productsDao() } @After fun closeDb() { appDb.clearAllTables() appDb.close() } @Test fun insertAndGetUser() { val user = UserData( "sdjm43892yfh948ehod", "Vishal", "+919999988888", "vishal@somemail.com", "dh94328hd", ArrayList(), ArrayList(), ArrayList() ) runBlocking { userDao.insert(user) val result = userDao.getById("sdjm43892yfh948ehod") assertThat(result?.userId, `is`(user.userId)) } } @Test fun noData_returnsNull() { runBlocking { val result = userDao.getById("1232") assertThat(result, `is`(nullValue())) } } @Test fun insertClearUser_returnsNull() { val user = UserData( "sdjm43892yfh948ehod", "Vishal", "+919999988888", "vishal@somemail.com", "dh94328hd", emptyList(), emptyList(), emptyList() ) runBlocking { userDao.insert(user) userDao.clear() val result = userDao.getById("sdjm43892yfh948ehod") assertThat(result, `is`(nullValue())) } } @Test fun insertAndGetProduct() { runBlocking { productsDao.insert(pro1) val result = productsDao.getProductById(pro1.productId) assertEquals(pro1, result) } } @Test fun insertClearProduct_returnsNull() = runBlocking { productsDao.insert(pro1) productsDao.deleteAllProducts() val result = productsDao.getAllProducts() assertEquals(0, result.size) } @Test fun deleteProductById() = runBlocking { productsDao.insert(pro2) productsDao.deleteProductById(pro2.productId) val result = productsDao.getProductById(pro2.productId) assertThat(result, `is`(nullValue())) } @Test fun noProducts_returnsEmptyList() = runBlocking { val result = productsDao.getAllProducts() assertThat(result.size, `is`(0)) } @Test fun deleteAllProducts_returnsEmptyList() = runBlocking { productsDao.insert(pro2) productsDao.deleteAllProducts() val result = productsDao.getAllProducts() assertThat(result.size, `is`(0)) } @Test fun getProductsByOwner_returnsData() = runBlocking { productsDao.insert(pro2) val result = productsDao.getProductsByOwnerId(pro2.owner) assertThat(result.size, `is`(1)) } @Test fun insertMultipleProducts() = runBlocking { productsDao.insertListOfProducts(listOf(pro1, pro2)) val result = productsDao.getAllProducts() assertThat(result.size, `is`(2)) } @Test fun observeProducts_returnsLiveData() = runBlocking { val initialRes = productsDao.observeProducts() productsDao.insert(pro1) val newValue = productsDao.observeProducts().getOrAwaitValue() assertThat(initialRes.value, not(newValue)) assertThat(initialRes.value, `is`(nullValue())) assertThat(newValue.size, `is`(1)) } @Test fun observeProductsByOwner_returnsLiveData() = runBlocking { val initialRes = productsDao.observeProductsByOwner(pro1.owner) productsDao.insert(pro1) val newValue = productsDao.observeProductsByOwner(pro1.owner).getOrAwaitValue() assertThat(initialRes.value, not(newValue)) assertThat(initialRes.value, `is`(nullValue())) assertThat(newValue.size, `is`(1)) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ClickClickableSpan.kt ================================================ package com.vishalgaur.shoppingapp import android.text.SpannableString import android.text.style.ClickableSpan import android.view.View import android.widget.TextView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import org.hamcrest.Matcher import org.hamcrest.Matchers internal fun clickClickableSpan(textToClick: CharSequence): ViewAction { return object : ViewAction { override fun getConstraints(): Matcher { return Matchers.instanceOf(TextView::class.java) } override fun getDescription(): String { return "clicking on a ClickableSpan" } override fun perform(uiController: UiController, view: View) { val textView = view as TextView val spannableString = textView.text as SpannableString if (spannableString.isEmpty()) { // TextView is empty, nothing to do throw NoMatchingViewException.Builder() .includeViewHierarchy(true) .withRootView(textView) .build() } // Get the links inside the TextView and check if we find textToClick val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java) if (spans.isNotEmpty()) { var spanCandidate: ClickableSpan for (span: ClickableSpan in spans) { spanCandidate = span val start = spannableString.getSpanStart(spanCandidate) val end = spannableString.getSpanEnd(spanCandidate) val sequence = spannableString.subSequence(start, end) if (textToClick.toString() == sequence.toString()) { span.onClick(textView) return } } } // textToClick not found in TextView throw NoMatchingViewException.Builder() .includeViewHierarchy(true) .withRootView(textView) .build() } } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ExampleInstrumentedTest.kt ================================================ package com.vishalgaur.shoppingapp import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.vishalgaur.shoppingapp", appContext.packageName) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/LiveDataTestUtil.kt ================================================ package com.vishalgaur.shoppingapp import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException /* Copyright 2019 Google LLC. SPDX-License-Identifier: Apache-2.0 */ fun LiveData.getOrAwaitValue( time: Long = 2, timeUnit: TimeUnit = TimeUnit.SECONDS ): T { var data: T? = null val latch = CountDownLatch(1) val observer = object : Observer { override fun onChanged(o: T?) { data = o latch.countDown() this@getOrAwaitValue.removeObserver(this) } } this.observeForever(observer) // Don't wait indefinitely if the LiveData is not set. if (!latch.await(time, timeUnit)) { throw TimeoutException("LiveData value was never set.") } @Suppress("UNCHECKED_CAST") return data as T } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/MainCoroutineRule.kt ================================================ package com.vishalgaur.shoppingapp import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher import org.junit.runner.Description import kotlin.coroutines.ContinuationInterceptor @ExperimentalCoroutinesApi class MainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/RecyclerViewMatcherUtils.kt ================================================ package com.vishalgaur.shoppingapp import android.view.View import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAssertion import androidx.test.espresso.matcher.BoundedMatcher import androidx.test.espresso.matcher.ViewMatchers.assertThat import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.Matchers.`is` fun atPosition(position: Int, itemMatcher: Matcher): Matcher { return object : BoundedMatcher(RecyclerView::class.java) { override fun describeTo(description: Description) { description.appendText("has item at position $position: ") itemMatcher.describeTo(description) } override fun matchesSafely(view: RecyclerView): Boolean { val viewHolder = view.findViewHolderForAdapterPosition(position) ?: // has no item on such position return false return itemMatcher.matches(viewHolder.itemView) } } } abstract class RecyclerViewItemAction: ViewAction { override fun getConstraints(): Matcher? { return null } override fun getDescription(): String { return "Action on a specific Button" } } class RecyclerViewItemCountAssertion(private val expectedCount: Int) : ViewAssertion { override fun check(view: View, noViewFoundException: NoMatchingViewException?) { if (noViewFoundException != null) { throw noViewFoundException } val recyclerView = view as RecyclerView val adapter = recyclerView.adapter assertThat(adapter!!.itemCount, `is`(expectedCount)) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/FakeAuthRepository.kt ================================================ package com.vishalgaur.shoppingapp.data.source import android.content.Context import androidx.lifecycle.MutableLiveData import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.PhoneAuthCredential import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.data.utils.EmailMobileData import com.vishalgaur.shoppingapp.data.utils.SignUpErrors import com.vishalgaur.shoppingapp.data.utils.UserType class FakeAuthRepository(private val sessionManager: ShoppingAppSessionManager) : AuthRepoInterface { private var emailMobileData = EmailMobileData() private var uData: UserData? = null override suspend fun refreshData() { // no implementation } override suspend fun signUp(userData: UserData) { uData = userData sessionManager.createLoginSession( userData.userId, userData.name, userData.mobile, false, userData.userType == UserType.SELLER.name ) } override fun login(userData: UserData, rememberMe: Boolean) { uData = userData sessionManager.createLoginSession( userData.userId, userData.name, userData.mobile, rememberMe, userData.userType == UserType.SELLER.name ) } override suspend fun checkEmailAndMobile( email: String, mobile: String, context: Context ): SignUpErrors { // no implementation return SignUpErrors.NONE } override suspend fun checkLogin(mobile: String, password: String): UserData? { uData?.let { if (it.mobile == mobile && it.password == password) { return it } } return null } override suspend fun signOut() { uData = null sessionManager.logoutFromSession() } override suspend fun hardRefreshUserData() { // no implementation } override suspend fun insertProductToLikes(productId: String, userId: String): Result { uData?.let { if (it.userId == userId) { val likes = it.likes.toMutableList() likes.add(productId) it.likes = likes return Result.Success(true) } } return Result.Error(Exception("User Not Found")) } override suspend fun removeProductFromLikes( productId: String, userId: String ): Result { uData?.let { if (it.userId == userId) { val likes = it.likes.toMutableList() likes.remove(productId) it.likes = likes return Result.Success(true) } } return Result.Error(Exception("User Not Found")) } override suspend fun insertAddress( newAddress: UserData.Address, userId: String ): Result { uData?.let { if (it.userId == userId) { val addresses = it.addresses.toMutableList() addresses.add(newAddress) it.addresses = addresses return Result.Success(true) } } return Result.Error(Exception("User Not Found")) } override suspend fun updateAddress( newAddress: UserData.Address, userId: String ): Result { uData?.let { if (it.userId == userId) { val addresses = it.addresses.toMutableList() addresses.add(newAddress) val pos = it.addresses.indexOfFirst { address -> address.addressId == newAddress.addressId } if (pos >= 0) { addresses[pos] = newAddress } it.addresses = addresses return Result.Success(true) } } return Result.Error(Exception("User Not Found")) } override suspend fun deleteAddressById(addressId: String, userId: String): Result { uData?.let { if (it.userId == userId) { val addresses = it.addresses.toMutableList() val pos = it.addresses.indexOfFirst { address -> address.addressId == addressId } if (pos >= 0) { addresses.removeAt(pos) } it.addresses = addresses return Result.Success(true) } } return Result.Error(Exception("User Not Found")) } override suspend fun insertCartItemByUserId( cartItem: UserData.CartItem, userId: String ): Result { uData?.let { if (it.userId == userId) { val cart = it.cart.toMutableList() cart.add(cartItem) it.cart = cart return Result.Success(true) } } return Result.Error(Exception("User Not Found")) } override suspend fun updateCartItemByUserId( cartItem: UserData.CartItem, userId: String ): Result { uData?.let { if (it.userId == userId) { val cart = it.cart.toMutableList() val pos = it.cart.indexOfFirst { item -> item.itemId == cartItem.itemId } if (pos >= 0) { cart[pos] = cartItem } it.cart = cart return Result.Success(true) } } return Result.Error(Exception("User Not Found")) } override suspend fun deleteCartItemByUserId(itemId: String, userId: String): Result { uData?.let { if (it.userId == userId) { val cart = it.cart.toMutableList() val pos = it.cart.indexOfFirst { item -> item.itemId == itemId } if (pos >= 0) { cart.removeAt(pos) } it.cart = cart return Result.Success(true) } } return Result.Error(Exception("User Not Found")) } override suspend fun getAddressesByUserId(userId: String): Result?> { uData?.let { if (it.userId == userId) { return Result.Success(it.addresses) } } return Result.Error(Exception("User Not Found")) } override suspend fun getLikesByUserId(userId: String): Result?> { uData?.let { if (it.userId == userId) { return Result.Success(it.likes) } } return Result.Error(Exception("User Not Found")) } override suspend fun getUserData(userId: String): Result { uData?.let { if (it.userId == userId) { return Result.Success(it) } } return Result.Error(Exception("User Not Found")) } override fun getFirebaseAuth(): FirebaseAuth { return Firebase.auth } override fun signInWithPhoneAuthCredential( credential: PhoneAuthCredential, isUserLoggedIn: MutableLiveData, context: Context ) { // no implementation } override fun isRememberMeOn(): Boolean { // no implementation return true } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/FakeProductsDataSource.kt ================================================ package com.vishalgaur.shoppingapp.data.source import android.net.Uri import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success class FakeProductsDataSource(private var products: MutableList? = mutableListOf()) : ProductDataSource { private val imagesStorage = mutableListOf() override fun observeProducts(): LiveData>?> { products?.let { pros -> val res = MutableLiveData(pros) return Transformations.map(res) { Success(it.toList()) } } return MutableLiveData(Error(Exception())) } override fun observeProductsByOwner(ownerId: String): LiveData>?> { products?.let { allPros -> val pros = allPros.filter { pr -> pr.owner == ownerId } val res = MutableLiveData(pros) return Transformations.map(res) { Success(it.toList()) } } return MutableLiveData(Error(Exception())) } override suspend fun getAllProducts(): Result> { products?.let { return Success(it) } return Error(Exception("Products Not Found")) } override suspend fun refreshProducts() { // No implementation } override suspend fun getProductById(productId: String): Result { products?.let { val res = it.filter { product -> product.productId == productId } return if (res.isNotEmpty()) { Success(res[0]) } else { Error(Exception("Product Not Found")) } } return Error(Exception("Product Not Found")) } override suspend fun insertProduct(newProduct: Product) { products?.add(newProduct) } override suspend fun updateProduct(proData: Product) { products?.let { val pos = it.indexOfFirst { product -> proData.productId == product.productId } it[pos] = proData } } override suspend fun deleteProduct(productId: String) { products?.let { val pos = it.indexOfFirst { product -> productId == product.productId } if (pos >= 0) it.removeAt(pos) else throw Exception("Product Not Found") } } override suspend fun getAllProductsByOwner(ownerId: String): Result> { val res = products?.filter { product -> product.owner == ownerId } return if (res != null) { Success(res) } else { Success(emptyList()) } } override suspend fun deleteAllProducts() { products = mutableListOf() } override suspend fun insertMultipleProducts(data: List) { products?.addAll(data) } override suspend fun uploadImage(uri: Uri, fileName: String): Uri { val res = uri.toString() + fileName if (res.contains("invalidinvalidinvalid")) { throw Exception("Error uploading Images") } imagesStorage.add(res) return res.toUri() } override fun revertUpload(fileName: String) { val pos = imagesStorage.indexOfFirst { imageRef -> imageRef.contains(fileName) } if (pos >= 0) { imagesStorage.removeAt(pos) } } override fun deleteImage(imgUrl: String) { imagesStorage.remove(imgUrl) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/FakeProductsRepository.kt ================================================ package com.vishalgaur.shoppingapp.data.source import android.net.Uri import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import kotlinx.coroutines.runBlocking import java.util.* import kotlin.collections.LinkedHashMap class FakeProductsRepository : ProductsRepoInterface { var productsServiceData: LinkedHashMap = LinkedHashMap() private val imagesStorage = mutableListOf() private val observableProducts = MutableLiveData>>() override suspend fun refreshProducts(): StoreDataStatus { observableProducts.value = Success(productsServiceData.values.toList()) return StoreDataStatus.DONE } override fun observeProducts(): LiveData>?> { runBlocking { refreshProducts() } return observableProducts } override fun observeProductsByOwner(ownerId: String): LiveData>?> { runBlocking { refreshProducts() } return Transformations.map(observableProducts) { products -> when (products) { is Result.Loading -> Result.Loading is Error -> Error(products.exception) is Success -> { val pros = products.data.filter { it.owner == ownerId } Success(pros) } } } } override suspend fun getAllProductsByOwner(ownerId: String): Result> { productsServiceData.values.let { pros -> val res = pros.filter { it.owner == ownerId } return Success(res) } } override suspend fun getProductById(productId: String, forceUpdate: Boolean): Result { productsServiceData[productId]?.let { return Success(it) } return Error(Exception("Product Not Found!")) } override suspend fun insertProduct(newProduct: Product): Result { productsServiceData[newProduct.productId] = newProduct return Success(true) } override suspend fun insertImages(imgList: List): List { val result = mutableListOf() imgList.forEach { uri -> val uniId = UUID.randomUUID().toString() val fileName = uniId + uri.lastPathSegment?.split("/")?.last() val res = uri.toString() + fileName imagesStorage.add(res) result.add(res) } return result } override suspend fun updateProduct(product: Product): Result { productsServiceData[product.productId] = product return Success(true) } override suspend fun updateImages(newList: List, oldList: List): List { val urlList = mutableListOf() newList.forEach { uri -> if (!oldList.contains(uri.toString())) { val uniId = UUID.randomUUID().toString() val fileName = uniId + uri.lastPathSegment?.split("/")?.last() val res = uri.toString() + fileName imagesStorage.add(res) urlList.add(res) } else { urlList.add(uri.toString()) } } oldList.forEach { imgUrl -> if (!newList.contains(imgUrl.toUri())) { imagesStorage.remove(imgUrl) } } return urlList } override suspend fun deleteProductById(productId: String): Result { productsServiceData.remove(productId) refreshProducts() return Success(true) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/FakeUserDataSource.kt ================================================ package com.vishalgaur.shoppingapp.data.source import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.EmailMobileData class FakeUserDataSource(private var uData: UserData?) : UserDataSource { private var emailMobileData = EmailMobileData() override suspend fun addUser(userData: UserData) { uData = userData } override suspend fun getUserById(userId: String): Result { uData?.let { if (it.userId == userId) { return Success(it) } } return Error(Exception("User Not Found")) } override suspend fun getEmailsAndMobiles(): EmailMobileData { return emailMobileData } override suspend fun getUserByMobileAndPassword( mobile: String, password: String ): MutableList { val res = mutableListOf() uData?.let { if (it.mobile == mobile && it.password == password) { res.add(it) } } return res } override suspend fun clearUser() { uData = null } override suspend fun getUserByMobile(phoneNumber: String): UserData? { return super.getUserByMobile(phoneNumber) } override fun updateEmailsAndMobiles(email: String, mobile: String) { emailMobileData.emails.add(email) emailMobileData.mobiles.add(mobile) } override suspend fun likeProduct(productId: String, userId: String) { uData?.let { if (it.userId == userId) { val likes = it.likes.toMutableList() likes.add(productId) it.likes = likes } } } override suspend fun dislikeProduct(productId: String, userId: String) { uData?.let { if (it.userId == userId) { val likes = it.likes.toMutableList() likes.remove(productId) it.likes = likes } } } override suspend fun insertAddress(newAddress: UserData.Address, userId: String) { uData?.let { if (it.userId == userId) { val addresses = it.addresses.toMutableList() addresses.add(newAddress) it.addresses = addresses } } } override suspend fun updateAddress(newAddress: UserData.Address, userId: String) { uData?.let { data -> if (data.userId == userId) { val addresses = data.addresses.toMutableList() val pos = data.addresses.indexOfFirst { it.addressId == newAddress.addressId } if (pos >= 0) { addresses[pos] = newAddress } data.addresses = addresses } } } override suspend fun deleteAddress(addressId: String, userId: String) { uData?.let { data -> if (data.userId == userId) { val addresses = data.addresses.toMutableList() val pos = data.addresses.indexOfFirst { it.addressId == addressId } if (pos >= 0) { addresses.removeAt(pos) } data.addresses = addresses } } } override suspend fun insertCartItem(newItem: UserData.CartItem, userId: String) { uData?.let { if (it.userId == userId) { val cart = it.cart.toMutableList() cart.add(newItem) it.cart = cart } } } override suspend fun updateCartItem(item: UserData.CartItem, userId: String) { uData?.let { data -> if (data.userId == userId) { val cart = data.cart.toMutableList() val pos = data.cart.indexOfFirst { it.itemId == item.itemId } if (pos >= 0) { cart[pos] = item } data.cart = cart } } } override suspend fun deleteCartItem(itemId: String, userId: String) { uData?.let { data -> if (data.userId == userId) { val cart = data.cart.toMutableList() val pos = data.cart.indexOfFirst { it.itemId == itemId } if (pos >= 0) { cart.removeAt(pos) } data.cart = cart } } } override suspend fun getAddressesByUserId(userId: String): Result?> { uData?.let { if (it.userId == userId) { return Success(it.addresses) } } return Error(Exception("User Not Found")) } override suspend fun getLikesByUserId(userId: String): Result?> { uData?.let { if (it.userId == userId) { return Success(it.likes) } } return Error(Exception("User Not Found")) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/repository/AuthRepositoryTest.kt ================================================ package com.vishalgaur.shoppingapp.data.source.repository import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread import com.google.firebase.FirebaseApp import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.FakeUserDataSource import com.vishalgaur.shoppingapp.data.utils.SignUpErrors import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.nullValue import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @ExperimentalCoroutinesApi class AuthRepositoryTest { private val userSeller = UserData( "weoifhwenf29385", "Seller Name", "+919999990000", "somemail@mail.com", "12345", emptyList(), emptyList(), emptyList(), "SELLER", ) private val userCustomer = UserData( "dwoeihwjklvn48329752", "Customer Name", "+919090909090", "somemail1232@mail.com", "12345", emptyList(), emptyList(), emptyList(), "CUSTOMER", ) private lateinit var context: Context private lateinit var userLocalDataSource: FakeUserDataSource private lateinit var authRemoteDataSource: FakeUserDataSource private lateinit var sessionManager: ShoppingAppSessionManager // class under test private lateinit var authRepository: AuthRepository @Before fun createRepository() { context = ApplicationProvider.getApplicationContext() FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext()) userLocalDataSource = FakeUserDataSource(userSeller) authRemoteDataSource = FakeUserDataSource(userCustomer) sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext()) authRepository = AuthRepository( userLocalDataSource, authRemoteDataSource, sessionManager ) } @Test fun login_getUserDetailFromSession() = runBlockingTest { authRepository.login(userSeller, true) val result = sessionManager.getUserDataFromSession() assertThat(result["userName"], `is`(userSeller.name)) assertThat(result["userId"], `is`(userSeller.userId)) assertThat(result["userMobile"], `is`(userSeller.mobile)) } @Test fun singUp_addsUserToSources() = runBlockingTest { authRepository.signUp(userCustomer) val resultSession = sessionManager.getUserDataFromSession() assertThat(resultSession["userName"], `is`(userCustomer.name)) assertThat(resultSession["userId"], `is`(userCustomer.userId)) assertThat(resultSession["userMobile"], `is`(userCustomer.mobile)) val localRes = userLocalDataSource.getUserById(userCustomer.userId) assertThat(localRes, `is`(Success(userCustomer))) val remoteRes = authRemoteDataSource.getUserById(userCustomer.userId) assertThat(remoteRes, `is`(Success(userCustomer))) } @Test fun checkEmailAndMobile_existingEmail_returnsError() { authRemoteDataSource.updateEmailsAndMobiles("mail123@mail.com", "+919999988888") runOnUiThread { runBlockingTest { val result = authRepository.checkEmailAndMobile("mail123@mail.com", "+919685", context) assertThat(result, `is`(SignUpErrors.SERR)) } } } @Test fun checkEmailAndMobile_existingMobile_returnsError() { authRemoteDataSource.updateEmailsAndMobiles("mail123@mail.com", "+919999988888") runOnUiThread { runBlockingTest { val result = authRepository.checkEmailAndMobile("mail999@mail.com", "+919999988888", context) assertThat(result, `is`(SignUpErrors.SERR)) } } } @Test fun checkEmailAndMobile_existingMobileAndEmail_returnsError() { authRemoteDataSource.updateEmailsAndMobiles("mail123@mail.com", "+919999988888") runOnUiThread { runBlockingTest { val result = authRepository.checkEmailAndMobile("mail123@mail.com", "+919999988888", context) assertThat(result, `is`(SignUpErrors.SERR)) } } } @Test fun checkEmailAndMobile_newData_returnsError() { authRemoteDataSource.updateEmailsAndMobiles("mail123@mail.com", "+919999988888") runOnUiThread { runBlockingTest { val result = authRepository.checkEmailAndMobile( "somemail123@mail.com", "+919999977777", context ) assertThat(result, `is`(SignUpErrors.NONE)) } } } @Test fun checkLogin_existingUser_returnsData() = runBlockingTest { val result = authRepository.checkLogin(userCustomer.mobile, userCustomer.password) assertThat(result, `is`(userCustomer)) } @Test fun checkLogin_newCredentials_returnsNull() = runBlockingTest { val result = authRepository.checkLogin("+919879879879", "sdygt4") assertThat(result, `is`(nullValue())) } @Test fun signOut_clearsSessionAndData() = runBlockingTest { authRepository.signOut() val sessionRes = sessionManager.isLoggedIn() val localRes = userLocalDataSource.getUserById(userSeller.userId) assertThat(sessionRes, `is`(false)) if (localRes is Success) assert(false) else if (localRes is Error) { assertEquals(localRes.exception.message, "User Not Found") } } @Test fun getLikes_returnsLikes() = runBlockingTest { authRepository.signUp(userCustomer) val res = authRepository.getLikesByUserId(userCustomer.userId) if (res is Success) { assertThat(res.data, `is`(userCustomer.likes)) } else { assert(false) } } @Test fun likeProduct() = runBlockingTest { authRepository.signUp(userCustomer) authRepository.insertProductToLikes("some-id", userCustomer.userId) val res = authRepository.getLikesByUserId(userCustomer.userId) if (res is Success) { assertThat(res.data?.size, `is`(1)) } else { assert(false) } } @Test fun dislikeProduct() = runBlockingTest { authRepository.signUp(userCustomer) authRepository.insertProductToLikes("some-id", userCustomer.userId) authRepository.removeProductFromLikes("some-id", userCustomer.userId) val res = authRepository.getLikesByUserId(userCustomer.userId) if (res is Success) { assertThat(res.data?.contains("some-id"), `is`(false)) } else { assert(false) } } @Test fun getAddresses() = runBlockingTest { authRepository.signUp(userCustomer) val res = authRepository.getAddressesByUserId(userCustomer.userId) if (res is Success) { assertThat(res.data, `is`(userCustomer.addresses)) } else { assert(false) } } @Test fun addAddress() = runBlockingTest { authRepository.signUp(userCustomer) val address = UserData.Address( "id123-add", "namefirst", "lname", "IN", "2341 weg", "", "kanopwe", "up", "209876", "+919999988888" ) authRepository.insertAddress(address, userCustomer.userId) val res = authRepository.getAddressesByUserId(userCustomer.userId) if (res is Success) { assertThat(res.data?.size, `is`(1)) } else { assert(false) } } @Test fun removeAddress() = runBlockingTest { authRepository.signUp(userCustomer) val address = UserData.Address( "id123-add", "namefirst", "lname", "IN", "2341 weg", "", "kanopwe", "up", "209876", "+919999988888" ) authRepository.insertAddress(address, userCustomer.userId) authRepository.deleteAddressById(address.addressId, userCustomer.userId) val res = authRepository.getAddressesByUserId(userCustomer.userId) if (res is Success) { assertThat(res.data?.contains(address), `is`(false)) } else { assert(false) } } @Test fun updateAddress() = runBlockingTest { authRepository.signUp(userCustomer) val address = UserData.Address( "id123-add", "namefirst", "lname", "IN", "2341 weg", "", "kanopwe", "up", "209876", "+919999988888" ) val newAddress = UserData.Address( "id123-add", "namefirst", "lname", "IN", "2341 wesfgeg", "vdfg, heth", "kanopwe", "up", "209876", "+919999988888" ) authRepository.insertAddress(address, userCustomer.userId) authRepository.updateAddress(newAddress, userCustomer.userId) val res = authRepository.getAddressesByUserId(userCustomer.userId) if (res is Success) { assertThat(res.data?.contains(newAddress), `is`(true)) } else { assert(false) } } @Test fun addItemToCart() = runBlockingTest { authRepository.signUp(userCustomer) val item = UserData.CartItem( "item-id-123", "pro-122", "owner-1213", 1, "black", 7 ) authRepository.insertCartItemByUserId(item, userCustomer.userId) val userRes = userLocalDataSource.getUserById(userCustomer.userId) if (userRes is Success) { val data = userRes.data if (data != null) { val cart = data.cart assertThat(cart.contains(item), `is`(true)) } else { assert(false) } } else { assert(false) } } @Test fun removeItemFromCart() = runBlockingTest { authRepository.signUp(userCustomer) val item = UserData.CartItem( "item-id-123", "pro-122", "owner-1213", 1, "black", 7 ) authRepository.insertCartItemByUserId(item, userCustomer.userId) authRepository.deleteCartItemByUserId(item.itemId, userCustomer.userId) val userRes = userLocalDataSource.getUserById(userCustomer.userId) if (userRes is Success) { val data = userRes.data if (data != null) { val cart = data.cart assertThat(cart.contains(item), `is`(false)) } else { assert(false) } } else { assert(false) } } @Test fun updateItemInCart() = runBlockingTest { authRepository.signUp(userCustomer) val item = UserData.CartItem( "item-id-123", "pro-122", "owner-1213", 1, "black", 7 ) val newItem = UserData.CartItem( "item-id-123", "pro-122", "owner-1213", 5, "black", 7 ) authRepository.insertCartItemByUserId(item, userCustomer.userId) authRepository.updateCartItemByUserId(newItem, userCustomer.userId) val userRes = userLocalDataSource.getUserById(userCustomer.userId) if (userRes is Success) { val data = userRes.data if (data != null) { val cart = data.cart assertThat(cart.contains(newItem), `is`(true)) } else { assert(false) } } else { assert(false) } } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/repository/ProductsRepositoryTest.kt ================================================ package com.vishalgaur.shoppingapp.data.source.repository import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.core.net.toUri import androidx.test.espresso.matcher.ViewMatchers.assertThat import com.vishalgaur.shoppingapp.ERR_UPLOAD import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.source.FakeProductsDataSource import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.getOrAwaitValue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.test.runBlockingTest import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.greaterThan import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before import org.junit.Rule import org.junit.Test @ExperimentalCoroutinesApi class ProductsRepositoryTest { private val pro1 = Product( "pro-owner1-shoe-101", "Shoe Name 101", "owner1", "some description", "Shoes", 250.0, 300.0, listOf(5, 6, 7, 8), listOf("Red", "Blue"), listOf("http://image-ref-uri/shoe-101-01.jpg", "http://image-ref-uri/-shoe-101-02.jpg"), 2.5 ) private val pro2 = Product( "pro-owner1-slipper-101", "Slipper Name 101", "owner1", "some description", "Slippers", 50.0, 80.0, listOf(6, 7, 8), listOf("Black", "Blue"), listOf( "http://image-ref-uri/-slipper-101-01.jpg", "http://image-ref-uri/-slipper-101-02.jpg" ), 4.0 ) private val pro3 = Product( "pro-owner1-shoe-102", "Shoe Name 102", "owner2", "some description", "Shoes", 450.0, 600.0, listOf(4, 5, 7, 8, 10), listOf("Red", "Blue", "White"), listOf("http://image-ref-uri/-shoe-102-01.jpg", "http://image-ref-uri/-shoe-102-02.jpg"), 3.5 ) private lateinit var productsLocalDataSource: FakeProductsDataSource private lateinit var productsRemoteDataSource: FakeProductsDataSource // class under test private lateinit var productsRepository: ProductsRepository @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun createRepository() { productsLocalDataSource = FakeProductsDataSource(mutableListOf()) productsRemoteDataSource = FakeProductsDataSource(mutableListOf(pro1, pro3)) productsRepository = ProductsRepository(productsRemoteDataSource, productsLocalDataSource) } @Test fun getProductsById_invalidId_returnsError() = runBlockingTest { val resultRes = productsRepository.getProductById("invalidId", false) if (resultRes is Result.Success) assert(false) else if (resultRes is Result.Error) { assertEquals(resultRes.exception.message, "Product Not Found") } } @Test fun getProductsById_validId_returnsProduct() = runBlockingTest { productsRepository.insertProduct(pro1) val resultRes = productsRepository.getProductById(pro1.productId, false) if (resultRes is Result.Success) { assertThat(resultRes.data, `is`(pro1)) } else if (resultRes is Result.Error) { assert(false) } } @Test fun insertProduct_returnsSuccess() = runBlockingTest { val insertRes = productsRepository.insertProduct(pro1) if (insertRes is Result.Success) { assertThat(insertRes.data, `is`(true)) } else { assert(false) } } @Test fun insertImages_returnsSuccess() = runBlockingTest { val result = productsRepository.insertImages(pro1.images.map { it.toUri() }) assertThat(result.size, `is`(pro1.images.size)) } @Test fun insertImages_invalidImages_returnsError() = runBlockingTest { val result = productsRepository.insertImages(listOf("http://image-ref-uri/dwoeiovnwi-invalidinvalidinvalid/weoifhowf".toUri())) assertThat(result[0], `is`(ERR_UPLOAD)) } @Test fun updateProduct_returnsSuccess() = runBlockingTest { productsRepository.insertProduct(pro2) val updatedPro = pro2 updatedPro.availableSizes = listOf(5, 6, 10, 12) val insertRes = productsRepository.updateProduct(updatedPro) if (insertRes is Result.Success) { assertThat(insertRes.data, `is`(true)) } else { assert(false) } } @Test fun updateImages_returnsList() = runBlockingTest { val oldList = productsRepository.insertImages(pro1.images.map { it.toUri() }) val result = productsRepository.updateImages(pro3.images.map { it.toUri() }, oldList) assertThat(result.size, `is`(pro3.images.size)) } @Test fun updateImages_invalidImage_returnsError() = runBlockingTest { val oldList = productsRepository.insertImages(pro1.images.map { it.toUri() }) val newList = oldList.toMutableList() newList[0] = "http://csifduoskjgn/invalidinvalidinvalid/wehoiw" val result = productsRepository.updateImages(newList.map { it.toUri() }, oldList) assertThat(result[0], `is`(ERR_UPLOAD)) } @Test fun deleteProductById_returnsSuccess() = runBlockingTest { productsRepository.insertProduct(pro1) productsRepository.insertProduct(pro2) val result = productsRepository.deleteProductById(pro1.productId) assert(result is Result.Success) } @Test fun deleteProductById_invalidId_returnsError() = runBlockingTest { productsRepository.insertProduct(pro1) productsRepository.insertProduct(pro2) val result = productsRepository.deleteProductById(pro3.productId) assert(result is Result.Error) } @Test fun refreshProducts_returnsSuccess() = runBlockingTest { val result = productsRepository.refreshProducts() assertThat(result, `is`(StoreDataStatus.DONE)) } @Test fun observeProducts_noData_returnsNoData() = runBlockingTest { productsLocalDataSource.deleteAllProducts() val result = productsRepository.observeProducts().getOrAwaitValue() if (result is Result.Success) { assertThat(result.data.size, `is`(0)) } else { assert(false) } } @Test fun observeProducts_hasData_returnsSuccessWithData() = runBlockingTest { val initialValue = productsRepository.observeProducts().getOrAwaitValue() val insertRes = async { productsRepository.insertProduct(pro3) } insertRes.await() val refreshRes = async { productsRepository.refreshProducts() } assertThat(refreshRes.await(), `is`(StoreDataStatus.DONE)) val newValue = productsRepository.observeProducts().getOrAwaitValue() assertNotEquals(initialValue.toString(), newValue.toString()) if (initialValue is Result.Success) { assertThat(initialValue.data.size, `is`(0)) } else { assert(false) } if (newValue is Result.Success) { assertThat(newValue.data.size, `is`(greaterThan(0))) } else { assert(false) } } @Test fun observeProductsByOwner_noData_returnsNoData() = runBlockingTest { productsLocalDataSource.deleteAllProducts() val result = productsRepository.observeProductsByOwner(pro1.owner).getOrAwaitValue() if (result is Result.Success) { assertThat(result.data.size, `is`(0)) } else { assert(false) } } @Test fun observeProductsByOwner_hasData_returnsSuccessWithData() = runBlockingTest { val initialValue = productsRepository.observeProductsByOwner(pro3.owner).getOrAwaitValue() val insertRes = async { productsRepository.insertProduct(pro3) } insertRes.await() val refreshRes = async { productsRepository.refreshProducts() } assertThat(refreshRes.await(), `is`(StoreDataStatus.DONE)) val newValue = productsRepository.observeProductsByOwner(pro3.owner).getOrAwaitValue() assertNotEquals(initialValue.toString(), newValue.toString()) if (initialValue is Result.Success) { assertThat(initialValue.data.size, `is`(0)) } else { assert(false) } if (newValue is Result.Success) { assertThat(newValue.data.size, `is`(greaterThan(0))) } else { assert(false) } } @Test fun getAllProductsByOwner_noData_returnsNoData() = runBlockingTest { productsLocalDataSource.deleteAllProducts() val result = productsRepository.getAllProductsByOwner(pro1.owner) if (result is Result.Success) { assertThat(result.data.size, `is`(0)) } else { assert(false) } } @Test fun getAllProductsByOwner_hasData_returnsData() = runBlockingTest { productsRepository.refreshProducts() val result = productsRepository.getAllProductsByOwner(pro1.owner) if (result is Result.Success) { assertThat(result.data.size, `is`(greaterThan(0))) } else { assert(false) } } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ui/home/HomeFragmentTest.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.view.View import android.widget.ImageView import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.core.view.isVisible import androidx.fragment.app.testing.FragmentScenario import androidx.fragment.app.testing.launchFragmentInContainer import androidx.navigation.NavController import androidx.navigation.Navigation import androidx.navigation.testing.TestNavHostController import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.UiController import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread import com.vishalgaur.shoppingapp.* import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository import com.vishalgaur.shoppingapp.data.source.FakeProductsRepository import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runBlockingTest import org.hamcrest.Matchers.`is` import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @ExperimentalCoroutinesApi class HomeFragmentTest { private lateinit var homeScenario: FragmentScenario private lateinit var navController: NavController private lateinit var sessionManager: ShoppingAppSessionManager private lateinit var productsRepository: ProductsRepoInterface private lateinit var authRepository: AuthRepoInterface private val context = ApplicationProvider.getApplicationContext() private val userCustomer = UserData( "sdjm43892yfh948ehod", "Vishal", "+919999988888", "vishal@somemail.com", "dh94328hd", ArrayList(), ArrayList(), ArrayList(), "CUSTOMER" ) private val userSeller = UserData( "user1234selller", "Some Name", "+919999988888", "somemail123seller@somemail.com", "1234", emptyList(), emptyList(), emptyList(), "SELLER" ) private val pro1 = Product( "pro-owner1-shoe-101", "Shoe Name 101", "user1234selller", "some description", "Shoes", 250.0, 300.0, listOf(5, 6, 7, 8), listOf("Red", "Blue"), listOf("http://image-ref-uri/shoe-101-01.jpg", "http://image-ref-uri/-shoe-101-02.jpg"), 2.5 ) private val pro2 = Product( "pro-owner1-slipper-101", "Slipper Name 101", "owner1", "some description", "Slippers", 50.0, 80.0, listOf(6, 7, 8), listOf("Black", "Blue"), listOf( "http://image-ref-uri/-slipper-101-01.jpg", "http://image-ref-uri/-slipper-101-02.jpg" ), 4.0 ) @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { sessionManager = ShoppingAppSessionManager(context) authRepository = FakeAuthRepository(sessionManager) productsRepository = FakeProductsRepository() ServiceLocator.productsRepository = productsRepository } @After fun cleanUp() = runBlockingTest { authRepository.signOut() ServiceLocator.resetRepository() } @Test fun userCustomer_hideFABandEditDeleteButtons() = runBlockingTest { insertProducts() loginCustomer() onView(withId(R.id.home_fab_add_product)).check(matches(withEffectiveVisibility(Visibility.GONE))) //testing recyclerview items onView(withId(R.id.products_recycler_view)) .perform( RecyclerViewActions.actionOnItemAtPosition( 0, object : RecyclerViewItemAction() { override fun perform(uiController: UiController?, view: View) { val editButton: ImageView = view.findViewById(R.id.product_edit_button) val deleteButton: ImageView = view.findViewById(R.id.product_delete_button) assertThat(editButton.isVisible, `is`(false)) assertThat(deleteButton.isVisible, `is`(false)) } }) ) } @Test fun userSeller_showFABandEditDeleteButtons() = runBlockingTest { insertProducts() loginSeller() onView(withId(R.id.home_fab_add_product)).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) //testing recyclerview items onView(withId(R.id.products_recycler_view)) .perform( RecyclerViewActions.actionOnItemAtPosition( 0, object : RecyclerViewItemAction() { override fun perform(uiController: UiController?, view: View) { val editButton: ImageView = view.findViewById(R.id.product_edit_button) val deleteButton: ImageView = view.findViewById(R.id.product_delete_button) assertThat(editButton.isVisible, `is`(true)) assertThat(deleteButton.isVisible, `is`(true)) } }) ) } @Test fun onFABClick_openCategoryDialog() { loginSeller() onView(withId(R.id.home_fab_add_product)).perform(click()) onView(withText(R.string.pro_cat_dialog_title)).inRoot(isDialog()) .check(matches(isDisplayed())) } @Test fun onSelectCategory_openAddProductFragment() { loginSeller() onView(withId(R.id.home_fab_add_product)).perform(click()) onView(withText(R.string.pro_cat_dialog_ok_btn)).inRoot(isDialog()) .check(matches(isDisplayed())).perform(click()) assertThat(navController.currentDestination?.id, `is`(R.id.addEditProductFragment)) } @Test fun onFilterClick_openFilterDialog() = runBlockingTest{ insertProducts() loginCustomer() onView(withId(R.id.home_filter)).perform(click()) onView(withText("Shoes")).inRoot(isDialog()) .perform(click()) onView(withText(R.string.pro_cat_dialog_ok_btn)).inRoot(isDialog()) .perform(click()) onView(withId(R.id.products_recycler_view)).check(RecyclerViewItemCountAssertion(1)) } @Test fun onFilter_filterResults() { loginCustomer() onView(withId(R.id.home_filter)).perform(click()) onView(withText(R.string.pro_cat_dialog_title)).inRoot(isDialog()) .check(matches(isDisplayed())) } @Test fun enterSearch_filtersResults() { runBlocking { insertProducts() loginCustomer() onView(withId(R.id.home_search_edit_text)).perform(typeText("slipper")) delay(500) onView(withId(R.id.products_recycler_view)).check(RecyclerViewItemCountAssertion(1)) } } private fun loginSeller() { authRepository.login(userSeller, true) ServiceLocator.authRepository = authRepository setScenarioAndNav() } private fun loginCustomer() { authRepository.login(userCustomer, true) ServiceLocator.authRepository = authRepository setScenarioAndNav() } private fun setScenarioAndNav() { homeScenario = launchFragmentInContainer(Bundle(), R.style.Theme_ShoppingApp) navController = TestNavHostController(context) runOnUiThread { navController.setGraph(R.navigation.home_nav_graph) homeScenario.onFragment { Navigation.setViewNavController(it.requireView(), navController) } } } private suspend fun insertProducts() { productsRepository.insertProduct(pro1) productsRepository.insertProduct(pro2) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ui/home/ProductDetailsFragmentTest.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.fragment.app.testing.FragmentScenario import androidx.navigation.NavController import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vishalgaur.shoppingapp.ServiceLocator import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository import com.vishalgaur.shoppingapp.data.source.FakeProductsRepository import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @ExperimentalCoroutinesApi class ProductDetailsFragmentTest { private lateinit var productDetailScenario: FragmentScenario private lateinit var navController: NavController private lateinit var sessionManager: ShoppingAppSessionManager private lateinit var productsRepository: ProductsRepoInterface private lateinit var authRepository: AuthRepoInterface private val context = ApplicationProvider.getApplicationContext() private val pro1 = Product( "pro-owner1-shoe-101", "Shoe Name 101", "user1234selller", "some description", "Shoes", 250.0, 300.0, listOf(5, 6, 7, 8), listOf("Red", "Blue"), listOf("http://image-ref-uri/shoe-101-01.jpg", "http://image-ref-uri/-shoe-101-02.jpg"), 2.5 ) private val userCustomer = UserData( "sdjm43892yfh948ehod", "Vishal", "+919999988888", "vishal@somemail.com", "dh94328hd", ArrayList(), ArrayList(), ArrayList(), "CUSTOMER" ) private val userSeller = UserData( "user1234selller", "Some Name", "+919999988888", "somemail123seller@somemail.com", "1234", emptyList(), emptyList(), emptyList(), "SELLER" ) @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { sessionManager = ShoppingAppSessionManager(context) authRepository = FakeAuthRepository(sessionManager) productsRepository = FakeProductsRepository() ServiceLocator.productsRepository = productsRepository } @After fun cleanUp() = runBlockingTest { authRepository.signOut() ServiceLocator.resetRepository() } private suspend fun insertProducts() { productsRepository.insertProduct(pro1) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ui/loginSignup/LoginFragmentTest.kt ================================================ package com.vishalgaur.shoppingapp.ui.loginSignup import androidx.fragment.app.testing.FragmentScenario import androidx.fragment.app.testing.launchFragmentInContainer import androidx.navigation.NavController import androidx.navigation.Navigation import androidx.navigation.testing.TestNavHostController import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.* import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread import androidx.test.platform.app.InstrumentationRegistry import com.vishalgaur.shoppingapp.MOB_ERROR_TEXT import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.clickClickableSpan import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import org.hamcrest.Matchers.`is` import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LoginFragmentTest { private lateinit var loginScenario: FragmentScenario private lateinit var navController: NavController private lateinit var sessionManager: ShoppingAppSessionManager @Before fun setUp() { sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext()) sessionManager.logoutFromSession() loginScenario = launchFragmentInContainer(themeResId = R.style.Theme_ShoppingApp) navController = TestNavHostController(ApplicationProvider.getApplicationContext()) runOnUiThread { navController.setGraph(R.navigation.signup_nav_graph) loginScenario.onFragment { Navigation.setViewNavController(it.requireView(), navController) } } } @Test fun useAppContext() { val context = InstrumentationRegistry.getInstrumentation().targetContext Assert.assertEquals("com.vishalgaur.shoppingapp", context.packageName) } @Test fun userCanEnterMobile() { insertInMobileEditText("8976527465") } @Test fun userCanEnterPassword() { insertInPwdEditText("dh239048fy") } @Test fun userCanClickRemSwitch() { clickRememberSwitch() } @Test fun userCanClickSignUpText() { clickSignUpText() } @Test fun userCanClickForgotTextView() { clickForgotTextView() } @Test fun userCanClickLoginButton() { clickLoginButton() } @Test fun onSignUpClick_navigateToSignUpFragment() { clickSignUpText() assertThat(navController.currentDestination?.id, `is`(R.id.SignupFragment)) } @Test fun onLogin_emptyForm_showsError() { clickLoginButton() onView(withId(R.id.login_error_text_view)).check(matches(isDisplayed())) } @Test fun onLogin_invalidMobile_showsError() { insertInMobileEditText(" 467856 ") insertInPwdEditText("fd3g24") clickLoginButton() onView(withId(R.id.login_mobile_edit_text)).check(matches(hasErrorText(`is`(MOB_ERROR_TEXT)))) } @Test fun onLogin_validData_showsNoError() { Intents.init() insertInMobileEditText("9966339966") insertInPwdEditText("1234") clickLoginButton() intended(hasComponent(OtpActivity::class.java.name)) } private fun insertInMobileEditText(phone: String) = onView(withId(R.id.login_mobile_edit_text)).perform( scrollTo(), clearText(), typeText(phone) ) private fun insertInPwdEditText(pwd: String) = onView(withId(R.id.login_password_edit_text)).perform( scrollTo(), clearText(), typeText(pwd) ) private fun clickRememberSwitch() = onView(withId(R.id.login_rem_switch)) .perform(scrollTo(), click()) private fun clickForgotTextView() = onView(withId(R.id.login_forgot_tv)) .perform(scrollTo(), click()) private fun clickLoginButton() = onView(withId(R.id.login_login_btn)) .perform(scrollTo(), click()) private fun clickSignUpText() = onView(withId(R.id.login_signup_text_view)).perform( scrollTo(), clickClickableSpan("Sign Up") ) } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ui/loginSignup/SignupFragmentTest.kt ================================================ package com.vishalgaur.shoppingapp.ui.loginSignup import androidx.fragment.app.testing.FragmentScenario import androidx.fragment.app.testing.launchFragmentInContainer import androidx.navigation.NavController import androidx.navigation.Navigation import androidx.navigation.testing.TestNavHostController import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.* import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread import androidx.test.platform.app.InstrumentationRegistry import com.vishalgaur.shoppingapp.EMAIL_ERROR_TEXT import com.vishalgaur.shoppingapp.MOB_ERROR_TEXT import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.clickClickableSpan import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import org.hamcrest.Matchers.`is` import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SignupFragmentTest { private lateinit var signUpScenario: FragmentScenario private lateinit var navController: NavController private lateinit var sessionManager: ShoppingAppSessionManager @Before fun setUp() { sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext()) sessionManager.logoutFromSession() signUpScenario = launchFragmentInContainer(themeResId = R.style.Theme_ShoppingApp) navController = TestNavHostController(ApplicationProvider.getApplicationContext()) runOnUiThread { navController.setGraph(R.navigation.signup_nav_graph) signUpScenario.onFragment { Navigation.setViewNavController(it.requireView(), navController) } } } @Test fun useAppContext() { val context = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.vishalgaur.shoppingapp", context.packageName) } @Test fun userCanEnterName() { insertInNameEditText("Vishal Gaur ") } @Test fun userCanEnterMobile() { insertInMobileEditText("8976527465") } @Test fun userCanEnterEmail() { insertInEmailEditText(" weuiyjkh@ujhyew.dciwe") } @Test fun userCanEnterPassword() { insertInPwdEditText("dh239048fy") } @Test fun userCanEnterInCnfPassword() { insertInCnfPwdEditText("con34uyf98") } @Test fun userCanClickTermsSwitch() { clickTermsSwitch() } @Test fun userCanClickSellerSwitch() { clickSellerSwitch() } @Test fun userCanClickSignUp() { clickSignUpButton() } @Test fun userCanClickLogInText() { clickLoginText() } @Test fun onLoginClick_navigateToLoginFragment() { clickLoginText() assertEquals(navController.currentDestination?.id, R.id.LoginFragment) } @Test fun onSignUp_emptyForm_showsError() { clickSignUpButton() onView(withId(R.id.signup_error_text_view)).check(matches(isDisplayed())) } @Test fun onSignUp_invalidEmail_showsEmailError() { insertInNameEditText("Vishal Gaur ") insertInMobileEditText("8976527465 ") insertInEmailEditText(" weuiyjyew.dciwe") insertInPwdEditText("dh239048fy") insertInCnfPwdEditText("dh239048fy") clickTermsSwitch() clickSignUpButton() onView(withId(R.id.signup_email_edit_text)).check(matches(hasErrorText(`is`(EMAIL_ERROR_TEXT)))) } @Test fun onSignUp_invalidMobile_showsMobileError() { insertInNameEditText("Vishal Gaur ") insertInMobileEditText("86527465 ") insertInEmailEditText(" weuiyj@yew.dciwe") insertInPwdEditText("dh239048fy") insertInCnfPwdEditText("dh239048fy") clickTermsSwitch() clickSignUpButton() onView(withId(R.id.signup_mobile_edit_text)).check(matches(hasErrorText(`is`(MOB_ERROR_TEXT)))) } @Test fun onSignUp_notAcceptedTerms_showsError() { insertInNameEditText("Vishal Gaur ") insertInMobileEditText("8652744565 ") insertInEmailEditText(" weuiyj@yew.dciwe") insertInPwdEditText("dh239048fy") insertInCnfPwdEditText("dh239048fy") clickSignUpButton() onView(withId(R.id.signup_error_text_view)).check(matches(withText("Accept the Terms."))) } @Test fun onSignUp_notSamePasswords_showsError() { insertInNameEditText("Vishal Gaur ") insertInMobileEditText("8652744565 ") insertInEmailEditText(" weuiyj@yew.dciwe") insertInPwdEditText("dh2398fy") insertInCnfPwdEditText("dh239048fy") clickTermsSwitch() clickSignUpButton() onView(withId(R.id.signup_error_text_view)).check(matches(withText("Both passwords are not same!"))) } @Test fun onSignUp_validForm_showsNoError() { Intents.init() insertInNameEditText("Vishal Gaur ") insertInMobileEditText("8652744565 ") insertInEmailEditText(" weuiyj@yew.dciwe") insertInPwdEditText("dh2398fy") insertInCnfPwdEditText("dh2398fy") clickTermsSwitch() clickSignUpButton() intended(hasComponent(OtpActivity::class.java.name)) } private fun insertInNameEditText(name: String) = onView(withId(R.id.signup_name_edit_text)).perform(scrollTo(), clearText(), typeText(name)) private fun insertInMobileEditText(phone: String) = onView(withId(R.id.signup_mobile_edit_text)).perform( scrollTo(), clearText(), typeText(phone) ) private fun insertInEmailEditText(email: String) = onView(withId(R.id.signup_email_edit_text)).perform( scrollTo(), clearText(), typeText(email) ) private fun insertInPwdEditText(pwd: String) = onView(withId(R.id.signup_password_edit_text)).perform( scrollTo(), clearText(), typeText(pwd) ) private fun insertInCnfPwdEditText(pwd2: String) = onView(withId(R.id.signup_cnf_password_edit_text)).perform( scrollTo(), clearText(), typeText(pwd2) ) private fun clickTermsSwitch() = onView(withId(R.id.signup_policy_switch)).perform(scrollTo(), click()) private fun clickSellerSwitch() = onView(withId(R.id.signup_seller_switch)).perform(scrollTo(), click()) private fun clickSignUpButton() = onView(withId(R.id.signup_signup_btn)).perform(scrollTo(), click()) private fun clickLoginText() = onView(withId(R.id.signup_login_text_view)).perform( scrollTo(), clickClickableSpan("Log In") ) } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/AddEditAddressViewModelTest.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vishalgaur.shoppingapp.ServiceLocator import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.getOrAwaitValue import com.vishalgaur.shoppingapp.ui.AddAddressViewErrors import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest import org.hamcrest.Matchers.* import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @ExperimentalCoroutinesApi class AddEditAddressViewModelTest { private lateinit var addEditAddressViewModel: AddEditAddressViewModel private lateinit var authRepository: AuthRepoInterface val user = UserData( "sdjm43892yfh948ehod", "Vishal", "+919999988888", "vishal@somemail.com", "dh94328hd", ArrayList(), ArrayList(), ArrayList() ) @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { val context = ApplicationProvider.getApplicationContext() val sessionManager = ShoppingAppSessionManager(context) authRepository = FakeAuthRepository(sessionManager) authRepository.login(user, true) ServiceLocator.authRepository = authRepository addEditAddressViewModel = AddEditAddressViewModel(context) } @After fun cleanUp() = runBlockingTest { ServiceLocator.resetRepository() } @Test fun setIsEdit_setsValue() { runBlockingTest { addEditAddressViewModel.setIsEdit(false) val res = addEditAddressViewModel.isEdit.getOrAwaitValue() assertThat(res, `is`(false)) } } @Test fun setAddressData_setsData() = runBlockingTest { val address = UserData.Address( "add-id-121", "adg", "shgd", "IN", "sfhg45eyh", "", "kanpuit", "up", "309890", "9999988558" ) authRepository.insertAddress(address, user.userId) addEditAddressViewModel.setAddressData(address.addressId) val result = addEditAddressViewModel.addressData.getOrAwaitValue() assertThat(result, `is`(address)) } @Test fun submitAddress_emptyForm_returnsError() { val fname = "adg" val lname = "" val code = "IN" val streetAdd = "sfhg45eyh" val streetAdd2 = "" val city = "" val state = "up" val zip = "309890" val phone = "9999988558" addEditAddressViewModel.submitAddress( code, fname, lname, streetAdd, streetAdd2, city, state, zip, phone ) val result = addEditAddressViewModel.errorStatus.getOrAwaitValue() assertThat(result.size, `is`(greaterThan(0))) assertThat(result.contains(AddAddressViewErrors.ERR_CITY_EMPTY), `is`(true)) } @Test fun submitAddress_invalidZipcode_returnsError() { val fname = "adg" val lname = "serdg" val code = "IN" val streetAdd = "sfhg45eyh" val streetAdd2 = "" val city = "sfhg" val state = "up" val zip = "30990" val phone = "9999988558" addEditAddressViewModel.submitAddress( code, fname, lname, streetAdd, streetAdd2, city, state, zip, phone ) val result = addEditAddressViewModel.errorStatus.getOrAwaitValue() assertThat(result.size, `is`(greaterThan(0))) assertThat(result.contains(AddAddressViewErrors.ERR_ZIP_INVALID), `is`(true)) } @Test fun submitAddress_invalidPhone_returnsError() { val fname = "adg" val lname = "serdg" val code = "IN" val streetAdd = "sfhg45eyh" val streetAdd2 = "" val city = "sfhg" val state = "up" val zip = "309903" val phone = "9999988efg558" addEditAddressViewModel.submitAddress( code, fname, lname, streetAdd, streetAdd2, city, state, zip, phone ) val result = addEditAddressViewModel.errorStatus.getOrAwaitValue() assertThat(result.size, `is`(greaterThan(0))) assertThat(result.contains(AddAddressViewErrors.ERR_PHONE_INVALID), `is`(true)) } @Test fun submitAddress_validData_returnsNoError() { val fname = "adg" val lname = "serdg" val code = "IN" val streetAdd = "sfhg45eyh" val streetAdd2 = "" val city = "sfhg" val state = "up" val zip = "302203" val phone = "9879988558" addEditAddressViewModel.submitAddress( code, fname, lname, streetAdd, streetAdd2, city, state, zip, phone ) val result = addEditAddressViewModel.errorStatus.getOrAwaitValue() assertThat(result.size, `is`(0)) val resData = addEditAddressViewModel.newAddressData.getOrAwaitValue() assertThat(resData, `is`(notNullValue())) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/AddEditProductViewModelTest.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import android.net.Uri import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.core.net.toUri import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.getOrAwaitValue import com.vishalgaur.shoppingapp.ui.AddProductViewErrors import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.notNullValue import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AddEditProductViewModelTest { private lateinit var addEditProductViewModel: AddEditProductViewModel private lateinit var sessionManager: ShoppingAppSessionManager private val userSeller = UserData( "user1234selller", "Some Name", "+919999988888", "somemail123seller@somemail.com", "1234", emptyList(), emptyList(), emptyList(), "SELLER" ) @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext()) sessionManager.createLoginSession( userSeller.userId, userSeller.name, userSeller.mobile, false, true ) addEditProductViewModel = AddEditProductViewModel(ApplicationProvider.getApplicationContext()) } @Test fun setCategory_Shoes() { addEditProductViewModel.setCategory("Shoes") val result = addEditProductViewModel.selectedCategory.getOrAwaitValue() assertThat(result, `is`("Shoes")) } @Test fun setIsEdit_true() { addEditProductViewModel.setIsEdit(true) val result = addEditProductViewModel.isEdit.getOrAwaitValue() assertThat(result, `is`(true)) } @Test fun setIsEdit_false() { addEditProductViewModel.setIsEdit(false) val result = addEditProductViewModel.isEdit.getOrAwaitValue() assertThat(result, `is`(false)) } @Test fun submitProduct_noData_returnsEmptyError() { addEditProductViewModel.setIsEdit(false) val name = "" val price = null val mrp = null val desc = "" val sizes = emptyList() val colors = emptyList() val imgList = emptyList() addEditProductViewModel.submitProduct(name, price, mrp, desc, sizes, colors, imgList) val result = addEditProductViewModel.errorStatus.getOrAwaitValue() assertThat(result, `is`(AddProductViewErrors.EMPTY)) } @Test fun submitProduct_invalidPrice_returnsPriceError() { addEditProductViewModel.setIsEdit(false) val name = "vwsf" val mrp = 125.0 val price = 0.0 val desc = "crw rewg" val sizes = listOf(5, 6) val colors = listOf("red", "blue") val imgList = listOf("ffsd".toUri(), "sws".toUri()) addEditProductViewModel.submitProduct(name, price, mrp, desc, sizes, colors, imgList) val result = addEditProductViewModel.errorStatus.getOrAwaitValue() assertThat(result, `is`(AddProductViewErrors.ERR_PRICE_0)) } @Test fun submitProduct_allValid_returnsNoError() { addEditProductViewModel.setIsEdit(false) addEditProductViewModel.setCategory("Shoes") val name = "vwsf" val mrp = 125.0 val price = 100.0 val desc = "crw rewg" val sizes = listOf(5, 6) val colors = listOf("red", "blue") val imgList = listOf("ffsd".toUri(), "sws".toUri()) addEditProductViewModel.submitProduct(name, price, mrp, desc, sizes, colors, imgList) val result = addEditProductViewModel.errorStatus.getOrAwaitValue() val resultPro = addEditProductViewModel.newProductData.getOrAwaitValue() assertThat(result, `is`(AddProductViewErrors.NONE)) assertThat(resultPro, `is`(notNullValue())) assertThat(resultPro.name, `is`("vwsf")) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/AuthViewModelTest.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vishalgaur.shoppingapp.getOrAwaitValue import com.vishalgaur.shoppingapp.ui.LoginViewErrors import com.vishalgaur.shoppingapp.ui.SignUpViewErrors import org.hamcrest.Matchers.* import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AuthViewModelTest { private lateinit var authViewModel: AuthViewModel @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { authViewModel = AuthViewModel(ApplicationProvider.getApplicationContext()) } @Test fun signUpSubmitData_noData_returnsEmptyError() { val name = "" val mobile = "" val email = "" val pwd1 = "" val pwd2 = "" val isAccepted = false val isSeller = false authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller) val result = authViewModel.errorStatus.getOrAwaitValue() assertThat(result, `is`(SignUpViewErrors.ERR_EMPTY)) assertThat(authViewModel.userData.value, `is`(nullValue())) } @Test fun signUpSubmitData_notAccepted_returnsNotAccError() { val name = "uigbivs ihgfdsg" val mobile = "9988665555" val email = "owhfoi@oihw.cro" val pwd1 = "1234" val pwd2 = "1234" val isAccepted = false val isSeller = false authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller) val result = authViewModel.errorStatus.getOrAwaitValue() assertThat(result, `is`(SignUpViewErrors.ERR_NOT_ACC)) assertThat(authViewModel.userData.value, `is`(nullValue())) } @Test fun signUpSubmitData_pwdNotEq_returnsPwdError() { val name = "uigbivs ihgfdsg" val mobile = "9988665555" val email = "owhfoi@oihw.cro" val pwd1 = "12345" val pwd2 = "1234" val isAccepted = false val isSeller = false authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller) val result = authViewModel.errorStatus.getOrAwaitValue() assertThat(result, `is`(SignUpViewErrors.ERR_PWD12NS)) assertThat(authViewModel.userData.value, `is`(nullValue())) } @Test fun signUpSubmitData_invalidEmail_returnsEmailError() { val name = "uigbivs ihgfdsg" val mobile = "9988665555" val email = "owhfoi@oihwo" val pwd1 = "1234" val pwd2 = "1234" val isAccepted = true val isSeller = false authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller) val result = authViewModel.errorStatus.getOrAwaitValue() assertThat(result, `is`(SignUpViewErrors.ERR_EMAIL)) assertThat(authViewModel.userData.value, `is`(nullValue())) } @Test fun signUpSubmitData_invalidMobile_returnsMobError() { val name = "uigbivs ihgfdsg" val mobile = "9988665fng55" val email = "owhfoi@oihw.coo" val pwd1 = "1234" val pwd2 = "1234" val isAccepted = true val isSeller = false authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller) val result = authViewModel.errorStatus.getOrAwaitValue() assertThat(result, `is`(SignUpViewErrors.ERR_MOBILE)) assertThat(authViewModel.userData.value, `is`(nullValue())) } @Test fun signUpSubmitData_invalidEmailMobile_returnsEmailMobError() { val name = "uigbivs ihgfdsg" val mobile = "9988665fng55" val email = "owhfoi@oihwoo" val pwd1 = "1234" val pwd2 = "1234" val isAccepted = true val isSeller = false authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller) val result = authViewModel.errorStatus.getOrAwaitValue() assertThat(result, `is`(SignUpViewErrors.ERR_EMAIL_MOBILE)) assertThat(authViewModel.userData.value, `is`(nullValue())) } @Test fun signUpSubmitData_validData_returnsNoError() { val name = " uigbivs ihgfdsg" val mobile = " 9988665755" val email = "owhfoi@oihwoo.cwdo " val pwd1 = "1234" val pwd2 = "1234" val isAccepted = true val isSeller = false authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller) val result = authViewModel.errorStatus.getOrAwaitValue() val dataRes = authViewModel.userData.getOrAwaitValue() assertThat(result, `is`(SignUpViewErrors.NONE)) assertThat(dataRes, `is`(notNullValue())) assertThat(dataRes.name, `is`("uigbivs ihgfdsg")) } @Test fun loginSubmitData_noData_returnsEmptyError() { val mobile = "" val pwd = "" authViewModel.loginSubmitData(mobile, pwd) val result = authViewModel.errorStatusLoginFragment.getOrAwaitValue() assertThat(result, `is`(LoginViewErrors.ERR_EMPTY)) } @Test fun loginSubmitData_invalidMobile_returnsMobileError() { val mobile = "9fwd988556699" val pwd = "123" authViewModel.loginSubmitData(mobile, pwd) val result = authViewModel.errorStatusLoginFragment.getOrAwaitValue() assertThat(result, `is`(LoginViewErrors.ERR_MOBILE)) } @Test fun loginSubmitData_validData_returnsNoError() { val mobile = "9988556699" val pwd = "123" authViewModel.loginSubmitData(mobile, pwd) val result = authViewModel.errorStatusLoginFragment.getOrAwaitValue() assertThat(result, `is`(LoginViewErrors.NONE)) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/HomeViewModelTest.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vishalgaur.shoppingapp.ServiceLocator import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.getOrAwaitValue import org.hamcrest.Matchers.`is` import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class HomeViewModelTest { private lateinit var homeViewModel: HomeViewModel private lateinit var authRepository: AuthRepoInterface @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { val sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext()) authRepository = FakeAuthRepository(sessionManager) ServiceLocator.authRepository = authRepository homeViewModel = HomeViewModel(ApplicationProvider.getApplicationContext()) } @After fun cleanUp() { ServiceLocator.resetRepository() } @Test fun setDataLoaded_setsValue() { homeViewModel.setDataLoaded() val result = homeViewModel.storeDataStatus.getOrAwaitValue() assertThat(result, `is`(StoreDataStatus.DONE)) } @Test fun filterProducts_All() { homeViewModel.filterProducts("All") val result = homeViewModel.filterCategory.getOrAwaitValue() assertThat(result, `is`("All")) } @Test fun filterProducts_Shoes() { homeViewModel.filterProducts("Shoes") val result = homeViewModel.filterCategory.getOrAwaitValue() assertThat(result, `is`("Shoes")) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/OrderViewModelTest.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vishalgaur.shoppingapp.ServiceLocator import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository import com.vishalgaur.shoppingapp.data.source.FakeProductsRepository import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.getOrAwaitValue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runBlockingTest import org.hamcrest.Matchers.* import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @ExperimentalCoroutinesApi class OrderViewModelTest { private lateinit var orderViewModel: OrderViewModel private lateinit var productsRepository: ProductsRepoInterface private lateinit var authRepository: AuthRepoInterface val user = UserData( "sdjm43892yfh948ehod", "Vishal", "+919999988888", "vishal@somemail.com", "dh94328hd", ArrayList(), ArrayList(), ArrayList() ) val address = UserData.Address( "add-id-121", "adg", "shgd", "IN", "sfhg45eyh", "", "kanpuit", "up", "309890", "9999988558" ) val item1 = UserData.CartItem( "item2123", "pro123", "owner23", 1, "Red", 10 ) val item2 = UserData.CartItem( "item2123456347", "pro12345", "owner23", 1, "Blue", 9 ) @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { productsRepository = FakeProductsRepository() val context = ApplicationProvider.getApplicationContext() val sessionManager = ShoppingAppSessionManager(context) authRepository = FakeAuthRepository(sessionManager) authRepository.login(user, true) ServiceLocator.productsRepository = productsRepository ServiceLocator.authRepository = authRepository orderViewModel = OrderViewModel(context) } @After fun cleanUp() = runBlockingTest { ServiceLocator.resetRepository() } @Test fun getCartItems_loadsData() = runBlocking { orderViewModel.getCartItems() delay(200) val result = orderViewModel.dataStatus.getOrAwaitValue() assertThat(result, `is`(StoreDataStatus.DONE)) } @Test fun getAddresses_noAddress_loadsData() = runBlocking { orderViewModel.getUserAddresses() delay(200) val result = orderViewModel.dataStatus.getOrAwaitValue() assertThat(result, `is`(StoreDataStatus.DONE)) val resAdd = orderViewModel.userAddresses.getOrAwaitValue() assertThat(resAdd.size, `is`(0)) } @Test fun getAddresses_hasAddress_loadsData() = runBlocking { authRepository.insertAddress(address, user.userId) delay(100) orderViewModel.getUserAddresses() delay(100) val result = orderViewModel.dataStatus.getOrAwaitValue() assertThat(result, `is`(StoreDataStatus.DONE)) val resAdd = orderViewModel.userAddresses.getOrAwaitValue() assertThat(resAdd.size, `is`(1)) } @Test fun deleteAddress_deletesAddress() = runBlocking{ authRepository.insertAddress(address, user.userId) delay(100) orderViewModel.getUserAddresses() delay(100) val resAdd = orderViewModel.userAddresses.getOrAwaitValue() assertThat(resAdd.size, `is`(1)) orderViewModel.deleteAddress(address.addressId) delay(100) val resAdd2 = orderViewModel.userAddresses.getOrAwaitValue() assertThat(resAdd2.size, `is`(0)) } @Test fun getItemsPriceTotal_returnsTotal() { runBlocking { authRepository.insertCartItemByUserId(item1, user.userId) delay(100) val result = orderViewModel.getItemsPriceTotal() assertThat(result, `is`(0.0)) } } @Test fun toggleLike() { runBlocking { val res1 = orderViewModel.userLikes.getOrAwaitValue() orderViewModel.toggleLikeProduct("pro-if2r3") delay(100) val res2 = orderViewModel.userLikes.getOrAwaitValue() assertThat(res1.size, not(res2.size)) } } @Test fun setQuantity_setsQuantity() = runBlocking { authRepository.insertCartItemByUserId(item1, user.userId) delay(100) val res1 = orderViewModel.cartItems.getOrAwaitValue().find { it.itemId == item1.itemId } val size1 = res1?.quantity ?: 0 orderViewModel.setQuantityOfItem(item1.itemId, 1) delay(100) val res2 = orderViewModel.cartItems.getOrAwaitValue().find { it.itemId == item1.itemId } val size2 = res2?.quantity ?: 0 assertThat(size1, `is`(1)) assertThat(size2, `is`(2)) } @Test fun deleteItemFromCart_deletesItem() = runBlocking { authRepository.insertCartItemByUserId(item1, user.userId) delay(100) val res1 = orderViewModel.cartItems.getOrAwaitValue().find { it.itemId == item1.itemId } assertThat(res1, `is`(notNullValue())) orderViewModel.deleteItemFromCart(item1.itemId) delay(100) val res2 = orderViewModel.cartItems.getOrAwaitValue().find { it.itemId == item1.itemId } assertThat(res2, `is`(nullValue())) } } ================================================ FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/ProductViewModelTest.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vishalgaur.shoppingapp.ServiceLocator import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository import com.vishalgaur.shoppingapp.data.source.FakeProductsRepository import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface import com.vishalgaur.shoppingapp.getOrAwaitValue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runBlockingTest import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.not import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @ExperimentalCoroutinesApi class ProductViewModelTest { private lateinit var productViewModel: ProductViewModel private lateinit var productId: String private lateinit var productsRepository: ProductsRepoInterface private lateinit var authRepository: AuthRepoInterface val user = UserData( "sdjm43892yfh948ehod", "Vishal", "+919999988888", "vishal@somemail.com", "dh94328hd", ArrayList(), ArrayList(), ArrayList() ) @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp() { productsRepository = FakeProductsRepository() val sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext()) authRepository = FakeAuthRepository(sessionManager) authRepository.login(user, true) ServiceLocator.productsRepository = productsRepository ServiceLocator.authRepository = authRepository productId = "pro-shoes-wofwopjf-1" productViewModel = ProductViewModel(productId, ApplicationProvider.getApplicationContext()) } @After fun cleanUp() = runBlockingTest { ServiceLocator.resetRepository() } @Test fun toggleLikeProduct_false_true() { val result1 = productViewModel.isLiked.value runBlocking { productViewModel.toggleLikeProduct() delay(1000) val result2 = productViewModel.isLiked.getOrAwaitValue() assertThat(result1, not(result2)) } } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ServiceLocator.kt ================================================ package com.vishalgaur.shoppingapp import android.content.Context import androidx.annotation.VisibleForTesting import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.source.ProductDataSource import com.vishalgaur.shoppingapp.data.source.UserDataSource import com.vishalgaur.shoppingapp.data.source.local.ProductsLocalDataSource import com.vishalgaur.shoppingapp.data.source.local.ShoppingAppDatabase import com.vishalgaur.shoppingapp.data.source.local.UserLocalDataSource import com.vishalgaur.shoppingapp.data.source.remote.AuthRemoteDataSource import com.vishalgaur.shoppingapp.data.source.remote.ProductsRemoteDataSource import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.data.source.repository.AuthRepository import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepository object ServiceLocator { private var database: ShoppingAppDatabase? = null private val lock = Any() @Volatile var authRepository: AuthRepoInterface? = null @VisibleForTesting set @Volatile var productsRepository: ProductsRepoInterface? = null @VisibleForTesting set fun provideAuthRepository(context: Context): AuthRepoInterface { synchronized(this) { return authRepository ?: createAuthRepository(context) } } fun provideProductsRepository(context: Context): ProductsRepoInterface { synchronized(this) { return productsRepository ?: createProductsRepository(context) } } @VisibleForTesting fun resetRepository() { synchronized(lock) { database?.apply { clearAllTables() close() } database = null authRepository = null } } private fun createProductsRepository(context: Context): ProductsRepoInterface { val newRepo = ProductsRepository(ProductsRemoteDataSource(), createProductsLocalDataSource(context)) productsRepository = newRepo return newRepo } private fun createAuthRepository(context: Context): AuthRepoInterface { val appSession = ShoppingAppSessionManager(context.applicationContext) val newRepo = AuthRepository(createUserLocalDataSource(context), AuthRemoteDataSource(), appSession) authRepository = newRepo return newRepo } private fun createProductsLocalDataSource(context: Context): ProductDataSource { val database = database ?: ShoppingAppDatabase.getInstance(context.applicationContext) return ProductsLocalDataSource(database.productsDao()) } private fun createUserLocalDataSource(context: Context): UserDataSource { val database = database ?: ShoppingAppDatabase.getInstance(context.applicationContext) return UserLocalDataSource(database.userDao()) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ShoppingApplication.kt ================================================ package com.vishalgaur.shoppingapp import android.app.Application import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface class ShoppingApplication : Application() { val authRepository: AuthRepoInterface get() = ServiceLocator.provideAuthRepository(this) val productsRepository: ProductsRepoInterface get() = ServiceLocator.provideProductsRepository(this) override fun onCreate() { super.onCreate() } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/Utils.kt ================================================ package com.vishalgaur.shoppingapp import java.util.* import java.util.regex.Pattern import kotlin.math.roundToInt const val MOB_ERROR_TEXT = "Enter valid mobile number!" const val EMAIL_ERROR_TEXT = "Enter valid email address!" const val ERR_INIT = "ERROR" const val ERR_EMAIL = "_EMAIL" const val ERR_MOBILE = "_MOBILE" const val ERR_UPLOAD = "UploadErrorException" internal fun isEmailValid(email: String): Boolean { val EMAIL_PATTERN = Pattern.compile( "\\s*[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@" + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+\\s*" ) return if (email.isEmpty()) { false } else { EMAIL_PATTERN.matcher(email).matches() } } internal fun isPhoneValid(phone: String): Boolean { val PHONE_PATTERN = Pattern.compile("^\\s*[6-9]\\d{9}\\s*\$") return if (phone.isEmpty()) { false } else { PHONE_PATTERN.matcher(phone).matches() } } internal fun isZipCodeValid(zipCode: String): Boolean { val ZIPCODE_PATTERN = Pattern.compile("^\\s*[1-9]\\d{5}\\s*\$") return if (zipCode.isEmpty()) { false } else { ZIPCODE_PATTERN.matcher(zipCode).matches() } } internal fun getRandomString(length: Int, uNum: String, endLength: Int): String { val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') fun getStr(l: Int): String = (1..l).map { allowedChars.random() }.joinToString("") return getStr(length) + uNum + getStr(endLength) } internal fun getProductId(ownerId: String, proCategory: String): String { val uniqueId = UUID.randomUUID().toString() return "pro-$proCategory-$ownerId-$uniqueId" } internal fun getOfferPercentage(costPrice: Double, sellingPrice: Double): Int { if (costPrice == 0.0 || sellingPrice == 0.0 || costPrice <= sellingPrice) return 0 val off = ((costPrice - sellingPrice) * 100) / costPrice return off.roundToInt() } internal fun getAddressId(userId: String): String { val uniqueId = UUID.randomUUID().toString() return "$userId-$uniqueId" } internal fun shouldBypassOTPValidation() : Boolean { return false } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/Product.kt ================================================ package com.vishalgaur.shoppingapp.data import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey import kotlinx.parcelize.Parcelize @Parcelize @Entity(tableName = "products") data class Product @JvmOverloads constructor( @PrimaryKey var productId: String = "", var name: String = "", var owner: String = "", var description: String = "", var category: String = "", var price: Double = 0.0, var mrp: Double = 0.0, var availableSizes: List = ArrayList(), var availableColors: List = ArrayList(), var images: List = ArrayList(), var rating: Double = 0.0 ) : Parcelable { fun toHashMap(): HashMap { return hashMapOf( "productId" to productId, "name" to name, "owner" to owner, "description" to description, "category" to category, "price" to price, "mrp" to mrp, "availableSizes" to availableSizes, "availableColors" to availableColors, "images" to images, "rating" to rating ) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/Result.kt ================================================ package com.vishalgaur.shoppingapp.data import com.vishalgaur.shoppingapp.data.Result.Success /** * A generic class that holds a value with its loading status. * @param */ sealed class Result { data class Success(val data: T) : Result() data class Error(val exception: Exception) : Result() object Loading : Result() override fun toString(): String { return when (this) { is Success<*> -> "Success[data=$data]" is Error -> "Error[exception=$exception]" Loading -> "Loading" } } } /** * `true` if [Result] is of type [Success] & holds non-null [Success.data]. */ val Result<*>.succeeded get() = this is Success && data != null ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/ShoppingAppSessionManager.kt ================================================ package com.vishalgaur.shoppingapp.data import android.content.Context import android.content.SharedPreferences class ShoppingAppSessionManager(context: Context) { var userSession: SharedPreferences = context.getSharedPreferences("userSessionData", Context.MODE_PRIVATE) var editor: SharedPreferences.Editor = userSession.edit() fun createLoginSession( id: String, name: String, mobile: String, isRemOn: Boolean, isSeller: Boolean ) { editor.putBoolean(IS_LOGIN, true) editor.putString(KEY_ID, id) editor.putString(KEY_NAME, name) editor.putString(KEY_MOBILE, mobile) editor.putBoolean(KEY_REMEMBER_ME, isRemOn) editor.putBoolean(KEY_IS_SELLER, isSeller) editor.commit() } fun isUserSeller(): Boolean = userSession.getBoolean(KEY_IS_SELLER, false) fun isRememberMeOn(): Boolean = userSession.getBoolean(KEY_REMEMBER_ME, false) fun getPhoneNumber(): String? = userSession.getString(KEY_MOBILE, null) fun getUserDataFromSession(): HashMap { return hashMapOf( KEY_ID to userSession.getString(KEY_ID, null), KEY_NAME to userSession.getString(KEY_NAME, null), KEY_MOBILE to userSession.getString(KEY_MOBILE, null) ) } fun getUserIdFromSession(): String? = userSession.getString(KEY_ID, null) fun isLoggedIn(): Boolean = userSession.getBoolean(IS_LOGIN, false) fun logoutFromSession() { editor.clear() editor.commit() } companion object { private const val IS_LOGIN = "isLoggedIn" private const val KEY_NAME = "userName" private const val KEY_MOBILE = "userMobile" private const val KEY_ID = "userId" private const val KEY_REMEMBER_ME = "isRemOn" private const val KEY_IS_SELLER = "isSeller" } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/UserData.kt ================================================ package com.vishalgaur.shoppingapp.data import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverters import com.vishalgaur.shoppingapp.data.utils.ObjectListTypeConvertor import com.vishalgaur.shoppingapp.data.utils.OrderStatus import com.vishalgaur.shoppingapp.data.utils.UserType import kotlinx.parcelize.Parcelize import java.util.* import kotlin.collections.ArrayList @Parcelize @Entity(tableName = "users") data class UserData( @PrimaryKey var userId: String = "", var name: String = "", var mobile: String = "", var email: String = "", var password: String = "", var likes: List = ArrayList(), @TypeConverters(ObjectListTypeConvertor::class) var addresses: List
= ArrayList(), @TypeConverters(ObjectListTypeConvertor::class) var cart: List = ArrayList(), @TypeConverters(ObjectListTypeConvertor::class) var orders: List = ArrayList(), var userType: String = UserType.CUSTOMER.name ) : Parcelable { fun toHashMap(): HashMap { return hashMapOf( "userId" to userId, "name" to name, "email" to email, "mobile" to mobile, "password" to password, "likes" to likes, "addresses" to addresses.map { it.toHashMap() }, "userType" to userType ) } @Parcelize data class OrderItem( var orderId: String = "", var customerId: String = "", var items: List = ArrayList(), var itemsPrices: Map = mapOf(), var deliveryAddress: Address = Address(), var shippingCharges: Double = 0.0, var paymentMethod: String = "", var orderDate: Date = Date(), var status: String = OrderStatus.CONFIRMED.name ) : Parcelable { fun toHashMap(): HashMap { return hashMapOf( "orderId" to orderId, "customerId" to customerId, "items" to items.map { it.toHashMap() }, "itemsPrices" to itemsPrices, "deliveryAddress" to deliveryAddress.toHashMap(), "shippingCharges" to shippingCharges, "paymentMethod" to paymentMethod, "orderDate" to orderDate, "status" to status ) } } @Parcelize data class Address( var addressId: String = "", var fName: String = "", var lName: String = "", var countryISOCode: String = "", var streetAddress: String = "", var streetAddress2: String = "", var city: String = "", var state: String = "", var zipCode: String = "", var phoneNumber: String = "" ) : Parcelable { fun toHashMap(): HashMap { return hashMapOf( "addressId" to addressId, "fName" to fName, "lName" to lName, "countryISOCode" to countryISOCode, "streetAddress" to streetAddress, "streetAddress2" to streetAddress2, "city" to city, "state" to state, "zipCode" to zipCode, "phoneNumber" to phoneNumber ) } } @Parcelize data class CartItem( var itemId: String = "", var productId: String = "", var ownerId: String = "", var quantity: Int = 0, var color: String?, var size: Int? ) : Parcelable { constructor() : this("", "", "", 0, "NA", -1) fun toHashMap(): HashMap { val hashMap = hashMapOf() hashMap["itemId"] = itemId hashMap["productId"] = productId hashMap["ownerId"] = ownerId hashMap["quantity"] = quantity if (color != null) hashMap["color"] = color!! if (size != null) hashMap["size"] = size!! return hashMap } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/ProductDataSource.kt ================================================ package com.vishalgaur.shoppingapp.data.source import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result interface ProductDataSource { fun observeProducts(): LiveData>?> suspend fun getAllProducts(): Result> suspend fun refreshProducts() {} suspend fun getProductById(productId: String): Result suspend fun insertProduct(newProduct: Product) suspend fun updateProduct(proData: Product) fun observeProductsByOwner(ownerId: String): LiveData>?> { return MutableLiveData() } suspend fun getAllProductsByOwner(ownerId: String): Result> { return Result.Success(emptyList()) } suspend fun uploadImage(uri: Uri, fileName: String): Uri? { return null } fun revertUpload(fileName: String) {} fun deleteImage(imgUrl: String) {} suspend fun deleteProduct(productId: String) suspend fun deleteAllProducts() {} suspend fun insertMultipleProducts(data: List) {} } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/UserDataSource.kt ================================================ package com.vishalgaur.shoppingapp.data.source import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.EmailMobileData interface UserDataSource { suspend fun addUser(userData: UserData) suspend fun getUserById(userId: String): Result fun updateEmailsAndMobiles(email: String, mobile: String) {} suspend fun getEmailsAndMobiles(): EmailMobileData? { return null } suspend fun getUserByMobileAndPassword( mobile: String, password: String ): MutableList { return mutableListOf() } suspend fun likeProduct(productId: String, userId: String) {} suspend fun dislikeProduct(productId: String, userId: String) {} suspend fun insertAddress(newAddress: UserData.Address, userId: String) {} suspend fun updateAddress(newAddress: UserData.Address, userId: String) {} suspend fun deleteAddress(addressId: String, userId: String) {} suspend fun insertCartItem(newItem: UserData.CartItem, userId: String) {} suspend fun updateCartItem(item: UserData.CartItem, userId: String) {} suspend fun deleteCartItem(itemId: String, userId: String) {} suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String) {} suspend fun setStatusOfOrderByUserId(orderId: String, userId: String, status: String) {} suspend fun clearUser() {} suspend fun getUserByMobile(phoneNumber: String): UserData? { return null } suspend fun getOrdersByUserId(userId: String): Result?> suspend fun getAddressesByUserId(userId: String): Result?> suspend fun getLikesByUserId(userId: String): Result?> } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/ProductsDao.kt ================================================ package com.vishalgaur.shoppingapp.data.source.local import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import com.vishalgaur.shoppingapp.data.Product @Dao interface ProductsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(product: Product) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertListOfProducts(products: List) @Query("SELECT * FROM products") suspend fun getAllProducts(): List @Query("SELECT * FROM products") fun observeProducts(): LiveData> @Query("SELECT * FROM products WHERE owner = :ownerId") fun observeProductsByOwner(ownerId: String): LiveData> @Query("SELECT * FROM products WHERE productId = :proId") suspend fun getProductById(proId: String): Product? @Query("SELECT * FROM products WHERE owner = :ownerId") suspend fun getProductsByOwnerId(ownerId: String): List @Query("DELETE FROM products WHERE productId = :proId") suspend fun deleteProductById(proId: String): Int @Query("DELETE FROM products") suspend fun deleteAllProducts() } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/ProductsLocalDataSource.kt ================================================ package com.vishalgaur.shoppingapp.data.source.local import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.* import com.vishalgaur.shoppingapp.data.source.ProductDataSource import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class ProductsLocalDataSource internal constructor( private val productsDao: ProductsDao, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) : ProductDataSource { override fun observeProducts(): LiveData>?> { return try { Transformations.map(productsDao.observeProducts()) { Success(it) } } catch (e: Exception) { Transformations.map(MutableLiveData(e)) { Error(e) } } } override fun observeProductsByOwner(ownerId: String): LiveData>?> { return try { Transformations.map(productsDao.observeProductsByOwner(ownerId)) { Success(it) } } catch (e: Exception) { Transformations.map(MutableLiveData(e)) { Error(e) } } } override suspend fun getAllProducts(): Result> = withContext(ioDispatcher) { return@withContext try { Success(productsDao.getAllProducts()) } catch (e: Exception) { Error(e) } } override suspend fun getAllProductsByOwner(ownerId: String): Result> = withContext(ioDispatcher) { return@withContext try { Success(productsDao.getProductsByOwnerId(ownerId)) } catch (e: Exception) { Error(e) } } override suspend fun getProductById(productId: String): Result = withContext(ioDispatcher) { try { val product = productsDao.getProductById(productId) if (product != null) { return@withContext Success(product) } else { return@withContext Error(Exception("Product Not Found!")) } } catch (e: Exception) { return@withContext Error(e) } } override suspend fun insertProduct(newProduct: Product) = withContext(ioDispatcher) { productsDao.insert(newProduct) } override suspend fun updateProduct(proData: Product) = withContext(ioDispatcher) { productsDao.insert(proData) } override suspend fun insertMultipleProducts(data: List) = withContext(ioDispatcher) { productsDao.insertListOfProducts(data) } override suspend fun deleteProduct(productId: String): Unit = withContext(ioDispatcher) { productsDao.deleteProductById(productId) } override suspend fun deleteAllProducts() = withContext(ioDispatcher) { productsDao.deleteAllProducts() } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/ShoppingAppDatabase.kt ================================================ package com.vishalgaur.shoppingapp.data.source.local import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.DateTypeConvertors import com.vishalgaur.shoppingapp.data.utils.ListTypeConverter import com.vishalgaur.shoppingapp.data.utils.ObjectListTypeConvertor @Database(entities = [UserData::class, Product::class], version = 2) @TypeConverters(ListTypeConverter::class, ObjectListTypeConvertor::class, DateTypeConvertors::class) abstract class ShoppingAppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun productsDao(): ProductsDao companion object { @Volatile private var INSTANCE: ShoppingAppDatabase? = null fun getInstance(context: Context): ShoppingAppDatabase = INSTANCE ?: synchronized(this) { INSTANCE ?: buildDatabase(context).also { INSTANCE = it } } private fun buildDatabase(context: Context) = Room.databaseBuilder( context.applicationContext, ShoppingAppDatabase::class.java, "ShoppingAppDb" ) .fallbackToDestructiveMigration() .allowMainThreadQueries() .build() } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/UserDao.kt ================================================ package com.vishalgaur.shoppingapp.data.source.local import androidx.room.* import com.vishalgaur.shoppingapp.data.UserData @Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(uData: UserData) @Query("SELECT * FROM users WHERE userId = :userId") suspend fun getById(userId: String): UserData? @Query("SELECT * FROM users WHERE mobile = :mobile") suspend fun getByMobile(mobile: String): UserData? @Update(entity = UserData::class) suspend fun updateUser(obj: UserData) @Query("DELETE FROM users") suspend fun clear() } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/UserLocalDataSource.kt ================================================ package com.vishalgaur.shoppingapp.data.source.local import android.util.Log import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.* import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.UserDataSource import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class UserLocalDataSource internal constructor( private val userDao: UserDao, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) : UserDataSource { override suspend fun addUser(userData: UserData) { withContext(ioDispatcher) { userDao.clear() userDao.insert(userData) } } override suspend fun getUserById(userId: String): Result = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { return@withContext Success(uData) } else { return@withContext Error(Exception("User Not Found!")) } } catch (e: Exception) { return@withContext Error(e) } } override suspend fun getUserByMobile(phoneNumber: String): UserData? = withContext(ioDispatcher) { try { val uData = userDao.getByMobile(phoneNumber) if (uData != null) { return@withContext uData } else { return@withContext null } } catch (e: Exception) { Log.d("UserLocalSource", "onGetUser: Error Occurred, $e") return@withContext null } } override suspend fun getOrdersByUserId(userId: String): Result?> = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val ordersList = uData.orders return@withContext Success(ordersList) } else { return@withContext Error(Exception("User Not Found")) } } catch (e: Exception) { Log.d("UserLocalSource", "onGetOrders: Error Occurred, ${e.message}") return@withContext Error(e) } } override suspend fun getAddressesByUserId(userId: String): Result?> = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val addressList = uData.addresses return@withContext Success(addressList) } else { return@withContext Error(Exception("User Not Found")) } } catch (e: Exception) { Log.d("UserLocalSource", "onGetAddress: Error Occurred, ${e.message}") return@withContext Error(e) } } override suspend fun getLikesByUserId(userId: String): Result?> = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val likesList = uData.likes return@withContext Success(likesList) } else { return@withContext Error(Exception("User Not Found")) } } catch (e: Exception) { Log.d("UserLocalSource", "onGetLikes: Error Occurred, ${e.message}") return@withContext Error(e) } } override suspend fun dislikeProduct(productId: String, userId: String) = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val likesList = uData.likes.toMutableList() likesList.remove(productId) uData.likes = likesList userDao.updateUser(uData) } else { throw Exception("User Not Found") } } catch (e: Exception) { Log.d("UserLocalSource", "onGetLikes: Error Occurred, ${e.message}") throw e } } override suspend fun likeProduct(productId: String, userId: String) = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val likesList = uData.likes.toMutableList() likesList.add(productId) uData.likes = likesList userDao.updateUser(uData) } else { throw Exception("User Not Found") } } catch (e: Exception) { Log.d("UserLocalSource", "onGetLikes: Error Occurred, ${e.message}") throw e } } override suspend fun insertCartItem(newItem: UserData.CartItem, userId: String) = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val cartItems = uData.cart.toMutableList() cartItems.add(newItem) uData.cart = cartItems userDao.updateUser(uData) } else { throw Exception("User Not Found") } } catch (e: Exception) { Log.d("UserLocalSource", "onInsertCartItem: Error Occurred, ${e.message}") throw e } } override suspend fun updateCartItem(item: UserData.CartItem, userId: String) = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val cartItems = uData.cart.toMutableList() val pos = cartItems.indexOfFirst { it.itemId == item.itemId } if (pos >= 0) { cartItems[pos] = item } uData.cart = cartItems userDao.updateUser(uData) } else { throw Exception("User Not Found") } } catch (e: Exception) { Log.d("UserLocalSource", "onInsertCartItem: Error Occurred, ${e.message}") throw e } } override suspend fun deleteCartItem(itemId: String, userId: String) = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val cartItems = uData.cart.toMutableList() val pos = cartItems.indexOfFirst { it.itemId == itemId } if (pos >= 0) { cartItems.removeAt(pos) } uData.cart = cartItems userDao.updateUser(uData) } else { throw Exception("User Not Found") } } catch (e: Exception) { Log.d("UserLocalSource", "onInsertCartItem: Error Occurred, ${e.message}") throw e } } override suspend fun setStatusOfOrderByUserId(orderId: String, userId: String, status: String) = withContext(ioDispatcher) { try { val uData = userDao.getById(userId) if (uData != null) { val orders = uData.orders.toMutableList() val pos = orders.indexOfFirst { it.orderId == orderId } if (pos >= 0) { orders[pos].status = status val custId = orders[pos].customerId val custData = userDao.getById(custId) if (custData != null) { val orderList = custData.orders.toMutableList() val idx = orderList.indexOfFirst { it.orderId == orderId } if (idx >= 0) { orderList[idx].status = status } custData.orders = orderList userDao.updateUser(custData) } } uData.orders = orders userDao.updateUser(uData) } else { throw Exception("User Not Found") } } catch (e: Exception) { Log.d("UserLocalSource", "onInsertCartItem: Error Occurred, ${e.message}") throw e } } override suspend fun clearUser() { withContext(ioDispatcher) { userDao.clear() } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/remote/AuthRemoteDataSource.kt ================================================ package com.vishalgaur.shoppingapp.data.source.remote import android.util.Log import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.ktx.firestore import com.google.firebase.ktx.Firebase import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.UserDataSource import com.vishalgaur.shoppingapp.data.utils.EmailMobileData import com.vishalgaur.shoppingapp.data.utils.OrderStatus import kotlinx.coroutines.tasks.await class AuthRemoteDataSource : UserDataSource { private val firebaseDb: FirebaseFirestore = Firebase.firestore private fun usersCollectionRef() = firebaseDb.collection(USERS_COLLECTION) private fun allEmailsMobilesRef() = firebaseDb.collection(USERS_COLLECTION).document(EMAIL_MOBILE_DOC) override suspend fun getUserById(userId: String): Result { val resRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() return if (!resRef.isEmpty) { Success(resRef.toObjects(UserData::class.java)[0]) } else { Error(Exception("User Not Found!")) } } override suspend fun addUser(userData: UserData) { usersCollectionRef().add(userData.toHashMap()) .addOnSuccessListener { Log.d(TAG, "Doc added") } .addOnFailureListener { e -> Log.d(TAG, "firestore error occurred: $e") } } override suspend fun getUserByMobile(phoneNumber: String): UserData = usersCollectionRef().whereEqualTo(USERS_MOBILE_FIELD, phoneNumber).get().await() .toObjects(UserData::class.java)[0] override suspend fun getOrdersByUserId(userId: String): Result?> { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() return if (!userRef.isEmpty) { val userData = userRef.documents[0].toObject(UserData::class.java) Success(userData!!.orders) } else { Error(Exception("User Not Found!")) } } override suspend fun getAddressesByUserId(userId: String): Result?> { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() return if (!userRef.isEmpty) { val userData = userRef.documents[0].toObject(UserData::class.java) Success(userData!!.addresses) } else { Error(Exception("User Not Found!")) } } override suspend fun getLikesByUserId(userId: String): Result?> { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() return if (!userRef.isEmpty) { val userData = userRef.documents[0].toObject(UserData::class.java) Success(userData!!.likes) } else { Error(Exception("User Not Found!")) } } override suspend fun getUserByMobileAndPassword( mobile: String, password: String ): MutableList = usersCollectionRef().whereEqualTo(USERS_MOBILE_FIELD, mobile) .whereEqualTo(USERS_PWD_FIELD, password).get().await().toObjects(UserData::class.java) override suspend fun likeProduct(productId: String, userId: String) { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id usersCollectionRef().document(docId) .update(USERS_LIKES_FIELD, FieldValue.arrayUnion(productId)) } } override suspend fun dislikeProduct(productId: String, userId: String) { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id usersCollectionRef().document(docId) .update(USERS_LIKES_FIELD, FieldValue.arrayRemove(productId)) } } override suspend fun insertAddress(newAddress: UserData.Address, userId: String) { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id usersCollectionRef().document(docId) .update(USERS_ADDRESSES_FIELD, FieldValue.arrayUnion(newAddress.toHashMap())) } } override suspend fun updateAddress(newAddress: UserData.Address, userId: String) { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id val oldAddressList = userRef.documents[0].toObject(UserData::class.java)?.addresses?.toMutableList() val idx = oldAddressList?.indexOfFirst { it.addressId == newAddress.addressId } ?: -1 if (idx != -1) { oldAddressList?.set(idx, newAddress) } usersCollectionRef().document(docId) .update(USERS_ADDRESSES_FIELD, oldAddressList?.map { it.toHashMap() }) } } override suspend fun deleteAddress(addressId: String, userId: String) { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id val oldAddressList = userRef.documents[0].toObject(UserData::class.java)?.addresses?.toMutableList() val idx = oldAddressList?.indexOfFirst { it.addressId == addressId } ?: -1 if (idx != -1) { oldAddressList?.removeAt(idx) } usersCollectionRef().document(docId) .update(USERS_ADDRESSES_FIELD, oldAddressList?.map { it.toHashMap() }) } } override suspend fun insertCartItem(newItem: UserData.CartItem, userId: String) { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id usersCollectionRef().document(docId) .update(USERS_CART_FIELD, FieldValue.arrayUnion(newItem.toHashMap())) } } override suspend fun updateCartItem(item: UserData.CartItem, userId: String) { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id val oldCart = userRef.documents[0].toObject(UserData::class.java)?.cart?.toMutableList() val idx = oldCart?.indexOfFirst { it.itemId == item.itemId } ?: -1 if (idx != -1) { oldCart?.set(idx, item) } usersCollectionRef().document(docId) .update(USERS_CART_FIELD, oldCart?.map { it.toHashMap() }) } } override suspend fun deleteCartItem(itemId: String, userId: String) { val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id val oldCart = userRef.documents[0].toObject(UserData::class.java)?.cart?.toMutableList() val idx = oldCart?.indexOfFirst { it.itemId == itemId } ?: -1 if (idx != -1) { oldCart?.removeAt(idx) } usersCollectionRef().document(docId) .update(USERS_CART_FIELD, oldCart?.map { it.toHashMap() }) } } override suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String) { // add order to customer and // specific items to their owners // empty customers cart val ownerProducts: MutableMap> = mutableMapOf() for (item in newOrder.items) { if (!ownerProducts.containsKey(item.ownerId)) { ownerProducts[item.ownerId] = mutableListOf() } ownerProducts[item.ownerId]?.add(item) } ownerProducts.forEach { (ownerId, items) -> run { val itemPrices = mutableMapOf() items.forEach { item -> itemPrices[item.itemId] = newOrder.itemsPrices[item.itemId] ?: 0.0 } val ownerOrder = UserData.OrderItem( newOrder.orderId, userId, items, itemPrices, newOrder.deliveryAddress, newOrder.shippingCharges, newOrder.paymentMethod, newOrder.orderDate, OrderStatus.CONFIRMED.name ) val ownerRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, ownerId).get().await() if (!ownerRef.isEmpty) { val docId = ownerRef.documents[0].id usersCollectionRef().document(docId) .update(USERS_ORDERS_FIELD, FieldValue.arrayUnion(ownerOrder.toHashMap())) } } } val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id usersCollectionRef().document(docId) .update(USERS_ORDERS_FIELD, FieldValue.arrayUnion(newOrder.toHashMap())) usersCollectionRef().document(docId) .update(USERS_CART_FIELD, ArrayList()) } } override suspend fun setStatusOfOrderByUserId(orderId: String, userId: String, status: String) { // update on customer and owner val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await() if (!userRef.isEmpty) { val docId = userRef.documents[0].id val ordersList = userRef.documents[0].toObject(UserData::class.java)?.orders?.toMutableList() val idx = ordersList?.indexOfFirst { it.orderId == orderId } ?: -1 if (idx != -1) { val orderData = ordersList?.get(idx) if (orderData != null) { usersCollectionRef().document(docId) .update(USERS_ORDERS_FIELD, FieldValue.arrayRemove(orderData.toHashMap())) orderData.status = status usersCollectionRef().document(docId) .update(USERS_ORDERS_FIELD, FieldValue.arrayUnion(orderData.toHashMap())) // updating customer status val custRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, orderData.customerId) .get().await() if (!custRef.isEmpty) { val did = custRef.documents[0].id val orders = custRef.documents[0].toObject(UserData::class.java)?.orders?.toMutableList() val pos = orders?.indexOfFirst { it.orderId == orderId } ?: -1 if (pos != -1) { val order = orders?.get(pos) if (order != null) { usersCollectionRef().document(did).update( USERS_ORDERS_FIELD, FieldValue.arrayRemove(order.toHashMap()) ) order.status = status usersCollectionRef().document(did).update( USERS_ORDERS_FIELD, FieldValue.arrayUnion(order.toHashMap()) ) } } } } } } } override fun updateEmailsAndMobiles(email: String, mobile: String) { allEmailsMobilesRef().update(EMAIL_MOBILE_EMAIL_FIELD, FieldValue.arrayUnion(email)) allEmailsMobilesRef().update(EMAIL_MOBILE_MOB_FIELD, FieldValue.arrayUnion(mobile)) } override suspend fun getEmailsAndMobiles() = allEmailsMobilesRef().get().await().toObject( EmailMobileData::class.java ) companion object { private const val USERS_COLLECTION = "users" private const val USERS_ID_FIELD = "userId" private const val USERS_ADDRESSES_FIELD = "addresses" private const val USERS_LIKES_FIELD = "likes" private const val USERS_CART_FIELD = "cart" private const val USERS_ORDERS_FIELD = "orders" private const val USERS_MOBILE_FIELD = "mobile" private const val USERS_PWD_FIELD = "password" private const val EMAIL_MOBILE_DOC = "emailAndMobiles" private const val EMAIL_MOBILE_EMAIL_FIELD = "emails" private const val EMAIL_MOBILE_MOB_FIELD = "mobiles" private const val TAG = "AuthRemoteDataSource" } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/remote/ProductsRemoteDataSource.kt ================================================ package com.vishalgaur.shoppingapp.data.source.remote import android.net.Uri import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.ktx.firestore import com.google.firebase.ktx.Firebase import com.google.firebase.storage.FirebaseStorage import com.google.firebase.storage.ktx.storage import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.source.ProductDataSource import kotlinx.coroutines.tasks.await class ProductsRemoteDataSource : ProductDataSource { private val firebaseDb: FirebaseFirestore = Firebase.firestore private val firebaseStorage: FirebaseStorage = Firebase.storage private val observableProducts = MutableLiveData>?>() private fun storageRef() = firebaseStorage.reference private fun productsCollectionRef() = firebaseDb.collection(PRODUCT_COLLECTION) override suspend fun refreshProducts() { observableProducts.value = getAllProducts() } override fun observeProducts(): LiveData>?> { return observableProducts } override suspend fun getAllProducts(): Result> { val resRef = productsCollectionRef().get().await() return if (!resRef.isEmpty) { Success(resRef.toObjects(Product::class.java)) } else { Error(Exception("Error getting Products!")) } } override suspend fun insertProduct(newProduct: Product) { productsCollectionRef().add(newProduct.toHashMap()).await() } override suspend fun updateProduct(proData: Product) { val resRef = productsCollectionRef().whereEqualTo(PRODUCT_ID_FIELD, proData.productId).get().await() if (!resRef.isEmpty) { val docId = resRef.documents[0].id productsCollectionRef().document(docId).set(proData.toHashMap()).await() } else { Log.d(TAG, "onUpdateProduct: product with id: $proData.productId not found!") } } override suspend fun getProductById(productId: String): Result { val resRef = productsCollectionRef().whereEqualTo(PRODUCT_ID_FIELD, productId).get().await() return if (!resRef.isEmpty) { Success(resRef.toObjects(Product::class.java)[0]) } else { Error(Exception("Product with id: $productId Not Found!")) } } override suspend fun deleteProduct(productId: String) { Log.d(TAG, "onDeleteProduct: delete product with Id: $productId initiated") val resRef = productsCollectionRef().whereEqualTo(PRODUCT_ID_FIELD, productId).get().await() if (!resRef.isEmpty) { val product = resRef.documents[0].toObject(Product::class.java) val imgUrls = product?.images //deleting images first imgUrls?.forEach { imgUrl -> deleteImage(imgUrl) } //deleting doc containing product val docId = resRef.documents[0].id productsCollectionRef().document(docId).delete().addOnSuccessListener { Log.d(TAG, "onDelete: DocumentSnapshot successfully deleted!") }.addOnFailureListener { e -> Log.w(TAG, "onDelete: Error deleting document", e) } } else { Log.d(TAG, "onDeleteProduct: product with id: $productId not found!") } } override suspend fun uploadImage(uri: Uri, fileName: String): Uri? { val imgRef = storageRef().child("$SHOES_STORAGE_PATH/$fileName") val uploadTask = imgRef.putFile(uri) val uriRef = uploadTask.continueWithTask { task -> if (!task.isSuccessful) { task.exception?.let { throw it } } imgRef.downloadUrl } return uriRef.await() } override fun deleteImage(imgUrl: String) { val ref = firebaseStorage.getReferenceFromUrl(imgUrl) ref.delete().addOnSuccessListener { Log.d(TAG, "onDelete: image deleted successfully!") }.addOnFailureListener { e -> Log.d(TAG, "onDelete: Error deleting image, error: $e") } } override fun revertUpload(fileName: String) { val imgRef = storageRef().child("${SHOES_STORAGE_PATH}/$fileName") imgRef.delete().addOnSuccessListener { Log.d(TAG, "onRevert: File with name: $fileName deleted successfully!") }.addOnFailureListener { e -> Log.d(TAG, "onRevert: Error deleting file with name = $fileName, error: $e") } } companion object { private const val PRODUCT_COLLECTION = "products" private const val PRODUCT_ID_FIELD = "productId" private const val SHOES_STORAGE_PATH = "Shoes" private const val TAG = "ProductsRemoteSource" } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/repository/AuthRepoInterface.kt ================================================ package com.vishalgaur.shoppingapp.data.source.repository import android.content.Context import androidx.lifecycle.MutableLiveData import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.PhoneAuthCredential import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.SignUpErrors interface AuthRepoInterface { suspend fun refreshData() suspend fun signUp(userData: UserData) fun login(userData: UserData, rememberMe: Boolean) suspend fun checkEmailAndMobile(email: String, mobile: String, context: Context): SignUpErrors? suspend fun checkLogin(mobile: String, password: String): UserData? suspend fun signOut() suspend fun hardRefreshUserData() suspend fun insertProductToLikes(productId: String, userId: String): Result suspend fun removeProductFromLikes(productId: String, userId: String): Result suspend fun insertAddress(newAddress: UserData.Address, userId: String): Result suspend fun updateAddress(newAddress: UserData.Address, userId: String): Result suspend fun deleteAddressById(addressId: String, userId: String): Result suspend fun insertCartItemByUserId(cartItem: UserData.CartItem, userId: String): Result suspend fun updateCartItemByUserId(cartItem: UserData.CartItem, userId: String): Result suspend fun deleteCartItemByUserId(itemId: String, userId: String): Result suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String): Result suspend fun setStatusOfOrder(orderId: String, userId: String, status: String): Result suspend fun getOrdersByUserId(userId: String): Result?> suspend fun getAddressesByUserId(userId: String): Result?> suspend fun getLikesByUserId(userId: String): Result?> suspend fun getUserData(userId: String): Result fun getFirebaseAuth(): FirebaseAuth fun signInWithPhoneAuthCredential( credential: PhoneAuthCredential, isUserLoggedIn: MutableLiveData, context: Context ) fun isRememberMeOn(): Boolean } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/repository/AuthRepository.kt ================================================ package com.vishalgaur.shoppingapp.data.source.repository import android.content.Context import android.util.Log import android.widget.Toast import androidx.lifecycle.MutableLiveData import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.PhoneAuthCredential import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.UserDataSource import com.vishalgaur.shoppingapp.data.utils.SignUpErrors import com.vishalgaur.shoppingapp.data.utils.UserType import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope class AuthRepository( private val userLocalDataSource: UserDataSource, private val authRemoteDataSource: UserDataSource, private var sessionManager: ShoppingAppSessionManager ) : AuthRepoInterface { private var firebaseAuth: FirebaseAuth = Firebase.auth companion object { private const val TAG = "AuthRepository" } override fun getFirebaseAuth() = firebaseAuth override fun isRememberMeOn() = sessionManager.isRememberMeOn() override suspend fun refreshData() { Log.d(TAG, "refreshing userdata") if (sessionManager.isLoggedIn()) { updateUserInLocalSource(sessionManager.getPhoneNumber()) } else { sessionManager.logoutFromSession() deleteUserFromLocalSource() } } override suspend fun signUp(userData: UserData) { val isSeller = userData.userType == UserType.SELLER.name sessionManager.createLoginSession( userData.userId, userData.name, userData.mobile, false, isSeller ) Log.d(TAG, "on SignUp: Updating user in Local Source") userLocalDataSource.addUser(userData) Log.d(TAG, "on SignUp: Updating userdata on Remote Source") authRemoteDataSource.addUser(userData) authRemoteDataSource.updateEmailsAndMobiles(userData.email, userData.mobile) } override fun login(userData: UserData, rememberMe: Boolean) { val isSeller = userData.userType == UserType.SELLER.name sessionManager.createLoginSession( userData.userId, userData.name, userData.mobile, rememberMe, isSeller ) } override suspend fun checkEmailAndMobile( email: String, mobile: String, context: Context ): SignUpErrors? { Log.d(TAG, "on SignUp: Checking email and mobile") var sErr: SignUpErrors? = null try { val queryResult = authRemoteDataSource.getEmailsAndMobiles() if (queryResult != null) { val mob = queryResult.mobiles.contains(mobile) val em = queryResult.emails.contains(email) if (!mob && !em) { sErr = SignUpErrors.NONE } else { sErr = SignUpErrors.SERR when { !mob && em -> makeErrToast("Email is already registered!", context) mob && !em -> makeErrToast("Mobile is already registered!", context) mob && em -> makeErrToast( "Email and mobile is already registered!", context ) } } } } catch (e: Exception) { makeErrToast("Some Error Occurred", context) } return sErr } override suspend fun checkLogin(mobile: String, password: String): UserData? { Log.d(TAG, "on Login: checking mobile and password") var queryResult = mutableListOf() try { queryResult = authRemoteDataSource.getUserByMobileAndPassword(mobile, password) } catch (e: Exception) { // No Handling } return if (queryResult.size > 0) { queryResult[0] } else { null } } override fun signInWithPhoneAuthCredential( credential: PhoneAuthCredential, isUserLoggedIn: MutableLiveData, context: Context ) { try { firebaseAuth.signInWithCredential(credential) .addOnCompleteListener { task -> if (task.isSuccessful) { Log.d(TAG, "signInWithCredential:success") val user = task.result?.user if (user != null) { isUserLoggedIn.postValue(true) } } else { Log.w(TAG, "signInWithCredential:failure", task.exception) if (task.exception is FirebaseAuthInvalidCredentialsException) { Log.d(TAG, "createUserWithMobile:failure", task.exception) isUserLoggedIn.postValue(false) makeErrToast("Wrong OTP!", context) } } }.addOnFailureListener { Log.d(TAG, "createUserWithMobile:failure", it) isUserLoggedIn.postValue(false) makeErrToast("Invalid Request!", context) } } catch (e: Exception) { makeErrToast("Some Error Occurred", context) } } override suspend fun signOut() { sessionManager.logoutFromSession() firebaseAuth.signOut() userLocalDataSource.clearUser() } private fun makeErrToast(text: String, context: Context) { Toast.makeText(context, text, Toast.LENGTH_LONG).show() } private suspend fun deleteUserFromLocalSource() { userLocalDataSource.clearUser() } private suspend fun updateUserInLocalSource(phoneNumber: String?) { coroutineScope { launch { if (phoneNumber != null) { val getUser = userLocalDataSource.getUserByMobile(phoneNumber) if (getUser == null) { userLocalDataSource.clearUser() val uData = authRemoteDataSource.getUserByMobile(phoneNumber) if (uData != null) { userLocalDataSource.addUser(uData) } } } } } } override suspend fun hardRefreshUserData() { userLocalDataSource.clearUser() val mobile = sessionManager.getPhoneNumber() if (mobile != null) { val uData = authRemoteDataSource.getUserByMobile(mobile) if (uData != null) { userLocalDataSource.addUser(uData) } } } override suspend fun insertProductToLikes(productId: String, userId: String): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onLikeProduct: adding product to remote source") authRemoteDataSource.likeProduct(productId, userId) } val localRes = async { Log.d(TAG, "onLikeProduct: updating product to local source") userLocalDataSource.likeProduct(productId, userId) } try { localRes.await() remoteRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun removeProductFromLikes( productId: String, userId: String ): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onDislikeProduct: deleting product from remote source") authRemoteDataSource.dislikeProduct(productId, userId) } val localRes = async { Log.d(TAG, "onDislikeProduct: updating product to local source") userLocalDataSource.dislikeProduct(productId, userId) } try { localRes.await() remoteRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun insertAddress( newAddress: UserData.Address, userId: String ): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onInsertAddress: adding address to remote source") authRemoteDataSource.insertAddress(newAddress, userId) } val localRes = async { Log.d(TAG, "onInsertAddress: updating address to local source") val userRes = authRemoteDataSource.getUserById(userId) if (userRes is Success) { userLocalDataSource.clearUser() userLocalDataSource.addUser(userRes.data!!) } else if (userRes is Error) { throw userRes.exception } } try { remoteRes.await() localRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun updateAddress( newAddress: UserData.Address, userId: String ): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onUpdateAddress: updating address on remote source") authRemoteDataSource.updateAddress(newAddress, userId) } val localRes = async { Log.d(TAG, "onUpdateAddress: updating address on local source") val userRes = authRemoteDataSource.getUserById(userId) if (userRes is Success) { userLocalDataSource.clearUser() userLocalDataSource.addUser(userRes.data!!) } else if (userRes is Error) { throw userRes.exception } } try { remoteRes.await() localRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun deleteAddressById(addressId: String, userId: String): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onDelete: deleting address from remote source") authRemoteDataSource.deleteAddress(addressId, userId) } val localRes = async { Log.d(TAG, "onDelete: deleting address from local source") val userRes = authRemoteDataSource.getUserById(userId) if (userRes is Success) { userLocalDataSource.clearUser() userLocalDataSource.addUser(userRes.data!!) } else if (userRes is Error) { throw userRes.exception } } try { remoteRes.await() localRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun insertCartItemByUserId( cartItem: UserData.CartItem, userId: String ): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onInsertCartItem: adding item to remote source") authRemoteDataSource.insertCartItem(cartItem, userId) } val localRes = async { Log.d(TAG, "onInsertCartItem: updating item to local source") userLocalDataSource.insertCartItem(cartItem, userId) } try { localRes.await() remoteRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun updateCartItemByUserId( cartItem: UserData.CartItem, userId: String ): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onUpdateCartItem: updating cart item on remote source") authRemoteDataSource.updateCartItem(cartItem, userId) } val localRes = async { Log.d(TAG, "onUpdateCartItem: updating cart item on local source") userLocalDataSource.updateCartItem(cartItem, userId) } try { localRes.await() remoteRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun deleteCartItemByUserId(itemId: String, userId: String): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onDelete: deleting cart item from remote source") authRemoteDataSource.deleteCartItem(itemId, userId) } val localRes = async { Log.d(TAG, "onDelete: deleting cart item from local source") userLocalDataSource.deleteCartItem(itemId, userId) } try { localRes.await() remoteRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onPlaceOrder: adding item to remote source") authRemoteDataSource.placeOrder(newOrder, userId) } val localRes = async { Log.d(TAG, "onPlaceOrder: adding item to local source") val userRes = authRemoteDataSource.getUserById(userId) if (userRes is Success) { userLocalDataSource.clearUser() userLocalDataSource.addUser(userRes.data!!) } else if (userRes is Error) { throw userRes.exception } } try { remoteRes.await() localRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun setStatusOfOrder( orderId: String, userId: String, status: String ): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onSetStatus: updating status on remote source") authRemoteDataSource.setStatusOfOrderByUserId(orderId, userId, status) } val localRes = async { Log.d(TAG, "onSetStatus: updating status on local source") userLocalDataSource.setStatusOfOrderByUserId(orderId, userId, status) } try { localRes.await() remoteRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun getOrdersByUserId(userId: String): Result?> { return userLocalDataSource.getOrdersByUserId(userId) } override suspend fun getAddressesByUserId(userId: String): Result?> { return userLocalDataSource.getAddressesByUserId(userId) } override suspend fun getLikesByUserId(userId: String): Result?> { return userLocalDataSource.getLikesByUserId(userId) } override suspend fun getUserData(userId: String): Result { return userLocalDataSource.getUserById(userId) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/repository/ProductsRepoInterface.kt ================================================ package com.vishalgaur.shoppingapp.data.source.repository import android.net.Uri import androidx.lifecycle.LiveData import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus interface ProductsRepoInterface { suspend fun refreshProducts(): StoreDataStatus? fun observeProducts(): LiveData>?> fun observeProductsByOwner(ownerId: String): LiveData>?> suspend fun getAllProductsByOwner(ownerId: String): Result> suspend fun getProductById(productId: String, forceUpdate: Boolean = false): Result suspend fun insertProduct(newProduct: Product): Result suspend fun insertImages(imgList: List): List suspend fun updateProduct(product: Product): Result suspend fun updateImages(newList: List, oldList: List): List suspend fun deleteProductById(productId: String): Result } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/repository/ProductsRepository.kt ================================================ package com.vishalgaur.shoppingapp.data.source.repository import android.net.Uri import android.util.Log import androidx.core.net.toUri import androidx.lifecycle.LiveData import com.vishalgaur.shoppingapp.ERR_UPLOAD import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.* import com.vishalgaur.shoppingapp.data.source.ProductDataSource import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import java.util.* class ProductsRepository( private val productsRemoteSource: ProductDataSource, private val productsLocalSource: ProductDataSource ) : ProductsRepoInterface { companion object { private const val TAG = "ProductsRepository" } override suspend fun refreshProducts(): StoreDataStatus? { Log.d(TAG, "Updating Products in Room") return updateProductsFromRemoteSource() } override fun observeProducts(): LiveData>?> { return productsLocalSource.observeProducts() } override fun observeProductsByOwner(ownerId: String): LiveData>?> { return productsLocalSource.observeProductsByOwner(ownerId) } override suspend fun getAllProductsByOwner(ownerId: String): Result> { return productsLocalSource.getAllProductsByOwner(ownerId) } override suspend fun getProductById(productId: String, forceUpdate: Boolean): Result { if (forceUpdate) { updateProductFromRemoteSource(productId) } return productsLocalSource.getProductById(productId) } override suspend fun insertProduct(newProduct: Product): Result { return supervisorScope { val localRes = async { Log.d(TAG, "onInsertProduct: adding product to local source") productsLocalSource.insertProduct(newProduct) } val remoteRes = async { Log.d(TAG, "onInsertProduct: adding product to remote source") productsRemoteSource.insertProduct(newProduct) } try { localRes.await() remoteRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun insertImages(imgList: List): List { var urlList = mutableListOf() imgList.forEach label@{ uri -> val uniId = UUID.randomUUID().toString() val fileName = uniId + uri.lastPathSegment?.split("/")?.last() try { val downloadUrl = productsRemoteSource.uploadImage(uri, fileName) urlList.add(downloadUrl.toString()) } catch (e: Exception) { productsRemoteSource.revertUpload(fileName) Log.d(TAG, "exception: message = $e") urlList = mutableListOf() urlList.add(ERR_UPLOAD) return@label } } return urlList } override suspend fun updateProduct(product: Product): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onUpdate: updating product in remote source") productsRemoteSource.updateProduct(product) } val localRes = async { Log.d(TAG, "onUpdate: updating product in local source") productsLocalSource.insertProduct(product) } try { remoteRes.await() localRes.await() Success(true) } catch (e: Exception) { Error(e) } } } override suspend fun updateImages(newList: List, oldList: List): List { var urlList = mutableListOf() newList.forEach label@{ uri -> if (!oldList.contains(uri.toString())) { val uniId = UUID.randomUUID().toString() val fileName = uniId + uri.lastPathSegment?.split("/")?.last() try { val downloadUrl = productsRemoteSource.uploadImage(uri, fileName) urlList.add(downloadUrl.toString()) } catch (e: Exception) { productsRemoteSource.revertUpload(fileName) Log.d(TAG, "exception: message = $e") urlList = mutableListOf() urlList.add(ERR_UPLOAD) return@label } } else { urlList.add(uri.toString()) } } oldList.forEach { imgUrl -> if (!newList.contains(imgUrl.toUri())) { productsRemoteSource.deleteImage(imgUrl) } } return urlList } override suspend fun deleteProductById(productId: String): Result { return supervisorScope { val remoteRes = async { Log.d(TAG, "onDelete: deleting product from remote source") productsRemoteSource.deleteProduct(productId) } val localRes = async { Log.d(TAG, "onDelete: deleting product from local source") productsLocalSource.deleteProduct(productId) } try { remoteRes.await() localRes.await() Success(true) } catch (e: Exception) { Error(e) } } } private suspend fun updateProductsFromRemoteSource(): StoreDataStatus? { var res: StoreDataStatus? = null try { val remoteProducts = productsRemoteSource.getAllProducts() if (remoteProducts is Success) { Log.d(TAG, "pro list = ${remoteProducts.data}") productsLocalSource.deleteAllProducts() productsLocalSource.insertMultipleProducts(remoteProducts.data) res = StoreDataStatus.DONE } else { res = StoreDataStatus.ERROR if (remoteProducts is Error) throw remoteProducts.exception } } catch (e: Exception) { Log.d(TAG, "onUpdateProductsFromRemoteSource: Exception occurred, ${e.message}") } return res } private suspend fun updateProductFromRemoteSource(productId: String): StoreDataStatus? { var res: StoreDataStatus? = null try { val remoteProduct = productsRemoteSource.getProductById(productId) if (remoteProduct is Success) { productsLocalSource.insertProduct(remoteProduct.data) res = StoreDataStatus.DONE } else { res = StoreDataStatus.ERROR if (remoteProduct is Error) throw remoteProduct.exception } } catch (e: Exception) { Log.d(TAG, "onUpdateProductFromRemoteSource: Exception occurred, ${e.message}") } return res } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/DateTypeConvertors.kt ================================================ package com.vishalgaur.shoppingapp.data.utils import androidx.room.TypeConverter import java.util.* class DateTypeConvertors { @TypeConverter fun toDate(dateLong: Long?): Date? { return dateLong?.let { Date(it) } } @TypeConverter fun fromDate(date: Date?): Long? { return date?.time } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/EmailMobileData.kt ================================================ package com.vishalgaur.shoppingapp.data.utils data class EmailMobileData( val emails: ArrayList = ArrayList(), val mobiles: ArrayList = ArrayList() ) ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/ListTypeConverter.kt ================================================ package com.vishalgaur.shoppingapp.data.utils import androidx.room.TypeConverter class ListTypeConverter { @TypeConverter fun fromStringToStringList(value: String): List { return value.split(",").map { it } } @TypeConverter fun fromStringListToString(value: List): String { return value.joinToString(separator = ",") } @TypeConverter fun fromStringToIntegerList(value: String): List { return value.split(",").map { it.toInt() } } @TypeConverter fun fromIntegerListToString(value: List): String { return value.joinToString(separator = ",") } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/ObjectListTypeConvertor.kt ================================================ package com.vishalgaur.shoppingapp.data.utils import androidx.room.TypeConverter import com.google.common.reflect.TypeToken import com.google.gson.Gson import com.vishalgaur.shoppingapp.data.UserData class ObjectListTypeConvertor { @TypeConverter fun stringToAddressObjectList(data: String?): List { if (data.isNullOrBlank()) { return emptyList() } val listType = object : TypeToken>() {}.type val gson = Gson() return gson.fromJson(data, listType) } @TypeConverter fun addressObjectListToString(addressList: List): String { if (addressList.isEmpty()) { return "" } val gson = Gson() val listType = object : TypeToken>() {}.type return gson.toJson(addressList, listType) } @TypeConverter fun stringToCartObjectList(data: String?): List { if (data.isNullOrBlank()) { return emptyList() } val listType = object : TypeToken>() {}.type val gson = Gson() return gson.fromJson(data, listType) } @TypeConverter fun cartObjectListToString(cartList: List): String { if (cartList.isEmpty()) { return "" } val gson = Gson() val listType = object : TypeToken>() {}.type return gson.toJson(cartList, listType) } @TypeConverter fun stringToOrderObjectList(data: String?): List { if (data.isNullOrBlank()) { return emptyList() } val listType = object : TypeToken>() {}.type val gson = Gson() return gson.fromJson(data, listType) } @TypeConverter fun orderObjectListToString(orderList: List): String { if (orderList.isEmpty()) { return "" } val gson = Gson() val listType = object : TypeToken>() {}.type return gson.toJson(orderList, listType) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/ProductUtils.kt ================================================ package com.vishalgaur.shoppingapp.data.utils val ShoeSizes = mapOf( "UK4" to 4, "UK5" to 5, "UK6" to 6, "UK7" to 7, "UK8" to 8, "UK9" to 9, "UK10" to 10, "UK11" to 11, "UK12" to 12 ) val ShoeColors = mapOf( "black" to "#000000", "white" to "#FFFFFF", "red" to "#FF0000", "green" to "#00FF00", "blue" to "#0000FF", "yellow" to "#FFFF00", "cyan" to "#00FFFF", "magenta" to "#FF00FF" ) val ProductCategories = arrayOf("Shoes", "Slippers") ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/Utils.kt ================================================ package com.vishalgaur.shoppingapp.data.utils import java.util.* enum class SignUpErrors { NONE, SERR } enum class LogInErrors { NONE, LERR } enum class AddProductErrors { NONE, ERR_ADD, ERR_ADD_IMG, ADDING } enum class AddObjectStatus { DONE, ERR_ADD, ADDING } enum class UserType { CUSTOMER, SELLER } enum class OrderStatus { CONFIRMED, PACKAGING, PACKED, SHIPPING, SHIPPED, ARRIVING, DELIVERED } enum class StoreDataStatus { LOADING, ERROR, DONE } fun getISOCountriesMap(): Map { val result = mutableMapOf() val isoCountries = Locale.getISOCountries() val countriesList = isoCountries.map { isoCountry -> result[isoCountry] = Locale("", isoCountry).displayCountry } return result } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/LaunchActivity.kt ================================================ package com.vishalgaur.shoppingapp.ui import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.Looper import androidx.appcompat.app.AppCompatActivity import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.ui.loginSignup.LoginSignupActivity class LaunchActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_launch) setLaunchScreenTimeOut() } private fun setLaunchScreenTimeOut() { Looper.myLooper()?.let { Handler(it).postDelayed({ startPreferredActivity() }, TIME_OUT) } } private fun startPreferredActivity() { val sessionManager = ShoppingAppSessionManager(this) if (sessionManager.isLoggedIn()) { launchHome(this) finish() } else { val intent = Intent(this, LoginSignupActivity::class.java) startActivity(intent) finish() } } companion object { private const val TIME_OUT: Long = 1500 } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/RecyclerViewPaddingItemDecoration.kt ================================================ package com.vishalgaur.shoppingapp.ui import android.content.Context import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView class RecyclerViewPaddingItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() { private val paddingSpace = 16 override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { super.getItemOffsets(outRect, view, parent, state) outRect.set(paddingSpace, paddingSpace, paddingSpace, paddingSpace) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/UiUtils.kt ================================================ package com.vishalgaur.shoppingapp.ui import android.app.Activity import android.content.Context import android.content.Intent import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint import android.graphics.Rect import android.view.View import android.view.WindowManager import android.view.inputmethod.InputMethodManager import androidx.annotation.ColorInt import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.getISOCountriesMap import com.vishalgaur.shoppingapp.ui.home.MainActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.max enum class SignUpViewErrors { NONE, ERR_EMAIL, ERR_MOBILE, ERR_EMAIL_MOBILE, ERR_EMPTY, ERR_NOT_ACC, ERR_PWD12NS } enum class LoginViewErrors { NONE, ERR_EMPTY, ERR_MOBILE } enum class OTPStatus { NONE, CORRECT, WRONG, INVALID_REQ } enum class AddProductViewErrors { NONE, EMPTY, ERR_PRICE_0 } enum class AddAddressViewErrors { EMPTY, ERR_FNAME_EMPTY, ERR_LNAME_EMPTY, ERR_STR1_EMPTY, ERR_CITY_EMPTY, ERR_STATE_EMPTY, ERR_ZIP_EMPTY, ERR_ZIP_INVALID, ERR_PHONE_INVALID, ERR_PHONE_EMPTY } enum class AddItemErrors { ERROR_SIZE, ERROR_COLOR } class MyOnFocusChangeListener : View.OnFocusChangeListener { override fun onFocusChange(v: View?, hasFocus: Boolean) { if (v != null) { val inputManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager if (!hasFocus) { inputManager.hideSoftInputFromWindow(v.windowToken, 0) } else { inputManager.toggleSoftInputFromWindow(v.windowToken, 0, 0) } } } } fun throttleLatest( intervalMs: Long = 300L, coroutineScope: CoroutineScope, destinationFunction: (T) -> Unit ): (T) -> Unit { var throttleJob: Job? = null var latestParam: T return { param: T -> latestParam = param if (throttleJob?.isCompleted != false) { throttleJob = coroutineScope.launch { delay(intervalMs) latestParam.let(destinationFunction) } } } } fun debounce( waitMs: Long = 300L, coroutineScope: CoroutineScope, destinationFunction: (T) -> Unit ): (T) -> Unit { var debounceJob: Job? = null return { param: T -> debounceJob?.cancel() debounceJob = coroutineScope.launch { delay(waitMs) destinationFunction(param) } } } class DotsIndicatorDecoration( private val radius: Float, private val indicatorItemPadding: Float, private val indicatorHeight: Int, @ColorInt private val colorInactive: Int, @ColorInt private val colorActive: Int ) : RecyclerView.ItemDecoration() { private val inactivePaint = Paint() private val activePaint = Paint() init { val width = Resources.getSystem().displayMetrics.density * 1 inactivePaint.apply { strokeCap = Paint.Cap.ROUND strokeWidth = width style = Paint.Style.STROKE isAntiAlias = true color = colorInactive } activePaint.apply { strokeCap = Paint.Cap.ROUND strokeWidth = width style = Paint.Style.FILL_AND_STROKE isAntiAlias = true color = colorActive } } override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDrawOver(c, parent, state) val adapter = parent.adapter ?: return val itemCount = adapter.itemCount val totalLength: Float = (radius * 2 * itemCount) val padBWItems = max(0, itemCount - 1) * indicatorItemPadding val indicatorTotalWidth = totalLength + padBWItems val indicatorStartX = (parent.width - indicatorTotalWidth) / 2F val indicatorPosY = parent.height - indicatorHeight / 2F drawInactiveDots(c, indicatorStartX, indicatorPosY, itemCount) val activePos: Int = (parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() if (activePos == RecyclerView.NO_POSITION) { return } val activeChild = (parent.layoutManager as LinearLayoutManager).findViewByPosition(activePos) ?: return drawActiveDot(c, indicatorStartX, indicatorPosY, activePos) } private fun drawInactiveDots( c: Canvas, indicatorStartX: Float, indicatorPosY: Float, itemCount: Int ) { val w = radius * 2 + indicatorItemPadding var st = indicatorStartX + radius for (i in 1..itemCount) { c.drawCircle(st, indicatorPosY, radius, inactivePaint) st += w } } private fun drawActiveDot( c: Canvas, indicatorStartX: Float, indicatorPosY: Float, highlightPos: Int ) { val w = radius * 2 + indicatorItemPadding val highStart = indicatorStartX + radius + w * highlightPos c.drawCircle(highStart, indicatorPosY, radius, activePaint) } override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { super.getItemOffsets(outRect, view, parent, state) outRect.bottom = indicatorHeight } } internal fun launchHome(context: Context) { val homeIntent = Intent(context, MainActivity::class.java) homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(homeIntent) } internal fun getCompleteAddress(address: UserData.Address): String { return if (address.streetAddress2.isBlank()) { "${address.streetAddress}, ${address.city}, ${address.state} - ${address.zipCode}, ${getISOCountriesMap()[address.countryISOCode]}" } else { "${address.streetAddress}, ${address.streetAddress2}, ${address.city}, ${address.state} - ${address.zipCode}, ${getISOCountriesMap()[address.countryISOCode]}" } } internal fun disableClickOnWindow(activity: Activity) { activity.window.setFlags( WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE ) } internal fun enableClickOnWindow(activity: Activity) { activity.window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AccountFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Intent import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.databinding.FragmentAccountBinding import com.vishalgaur.shoppingapp.ui.loginSignup.LoginSignupActivity import com.vishalgaur.shoppingapp.viewModels.HomeViewModel private const val TAG = "AccountFragment" class AccountFragment : Fragment() { private lateinit var binding: FragmentAccountBinding private val viewModel: HomeViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentAccountBinding.inflate(layoutInflater) setViews() return binding.root } private fun setViews() { binding.accountTopAppBar.topAppBar.title = getString(R.string.account_fragment_title) binding.accountProfileTv.setOnClickListener { Log.d(TAG, "Profile Selected") findNavController().navigate(R.id.action_accountFragment_to_profileFragment) } binding.accountOrdersTv.setOnClickListener { Log.d(TAG, "Orders Selected") findNavController().navigate(R.id.action_accountFragment_to_ordersFragment) } binding.accountAddressTv.setOnClickListener { Log.d(TAG, "Address Selected") findNavController().navigate(R.id.action_accountFragment_to_addressFragment) } binding.accountSignOutTv.setOnClickListener { Log.d(TAG, "Sign Out Selected") showSignOutDialog() } } private fun showSignOutDialog() { context?.let { MaterialAlertDialogBuilder(it) .setTitle(getString(R.string.sign_out_dialog_title_text)) .setMessage(getString(R.string.sign_out_dialog_message_text)) .setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ -> dialog.cancel() } .setPositiveButton(getString(R.string.dialog_sign_out_btn_text)) { dialog, _ -> viewModel.signOut() navigateToSignUpActivity() dialog.cancel() } .show() } } private fun navigateToSignUpActivity() { val homeIntent = Intent(context, LoginSignupActivity::class.java) homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context?.startActivity(homeIntent) requireActivity().finish() } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddEditAddressFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.google.android.material.textfield.TextInputLayout import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.AddObjectStatus import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.data.utils.getISOCountriesMap import com.vishalgaur.shoppingapp.databinding.FragmentAddEditAddressBinding import com.vishalgaur.shoppingapp.ui.AddAddressViewErrors import com.vishalgaur.shoppingapp.ui.MyOnFocusChangeListener import com.vishalgaur.shoppingapp.viewModels.AddEditAddressViewModel import java.util.* import kotlin.properties.Delegates private const val TAG = "AddAddressFragment" class AddEditAddressFragment : Fragment() { private lateinit var binding: FragmentAddEditAddressBinding private val focusChangeListener = MyOnFocusChangeListener() private val viewModel by viewModels() private var isEdit by Delegates.notNull() private lateinit var addressId: String override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentAddEditAddressBinding.inflate(layoutInflater) isEdit = arguments?.getBoolean("isEdit") == true addressId = arguments?.getString("addressId").toString() initViewModel() setViews() setObservers() return binding.root } private fun initViewModel() { viewModel.setIsEdit(isEdit) if (isEdit) { viewModel.setAddressData(addressId) } } private fun setViews() { if (!isEdit) { binding.addAddressTopAppBar.topAppBar.title = "Add Address" } else { binding.addAddressTopAppBar.topAppBar.title = "Edit Address" } binding.addAddressTopAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.addressFirstNameEditText.onFocusChangeListener = focusChangeListener binding.addressLastNameEditText.onFocusChangeListener = focusChangeListener binding.addressStreetAddEditText.onFocusChangeListener = focusChangeListener binding.addressStreetAdd2EditText.onFocusChangeListener = focusChangeListener binding.addressCityEditText.onFocusChangeListener = focusChangeListener binding.addressStateEditText.onFocusChangeListener = focusChangeListener binding.addressZipcodeEditText.onFocusChangeListener = focusChangeListener binding.addressPhoneEditText.onFocusChangeListener = focusChangeListener setCountrySelectTextField() binding.addAddressSaveBtn.setOnClickListener { onAddAddress() if (viewModel.errorStatus.value?.isEmpty() == true) { viewModel.addAddressStatus.observe(viewLifecycleOwner) { status -> if (status == AddObjectStatus.DONE) { makeToast("Address Saved!") findNavController().navigateUp() } } } } } private fun setObservers() { viewModel.errorStatus.observe(viewLifecycleOwner) { errList -> if (errList.isEmpty()) { binding.addAddressErrorTextView.visibility = View.GONE } else { modifyErrors(errList) } } viewModel.dataStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> setLoaderState(View.VISIBLE) StoreDataStatus.ERROR -> { setLoaderState() makeToast("Error getting Data, Try Again!") } StoreDataStatus.DONE -> { fillDataInViews() setLoaderState() } else -> { setLoaderState() } } } viewModel.addAddressStatus.observe(viewLifecycleOwner) { status -> when (status) { AddObjectStatus.DONE -> setLoaderState() AddObjectStatus.ERR_ADD -> { setLoaderState() binding.addAddressErrorTextView.visibility = View.VISIBLE binding.addAddressErrorTextView.text = getString(R.string.save_address_error_text) makeToast(getString(R.string.save_address_error_text)) } AddObjectStatus.ADDING -> { setLoaderState(View.VISIBLE) } else -> setLoaderState() } } } private fun fillDataInViews() { viewModel.addressData.value?.let { address -> binding.addAddressTopAppBar.topAppBar.title = "Edit Address" val countryName = getISOCountriesMap()[address.countryISOCode] binding.addressCountryEditText.setText(countryName, false) binding.addressFirstNameEditText.setText(address.fName) binding.addressLastNameEditText.setText(address.lName) binding.addressStreetAddEditText.setText(address.streetAddress) binding.addressStreetAdd2EditText.setText(address.streetAddress2) binding.addressCityEditText.setText(address.city) binding.addressStateEditText.setText(address.state) binding.addressZipcodeEditText.setText(address.zipCode) binding.addressPhoneEditText.setText(address.phoneNumber.substringAfter("+91")) binding.addAddressSaveBtn.setText(R.string.save_address_btn_text) } } private fun makeToast(errText: String) { Toast.makeText(context, errText, Toast.LENGTH_LONG).show() } private fun setLoaderState(isVisible: Int = View.GONE) { binding.loaderLayout.loaderFrameLayout.visibility = isVisible if (isVisible == View.GONE) { binding.loaderLayout.circularLoader.hideAnimationBehavior } else { binding.loaderLayout.circularLoader.showAnimationBehavior } } private fun onAddAddress() { val countryName = binding.addressCountryEditText.text.toString() val firstName = binding.addressFirstNameEditText.text.toString() val lastName = binding.addressLastNameEditText.text.toString() val streetAdd = binding.addressStreetAddEditText.text.toString() val streetAdd2 = binding.addressStreetAdd2EditText.text.toString() val city = binding.addressCityEditText.text.toString() val state = binding.addressStateEditText.text.toString() val zipCode = binding.addressZipcodeEditText.text.toString() val phoneNumber = binding.addressPhoneEditText.text.toString() val countryCode = getISOCountriesMap().keys.find { Locale("", it).displayCountry == countryName } Log.d(TAG, "onAddAddress: Add/Edit Address Initiated") viewModel.submitAddress( countryCode!!, firstName, lastName, streetAdd, streetAdd2, city, state, zipCode, phoneNumber ) } private fun setCountrySelectTextField() { val isoCountriesMap = getISOCountriesMap() val countries = isoCountriesMap.values.toSortedSet().toList() val defaultCountry = Locale.getDefault().displayCountry val countryAdapter = ArrayAdapter(requireContext(), R.layout.country_list_item, countries) (binding.addressCountryEditText as? AutoCompleteTextView)?.let { it.setText(defaultCountry, false) it.setAdapter(countryAdapter) } } private fun modifyErrors(errList: List) { binding.fNameOutlinedTextField.error = null binding.lNameOutlinedTextField.error = null binding.streetAddOutlinedTextField.error = null binding.cityOutlinedTextField.error = null binding.stateOutlinedTextField.error = null binding.zipCodeOutlinedTextField.error = null binding.phoneOutlinedTextField.error = null errList.forEach { err -> when (err) { AddAddressViewErrors.EMPTY -> setEditTextsError(true) AddAddressViewErrors.ERR_FNAME_EMPTY -> setEditTextsError(true, binding.fNameOutlinedTextField) AddAddressViewErrors.ERR_LNAME_EMPTY -> setEditTextsError(true, binding.lNameOutlinedTextField) AddAddressViewErrors.ERR_STR1_EMPTY -> setEditTextsError(true, binding.streetAddOutlinedTextField) AddAddressViewErrors.ERR_CITY_EMPTY -> setEditTextsError(true, binding.cityOutlinedTextField) AddAddressViewErrors.ERR_STATE_EMPTY -> setEditTextsError(true, binding.stateOutlinedTextField) AddAddressViewErrors.ERR_ZIP_EMPTY -> setEditTextsError(true, binding.zipCodeOutlinedTextField) AddAddressViewErrors.ERR_ZIP_INVALID -> setEditTextsError(false, binding.zipCodeOutlinedTextField) AddAddressViewErrors.ERR_PHONE_INVALID -> setEditTextsError(false, binding.phoneOutlinedTextField) AddAddressViewErrors.ERR_PHONE_EMPTY -> setEditTextsError(true, binding.phoneOutlinedTextField) } } } private fun setEditTextsError(isEmpty: Boolean, editText: TextInputLayout? = null) { if (isEmpty) { binding.addAddressErrorTextView.visibility = View.VISIBLE if (editText != null) { editText.error = "Please Fill the Form" editText.errorIconDrawable = null } } else { binding.addAddressErrorTextView.visibility = View.GONE editText!!.error = "Invalid!" editText.errorIconDrawable = null } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddEditProductFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.res.ColorStateList import android.graphics.Color import android.net.Uri import android.os.Bundle import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.google.android.material.chip.Chip import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.AddProductErrors import com.vishalgaur.shoppingapp.data.utils.ShoeColors import com.vishalgaur.shoppingapp.data.utils.ShoeSizes import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentAddEditProductBinding import com.vishalgaur.shoppingapp.ui.AddProductViewErrors import com.vishalgaur.shoppingapp.ui.MyOnFocusChangeListener import com.vishalgaur.shoppingapp.viewModels.AddEditProductViewModel import kotlin.properties.Delegates private const val TAG = "AddProductFragment" class AddEditProductFragment : Fragment() { private lateinit var binding: FragmentAddEditProductBinding private val viewModel by viewModels() private val focusChangeListener = MyOnFocusChangeListener() // arguments private var isEdit by Delegates.notNull() private lateinit var catName: String private lateinit var productId: String private var sizeList = mutableSetOf() private var colorsList = mutableSetOf() private var imgList = mutableListOf() private val getImages = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { result -> imgList.addAll(result) if (imgList.size > 3) { imgList = imgList.subList(0, 3) makeToast("Maximum 3 images are allowed!") } val adapter = context?.let { AddProductImagesAdapter(it, imgList) } binding.addProImagesRv.adapter = adapter } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment binding = FragmentAddEditProductBinding.inflate(layoutInflater) isEdit = arguments?.getBoolean("isEdit") == true catName = arguments?.getString("categoryName").toString() productId = arguments?.getString("productId").toString() initViewModel() setViews() setObservers() return binding.root } private fun initViewModel() { Log.d(TAG, "init view model, isedit = $isEdit") viewModel.setIsEdit(isEdit) if (isEdit) { Log.d(TAG, "init view model, isedit = true, $productId") viewModel.setProductData(productId) } else { Log.d(TAG, "init view model, isedit = false, $catName") viewModel.setCategory(catName) } } private fun setObservers() { viewModel.errorStatus.observe(viewLifecycleOwner) { err -> modifyErrors(err) } viewModel.dataStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior } StoreDataStatus.DONE -> { binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior fillDataInAllViews() } else -> { binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior makeToast("Error getting Data, Try Again!") } } } viewModel.addProductErrors.observe(viewLifecycleOwner) { status -> when (status) { AddProductErrors.ADDING -> { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior } AddProductErrors.ERR_ADD_IMG -> { setAddProductErrors(getString(R.string.add_product_error_img_upload)) } AddProductErrors.ERR_ADD -> { setAddProductErrors(getString(R.string.add_product_insert_error)) } AddProductErrors.NONE -> { binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior } } } } private fun setAddProductErrors(errText: String) { binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior binding.addProErrorTextView.visibility = View.VISIBLE binding.addProErrorTextView.text = errText } private fun fillDataInAllViews() { viewModel.productData.value?.let { product -> Log.d(TAG, "fill data in views") binding.addProAppBar.topAppBar.title = "Edit Product - ${product.name}" binding.proNameEditText.setText(product.name) binding.proPriceEditText.setText(product.price.toString()) binding.proMrpEditText.setText(product.mrp.toString()) binding.proDescEditText.setText(product.description) imgList = product.images.map { it.toUri() } as MutableList val adapter = AddProductImagesAdapter(requireContext(), imgList) binding.addProImagesRv.adapter = adapter setShoeSizesChips(product.availableSizes) setShoeColorsChips(product.availableColors) binding.addProBtn.setText(R.string.edit_product_btn_text) } } private fun setViews() { Log.d(TAG, "set views") if (!isEdit) { binding.addProAppBar.topAppBar.title = "Add Product - ${viewModel.selectedCategory.value}" val adapter = AddProductImagesAdapter(requireContext(), imgList) binding.addProImagesRv.adapter = adapter } binding.addProImagesBtn.setOnClickListener { getImages.launch("image/*") } binding.addProAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.loaderLayout.loaderFrameLayout.visibility = View.GONE setShoeSizesChips() setShoeColorsChips() binding.addProErrorTextView.visibility = View.GONE binding.proNameEditText.onFocusChangeListener = focusChangeListener binding.proPriceEditText.onFocusChangeListener = focusChangeListener binding.proMrpEditText.onFocusChangeListener = focusChangeListener binding.proDescEditText.onFocusChangeListener = focusChangeListener binding.addProBtn.setOnClickListener { onAddProduct() if (viewModel.errorStatus.value == AddProductViewErrors.NONE) { viewModel.addProductErrors.observe(viewLifecycleOwner) { err -> if (err == AddProductErrors.NONE) { findNavController().navigate(R.id.action_addProductFragment_to_homeFragment) } } } } } private fun onAddProduct() { val name = binding.proNameEditText.text.toString() val price = binding.proPriceEditText.text.toString().toDoubleOrNull() val mrp = binding.proMrpEditText.text.toString().toDoubleOrNull() val desc = binding.proDescEditText.text.toString() Log.d( TAG, "onAddProduct: Add product initiated, $name, $price, $mrp, $desc, $sizeList, $colorsList, $imgList" ) viewModel.submitProduct( name, price, mrp, desc, sizeList.toList(), colorsList.toList(), imgList ) } private fun setShoeSizesChips(shoeList: List? = emptyList()) { binding.addProSizeChipGroup.apply { removeAllViews() for ((_, v) in ShoeSizes) { val chip = Chip(context) chip.id = v chip.tag = v chip.text = "$v" chip.isCheckable = true if (shoeList?.contains(v) == true) { chip.isChecked = true sizeList.add(chip.tag.toString().toInt()) } chip.setOnCheckedChangeListener { buttonView, isChecked -> val tag = buttonView.tag.toString().toInt() if (!isChecked) { sizeList.remove(tag) } else { sizeList.add(tag) } } addView(chip) } invalidate() } } private fun setShoeColorsChips(colorList: List? = emptyList()) { binding.addProColorChipGroup.apply { removeAllViews() var ind = 1 for ((k, v) in ShoeColors) { val chip = Chip(context) chip.id = ind chip.tag = k chip.chipStrokeColor = ColorStateList.valueOf(Color.BLACK) chip.chipStrokeWidth = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 1F, context.resources.displayMetrics ) chip.chipBackgroundColor = ColorStateList.valueOf(Color.parseColor(v)) chip.isCheckable = true if (colorList?.contains(k) == true) { chip.isChecked = true colorsList.add(chip.tag.toString()) } chip.setOnCheckedChangeListener { buttonView, isChecked -> val tag = buttonView.tag.toString() if (!isChecked) { colorsList.remove(tag) } else { colorsList.add(tag) } } addView(chip) ind++ } invalidate() } } private fun modifyErrors(err: AddProductViewErrors) { when (err) { AddProductViewErrors.NONE -> binding.addProErrorTextView.visibility = View.GONE AddProductViewErrors.EMPTY -> { binding.addProErrorTextView.visibility = View.VISIBLE binding.addProErrorTextView.text = getString(R.string.add_product_error_string) } AddProductViewErrors.ERR_PRICE_0 -> { binding.addProErrorTextView.visibility = View.VISIBLE binding.addProErrorTextView.text = getString(R.string.add_pro_error_price_string) } } } private fun makeToast(text: String) { Toast.makeText(context, text, Toast.LENGTH_LONG).show() } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddProductImagesAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Context import android.net.Uri import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.vishalgaur.shoppingapp.databinding.AddImagesItemBinding class AddProductImagesAdapter(private val context: Context, images: List) : RecyclerView.Adapter() { private var data: MutableList = images as MutableList inner class ViewHolder(private var binding: AddImagesItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(imgUrl: Uri, pos: Int) { binding.addImgCloseBtn.setOnClickListener { deleteItem(pos) } if (imgUrl.toString().contains("https://")) { Glide.with(context) .asBitmap() .load(imgUrl.buildUpon().scheme("https").build()) .into(binding.addImagesImageView) } else { binding.addImagesImageView.setImageURI(imgUrl) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( AddImagesItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val imageUrl = data[position] holder.bind(imageUrl, position) } override fun getItemCount(): Int = data.size fun deleteItem(index: Int) { data.removeAt(index) notifyDataSetChanged() } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddressAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Context import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.databinding.LayoutAddressCardBinding import com.vishalgaur.shoppingapp.ui.getCompleteAddress private const val TAG = "AddressAdapter" class AddressAdapter( private val context: Context, addresses: List, private val isSelect: Boolean ) : RecyclerView.Adapter() { lateinit var onClickListener: OnClickListener var data: List = addresses var lastCheckedAddress: String? = null private var lastCheckedCard: MaterialCardView? = null var selectedAddressPos = -1 inner class ViewHolder(private var binding: LayoutAddressCardBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(address: UserData.Address, position: Int) { binding.addressCard.isChecked = position == selectedAddressPos binding.addressPersonNameTv.text = context.getString(R.string.person_name, address.fName, address.lName) binding.addressCompleteAddressTv.text = getCompleteAddress(address) binding.addressMobileTv.text = address.phoneNumber if (isSelect) { binding.addressCard.setOnClickListener { onCardClick(position, address.addressId, it as MaterialCardView) } } binding.addressEditBtn.setOnClickListener { onClickListener.onEditClick(address.addressId) } binding.addressDeleteBtn.setOnClickListener { onClickListener.onDeleteClick(address.addressId) notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( LayoutAddressCardBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(data[position], position) } override fun getItemCount(): Int = data.size interface OnClickListener { fun onEditClick(addressId: String) fun onDeleteClick(addressId: String) } private fun onCardClick(position: Int, addressTd: String, card: MaterialCardView) { if (addressTd != lastCheckedAddress) { card.apply { strokeColor = context.getColor(R.color.blue_accent_300) isChecked = true strokeWidth = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2F, resources.displayMetrics ).toInt() } lastCheckedCard?.apply { strokeColor = context.getColor(R.color.light_gray) isChecked = false strokeWidth = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 1F, resources.displayMetrics ).toInt() } lastCheckedAddress = addressTd lastCheckedCard = card selectedAddressPos = position Log.d(TAG, "onCardClick: selected address = $addressTd") } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddressFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentAddressBinding import com.vishalgaur.shoppingapp.viewModels.HomeViewModel private const val TAG = "AddressFragment" class AddressFragment : Fragment() { private lateinit var binding: FragmentAddressBinding private lateinit var addressAdapter: AddressAdapter private val viewModel: HomeViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentAddressBinding.inflate(layoutInflater) setViews() setObservers() return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.getUserAddresses() } private fun setViews() { binding.addressAppBar.topAppBar.title = "Address" binding.addressAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.addressAddBtn.visibility = View.GONE binding.addressAddBtn.setOnClickListener { navigateToAddEditAddress(false) } binding.addressEmptyTextView.visibility = View.GONE if (context != null) { val addressList = viewModel.userAddresses.value ?: emptyList() addressAdapter = AddressAdapter(requireContext(), addressList, false) addressAdapter.onClickListener = object : AddressAdapter.OnClickListener { override fun onEditClick(addressId: String) { Log.d(TAG, "onEditAddress: initiated") navigateToAddEditAddress(true, addressId) } override fun onDeleteClick(addressId: String) { Log.d(TAG, "onDeleteAddress: initiated") showDeleteDialog(addressId) } } binding.addressAddressesRecyclerView.adapter = addressAdapter } } private fun setObservers() { viewModel.dataStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> { binding.addressEmptyTextView.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior } else -> { binding.addressAddBtn.visibility = View.VISIBLE binding.loaderLayout.circularLoader.hideAnimationBehavior binding.loaderLayout.loaderFrameLayout.visibility = View.GONE } } if (status != null && status != StoreDataStatus.LOADING) { viewModel.userAddresses.observe(viewLifecycleOwner) { addressList -> if (addressList.isNotEmpty()) { addressAdapter.data = addressList binding.addressAddressesRecyclerView.adapter = addressAdapter binding.addressAddressesRecyclerView.adapter?.notifyDataSetChanged() } else if (addressList.isEmpty()) { binding.addressAddressesRecyclerView.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior binding.addressEmptyTextView.visibility = View.VISIBLE } } binding.addressAddBtn.visibility = View.VISIBLE } } } private fun showDeleteDialog(addressId: String) { context?.let { MaterialAlertDialogBuilder(it) .setTitle(getString(R.string.delete_dialog_title_text)) .setMessage(getString(R.string.delete_address_message_text)) .setNeutralButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ -> dialog.cancel() } .setPositiveButton(getString(R.string.delete_dialog_delete_btn_text)) { dialog, _ -> viewModel.deleteAddress(addressId) dialog.cancel() } .show() } } private fun navigateToAddEditAddress(isEdit: Boolean, addressId: String? = null) { findNavController().navigate( R.id.action_addressFragment_to_addEditAddressFragment, bundleOf("isEdit" to isEdit, "addressId" to addressId) ) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/CartFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentCartBinding import com.vishalgaur.shoppingapp.databinding.LayoutCircularLoaderBinding import com.vishalgaur.shoppingapp.databinding.LayoutPriceCardBinding import com.vishalgaur.shoppingapp.viewModels.OrderViewModel private const val TAG = "CartFragment" class CartFragment : Fragment() { private lateinit var binding: FragmentCartBinding private val orderViewModel: OrderViewModel by activityViewModels() private lateinit var itemsAdapter: CartItemAdapter private lateinit var concatAdapter: ConcatAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentCartBinding.inflate(layoutInflater) setViews() setObservers() return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) orderViewModel.getUserLikes() orderViewModel.getCartItems() } private fun setViews() { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior binding.cartAppBar.topAppBar.title = getString(R.string.cart_fragment_label) binding.cartEmptyTextView.visibility = View.GONE binding.cartCheckOutBtn.setOnClickListener { navigateToSelectAddress() } if (context != null) { setItemsAdapter(orderViewModel.cartItems.value) concatAdapter = ConcatAdapter(itemsAdapter, PriceCardAdapter()) binding.cartProductsRecyclerView.adapter = concatAdapter } } private fun setObservers() { orderViewModel.dataStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> { binding.cartProductsRecyclerView.visibility = View.GONE binding.cartCheckOutBtn.visibility = View.GONE binding.cartEmptyTextView.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior } else -> { binding.loaderLayout.circularLoader.hideAnimationBehavior binding.loaderLayout.loaderFrameLayout.visibility = View.GONE } } } orderViewModel.dataStatus.observe(viewLifecycleOwner) { status -> if (status != null && status != StoreDataStatus.LOADING) { orderViewModel.cartProducts.observe(viewLifecycleOwner) { itemList -> if (itemList.isNotEmpty()) { updateAdapter() binding.cartEmptyTextView.visibility = View.GONE binding.cartProductsRecyclerView.visibility = View.VISIBLE binding.cartCheckOutBtn.visibility = View.VISIBLE } else if (itemList.isEmpty()) { binding.cartProductsRecyclerView.visibility = View.GONE binding.cartCheckOutBtn.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior binding.cartEmptyTextView.visibility = View.VISIBLE } } } } orderViewModel.cartItems.observe(viewLifecycleOwner) { items -> if (items.isNotEmpty()) { updateAdapter() } } orderViewModel.priceList.observe(viewLifecycleOwner) { if (it.isNotEmpty()) { updateAdapter() } } orderViewModel.userLikes.observe(viewLifecycleOwner) { if (it.isNotEmpty()) { updateAdapter() } } } private fun updateAdapter() { val items = orderViewModel.cartItems.value ?: emptyList() val likeList = orderViewModel.userLikes.value ?: emptyList() val prosList = orderViewModel.cartProducts.value ?: emptyList() itemsAdapter.apply { data = items proList = prosList likesList = likeList } concatAdapter = ConcatAdapter(itemsAdapter, PriceCardAdapter()) binding.cartProductsRecyclerView.adapter = concatAdapter binding.cartProductsRecyclerView.adapter?.notifyDataSetChanged() } private fun setItemsAdapter(itemList: List?) { val items = itemList ?: emptyList() val likesList = orderViewModel.userLikes.value ?: emptyList() val proList = orderViewModel.cartProducts.value ?: emptyList() itemsAdapter = CartItemAdapter(requireContext(), items, proList, likesList) itemsAdapter.onClickListener = object : CartItemAdapter.OnClickListener { override fun onLikeClick(productId: String) { Log.d(TAG, "onToggle Like Clicked") orderViewModel.toggleLikeProduct(productId) } override fun onDeleteClick(itemId: String, itemBinding: LayoutCircularLoaderBinding) { Log.d(TAG, "onDelete: initiated") showDeleteDialog(itemId, itemBinding) } override fun onPlusClick(itemId: String) { Log.d(TAG, "onPlus: Increasing quantity") orderViewModel.setQuantityOfItem(itemId, 1) } override fun onMinusClick(itemId: String, currQuantity: Int,itemBinding: LayoutCircularLoaderBinding) { Log.d(TAG, "onMinus: decreasing quantity") if (currQuantity == 1) { showDeleteDialog(itemId, itemBinding) } else { orderViewModel.setQuantityOfItem(itemId, -1) } } } } private fun navigateToSelectAddress() { findNavController().navigate(R.id.action_cartFragment_to_selectAddressFragment) } private fun showDeleteDialog(itemId: String, itemBinding: LayoutCircularLoaderBinding) { context?.let { MaterialAlertDialogBuilder(it) .setTitle(getString(R.string.delete_dialog_title_text)) .setMessage(getString(R.string.delete_cart_item_message_text)) .setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ -> dialog.cancel() itemBinding.loaderFrameLayout.visibility = View.GONE } .setPositiveButton(getString(R.string.delete_dialog_delete_btn_text)) { dialog, _ -> orderViewModel.deleteItemFromCart(itemId) dialog.cancel() }.setOnCancelListener { itemBinding.loaderFrameLayout.visibility = View.GONE } .show() } } inner class PriceCardAdapter : RecyclerView.Adapter() { inner class ViewHolder(private val priceCardBinding: LayoutPriceCardBinding) : RecyclerView.ViewHolder(priceCardBinding.root) { fun bind() { priceCardBinding.priceItemsLabelTv.text = getString( R.string.price_card_items_string, orderViewModel.getItemsCount().toString() ) priceCardBinding.priceItemsAmountTv.text = getString(R.string.price_text, orderViewModel.getItemsPriceTotal().toString()) priceCardBinding.priceShippingAmountTv.text = getString(R.string.price_text, "0") priceCardBinding.priceChargesAmountTv.text = getString(R.string.price_text, "0") priceCardBinding.priceTotalAmountTv.text = getString(R.string.price_text, orderViewModel.getItemsPriceTotal().toString()) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( LayoutPriceCardBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind() } override fun getItemCount() = 1 } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/CartItemAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.databinding.CartListItemBinding import com.vishalgaur.shoppingapp.databinding.LayoutCircularLoaderBinding class CartItemAdapter( private val context: Context, items: List, products: List, userLikes: List ) : RecyclerView.Adapter() { lateinit var onClickListener: OnClickListener var data: List = items var proList: List = products var likesList: List = userLikes inner class ViewHolder(private val binding: CartListItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(itemData: UserData.CartItem) { binding.loaderLayout.loaderFrameLayout.visibility = View.GONE val proData = proList.find { it.productId == itemData.productId } ?: Product() binding.cartProductTitleTv.text = proData.name binding.cartProductPriceTv.text = context.getString(R.string.price_text, proData.price.toString()) if (proData.images.isNotEmpty()) { val imgUrl = proData.images[0].toUri().buildUpon().scheme("https").build() Glide.with(context) .asBitmap() .load(imgUrl) .into(binding.productImageView) binding.productImageView.clipToOutline = true } binding.cartProductQuantityTextView.text = itemData.quantity.toString() if (likesList.contains(proData.productId)) { binding.cartProductLikeBtn.setImageResource(R.drawable.liked_heart_drawable) } else { binding.cartProductLikeBtn.setImageResource(R.drawable.heart_icon_drawable) } binding.cartProductLikeBtn.setOnClickListener { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE if (!likesList.contains(proData.productId)) { binding.cartProductLikeBtn.setImageResource(R.drawable.liked_heart_drawable) } else { binding.cartProductLikeBtn.setImageResource(R.drawable.heart_icon_drawable) } onClickListener.onLikeClick(proData.productId) } binding.cartProductDeleteBtn.setOnClickListener { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE onClickListener.onDeleteClick(itemData.itemId, binding.loaderLayout) } binding.cartProductPlusBtn.setOnClickListener { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE onClickListener.onPlusClick(itemData.itemId) } binding.cartProductMinusBtn.setOnClickListener { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE onClickListener.onMinusClick(itemData.itemId, itemData.quantity, binding.loaderLayout) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( CartListItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(data[position]) } override fun getItemCount() = data.size interface OnClickListener { fun onLikeClick(productId: String) fun onDeleteClick(itemId: String, itemBinding: LayoutCircularLoaderBinding) fun onPlusClick(itemId: String) fun onMinusClick(itemId: String, currQuantity: Int, itemBinding: LayoutCircularLoaderBinding) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/FavoritesFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentFavoritesBinding import com.vishalgaur.shoppingapp.ui.RecyclerViewPaddingItemDecoration import com.vishalgaur.shoppingapp.viewModels.HomeViewModel private const val TAG = "FavoritesFragment" class FavoritesFragment : Fragment() { private lateinit var binding: FragmentFavoritesBinding private val viewModel: HomeViewModel by activityViewModels() private lateinit var productsAdapter: LikedProductAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentFavoritesBinding.inflate(layoutInflater) setViews() setObservers() return binding.root } private fun setViews() { viewModel.setDataLoading() viewModel.getLikedProducts() binding.favTopAppBar.topAppBar.title = "Favorite Products" binding.favTopAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.favEmptyTextView.visibility = View.GONE if (context != null) { val proList = viewModel.likedProducts.value ?: emptyList() productsAdapter = LikedProductAdapter(proList, requireContext()) productsAdapter.onClickListener = object : LikedProductAdapter.OnClickListener { override fun onClick(productData: Product) { Log.d(TAG, "Product: ${productData.productId} clicked") findNavController().navigate( R.id.action_favoritesFragment_to_productDetailsFragment, bundleOf("productId" to productData.productId) ) } override fun onDeleteClick(productId: String) { viewModel.toggleLikeByProductId(productId) } } binding.favProductsRecyclerView.apply { val itemDecoration = RecyclerViewPaddingItemDecoration(requireContext()) if (itemDecorationCount == 0) { addItemDecoration(itemDecoration) } } } } private fun setObservers() { viewModel.dataStatus.observe(viewLifecycleOwner) { status -> if (status == StoreDataStatus.LOADING) { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior binding.favEmptyTextView.visibility = View.GONE } else if (status != null) { viewModel.likedProducts.observe(viewLifecycleOwner) { if (it.isNotEmpty()) { productsAdapter.data = viewModel.likedProducts.value!! binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior productsAdapter.data = it binding.favProductsRecyclerView.adapter = productsAdapter binding.favProductsRecyclerView.adapter?.apply { notifyDataSetChanged() } } else if (it.isEmpty()) { binding.favEmptyTextView.visibility = View.VISIBLE binding.favProductsRecyclerView.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior } } } } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/HomeFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.CheckBox import android.widget.ImageView import androidx.core.os.bundleOf import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.utils.ProductCategories import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentHomeBinding import com.vishalgaur.shoppingapp.ui.MyOnFocusChangeListener import com.vishalgaur.shoppingapp.ui.RecyclerViewPaddingItemDecoration import com.vishalgaur.shoppingapp.viewModels.HomeViewModel import kotlinx.coroutines.* private const val TAG = "HomeFragment" class HomeFragment : Fragment() { private lateinit var binding: FragmentHomeBinding private val viewModel: HomeViewModel by activityViewModels() private val focusChangeListener = MyOnFocusChangeListener() private lateinit var productAdapter: ProductAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment binding = FragmentHomeBinding.inflate(layoutInflater) setViews() setObservers() return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.getUserLikes() } // override fun onResume() { // super.onResume() // viewModel.getLikedProducts() // } private fun setViews() { setHomeTopAppBar() if (context != null) { setProductsAdapter(viewModel.products.value) binding.productsRecyclerView.apply { val gridLayoutManager = GridLayoutManager(context, 2) gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return when (productAdapter.getItemViewType(position)) { 2 -> 2 //ad else -> { val proCount = productAdapter.data.count { it is Product } val adCount = productAdapter.data.size - proCount val totalCount = proCount + (adCount * 2) // product, full for last item if (position + 1 == productAdapter.data.size && totalCount % 2 == 1) 2 else 1 } } } } layoutManager = gridLayoutManager adapter = productAdapter val itemDecoration = RecyclerViewPaddingItemDecoration(requireContext()) if (itemDecorationCount == 0) { addItemDecoration(itemDecoration) } } } if (!viewModel.isUserASeller) { binding.homeFabAddProduct.visibility = View.GONE } binding.homeFabAddProduct.setOnClickListener { showDialogWithItems(ProductCategories, 0, false) } binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior } private fun setObservers() { viewModel.storeDataStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior binding.productsRecyclerView.visibility = View.GONE } else -> { binding.loaderLayout.circularLoader.hideAnimationBehavior binding.loaderLayout.loaderFrameLayout.visibility = View.GONE } } if (status != null && status != StoreDataStatus.LOADING) { viewModel.products.observe(viewLifecycleOwner) { productsList -> if (productsList.isNotEmpty()) { binding.loaderLayout.circularLoader.hideAnimationBehavior binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.productsRecyclerView.visibility = View.VISIBLE binding.productsRecyclerView.adapter?.apply { productAdapter.data = getMixedDataList(productsList, getAdsList()) notifyDataSetChanged() } } } } } viewModel.allProducts.observe(viewLifecycleOwner) { if (it.isNotEmpty()) { viewModel.setDataLoaded() viewModel.filterProducts("All") } } viewModel.userLikes.observe(viewLifecycleOwner) { if (it.isNotEmpty()) { binding.productsRecyclerView.adapter?.apply { notifyDataSetChanged() } } } } private fun performSearch(query: String) { viewModel.filterBySearch(query) } private fun setAppBarItemClicks(menuItem: MenuItem): Boolean { return when (menuItem.itemId) { R.id.home_filter -> { val extraFilters = arrayOf("All", "None") val categoryList = ProductCategories.plus(extraFilters) val checkedItem = categoryList.indexOf(viewModel.filterCategory.value) showDialogWithItems(categoryList, checkedItem, true) true } R.id.home_favorites -> { // show favorite products list findNavController().navigate(R.id.action_homeFragment_to_favoritesFragment) true } else -> false } } private fun setHomeTopAppBar() { var lastInput = "" val debounceJob: Job? = null val uiScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) binding.homeTopAppBar.topAppBar.inflateMenu(R.menu.home_app_bar_menu) if (viewModel.isUserASeller) { binding.homeTopAppBar.topAppBar.menu.removeItem(R.id.home_favorites) } binding.homeTopAppBar.homeSearchEditText.onFocusChangeListener = focusChangeListener binding.homeTopAppBar.homeSearchEditText.doAfterTextChanged { editable -> if (editable != null) { val newtInput = editable.toString() debounceJob?.cancel() if (lastInput != newtInput) { lastInput = newtInput uiScope.launch { delay(500) if (lastInput == newtInput) { performSearch(newtInput) } } } } } binding.homeTopAppBar.homeSearchEditText.setOnEditorActionListener { textView, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_SEARCH) { textView.clearFocus() val inputManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputManager.hideSoftInputFromWindow(textView.windowToken, 0) performSearch(textView.text.toString()) true } else { false } } binding.homeTopAppBar.searchOutlinedTextLayout.setEndIconOnClickListener { it.clearFocus() binding.homeTopAppBar.homeSearchEditText.setText("") val inputManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputManager.hideSoftInputFromWindow(it.windowToken, 0) // viewModel.filterProducts("All") } binding.homeTopAppBar.topAppBar.setOnMenuItemClickListener { menuItem -> setAppBarItemClicks(menuItem) } } private fun setProductsAdapter(productsList: List?) { val likesList = viewModel.userLikes.value ?: emptyList() productAdapter = ProductAdapter(productsList ?: emptyList(), likesList, requireContext()) productAdapter.onClickListener = object : ProductAdapter.OnClickListener { override fun onClick(productData: Product) { findNavController().navigate( R.id.action_seeProduct, bundleOf("productId" to productData.productId) ) } override fun onDeleteClick(productData: Product) { Log.d(TAG, "onDeleteProduct: initiated for ${productData.productId}") showDeleteDialog(productData.name, productData.productId) } override fun onEditClick(productId: String) { Log.d(TAG, "onEditProduct: initiated for $productId") navigateToAddEditProductFragment(isEdit = true, productId = productId) } override fun onLikeClick(productId: String) { Log.d(TAG, "onToggleLike: initiated for $productId") viewModel.toggleLikeByProductId(productId) } override fun onAddToCartClick(productData: Product) { Log.d(TAG, "onToggleCartAddition: initiated") viewModel.toggleProductInCart(productData) } } productAdapter.bindImageButtons = object : ProductAdapter.BindImageButtons { @SuppressLint("ResourceAsColor") override fun setLikeButton(productId: String, button: CheckBox) { button.isChecked = viewModel.isProductLiked(productId) } override fun setCartButton(productId: String, imgView: ImageView) { if (viewModel.isProductInCart(productId)) { imgView.setImageResource(R.drawable.ic_remove_shopping_cart_24) } else { imgView.setImageResource(R.drawable.ic_add_shopping_cart_24) } } } } private fun showDeleteDialog(productName: String, productId: String) { context?.let { MaterialAlertDialogBuilder(it) .setTitle(getString(R.string.delete_dialog_title_text)) .setMessage(getString(R.string.delete_dialog_message_text, productName)) .setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ -> dialog.cancel() } .setPositiveButton(getString(R.string.delete_dialog_delete_btn_text)) { dialog, _ -> viewModel.deleteProduct(productId) dialog.cancel() } .show() } } private fun showDialogWithItems( categoryItems: Array, checkedOption: Int = 0, isFilter: Boolean ) { var checkedItem = checkedOption context?.let { MaterialAlertDialogBuilder(it) .setTitle(getString(R.string.pro_cat_dialog_title)) .setSingleChoiceItems(categoryItems, checkedItem) { _, which -> checkedItem = which } .setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ -> dialog.cancel() } .setPositiveButton(getString(R.string.pro_cat_dialog_ok_btn)) { dialog, _ -> if (checkedItem == -1) { dialog.cancel() } else { if (isFilter) { viewModel.filterProducts(categoryItems[checkedItem]) } else { navigateToAddEditProductFragment( isEdit = false, catName = categoryItems[checkedItem] ) } } dialog.cancel() } .show() } } private fun navigateToAddEditProductFragment( isEdit: Boolean, catName: String? = null, productId: String? = null ) { findNavController().navigate( R.id.action_goto_addProduct, bundleOf("isEdit" to isEdit, "categoryName" to catName, "productId" to productId) ) } private fun getMixedDataList(data: List, adsList: List): List { val itemsList = mutableListOf() itemsList.addAll(data.sortedBy { it.productId }) var currPos = 0 if (itemsList.size >= 4) { adsList.forEach label@{ ad -> if (itemsList.size > currPos + 1) { itemsList.add(currPos, ad) } else { return@label } currPos += 5 } } return itemsList } private fun getAdsList(): List { return listOf(R.drawable.ad_ex_2, R.drawable.ad_ex_1, R.drawable.ad_ex_3) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/LikedProductAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Context import android.graphics.Paint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.databinding.ProductsListItemBinding import com.vishalgaur.shoppingapp.getOfferPercentage class LikedProductAdapter(proList: List, private val context: Context) : RecyclerView.Adapter() { var data = proList lateinit var onClickListener: OnClickListener inner class ViewHolder(private val binding: ProductsListItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(productData: Product) { binding.productCard.setOnClickListener { onClickListener.onClick(productData) } binding.productNameTv.text = productData.name binding.productPriceTv.text = context.getString(R.string.pro_details_price_value, productData.price.toString()) binding.productRatingBar.rating = productData.rating.toFloat() binding.productActualPriceTv.apply { paintFlags = Paint.STRIKE_THRU_TEXT_FLAG text = context.getString( R.string.pro_details_actual_strike_value, productData.mrp.toString() ) } binding.productOfferValueTv.text = context.getString( R.string.pro_offer_precent_text, getOfferPercentage(productData.mrp, productData.price).toString() ) if (productData.images.isNotEmpty()) { val imgUrl = productData.images[0].toUri().buildUpon().scheme("https").build() Glide.with(context) .asBitmap() .load(imgUrl) .into(binding.productImageView) binding.productImageView.clipToOutline = true } //hiding unnecessary button binding.productAddToCartButton.visibility = View.GONE binding.productDeleteButton.visibility = View.GONE binding.productLikeCheckbox.visibility = View.GONE // setting edit button as delete button binding.productEditButton.setImageResource(R.drawable.ic_delete_24) binding.productEditButton.setOnClickListener { onClickListener.onDeleteClick(productData.productId) notifyDataSetChanged() } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( ProductsListItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(data[position]) } override fun getItemCount() = data.size interface OnClickListener { fun onClick(productData: Product) fun onDeleteClick(productId: String) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/MainActivity.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.databinding.ActivityMainBinding private const val TAG = "MainActivity" class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "onCreate starts") super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // Bottom Navigation setUpNav() } private fun setUpNav() { val navFragment = supportFragmentManager.findFragmentById(R.id.home_nav_host_fragment) as NavHostFragment NavigationUI.setupWithNavController(binding.homeBottomNavigation, navFragment.navController) navFragment.navController.addOnDestinationChangedListener { _, destination, _ -> when (destination.id) { R.id.homeFragment -> setBottomNavVisibility(View.VISIBLE) R.id.cartFragment -> setBottomNavVisibility(View.VISIBLE) R.id.accountFragment -> setBottomNavVisibility(View.VISIBLE) R.id.ordersFragment -> setBottomNavVisibility(View.VISIBLE) R.id.orderSuccessFragment -> setBottomNavVisibility(View.VISIBLE) else -> setBottomNavVisibility(View.GONE) } } val sessionManager = ShoppingAppSessionManager(this.applicationContext) if (sessionManager.isUserSeller()) { binding.homeBottomNavigation.menu.removeItem(R.id.cartFragment) }else { binding.homeBottomNavigation.menu.removeItem(R.id.ordersFragment) } } private fun setBottomNavVisibility(visibility: Int) { binding.homeBottomNavigation.visibility = visibility } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrderDetailsFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.OrderStatus import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentOrderDetailsBinding import com.vishalgaur.shoppingapp.ui.getCompleteAddress import com.vishalgaur.shoppingapp.viewModels.HomeViewModel import java.time.Month import java.util.* class OrderDetailsFragment : Fragment() { private lateinit var binding: FragmentOrderDetailsBinding private val viewModel: HomeViewModel by activityViewModels() private lateinit var orderId: String private lateinit var productsAdapter: OrderProductsAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentOrderDetailsBinding.inflate(layoutInflater) orderId = arguments?.getString("orderId").toString() viewModel.getOrderDetailsByOrderId(orderId) setViews() setObservers() return binding.root } private fun setViews() { binding.orderDetailAppBar.topAppBar.title = getString(R.string.order_details_fragment_title) binding.orderDetailAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.orderDetailsConstraintGroup.visibility = View.GONE if (context != null) { setProductsAdapter(viewModel.selectedOrder.value?.items) binding.orderDetailsProRecyclerView.adapter = productsAdapter } } private fun setObservers() { viewModel.storeDataStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior binding.orderDetailsConstraintGroup.visibility = View.GONE } else -> { binding.loaderLayout.circularLoader.hideAnimationBehavior binding.loaderLayout.loaderFrameLayout.visibility = View.GONE } } } viewModel.selectedOrder.observe(viewLifecycleOwner) { orderData -> if (orderData != null) { binding.orderDetailsConstraintGroup.visibility = View.VISIBLE setAllViews(orderData) val items = orderData.items val likeList = viewModel.userLikes.value ?: emptyList() val prosList = viewModel.orderProducts.value ?: emptyList() productsAdapter.apply { data = items proList = prosList likesList = likeList } binding.orderDetailsProRecyclerView.adapter = productsAdapter binding.orderDetailsProRecyclerView.adapter?.notifyDataSetChanged() } else { binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior binding.orderDetailsConstraintGroup.visibility = View.GONE } } } private fun setAllViews(orderData: UserData.OrderItem) { Log.d("OrderDetail", "set all views called") if (viewModel.isUserASeller) { binding.orderChangeStatusBtn.visibility = View.VISIBLE binding.orderChangeStatusBtn.setOnClickListener { val statusString = orderData.status.split(" ")[0] val pos = OrderStatus.values().map { it.name }.indexOf(statusString) showDialogWithItems(pos, orderData.orderId) } } else { binding.orderChangeStatusBtn.visibility = View.GONE } val calendar = Calendar.getInstance() calendar.time = orderData.orderDate binding.orderDetailsShippingAddLayout.shipDateValueTv.text = getString( R.string.order_date_text, Month.values()[(calendar.get(Calendar.MONTH))].name, calendar.get(Calendar.DAY_OF_MONTH).toString(), calendar.get(Calendar.YEAR).toString() ) binding.orderDetailsShippingAddLayout.shipAddValueTv.text = getCompleteAddress(orderData.deliveryAddress) binding.orderDetailsShippingAddLayout.shipCurrStatusValueTv.text = orderData.status setPriceCard(orderData) } private fun setPriceCard(orderData: UserData.OrderItem) { binding.orderDetailsPaymentLayout.priceItemsLabelTv.text = getString( R.string.price_card_items_string, getItemsCount(orderData.items).toString() ) val itemsPriceTotal = getItemsPriceTotal(orderData.itemsPrices, orderData.items) binding.orderDetailsPaymentLayout.priceItemsAmountTv.text = getString( R.string.price_text, itemsPriceTotal.toString() ) binding.orderDetailsPaymentLayout.priceShippingAmountTv.text = getString(R.string.price_text, "0") binding.orderDetailsPaymentLayout.priceChargesAmountTv.text = getString(R.string.price_text, "0") binding.orderDetailsPaymentLayout.priceTotalAmountTv.text = getString(R.string.price_text, (itemsPriceTotal + orderData.shippingCharges).toString()) } private fun setProductsAdapter(itemsList: List?) { val items = itemsList ?: emptyList() val likesList = viewModel.userLikes.value ?: emptyList() val proList = viewModel.orderProducts.value ?: emptyList() productsAdapter = OrderProductsAdapter(requireContext(), items, proList, likesList) } private fun showDialogWithItems(checkedOption: Int = 0, orderId: String) { val categoryItems: Array = OrderStatus.values().map { it.name }.toTypedArray() var checkedItem = checkedOption context?.let { MaterialAlertDialogBuilder(it) .setTitle(getString(R.string.status_dialog_title)) .setSingleChoiceItems(categoryItems, checkedItem) { _, which -> checkedItem = which } .setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ -> dialog.cancel() } .setPositiveButton(getString(R.string.pro_cat_dialog_ok_btn)) { dialog, _ -> if (checkedItem == -1) { dialog.cancel() } else { viewModel.onSetStatusOfOrder(orderId, categoryItems[checkedItem]) } dialog.cancel() } .show() } } private fun getItemsCount(cartItems: List): Int { var totalCount = 0 cartItems.forEach { totalCount += it.quantity } return totalCount } private fun getItemsPriceTotal( priceList: Map, cartItems: List ): Double { var totalPrice = 0.0 priceList.forEach { (itemId, price) -> totalPrice += price * (cartItems.find { it.itemId == itemId }?.quantity ?: 1) } return totalPrice } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrderProductsAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.databinding.CartListItemBinding class OrderProductsAdapter( private val context: Context, items: List, products: List, userLikes: List ) : RecyclerView.Adapter() { var data: List = items var proList: List = products var likesList: List = userLikes inner class ViewHolder(private val binding: CartListItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(itemData: UserData.CartItem) { binding.loaderLayout.loaderFrameLayout.visibility = View.GONE val proData = proList.find { it.productId == itemData.productId } ?: Product() binding.cartProductTitleTv.text = proData.name binding.cartProductPriceTv.text = context.getString(R.string.price_text, proData.price.toString()) if (proData.images.isNotEmpty()) { val imgUrl = proData.images[0].toUri().buildUpon().scheme("https").build() Glide.with(context) .asBitmap() .load(imgUrl) .into(binding.productImageView) binding.productImageView.clipToOutline = true } binding.cartProductDeleteBtn.visibility = View.GONE binding.cartProductQuantityButtonsLayout.visibility = View.GONE binding.cartProductLikeBtn.visibility = View.GONE } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( CartListItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(data[position]) } override fun getItemCount() = data.size } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrderSuccessFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.os.CountDownTimer import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentOrderSuccessBinding import com.vishalgaur.shoppingapp.viewModels.OrderViewModel class OrderSuccessFragment : Fragment() { private lateinit var binding: FragmentOrderSuccessBinding private val orderViewModel: OrderViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentOrderSuccessBinding.inflate(layoutInflater) binding.loaderLayout.loaderCard.visibility = View.VISIBLE binding.loaderLayout.loadingMessage.text = getString(R.string.process_order_msg) binding.loaderLayout.circularLoader.showAnimationBehavior binding.orderConstraintGroup.visibility = View.GONE setObservers() return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.backToHomeBtn.setOnClickListener { findNavController().navigate(R.id.action_orderSuccessFragment_to_homeFragment) } } private fun setObservers() { orderViewModel.orderStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> { binding.loaderLayout.loaderCard.visibility = View.VISIBLE } else -> { binding.orderConstraintGroup.visibility = View.VISIBLE binding.loaderLayout.loaderCard.visibility = View.GONE binding.redirectHomeTimerTv.text = getString(R.string.redirect_home_timer_text, "5") countDownTimer.start() } } } } private val countDownTimer = object : CountDownTimer(5000, 1000) { override fun onTick(millisUntilFinished: Long) { val sec = millisUntilFinished / 1000 binding.redirectHomeTimerTv.text = getString(R.string.redirect_home_timer_text, sec.toString()) } override fun onFinish() { findNavController().navigate(R.id.action_orderSuccessFragment_to_homeFragment) } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrdersAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.databinding.LayoutOrderSummaryCardBinding import java.time.Month import java.util.* class OrdersAdapter(ordersList: List, private val context: Context) : RecyclerView.Adapter() { lateinit var onClickListener: OnClickListener var data: List = ordersList inner class ViewHolder(private val binding: LayoutOrderSummaryCardBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(orderData: UserData.OrderItem) { binding.orderSummaryCard.setOnClickListener { onClickListener.onCardClick(orderData.orderId) } binding.orderSummaryIdTv.text = orderData.orderId val calendar = Calendar.getInstance() calendar.time = orderData.orderDate binding.orderSummaryDateTv.text = context.getString( R.string.order_date_text, Month.values()[(calendar.get(Calendar.MONTH))].name, calendar.get(Calendar.DAY_OF_MONTH).toString(), calendar.get(Calendar.YEAR).toString() ) binding.orderSummaryStatusValueTv.text = orderData.status val totalItems = orderData.items.map { it.quantity }.sum() binding.orderSummaryItemsCountTv.text = context.getString(R.string.order_items_count_text, totalItems.toString()) var totalAmount = 0.0 orderData.itemsPrices.forEach { (itemId, price) -> totalAmount += price * (orderData.items.find { it.itemId == itemId }?.quantity ?: 1) } binding.orderSummaryTotalAmountTv.text = context.getString(R.string.price_text, totalAmount.toString()) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( LayoutOrderSummaryCardBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(data[position]) } override fun getItemCount() = data.size interface OnClickListener { fun onCardClick(orderId: String) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrdersFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentOrdersBinding import com.vishalgaur.shoppingapp.viewModels.HomeViewModel private const val TAG = "OrdersFragment" class OrdersFragment : Fragment() { private lateinit var binding: FragmentOrdersBinding private lateinit var ordersAdapter: OrdersAdapter private val viewModel: HomeViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentOrdersBinding.inflate(layoutInflater) setViews() setObservers() return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.getAllOrders() } private fun setViews() { binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.ordersAppBar.topAppBar.title = getString(R.string.orders_fragment_title) binding.ordersAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.ordersEmptyTextView.visibility = View.GONE if (context != null) { ordersAdapter = OrdersAdapter(emptyList(), requireContext()) ordersAdapter.onClickListener = object : OrdersAdapter.OnClickListener { override fun onCardClick(orderId: String) { Log.d(TAG, "onOrderSummaryClick: Getting order details") findNavController().navigate( R.id.action_ordersFragment_to_orderDetailsFragment, bundleOf("orderId" to orderId) ) } } binding.orderAllOrdersRecyclerView.adapter = ordersAdapter } } private fun setObservers() { viewModel.storeDataStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> { binding.orderAllOrdersRecyclerView.visibility = View.GONE binding.ordersEmptyTextView.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior } else -> { binding.loaderLayout.circularLoader.hideAnimationBehavior binding.loaderLayout.loaderFrameLayout.visibility = View.GONE } } if (status != null && status != StoreDataStatus.LOADING) { viewModel.userOrders.observe(viewLifecycleOwner) { orders -> if (orders.isNotEmpty()) { ordersAdapter.data = orders.sortedByDescending { it.orderDate } binding.orderAllOrdersRecyclerView.adapter?.notifyDataSetChanged() binding.orderAllOrdersRecyclerView.visibility = View.VISIBLE } else if (orders.isEmpty()) { binding.loaderLayout.circularLoader.hideAnimationBehavior binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.ordersEmptyTextView.visibility = View.VISIBLE } } } } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/PayByAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.annotation.SuppressLint import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.databinding.LayoutListItemBinding private const val TAG = "PayByAdapter" class PayByAdapter(private val data: List) : RecyclerView.Adapter() { var lastCheckedMethod: String? = null private var lastCheckedCard: MaterialCardView? = null private var selectedMethodPos = -1 inner class ViewHolder(binding: LayoutListItemBinding) : RecyclerView.ViewHolder(binding.root) { val textView = binding.itemTitleTextView val cardView = binding.itemCard } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( LayoutListItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } @SuppressLint("ResourceAsColor") override fun onBindViewHolder(holder: ViewHolder, position: Int) { val title = data[position] holder.apply { textView.text = title cardView.setOnClickListener { onCardClick(position, data[position], it as MaterialCardView) } } } override fun getItemCount() = data.size private fun onCardClick(position: Int, method: String, cardView: MaterialCardView) { if (method != lastCheckedMethod) { cardView.apply { strokeColor = context.getColor(R.color.blue_accent_300) isChecked = true strokeWidth = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2F, resources.displayMetrics ).toInt() } lastCheckedCard?.apply { strokeColor = context.getColor(R.color.light_gray) isChecked = false strokeWidth = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 1F, resources.displayMetrics ).toInt() } lastCheckedCard = cardView lastCheckedMethod = method selectedMethodPos = position Log.d(TAG, "onSelectMethod: Selected Method = $lastCheckedMethod") } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/ProductAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Context import android.graphics.Paint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.CheckBox import android.widget.ImageView import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.databinding.LayoutHomeAdBinding import com.vishalgaur.shoppingapp.databinding.ProductsListItemBinding import com.vishalgaur.shoppingapp.getOfferPercentage class ProductAdapter(proList: List, userLikes: List, private val context: Context) : RecyclerView.Adapter() { var data = proList var likesList = userLikes lateinit var onClickListener: OnClickListener lateinit var bindImageButtons: BindImageButtons private val sessionManager = ShoppingAppSessionManager(context) inner class ItemViewHolder(binding: ProductsListItemBinding) : RecyclerView.ViewHolder(binding.root) { private val proName = binding.productNameTv private val proPrice = binding.productPriceTv private val productCard = binding.productCard private val productImage = binding.productImageView private val proDeleteButton = binding.productDeleteButton private val proEditBtn = binding.productEditButton private val proMrp = binding.productActualPriceTv private val proOffer = binding.productOfferValueTv private val proRatingBar = binding.productRatingBar private val proLikeButton = binding.productLikeCheckbox private val proCartButton = binding.productAddToCartButton fun bind(productData: Product) { productCard.setOnClickListener { onClickListener.onClick(productData) } proName.text = productData.name proPrice.text = context.getString(R.string.pro_details_price_value, productData.price.toString()) proRatingBar.rating = productData.rating.toFloat() proMrp.paintFlags = Paint.STRIKE_THRU_TEXT_FLAG proMrp.text = context.getString( R.string.pro_details_actual_strike_value, productData.mrp.toString() ) proOffer.text = context.getString( R.string.pro_offer_precent_text, getOfferPercentage(productData.mrp, productData.price).toString() ) if (productData.images.isNotEmpty()) { val imgUrl = productData.images[0].toUri().buildUpon().scheme("https").build() Glide.with(context) .asBitmap() .load(imgUrl) .into(productImage) productImage.clipToOutline = true } proLikeButton.isChecked = likesList.contains(productData.productId) if (sessionManager.isUserSeller()) { proLikeButton.visibility = View.GONE proCartButton.visibility = View.GONE proEditBtn.setOnClickListener { onClickListener.onEditClick(productData.productId) } proDeleteButton.setOnClickListener { onClickListener.onDeleteClick(productData) } } else { proEditBtn.visibility = View.GONE proDeleteButton.visibility = View.GONE bindImageButtons.setLikeButton(productData.productId, proLikeButton) bindImageButtons.setCartButton(productData.productId, proCartButton) proLikeButton.setOnCheckedChangeListener { _, _ -> } proLikeButton.setOnClickListener { onClickListener.onLikeClick(productData.productId) } proCartButton.setOnClickListener { onClickListener.onAddToCartClick(productData) } } } } inner class AdViewHolder(binding: LayoutHomeAdBinding) : RecyclerView.ViewHolder(binding.root) { val adImageView: ImageView = binding.adImageView } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_AD -> AdViewHolder( LayoutHomeAdBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) else -> ItemViewHolder( ProductsListItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (val proData = data[position]) { is Int -> (holder as AdViewHolder).adImageView.setImageResource(proData) is Product -> (holder as ItemViewHolder).bind(proData) } } override fun getItemCount(): Int = data.size companion object { const val VIEW_TYPE_PRODUCT = 1 const val VIEW_TYPE_AD = 2 } override fun getItemViewType(position: Int): Int { return when (data[position]) { is Int -> VIEW_TYPE_AD is Product -> VIEW_TYPE_PRODUCT else -> VIEW_TYPE_PRODUCT } } interface BindImageButtons { fun setLikeButton(productId: String, button: CheckBox) fun setCartButton(productId: String, imgView: ImageView) } interface OnClickListener { fun onClick(productData: Product) fun onDeleteClick(productData: Product) fun onEditClick(productId: String) {} fun onLikeClick(productId: String) {} fun onAddToCartClick(productData: Product) {} } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/ProductDetailsFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.annotation.SuppressLint import android.app.Application import android.content.res.ColorStateList import android.graphics.Color import android.graphics.PorterDuff import android.graphics.Typeface import android.os.Bundle import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.RadioButton import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.setMargins import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.PagerSnapHelper import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.AddObjectStatus import com.vishalgaur.shoppingapp.data.utils.ShoeColors import com.vishalgaur.shoppingapp.data.utils.ShoeSizes import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentProductDetailsBinding import com.vishalgaur.shoppingapp.ui.AddItemErrors import com.vishalgaur.shoppingapp.ui.DotsIndicatorDecoration import com.vishalgaur.shoppingapp.viewModels.ProductViewModel class ProductDetailsFragment : Fragment() { inner class ProductViewModelFactory( private val productId: String, private val application: Application ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(ProductViewModel::class.java)) { return ProductViewModel(productId, application) as T } throw IllegalArgumentException("Unknown ViewModel Class") } } private lateinit var binding: FragmentProductDetailsBinding private lateinit var viewModel: ProductViewModel private var selectedSize: Int? = null private var selectedColor: String? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentProductDetailsBinding.inflate(layoutInflater) val productId = arguments?.getString("productId") if (activity != null && productId != null) { val viewModelFactory = ProductViewModelFactory(productId, requireActivity().application) viewModel = ViewModelProvider(this, viewModelFactory).get(ProductViewModel::class.java) } if (viewModel.isSeller()) { binding.proDetailsAddCartBtn.visibility = View.GONE } else { binding.proDetailsAddCartBtn.visibility = View.VISIBLE binding.proDetailsAddCartBtn.setOnClickListener { if (viewModel.isItemInCart.value == true) { navigateToCartFragment() } else { onAddToCart() if (viewModel.errorStatus.value?.isEmpty() == true) { viewModel.addItemStatus.observe(viewLifecycleOwner) { status -> if (status == AddObjectStatus.DONE) { makeToast("Product Added To Cart") viewModel.checkIfInCart() } } } } } } binding.loaderLayout.loaderFrameLayout.background = ResourcesCompat.getDrawable(resources, R.color.white, null) binding.layoutViewsGroup.visibility = View.GONE binding.proDetailsAddCartBtn.visibility = View.GONE setObservers() return binding.root } override fun onResume() { super.onResume() viewModel.setLike() viewModel.checkIfInCart() selectedSize = null selectedColor = null } private fun setObservers() { viewModel.dataStatus.observe(viewLifecycleOwner) { when (it) { StoreDataStatus.DONE -> { binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.proDetailsLayout.visibility = View.VISIBLE setViews() } else -> { binding.proDetailsLayout.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE } } } viewModel.isLiked.observe(viewLifecycleOwner) { if (it == true) { binding.proDetailsLikeBtn.setImageResource(R.drawable.liked_heart_drawable) } else { binding.proDetailsLikeBtn.setImageResource(R.drawable.heart_icon_drawable) } } viewModel.isItemInCart.observe(viewLifecycleOwner) { if (it == true) { binding.proDetailsAddCartBtn.text = getString(R.string.pro_details_go_to_cart_btn_text) } else { binding.proDetailsAddCartBtn.text = getString(R.string.pro_details_add_to_cart_btn_text) } } viewModel.errorStatus.observe(viewLifecycleOwner) { if (it.isNotEmpty()) modifyErrors(it) } } @SuppressLint("ResourceAsColor") private fun modifyErrors(errList: List) { makeToast("Please Select Size and Color.") if (!errList.isNullOrEmpty()) { errList.forEach { err -> when (err) { AddItemErrors.ERROR_SIZE -> { binding.proDetailsSelectSizeLabel.setTextColor(R.color.red_600) } AddItemErrors.ERROR_COLOR -> { binding.proDetailsSelectColorLabel.setTextColor(R.color.red_600) } } } } } private fun setViews() { binding.layoutViewsGroup.visibility = View.VISIBLE binding.proDetailsAddCartBtn.visibility = View.VISIBLE binding.addProAppBar.topAppBar.title = viewModel.productData.value?.name binding.addProAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.addProAppBar.topAppBar.inflateMenu(R.menu.app_bar_menu) binding.addProAppBar.topAppBar.overflowIcon?.setTint( ContextCompat.getColor( requireContext(), R.color.gray ) ) setImagesView() binding.proDetailsTitleTv.text = viewModel.productData.value?.name ?: "" binding.proDetailsLikeBtn.apply { setOnClickListener { viewModel.toggleLikeProduct() } } binding.proDetailsRatingBar.rating = (viewModel.productData.value?.rating ?: 0.0).toFloat() binding.proDetailsPriceTv.text = resources.getString( R.string.pro_details_price_value, viewModel.productData.value?.price.toString() ) setShoeSizeButtons() setShoeColorsButtons() binding.proDetailsSpecificsText.text = viewModel.productData.value?.description ?: "" } private fun onAddToCart() { viewModel.addToCart(selectedSize, selectedColor) } private fun navigateToCartFragment() { findNavController().navigate(R.id.action_productDetailsFragment_to_cartFragment) } private fun makeToast(text: String) { Toast.makeText(context, text, Toast.LENGTH_LONG).show() } private fun setImagesView() { if (context != null) { binding.proDetailsImagesRecyclerview.isNestedScrollingEnabled = false val adapter = ProductImagesAdapter( requireContext(), viewModel.productData.value?.images ?: emptyList() ) binding.proDetailsImagesRecyclerview.adapter = adapter val rad = resources.getDimension(R.dimen.radius) val dotsHeight = resources.getDimensionPixelSize(R.dimen.dots_height) val inactiveColor = ContextCompat.getColor(requireContext(), R.color.gray) val activeColor = ContextCompat.getColor(requireContext(), R.color.blue_accent_300) val itemDecoration = DotsIndicatorDecoration(rad, rad * 4, dotsHeight, inactiveColor, activeColor) binding.proDetailsImagesRecyclerview.addItemDecoration(itemDecoration) PagerSnapHelper().attachToRecyclerView(binding.proDetailsImagesRecyclerview) } } private fun setShoeSizeButtons() { binding.proDetailsSizesRadioGroup.apply { for ((_, v) in ShoeSizes) { if (viewModel.productData.value?.availableSizes?.contains(v) == true) { val radioButton = RadioButton(context) radioButton.id = v radioButton.tag = v val param = binding.proDetailsSizesRadioGroup.layoutParams as ViewGroup.MarginLayoutParams param.setMargins(resources.getDimensionPixelSize(R.dimen.radio_margin_size)) param.width = ViewGroup.LayoutParams.WRAP_CONTENT param.height = ViewGroup.LayoutParams.WRAP_CONTENT radioButton.layoutParams = param radioButton.background = ContextCompat.getDrawable(context, R.drawable.radio_selector) radioButton.setButtonDrawable(R.color.transparent) radioButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14F) radioButton.setTextColor(Color.BLACK) radioButton.setTypeface(null, Typeface.BOLD) radioButton.textAlignment = View.TEXT_ALIGNMENT_CENTER radioButton.text = "$v" radioButton.setOnCheckedChangeListener { buttonView, isChecked -> val tag = buttonView.tag.toString().toInt() if (isChecked) { selectedSize = tag } } addView(radioButton) } } invalidate() } } private fun setShoeColorsButtons() { binding.proDetailsColorsRadioGroup.apply { var ind = 1 for ((k, v) in ShoeColors) { if (viewModel.productData.value?.availableColors?.contains(k) == true) { val radioButton = RadioButton(context) radioButton.id = ind radioButton.tag = k val param = binding.proDetailsColorsRadioGroup.layoutParams as ViewGroup.MarginLayoutParams param.setMargins(resources.getDimensionPixelSize(R.dimen.radio_margin_size)) param.width = ViewGroup.LayoutParams.WRAP_CONTENT param.height = ViewGroup.LayoutParams.WRAP_CONTENT radioButton.layoutParams = param radioButton.background = ContextCompat.getDrawable(context, R.drawable.color_radio_selector) radioButton.setButtonDrawable(R.color.transparent) radioButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor(v)) if (k == "white") { radioButton.backgroundTintMode = PorterDuff.Mode.MULTIPLY } else { radioButton.backgroundTintMode = PorterDuff.Mode.ADD } radioButton.setOnCheckedChangeListener { buttonView, isChecked -> val tag = buttonView.tag.toString() if (isChecked) { selectedColor = tag } } addView(radioButton) ind++ } } invalidate() } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/ProductImagesAdapter.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.vishalgaur.shoppingapp.databinding.ImagesItemBinding class ProductImagesAdapter(private val context: Context, private val images: List) : RecyclerView.Adapter() { class ViewHolder(binding: ImagesItemBinding) : RecyclerView.ViewHolder(binding.root) { val imageView = binding.rcImageView } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( ImagesItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val imageUrl = images[position] val imgUrl = imageUrl.toUri().buildUpon().scheme("https").build() Glide.with(context) .asBitmap() .load(imgUrl) .into(holder.imageView) } override fun getItemCount(): Int = images.size } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/ProfileFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.databinding.FragmentProfileBinding import com.vishalgaur.shoppingapp.viewModels.HomeViewModel class ProfileFragment : Fragment() { private lateinit var binding: FragmentProfileBinding private val viewModel: HomeViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentProfileBinding.inflate(layoutInflater) binding.profileTopAppBar.topAppBar.title = getString(R.string.account_profile_label) binding.profileTopAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.getUserData() setViews() } private fun setViews() { viewModel.userData.observe(viewLifecycleOwner) { if (it != null) { binding.profileNameTv.text = it.name binding.profileEmailTv.text = it.email binding.profileMobileTv.text = it.mobile } } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/SelectAddressFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.databinding.FragmentSelectAddressBinding import com.vishalgaur.shoppingapp.viewModels.OrderViewModel private const val TAG = "ShipToFragment" class SelectAddressFragment : Fragment() { private lateinit var binding: FragmentSelectAddressBinding private val orderViewModel: OrderViewModel by activityViewModels() private lateinit var addressAdapter: AddressAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentSelectAddressBinding.inflate(layoutInflater) setViews() setObservers() return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) orderViewModel.getUserAddresses() } private fun setObservers() { orderViewModel.dataStatus.observe(viewLifecycleOwner) { status -> when (status) { StoreDataStatus.LOADING -> { binding.addressEmptyTextView.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE binding.loaderLayout.circularLoader.showAnimationBehavior } else -> { binding.loaderLayout.circularLoader.hideAnimationBehavior binding.loaderLayout.loaderFrameLayout.visibility = View.GONE } } if(status != null && status != StoreDataStatus.LOADING) { orderViewModel.userAddresses.observe(viewLifecycleOwner) { addressList -> if (addressList.isNotEmpty()) { addressAdapter.data = addressList binding.shipToAddressesRecyclerView.adapter = addressAdapter binding.shipToAddressesRecyclerView.adapter?.notifyDataSetChanged() } else if (addressList.isEmpty()) { binding.shipToAddressesRecyclerView.visibility = View.GONE binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.loaderLayout.circularLoader.hideAnimationBehavior binding.addressEmptyTextView.visibility = View.VISIBLE } } } } } private fun setViews() { binding.shipToAppBar.topAppBar.title = getString(R.string.ship_to_title) binding.shipToAppBar.topAppBar.inflateMenu(R.menu.menu_with_add_only) binding.shipToAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.loaderLayout.loaderFrameLayout.visibility = View.GONE binding.shipToErrorTextView.visibility = View.GONE binding.addressEmptyTextView.visibility = View.GONE binding.shipToAppBar.topAppBar.setOnMenuItemClickListener { menuItem -> if (menuItem.itemId == R.id.add_item) { navigateToAddEditAddress(false) true } else { false } } if (context != null) { addressAdapter = AddressAdapter( requireContext(), orderViewModel.userAddresses.value ?: emptyList(), true ) addressAdapter.onClickListener = object : AddressAdapter.OnClickListener { override fun onEditClick(addressId: String) { Log.d(TAG, "onEditAddress: initiated") navigateToAddEditAddress(true, addressId) } override fun onDeleteClick(addressId: String) { Log.d(TAG, "onDeleteAddress: initiated") showDeleteDialog(addressId) } } binding.shipToAddressesRecyclerView.adapter = addressAdapter } binding.shipToNextBtn.setOnClickListener { navigateToPaymentFragment(addressAdapter.lastCheckedAddress) } } private fun showDeleteDialog(addressId: String) { context?.let { MaterialAlertDialogBuilder(it) .setTitle(getString(R.string.delete_dialog_title_text)) .setMessage(getString(R.string.delete_address_message_text)) .setNeutralButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ -> dialog.cancel() } .setPositiveButton(getString(R.string.delete_dialog_delete_btn_text)) { dialog, _ -> orderViewModel.deleteAddress(addressId) dialog.cancel() } .show() } } private fun navigateToPaymentFragment(addressId: String?) { if (addressId != null) { orderViewModel.setSelectedAddress(addressId) Log.d(TAG, "navigate to Payment") binding.shipToErrorTextView.visibility = View.GONE findNavController().navigate(R.id.action_selectAddressFragment_to_selectPaymentFragment) } else { Log.d(TAG, "error = select one address") binding.shipToErrorTextView.visibility = View.VISIBLE } } private fun navigateToAddEditAddress(isEdit: Boolean, addressId: String? = null) { findNavController().navigate( R.id.action_selectAddressFragment_to_addEditAddressFragment, bundleOf("isEdit" to isEdit, "addressId" to addressId) ) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/SelectPaymentFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.home import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.databinding.FragmentSelectPaymentBinding import com.vishalgaur.shoppingapp.viewModels.OrderViewModel private const val TAG = "SelectMethodFragment" class SelectPaymentFragment : Fragment() { private lateinit var binding: FragmentSelectPaymentBinding private var methodsAdapter = PayByAdapter(getPaymentMethods()) private val orderViewModel: OrderViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentSelectPaymentBinding.inflate(layoutInflater) setViews() return binding.root } private fun setViews() { binding.payByAppBar.topAppBar.title = getString(R.string.pay_by_title) binding.payByAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() } binding.payByErrorTextView.visibility = View.GONE binding.payByPaymentsRecyclerView.adapter = methodsAdapter binding.payByNextBtn.text = getString(R.string.pay_by_next_btn_text, orderViewModel.getItemsPriceTotal().toString()) binding.payByNextBtn.setOnClickListener { navigateToOrderSuccess(methodsAdapter.lastCheckedMethod) } } private fun navigateToOrderSuccess(method: String?) { if (method != null) { orderViewModel.setSelectedPaymentMethod(method) Log.d(TAG, "navigate to order Success") binding.payByErrorTextView.visibility = View.GONE orderViewModel.finalizeOrder() // save order // wait for save add observer // if success, navigate findNavController().navigate(R.id.action_selectPaymentFragment_to_orderSuccessFragment) } else { Log.d(TAG, "Error: Select a payment method!") binding.payByErrorTextView.visibility = View.VISIBLE } } private fun getPaymentMethods(): List { return listOf("UPI", "Debit Card", "Cash On Delivery") } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/loginSignup/LoginFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.loginSignup import android.text.SpannableString import android.text.Spanned import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.view.View import androidx.core.os.bundleOf import androidx.navigation.fragment.findNavController import com.vishalgaur.shoppingapp.MOB_ERROR_TEXT import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.LogInErrors import com.vishalgaur.shoppingapp.databinding.FragmentLoginBinding import com.vishalgaur.shoppingapp.ui.LoginViewErrors class LoginFragment : LoginSignupBaseFragment() { override fun setViewBinding(): FragmentLoginBinding { return FragmentLoginBinding.inflate(layoutInflater) } override fun observeView() { super.observeView() viewModel.errorStatusLoginFragment.observe(viewLifecycleOwner) { err -> modifyErrors(err) } viewModel.loginErrorStatus.observe(viewLifecycleOwner) { err -> when (err) { LogInErrors.LERR -> setErrorText(getString(R.string.login_error_text)) else -> binding.loginErrorTextView.visibility = View.GONE } } } override fun setUpViews() { super.setUpViews() binding.loginErrorTextView.visibility = View.GONE binding.loginMobileEditText.onFocusChangeListener = focusChangeListener binding.loginPasswordEditText.onFocusChangeListener = focusChangeListener binding.loginLoginBtn.setOnClickListener(object : OnClickListener { override fun onClick(v: View?) { onLogin() if (viewModel.errorStatusLoginFragment.value == LoginViewErrors.NONE) { viewModel.loginErrorStatus.observe(viewLifecycleOwner) { if (it == LogInErrors.NONE) { val isRemOn = binding.loginRemSwitch.isChecked val bundle = bundleOf( "uData" to viewModel.userData.value, "loginRememberMe" to isRemOn ) launchOtpActivity(getString(R.string.login_fragment_label), bundle) } } } } }) setUpClickableSignUpText() } private fun modifyErrors(err: LoginViewErrors) { when (err) { LoginViewErrors.NONE -> setEditTextErrors() LoginViewErrors.ERR_EMPTY -> setErrorText("Fill all details") LoginViewErrors.ERR_MOBILE -> setEditTextErrors(MOB_ERROR_TEXT) } } private fun setErrorText(errText: String?) { binding.loginErrorTextView.visibility = View.VISIBLE if (errText != null) { binding.loginErrorTextView.text = errText } } private fun setEditTextErrors(mobError: String? = null) { binding.loginErrorTextView.visibility = View.GONE binding.loginMobileEditText.error = mobError } private fun setUpClickableSignUpText() { val signUpText = getString(R.string.login_signup_text) val ss = SpannableString(signUpText) val clickableSpan = object : ClickableSpan() { override fun onClick(widget: View) { findNavController().navigateUp() } } ss.setSpan(clickableSpan, 10, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) binding.loginSignupTextView.apply { text = ss movementMethod = LinkMovementMethod.getInstance() } } private fun onLogin() { val mob = binding.loginMobileEditText.text.toString() val pwd = binding.loginPasswordEditText.text.toString() viewModel.loginSubmitData(mob, pwd) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/loginSignup/LoginSignupActivity.kt ================================================ package com.vishalgaur.shoppingapp.ui.loginSignup import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.vishalgaur.shoppingapp.R class LoginSignupActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_signup) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/loginSignup/LoginSignupBaseFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.loginSignup import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.viewbinding.ViewBinding import com.vishalgaur.shoppingapp.ui.MyOnFocusChangeListener import com.vishalgaur.shoppingapp.viewModels.AuthViewModel abstract class LoginSignupBaseFragment : Fragment() { protected val viewModel: AuthViewModel by activityViewModels() protected lateinit var binding: VBinding protected abstract fun setViewBinding(): VBinding protected val focusChangeListener = MyOnFocusChangeListener() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) init() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { setUpViews() observeView() return binding.root } fun launchOtpActivity(from: String, extras: Bundle) { val intent = Intent(context, OtpActivity::class.java).putExtra( "from", from ).putExtras(extras) startActivity(intent) } open fun setUpViews() {} open fun observeView() {} private fun init() { binding = setViewBinding() } interface OnClickListener : View.OnClickListener } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/loginSignup/OtpActivity.kt ================================================ package com.vishalgaur.shoppingapp.ui.loginSignup import android.app.Application import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.google.android.material.snackbar.Snackbar import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.databinding.ActivityOtpBinding import com.vishalgaur.shoppingapp.shouldBypassOTPValidation import com.vishalgaur.shoppingapp.ui.OTPStatus import com.vishalgaur.shoppingapp.ui.launchHome import com.vishalgaur.shoppingapp.viewModels.OtpViewModel class OtpActivity : AppCompatActivity() { private lateinit var binding: ActivityOtpBinding private lateinit var viewModel: OtpViewModel private lateinit var fromWhere: String class OtpViewModelFactory( private val application: Application, private val uData: UserData ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(OtpViewModel::class.java)) { return OtpViewModel(application, uData) as T } throw IllegalArgumentException("Unknown ViewModel Class") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityOtpBinding.inflate(layoutInflater) val uData: UserData? = intent.getParcelableExtra("uData") fromWhere = intent.getStringExtra("from").toString() if (uData != null) { val viewModelFactory = OtpViewModelFactory(application, uData) viewModel = ViewModelProvider(this, viewModelFactory).get(OtpViewModel::class.java) if (shouldBypassOTPValidation()) { viewModel.manuallyOverrideVerification() } else { viewModel.verifyPhoneOTPStart(uData.mobile, this) } } setViews() setObservers() setContentView(binding.root) } private fun setObservers() { viewModel.otpStatus.observe(this) { when (it) { OTPStatus.WRONG -> binding.otpVerifyError.visibility = View.VISIBLE OTPStatus.INVALID_REQ -> { binding.loaderLayout.loaderCard.visibility = View.GONE val contextView = binding.loaderLayout.loaderCard Snackbar.make(contextView, R.string.otp_invalid_req_failure, Snackbar.LENGTH_SHORT).show() } else -> binding.otpVerifyError.visibility = View.GONE } } viewModel.isUserLoggedIn.observe(this) { if (it == true) { if (fromWhere == getString(R.string.signup_fragment_label)) { viewModel.signUp() } else { val rememberMe = intent.getBooleanExtra("loginRememberMe", false) viewModel.login(rememberMe) } launchHome(this) finish() } } viewModel.isOTPSent.observe(this) { if(it == true) { binding.loaderLayout.loaderCard.visibility = View.GONE val contextView = binding.loaderLayout.loaderCard Snackbar.make(contextView, R.string.otp_sent_msg, Snackbar.LENGTH_SHORT).show() } } } private fun setViews() { binding.otpVerifyError.visibility = View.GONE binding.loaderLayout.loaderCard.visibility = View.VISIBLE binding.loaderLayout.loadingMessage.text = getString(R.string.sending_otp_msg) binding.loaderLayout.circularLoader.showAnimationBehavior binding.otpVerifyBtn.setOnClickListener { onVerify() } } private fun onVerify() { val otp = binding.otpOtpEditText.text.toString() viewModel.verifyOTP(otp) } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/loginSignup/SignupFragment.kt ================================================ package com.vishalgaur.shoppingapp.ui.loginSignup import android.text.SpannableString import android.text.Spanned import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.view.View import androidx.core.os.bundleOf import androidx.navigation.fragment.findNavController import com.vishalgaur.shoppingapp.EMAIL_ERROR_TEXT import com.vishalgaur.shoppingapp.MOB_ERROR_TEXT import com.vishalgaur.shoppingapp.R import com.vishalgaur.shoppingapp.data.utils.SignUpErrors import com.vishalgaur.shoppingapp.databinding.FragmentSignupBinding import com.vishalgaur.shoppingapp.ui.SignUpViewErrors class SignupFragment : LoginSignupBaseFragment() { override fun setViewBinding(): FragmentSignupBinding { return FragmentSignupBinding.inflate(layoutInflater) } override fun observeView() { super.observeView() viewModel.errorStatus.observe(viewLifecycleOwner) { err -> modifyErrors(err) } } override fun setUpViews() { super.setUpViews() binding.signupErrorTextView.visibility = View.GONE binding.signupNameEditText.onFocusChangeListener = focusChangeListener binding.signupMobileEditText.onFocusChangeListener = focusChangeListener binding.signupEmailEditText.onFocusChangeListener = focusChangeListener binding.signupPasswordEditText.onFocusChangeListener = focusChangeListener binding.signupCnfPasswordEditText.onFocusChangeListener = focusChangeListener binding.signupSignupBtn.setOnClickListener(object : OnClickListener { override fun onClick(v: View?) { onSignUp() if (viewModel.errorStatus.value == SignUpViewErrors.NONE) { viewModel.signErrorStatus.observe(viewLifecycleOwner) { if (it == SignUpErrors.NONE) { val bundle = bundleOf("uData" to viewModel.userData.value) launchOtpActivity(getString(R.string.signup_fragment_label), bundle) } } } } }) setUpClickableLoginText() } private fun setUpClickableLoginText() { val loginText = getString(R.string.signup_login_text) val ss = SpannableString(loginText) val clickableSpan = object : ClickableSpan() { override fun onClick(widget: View) { findNavController().navigate(R.id.action_signup_to_login) } } ss.setSpan(clickableSpan, 25, 31, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) binding.signupLoginTextView.apply { text = ss movementMethod = LinkMovementMethod.getInstance() } } private fun onSignUp() { val name = binding.signupNameEditText.text.toString() val mobile = binding.signupMobileEditText.text.toString() val email = binding.signupEmailEditText.text.toString() val password1 = binding.signupPasswordEditText.text.toString() val password2 = binding.signupCnfPasswordEditText.text.toString() val isAccepted = binding.signupPolicySwitch.isChecked val isSeller = binding.signupSellerSwitch.isChecked viewModel.signUpSubmitData(name, mobile, email, password1, password2, isAccepted, isSeller) } private fun modifyErrors(err: SignUpViewErrors) { when (err) { SignUpViewErrors.NONE -> setEditTextsError() SignUpViewErrors.ERR_EMAIL -> setEditTextsError(emailError = EMAIL_ERROR_TEXT) SignUpViewErrors.ERR_MOBILE -> setEditTextsError(mobError = MOB_ERROR_TEXT) SignUpViewErrors.ERR_EMAIL_MOBILE -> setEditTextsError(EMAIL_ERROR_TEXT, MOB_ERROR_TEXT) SignUpViewErrors.ERR_EMPTY -> setErrorText("Fill all details.") SignUpViewErrors.ERR_NOT_ACC -> setErrorText("Accept the Terms.") SignUpViewErrors.ERR_PWD12NS -> setErrorText("Both passwords are not same!") } } private fun setErrorText(errText: String?) { binding.signupErrorTextView.visibility = View.VISIBLE if (errText != null) { binding.signupErrorTextView.text = errText } } private fun setEditTextsError(emailError: String? = null, mobError: String? = null) { binding.signupEmailEditText.error = emailError binding.signupMobileEditText.error = mobError binding.signupErrorTextView.visibility = View.GONE } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/viewModels/AddEditAddressViewModel.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import android.app.Application import android.util.Log import androidx.annotation.VisibleForTesting import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.source.repository.AuthRepository import com.vishalgaur.shoppingapp.data.utils.AddObjectStatus import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.getAddressId import com.vishalgaur.shoppingapp.isPhoneValid import com.vishalgaur.shoppingapp.isZipCodeValid import com.vishalgaur.shoppingapp.ui.AddAddressViewErrors import kotlinx.coroutines.async import kotlinx.coroutines.launch class AddEditAddressViewModel(application: Application) : AndroidViewModel(application) { private val authRepository = (application as ShoppingApplication).authRepository private val sessionManager = ShoppingAppSessionManager(application.applicationContext) private val currentUser = sessionManager.getUserIdFromSession() private val _isEdit = MutableLiveData() val isEdit: LiveData get() = _isEdit private val _addressId = MutableLiveData() val addressId: LiveData get() = _addressId private val _dataStatus = MutableLiveData() val dataStatus: LiveData get() = _dataStatus private val _errorStatus = MutableLiveData>() val errorStatus: LiveData> get() = _errorStatus private val _addAddressStatus = MutableLiveData() val addAddressStatus: LiveData get() = _addAddressStatus private val _addressData = MutableLiveData() val addressData: LiveData get() = _addressData @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val newAddressData = MutableLiveData() init { _errorStatus.value = mutableListOf() } fun setIsEdit(state: Boolean) { _isEdit.value = state } fun setAddressData(addressId: String) { _addressId.value = addressId viewModelScope.launch { Log.d(TAG, "onLoad: Getting Address Data") _dataStatus.value = StoreDataStatus.LOADING val res = async { authRepository.getAddressesByUserId(currentUser!!) } val addRes = res.await() if (addRes is Success) { val addData = addRes.data?.find { address -> address.addressId == addressId } _addressData.value = addData ?: UserData.Address() Log.d(TAG, "onLoad: Success") _dataStatus.value = StoreDataStatus.DONE } else { _dataStatus.value = StoreDataStatus.ERROR _addressData.value = UserData.Address() if (addRes is Error) Log.d(TAG, "onLoad: Error, ${addRes.exception.message}") } } } fun submitAddress( countryCode: String, firstName: String, lastName: String, streetAdd: String, streetAdd2: String, city: String, state: String, zipCode: String, phoneNumber: String ) { val errorsList = mutableListOf() if (firstName.isBlank() || lastName.isBlank() || streetAdd.isBlank() || city.isBlank() || state.isBlank() || zipCode.isBlank() || phoneNumber.isBlank()) errorsList.add(AddAddressViewErrors.EMPTY) if (firstName.isBlank()) errorsList.add(AddAddressViewErrors.ERR_FNAME_EMPTY) if (lastName.isBlank()) errorsList.add(AddAddressViewErrors.ERR_LNAME_EMPTY) if (streetAdd.isBlank()) errorsList.add(AddAddressViewErrors.ERR_STR1_EMPTY) if (city.isBlank()) errorsList.add(AddAddressViewErrors.ERR_CITY_EMPTY) if (state.isBlank()) errorsList.add(AddAddressViewErrors.ERR_STATE_EMPTY) if (zipCode.isBlank()) errorsList.add(AddAddressViewErrors.ERR_ZIP_EMPTY) else if (!isZipCodeValid(zipCode)) errorsList.add(AddAddressViewErrors.ERR_ZIP_INVALID) if (phoneNumber.isBlank()) errorsList.add(AddAddressViewErrors.ERR_PHONE_EMPTY) else if (!isPhoneValid(phoneNumber)) errorsList.add(AddAddressViewErrors.ERR_PHONE_INVALID) _errorStatus.value = errorsList if (errorsList.isEmpty()) { val addressId = if (_isEdit.value == true) _addressId.value!! else getAddressId(currentUser!!) val newAddress = UserData.Address( addressId, firstName.trim(), lastName.trim(), countryCode.trim(), streetAdd.trim(), streetAdd2.trim(), city.trim(), state.trim(), zipCode.trim(), "+91" + phoneNumber.trim() ) newAddressData.value = newAddress if (_isEdit.value == true) { updateAddress() } else { insertAddress() } } } private fun updateAddress() { viewModelScope.launch { if (newAddressData.value != null && _addressData.value != null) { _addAddressStatus.value = AddObjectStatus.ADDING val updateRes = async { authRepository.updateAddress(newAddressData.value!!, currentUser!!) } val res = updateRes.await() if (res is Success) { authRepository.hardRefreshUserData() Log.d(TAG, "onUpdate: Success") _addAddressStatus.value = AddObjectStatus.DONE } else { Log.d(TAG, "onUpdate: Some error occurred!") _addAddressStatus.value = AddObjectStatus.ERR_ADD if (res is Error) Log.d(TAG, "onUpdate: Error, ${res.exception}") } } else { Log.d(TAG, "Address Null, Cannot Update!") } } } private fun insertAddress() { viewModelScope.launch { if (newAddressData.value != null) { _addAddressStatus.value = AddObjectStatus.ADDING val deferredRes = async { authRepository.insertAddress(newAddressData.value!!, currentUser!!) } val res = deferredRes.await() if (res is Success) { authRepository.hardRefreshUserData() Log.d(TAG, "onInsertAddress: Success") _addAddressStatus.value = AddObjectStatus.DONE } else { _addAddressStatus.value = AddObjectStatus.ERR_ADD if (res is Error) { Log.d(TAG, "onInsertAddress: Error, ${res.exception.message}") } } } else { Log.d(TAG, "Address is Null, Cannot Add!") } } } companion object { private const val TAG = "AddAddressViewModel" } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/viewModels/AddEditProductViewModel.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import android.app.Application import android.net.Uri import android.util.Log import androidx.annotation.VisibleForTesting import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.vishalgaur.shoppingapp.ERR_UPLOAD import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.utils.AddProductErrors import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.getProductId import com.vishalgaur.shoppingapp.ui.AddProductViewErrors import kotlinx.coroutines.async import kotlinx.coroutines.launch private const val TAG = "AddEditViewModel" class AddEditProductViewModel(application: Application) : AndroidViewModel(application) { private val productsRepository = (application.applicationContext as ShoppingApplication).productsRepository private val sessionManager = ShoppingAppSessionManager(application.applicationContext) private val currentUser = sessionManager.getUserIdFromSession() private val _selectedCategory = MutableLiveData() val selectedCategory: LiveData get() = _selectedCategory private val _productId = MutableLiveData() val productId: LiveData get() = _productId private val _isEdit = MutableLiveData() val isEdit: LiveData get() = _isEdit private val _errorStatus = MutableLiveData() val errorStatus: LiveData get() = _errorStatus private val _dataStatus = MutableLiveData() val dataStatus: LiveData get() = _dataStatus private val _addProductErrors = MutableLiveData() val addProductErrors: LiveData get() = _addProductErrors private val _productData = MutableLiveData() val productData: LiveData get() = _productData @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val newProductData = MutableLiveData() init { _errorStatus.value = AddProductViewErrors.NONE } fun setIsEdit(state: Boolean) { _isEdit.value = state } fun setCategory(catName: String) { _selectedCategory.value = catName } fun setProductData(productId: String) { _productId.value = productId viewModelScope.launch { Log.d(TAG, "onLoad: Getting product Data") _dataStatus.value = StoreDataStatus.LOADING val res = async { productsRepository.getProductById(productId) } val proRes = res.await() if (proRes is Success) { val proData = proRes.data _productData.value = proData _selectedCategory.value = _productData.value!!.category Log.d(TAG, "onLoad: Successfully retrieved product data") _dataStatus.value = StoreDataStatus.DONE } else if (proRes is Error) { _dataStatus.value = StoreDataStatus.ERROR Log.d(TAG, "onLoad: Error getting product data") _productData.value = Product() } } } fun submitProduct( name: String, price: Double?, mrp: Double?, desc: String, sizes: List, colors: List, imgList: List, ) { if (name.isBlank() || price == null || mrp == null || desc.isBlank() || sizes.isNullOrEmpty() || colors.isNullOrEmpty() || imgList.isNullOrEmpty()) { _errorStatus.value = AddProductViewErrors.EMPTY } else { if (price == 0.0 || mrp == 0.0) { _errorStatus.value = AddProductViewErrors.ERR_PRICE_0 } else { _errorStatus.value = AddProductViewErrors.NONE val proId = if (_isEdit.value == true) _productId.value!! else getProductId(currentUser!!, selectedCategory.value!!) val newProduct = Product( proId, name.trim(), currentUser!!, desc.trim(), _selectedCategory.value!!, price, mrp, sizes, colors, emptyList(), 0.0 ) newProductData.value = newProduct Log.d(TAG, "pro = $newProduct") if (_isEdit.value == true) { updateProduct(imgList) } else { insertProduct(imgList) } } } } private fun updateProduct(imgList: List) { viewModelScope.launch { if (newProductData.value != null && _productData.value != null) { _addProductErrors.value = AddProductErrors.ADDING val resImg = async { productsRepository.updateImages(imgList, _productData.value!!.images) } val imagesPaths = resImg.await() newProductData.value?.images = imagesPaths if (newProductData.value?.images?.isNotEmpty() == true) { if (imagesPaths[0] == ERR_UPLOAD) { Log.d(TAG, "error uploading images") _addProductErrors.value = AddProductErrors.ERR_ADD_IMG } else { val updateRes = async { productsRepository.updateProduct(newProductData.value!!) } val res = updateRes.await() if (res is Success) { Log.d(TAG, "onUpdate: Success") _addProductErrors.value = AddProductErrors.NONE } else { Log.d(TAG, "onUpdate: Some error occurred!") _addProductErrors.value = AddProductErrors.ERR_ADD if (res is Error) Log.d(TAG, "onUpdate: Error, ${res.exception}") } } } else { Log.d(TAG, "Product images empty, Cannot Add Product") } } else { Log.d(TAG, "Product is Null, Cannot Add Product") } } } private fun insertProduct(imgList: List) { viewModelScope.launch { if (newProductData.value != null) { _addProductErrors.value = AddProductErrors.ADDING val resImg = async { productsRepository.insertImages(imgList) } val imagesPaths = resImg.await() newProductData.value?.images = imagesPaths if (newProductData.value?.images?.isNotEmpty() == true) { if (imagesPaths[0] == ERR_UPLOAD) { Log.d(TAG, "error uploading images") _addProductErrors.value = AddProductErrors.ERR_ADD_IMG } else { val deferredRes = async { productsRepository.insertProduct(newProductData.value!!) } val res = deferredRes.await() if (res is Success) { Log.d(TAG, "onInsertProduct: Success") _addProductErrors.value = AddProductErrors.NONE } else { _addProductErrors.value = AddProductErrors.ERR_ADD if (res is Error) Log.d(TAG, "onInsertProduct: Error Occurred, ${res.exception}") } } } else { Log.d(TAG, "Product images empty, Cannot Add Product") } } else { Log.d(TAG, "Product is Null, Cannot Add Product") } } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/viewModels/AuthViewModel.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.vishalgaur.shoppingapp.* import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.LogInErrors import com.vishalgaur.shoppingapp.data.utils.SignUpErrors import com.vishalgaur.shoppingapp.data.utils.UserType import com.vishalgaur.shoppingapp.ui.LoginViewErrors import com.vishalgaur.shoppingapp.ui.SignUpViewErrors import kotlinx.coroutines.async import kotlinx.coroutines.launch private const val TAG = "AuthViewModel" class AuthViewModel(application: Application) : AndroidViewModel(application) { private val authRepository = (application as ShoppingApplication).authRepository private val productsRepository = (application as ShoppingApplication).productsRepository private val _userData = MutableLiveData() val userData: LiveData get() = _userData private val _signErrorStatus = MutableLiveData() val signErrorStatus: LiveData get() = _signErrorStatus private val _errorStatus = MutableLiveData() val errorStatus: LiveData get() = _errorStatus private val _errorStatusLoginFragment = MutableLiveData() val errorStatusLoginFragment: LiveData get() = _errorStatusLoginFragment private val _loginErrorStatus = MutableLiveData() val loginErrorStatus: LiveData get() = _loginErrorStatus init { _errorStatus.value = SignUpViewErrors.NONE _errorStatusLoginFragment.value = LoginViewErrors.NONE refreshStatus() } private fun refreshStatus() { viewModelScope.launch { getCurrUser() productsRepository.refreshProducts() } } fun signUpSubmitData( name: String, mobile: String, email: String, pwd1: String, pwd2: String, isAccepted: Boolean, isSeller: Boolean ) { if (name.isBlank() || mobile.isBlank() || email.isBlank() || pwd1.isBlank() || pwd2.isBlank()) { _errorStatus.value = SignUpViewErrors.ERR_EMPTY } else { if (pwd1 != pwd2) { _errorStatus.value = SignUpViewErrors.ERR_PWD12NS } else { if (!isAccepted) { _errorStatus.value = SignUpViewErrors.ERR_NOT_ACC } else { var err = ERR_INIT if (!isEmailValid(email)) { err += ERR_EMAIL } if (!isPhoneValid(mobile)) { err += ERR_MOBILE } when (err) { ERR_INIT -> { _errorStatus.value = SignUpViewErrors.NONE val uId = getRandomString(32, "91" + mobile.trim(), 6) val newData = UserData( uId, name.trim(), "+91" + mobile.trim(), email.trim(), pwd1.trim(), ArrayList(), ArrayList(), ArrayList(), ArrayList(), if (isSeller) UserType.SELLER.name else UserType.CUSTOMER.name ) _userData.value = newData checkUniqueUser(newData) } (ERR_INIT + ERR_EMAIL) -> _errorStatus.value = SignUpViewErrors.ERR_EMAIL (ERR_INIT + ERR_MOBILE) -> _errorStatus.value = SignUpViewErrors.ERR_MOBILE (ERR_INIT + ERR_EMAIL + ERR_MOBILE) -> _errorStatus.value = SignUpViewErrors.ERR_EMAIL_MOBILE } } } } } private fun checkUniqueUser(uData: UserData) { viewModelScope.launch { val res = async { authRepository.checkEmailAndMobile(uData.email, uData.mobile, getApplication().applicationContext) } _signErrorStatus.value = res.await() } } fun loginSubmitData(mobile: String, password: String) { if (mobile.isBlank() || password.isBlank()) { _errorStatusLoginFragment.value = LoginViewErrors.ERR_EMPTY } else { if (!isPhoneValid(mobile)) { _errorStatusLoginFragment.value = LoginViewErrors.ERR_MOBILE } else { _errorStatusLoginFragment.value = LoginViewErrors.NONE logIn("+91" + mobile.trim(), password) } } } private fun logIn(phoneNumber: String, pwd: String) { viewModelScope.launch { val res = async { authRepository.checkLogin(phoneNumber, pwd) } _userData.value = res.await() if (_userData.value != null) { _loginErrorStatus.value = LogInErrors.NONE } else { _loginErrorStatus.value = LogInErrors.LERR } } } private suspend fun getCurrUser() { Log.d(TAG, "refreshing data...") authRepository.refreshData() } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/viewModels/HomeViewModel.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import android.app.Application import android.util.Log import androidx.lifecycle.* import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import kotlinx.coroutines.async import kotlinx.coroutines.launch import java.time.Month import java.util.* private const val TAG = "HomeViewModel" class HomeViewModel(application: Application) : AndroidViewModel(application) { private val productsRepository = (application.applicationContext as ShoppingApplication).productsRepository private val authRepository = (application.applicationContext as ShoppingApplication).authRepository private val sessionManager = ShoppingAppSessionManager(application.applicationContext) private val currentUser = sessionManager.getUserIdFromSession() val isUserASeller = sessionManager.isUserSeller() private var _products = MutableLiveData>() val products: LiveData> get() = _products private lateinit var _allProducts: MutableLiveData> val allProducts: LiveData> get() = _allProducts private var _userProducts = MutableLiveData>() val userProducts: LiveData> get() = _userProducts private var _userOrders = MutableLiveData>() val userOrders: LiveData> get() = _userOrders private var _userAddresses = MutableLiveData>() val userAddresses: LiveData> get() = _userAddresses private var _selectedOrder = MutableLiveData() val selectedOrder: LiveData get() = _selectedOrder private var _orderProducts = MutableLiveData>() val orderProducts: LiveData> get() = _orderProducts private var _likedProducts = MutableLiveData>() val likedProducts: LiveData> get() = _likedProducts private var _userLikes = MutableLiveData>() val userLikes: LiveData> get() = _userLikes private var _filterCategory = MutableLiveData("All") val filterCategory: LiveData get() = _filterCategory private val _storeDataStatus = MutableLiveData() val storeDataStatus: LiveData get() = _storeDataStatus private val _dataStatus = MutableLiveData() val dataStatus: LiveData get() = _dataStatus private val _userData = MutableLiveData() val userData: LiveData get() = _userData init { viewModelScope.launch { authRepository.hardRefreshUserData() getUserLikes() } if (isUserASeller) getProductsByOwner() else getProducts() } fun setDataLoaded() { _storeDataStatus.value = StoreDataStatus.DONE } fun isProductLiked(productId: String): Boolean { return _userLikes.value?.contains(productId) == true } fun toggleLikeByProductId(productId: String) { Log.d(TAG, "Toggling Like") viewModelScope.launch { val isLiked = isProductLiked(productId) val allLikes = _userLikes.value?.toMutableList() ?: mutableListOf() val deferredRes = async { if (isLiked) { authRepository.removeProductFromLikes(productId, currentUser!!) } else { authRepository.insertProductToLikes(productId, currentUser!!) } } val res = deferredRes.await() if (res is Success) { if (isLiked) { allLikes.remove(productId) } else { allLikes.add(productId) } _userLikes.value = allLikes val proList = _likedProducts.value?.toMutableList() ?: mutableListOf() val pro = proList.find { it.productId == productId } if (pro != null) { proList.remove(pro) } _likedProducts.value = proList Log.d(TAG, "onToggleLike: Success") } else { if (res is Error) { Log.d(TAG, "onToggleLike: Error, ${res.exception}") } } } } fun isProductInCart(productId: String): Boolean { return false } fun toggleProductInCart(product: Product) { } fun setDataLoading() { _dataStatus.value = StoreDataStatus.LOADING } private fun getProducts() { _allProducts = Transformations.switchMap(productsRepository.observeProducts()) { getProductsLiveData(it) } as MutableLiveData> viewModelScope.launch { _storeDataStatus.value = StoreDataStatus.LOADING val res = async { productsRepository.refreshProducts() } res.await() Log.d(TAG, "getAllProducts: status = ${_storeDataStatus.value}") } } fun getUserLikes() { viewModelScope.launch { val res = authRepository.getLikesByUserId(currentUser!!) if (res is Success) { val data = res.data ?: emptyList() if (data[0] != "") { _userLikes.value = data } else { _userLikes.value = emptyList() } Log.d(TAG, "Getting Likes: Success") } else { _userLikes.value = emptyList() if (res is Error) Log.d(TAG, "Getting Likes: Error, ${res.exception}") } } } fun getLikedProducts() { val res: List = if (_userLikes.value != null) { val allLikes = _userLikes.value ?: emptyList() if (!allLikes.isNullOrEmpty()) { Log.d(TAG, "alllikes = ${allLikes.size}") _dataStatus.value = StoreDataStatus.DONE allLikes.map { proId -> _allProducts.value?.find { it.productId == proId } ?: Product() } } else { _dataStatus.value = StoreDataStatus.ERROR emptyList() } } else { _dataStatus.value = StoreDataStatus.ERROR emptyList() } _likedProducts.value = res } private fun getProductsLiveData(result: Result?>?): LiveData> { val res = MutableLiveData>() if (result is Success) { Log.d(TAG, "result is success") _storeDataStatus.value = StoreDataStatus.DONE res.value = result.data ?: emptyList() } else { Log.d(TAG, "result is not success") res.value = emptyList() _storeDataStatus.value = StoreDataStatus.ERROR if (result is Error) Log.d(TAG, "getProductsLiveData: Error Occurred: ${result.exception}") } return res } private fun getProductsByOwner() { _allProducts = Transformations.switchMap(productsRepository.observeProductsByOwner(currentUser!!)) { Log.d(TAG, it.toString()) getProductsLiveData(it) } as MutableLiveData> viewModelScope.launch { _storeDataStatus.value = StoreDataStatus.LOADING val res = async { productsRepository.refreshProducts() } res.await() Log.d(TAG, "getProductsByOwner: status = ${_storeDataStatus.value}") } } fun refreshProducts() { getProducts() } fun filterBySearch(queryText: String) { filterProducts(_filterCategory.value!!) _products.value = _products.value?.filter { product -> product.name.contains(queryText, true) or ((queryText.toDoubleOrNull() ?: 0.0).compareTo(product.price) == 0) } } fun filterProducts(filterType: String) { Log.d(TAG, "filterType is $filterType") _filterCategory.value = filterType _products.value = when (filterType) { "None" -> emptyList() "All" -> _allProducts.value else -> _allProducts.value?.filter { product -> product.category == filterType } } } fun deleteProduct(productId: String) { viewModelScope.launch { val delRes = async { productsRepository.deleteProductById(productId) } when (val res = delRes.await()) { is Success -> Log.d(TAG, "onDelete: Success") is Error -> Log.d(TAG, "onDelete: Error, ${res.exception}") else -> Log.d(TAG, "onDelete: Some error occurred!") } } } fun signOut() { viewModelScope.launch { val deferredRes = async { authRepository.signOut() } deferredRes.await() } } fun getAllOrders() { viewModelScope.launch { _storeDataStatus.value = StoreDataStatus.LOADING val deferredRes = async { authRepository.getOrdersByUserId(currentUser!!) } val res = deferredRes.await() if (res is Success) { _userOrders.value = res.data ?: emptyList() _storeDataStatus.value = StoreDataStatus.DONE Log.d(TAG, "Getting Orders: Success") } else { _userOrders.value = emptyList() _storeDataStatus.value = StoreDataStatus.ERROR if (res is Error) Log.d(TAG, "Getting Orders: Error, ${res.exception}") } } } fun getOrderDetailsByOrderId(orderId: String) { viewModelScope.launch { _storeDataStatus.value = StoreDataStatus.LOADING if (_userOrders.value != null) { val orderData = _userOrders.value!!.find { it.orderId == orderId } if (orderData != null) { _selectedOrder.value = orderData _orderProducts.value = orderData.items.map { _allProducts.value?.find { pro -> pro.productId == it.productId } ?: Product() } _storeDataStatus.value = StoreDataStatus.DONE } else { _selectedOrder.value = null _storeDataStatus.value = StoreDataStatus.ERROR } } } } fun onSetStatusOfOrder(orderId: String, status: String) { val currDate = Calendar.getInstance() val dateString = "${Month.values()[(currDate.get(Calendar.MONTH))].name} ${ currDate.get(Calendar.DAY_OF_MONTH) }, ${currDate.get(Calendar.YEAR)}" Log.d(TAG, "Selected Status is $status ON $dateString") setStatusOfOrder(orderId, "$status ON $dateString") } private fun setStatusOfOrder(orderId: String, statusString: String) { viewModelScope.launch { _storeDataStatus.value = StoreDataStatus.LOADING val deferredRes = async { authRepository.setStatusOfOrder(orderId, currentUser!!, statusString) } val res = deferredRes.await() if (res is Success) { val orderData = _selectedOrder.value orderData?.status = statusString _selectedOrder.value = orderData getOrderDetailsByOrderId(orderId) } else { _storeDataStatus.value = StoreDataStatus.ERROR if (res is Error) Log.d(TAG, "Error updating status, ${res.exception}") } } } fun getUserAddresses() { Log.d(TAG, "Getting Addresses") _dataStatus.value = StoreDataStatus.LOADING viewModelScope.launch { val res = authRepository.getAddressesByUserId(currentUser!!) if (res is Success) { _userAddresses.value = res.data ?: emptyList() _dataStatus.value = StoreDataStatus.DONE Log.d(TAG, "Getting Addresses: Success") } else { _userAddresses.value = emptyList() _dataStatus.value = StoreDataStatus.ERROR if (res is Error) Log.d(TAG, "Getting Addresses: Error Occurred, ${res.exception.message}") } } } fun deleteAddress(addressId: String) { viewModelScope.launch { val delRes = async { authRepository.deleteAddressById(addressId, currentUser!!) } when (val res = delRes.await()) { is Success -> { Log.d(TAG, "onDeleteAddress: Success") val addresses = _userAddresses.value?.toMutableList() addresses?.let { val pos = addresses.indexOfFirst { address -> address.addressId == addressId } if (pos >= 0) it.removeAt(pos) _userAddresses.value = it } } is Error -> Log.d(TAG, "onDeleteAddress: Error, ${res.exception}") else -> Log.d(TAG, "onDeleteAddress: Some error occurred!") } } } fun getUserData() { viewModelScope.launch { _dataStatus.value = StoreDataStatus.LOADING val deferredRes = async { authRepository.getUserData(currentUser!!) } val res = deferredRes.await() if (res is Success) { val uData = res.data _userData.value = uData _dataStatus.value = StoreDataStatus.DONE } else { _dataStatus.value = StoreDataStatus.ERROR _userData.value = null } } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/viewModels/OrderViewModel.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.getRandomString import kotlinx.coroutines.async import kotlinx.coroutines.launch import java.util.* private const val TAG = "OrderViewModel" class OrderViewModel(application: Application) : AndroidViewModel(application) { private val sessionManager = ShoppingAppSessionManager(application.applicationContext) private val currentUser = sessionManager.getUserIdFromSession() private val authRepository = (application as ShoppingApplication).authRepository private val productsRepository = (application as ShoppingApplication).productsRepository private val _userAddresses = MutableLiveData>() val userAddresses: LiveData> get() = _userAddresses private val _userLikes = MutableLiveData>() val userLikes: LiveData> get() = _userLikes private val _cartItems = MutableLiveData>() val cartItems: LiveData> get() = _cartItems private val _priceList = MutableLiveData>() val priceList: LiveData> get() = _priceList private val _cartProducts = MutableLiveData>() val cartProducts: LiveData> get() = _cartProducts private val _dataStatus = MutableLiveData() val dataStatus: LiveData get() = _dataStatus private val _orderStatus = MutableLiveData() val orderStatus: LiveData get() = _orderStatus private val _selectedAddress = MutableLiveData() private val _selectedPaymentMethod = MutableLiveData() private val newOrderData = MutableLiveData() init { viewModelScope.launch { getUserLikes() } } fun getCartItems() { Log.d(TAG, "Getting Cart Items") _dataStatus.value = StoreDataStatus.LOADING viewModelScope.launch { val deferredRes = async { authRepository.hardRefreshUserData() authRepository.getUserData(currentUser!!) } val userRes = deferredRes.await() if (userRes is Success) { val uData = userRes.data if (uData != null) { _cartItems.value = uData.cart val priceRes = async { getAllProductsInCart() } priceRes.await() Log.d(TAG, "Getting Cart Items: Success ${_priceList.value}") } else { _cartItems.value = emptyList() _dataStatus.value = StoreDataStatus.ERROR Log.d(TAG, "Getting Cart Items: User Not Found") } } else { _cartItems.value = emptyList() _dataStatus.value = StoreDataStatus.ERROR Log.d(TAG, "Getting Cart Items: Error Occurred") } } } fun getUserAddresses() { Log.d(TAG, "Getting Addresses") _dataStatus.value = StoreDataStatus.LOADING viewModelScope.launch { val res = authRepository.getAddressesByUserId(currentUser!!) if (res is Success) { _userAddresses.value = res.data ?: emptyList() _dataStatus.value = StoreDataStatus.DONE Log.d(TAG, "Getting Addresses: Success") } else { _userAddresses.value = emptyList() _dataStatus.value = StoreDataStatus.ERROR if (res is Error) Log.d(TAG, "Getting Addresses: Error Occurred, ${res.exception.message}") } } } fun getUserLikes() { Log.d(TAG, "Getting Likes") // _dataStatus.value = StoreDataStatus.LOADING viewModelScope.launch { val res = authRepository.getLikesByUserId(currentUser!!) if (res is Success) { val data = res.data ?: emptyList() if (data[0] != "") { _userLikes.value = data } else { _userLikes.value = emptyList() } _dataStatus.value = StoreDataStatus.DONE Log.d(TAG, "Getting Likes: Success") } else { _userLikes.value = emptyList() _dataStatus.value = StoreDataStatus.ERROR if (res is Error) Log.d(TAG, "Getting Likes: Error Occurred, ${res.exception.message}") } } } fun deleteAddress(addressId: String) { viewModelScope.launch { val delRes = async { authRepository.deleteAddressById(addressId, currentUser!!) } when (val res = delRes.await()) { is Success -> { Log.d(TAG, "onDeleteAddress: Success") val addresses = _userAddresses.value?.toMutableList() addresses?.let { val pos = addresses.indexOfFirst { address -> address.addressId == addressId } if (pos >= 0) it.removeAt(pos) _userAddresses.value = it } } is Error -> Log.d(TAG, "onDeleteAddress: Error, ${res.exception}") else -> Log.d(TAG, "onDeleteAddress: Some error occurred!") } } } fun getItemsPriceTotal(): Double { var totalPrice = 0.0 _priceList.value?.forEach { (itemId, price) -> totalPrice += price * (_cartItems.value?.find { it.itemId == itemId }?.quantity ?: 1) } return totalPrice } fun toggleLikeProduct(productId: String) { Log.d(TAG, "toggling Like") viewModelScope.launch { // _dataStatus.value = StoreDataStatus.LOADING val isLiked = _userLikes.value?.contains(productId) == true val allLikes = _userLikes.value?.toMutableList() ?: mutableListOf() val deferredRes = async { if (isLiked) { authRepository.removeProductFromLikes(productId, currentUser!!) } else { authRepository.insertProductToLikes(productId, currentUser!!) } } val res = deferredRes.await() if (res is Success) { if (isLiked) { allLikes.remove(productId) } else { allLikes.add(productId) } _userLikes.value = allLikes _dataStatus.value = StoreDataStatus.DONE } else { _dataStatus.value = StoreDataStatus.ERROR if (res is Error) Log.d(TAG, "onUpdateQuantity: Error Occurred: ${res.exception.message}") } } } fun getItemsCount(): Int { var totalCount = 0 _cartItems.value?.forEach { totalCount += it.quantity } return totalCount } fun setQuantityOfItem(itemId: String, value: Int) { viewModelScope.launch { // _dataStatus.value = StoreDataStatus.LOADING var cartList: MutableList _cartItems.value?.let { items -> val item = items.find { it.itemId == itemId } val itemPos = items.indexOfFirst { it.itemId == itemId } cartList = items.toMutableList() if (item != null) { item.quantity = item.quantity + value val deferredRes = async { authRepository.updateCartItemByUserId(item, currentUser!!) } val res = deferredRes.await() if (res is Success) { cartList[itemPos] = item _cartItems.value = cartList _dataStatus.value = StoreDataStatus.DONE } else { _dataStatus.value = StoreDataStatus.ERROR if (res is Error) Log.d(TAG, "onUpdateQuantity: Error Occurred: ${res.exception.message}") } } } } } fun deleteItemFromCart(itemId: String) { viewModelScope.launch { // _dataStatus.value = StoreDataStatus.LOADING var cartList: MutableList _cartItems.value?.let { items -> val itemPos = items.indexOfFirst { it.itemId == itemId } cartList = items.toMutableList() val deferredRes = async { authRepository.deleteCartItemByUserId(itemId, currentUser!!) } val res = deferredRes.await() if (res is Success) { cartList.removeAt(itemPos) _cartItems.value = cartList val priceRes = async { getAllProductsInCart() } priceRes.await() } else { _dataStatus.value = StoreDataStatus.ERROR if (res is Error) Log.d(TAG, "onUpdateQuantity: Error Occurred: ${res.exception.message}") } } } } fun setSelectedAddress(addressId: String) { _selectedAddress.value = addressId } fun setSelectedPaymentMethod(method: String) { _selectedPaymentMethod.value = method } fun finalizeOrder() { _orderStatus.value = StoreDataStatus.LOADING val deliveryAddress = _userAddresses.value?.find { it.addressId == _selectedAddress.value } val paymentMethod = _selectedPaymentMethod.value val currDate = Date() val orderId = getRandomString(6, currDate.time.toString(), 1) val items = _cartItems.value val itemPrices = _priceList.value val shippingCharges = 0.0 if (deliveryAddress != null && paymentMethod != null && !items.isNullOrEmpty() && !itemPrices.isNullOrEmpty()) { val newOrder = UserData.OrderItem( orderId, currentUser!!, items, itemPrices, deliveryAddress, shippingCharges, paymentMethod, currDate, ) newOrderData.value = newOrder insertOrder() } else { Log.d(TAG, "orFinalizeOrder: Error, data null or empty") _orderStatus.value = StoreDataStatus.ERROR } } private fun insertOrder() { viewModelScope.launch { if (newOrderData.value != null) { _orderStatus.value = StoreDataStatus.LOADING val deferredRes = async { authRepository.placeOrder(newOrderData.value!!, currentUser!!) } val res = deferredRes.await() if (res is Success) { Log.d(TAG, "onInsertOrder: Success") _cartItems.value = emptyList() _cartProducts.value = emptyList() _priceList.value = emptyMap() _orderStatus.value = StoreDataStatus.DONE } else { _orderStatus.value = StoreDataStatus.ERROR if (res is Error) { Log.d(TAG, "onInsertOrder: Error, ${res.exception}") } } } else { Log.d(TAG, "orInsertOrder: Error, newProduct Null") _orderStatus.value = StoreDataStatus.ERROR } } } private suspend fun getAllProductsInCart() { viewModelScope.launch { // _dataStatus.value = StoreDataStatus.LOADING val priceMap = mutableMapOf() val proList = mutableListOf() var res = true _cartItems.value?.let { itemList -> itemList.forEach label@{ item -> val productDeferredRes = async { productsRepository.getProductById(item.productId, true) } val proRes = productDeferredRes.await() if (proRes is Success) { val proData = proRes.data proList.add(proData) priceMap[item.itemId] = proData.price } else { res = false return@label } } } _priceList.value = priceMap _cartProducts.value = proList if (!res) { _dataStatus.value = StoreDataStatus.ERROR } else { _dataStatus.value = StoreDataStatus.DONE } } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/viewModels/OtpViewModel.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import android.app.Application import android.util.Log import androidx.fragment.app.FragmentActivity import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.google.firebase.FirebaseException import com.google.firebase.FirebaseTooManyRequestsException import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.PhoneAuthCredential import com.google.firebase.auth.PhoneAuthOptions import com.google.firebase.auth.PhoneAuthProvider import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.shouldBypassOTPValidation import com.vishalgaur.shoppingapp.ui.OTPStatus import kotlinx.coroutines.launch import java.util.concurrent.TimeUnit private const val TAG = "OtpViewModel" class OtpViewModel(application: Application, private val uData: UserData) : AndroidViewModel(application) { private val _otpStatus = MutableLiveData() val otpStatus: LiveData get() = _otpStatus private val _isOTPSent = MutableLiveData() val isOTPSent: LiveData get() = _isOTPSent private val authRepository = (application as ShoppingApplication).authRepository var isUserLoggedIn = MutableLiveData(false) var storedVerificationId: String? = "" private var verificationInProgress = false private lateinit var resendToken: PhoneAuthProvider.ForceResendingToken init { _isOTPSent.value = false } fun verifyOTP(otp: String) { viewModelScope.launch { verifyPhoneWithCode(storedVerificationId!!, otp, isUserLoggedIn) } } fun signUp() { viewModelScope.launch { authRepository.signUp(uData) } } fun login(rememberMe: Boolean) { viewModelScope.launch { authRepository.login(uData, rememberMe) } } fun verifyPhoneOTPStart(phoneNumber: String, activity: FragmentActivity) { val options = PhoneAuthOptions.newBuilder(authRepository.getFirebaseAuth()) .setPhoneNumber(phoneNumber) // Phone number to verify .setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit .setActivity(activity) // Activity (for callback binding) .setCallbacks(callbacks) // OnVerificationStateChangedCallbacks .build() PhoneAuthProvider.verifyPhoneNumber(options) verificationInProgress = true } fun manuallyOverrideVerification() { if (shouldBypassOTPValidation()) isUserLoggedIn.value = true } private val callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() { override fun onVerificationCompleted(credential: PhoneAuthCredential) { Log.d(TAG, "onVerificationCompleted:$credential") _otpStatus.value = OTPStatus.CORRECT authRepository.signInWithPhoneAuthCredential(credential, isUserLoggedIn, application.applicationContext) } override fun onVerificationFailed(e: FirebaseException) { Log.w(TAG, "onVerificationFailed", e) _otpStatus.value = OTPStatus.INVALID_REQ if (e is FirebaseAuthInvalidCredentialsException) { Log.w(TAG, "onVerificationFailed, invalid request, ", e) } else if (e is FirebaseTooManyRequestsException) { Log.w(TAG, "onVerificationFailed, sms quota exceeded, ", e) } } override fun onCodeSent( verificationId: String, token: PhoneAuthProvider.ForceResendingToken ) { // Save verification ID and resending token so we can use them later storedVerificationId = verificationId resendToken = token Log.w(TAG, "OTP SENT") _isOTPSent.value = true _otpStatus.value = OTPStatus.NONE } } private fun verifyPhoneWithCode(verificationId: String, code: String, isUserLoggedIn: MutableLiveData) { try { val credential = PhoneAuthProvider.getCredential(verificationId, code) authRepository.signInWithPhoneAuthCredential(credential, isUserLoggedIn, getApplication().applicationContext) } catch (e: Exception) { Log.d(TAG, "onVerifyWithCode: Exception Occurred: ${e.message}") _otpStatus.value = OTPStatus.INVALID_REQ } } } ================================================ FILE: app/src/main/java/com/vishalgaur/shoppingapp/viewModels/ProductViewModel.kt ================================================ package com.vishalgaur.shoppingapp.viewModels import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.vishalgaur.shoppingapp.ShoppingApplication import com.vishalgaur.shoppingapp.data.Product import com.vishalgaur.shoppingapp.data.Result.Error import com.vishalgaur.shoppingapp.data.Result.Success import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager import com.vishalgaur.shoppingapp.data.UserData import com.vishalgaur.shoppingapp.data.utils.AddObjectStatus import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus import com.vishalgaur.shoppingapp.ui.AddItemErrors import kotlinx.coroutines.async import kotlinx.coroutines.launch import java.util.* private const val TAG = "ProductViewModel" class ProductViewModel(private val productId: String, application: Application) : AndroidViewModel(application) { private val _productData = MutableLiveData() val productData: LiveData get() = _productData private val _dataStatus = MutableLiveData() val dataStatus: LiveData get() = _dataStatus private val _errorStatus = MutableLiveData>() val errorStatus: LiveData> get() = _errorStatus private val _addItemStatus = MutableLiveData() val addItemStatus: LiveData get() = _addItemStatus private val _isLiked = MutableLiveData() val isLiked: LiveData get() = _isLiked private val _isItemInCart = MutableLiveData() val isItemInCart: LiveData get() = _isItemInCart private val productsRepository =(application as ShoppingApplication).productsRepository private val authRepository = (application as ShoppingApplication).authRepository private val sessionManager = ShoppingAppSessionManager(application.applicationContext) private val currentUserId = sessionManager.getUserIdFromSession() init { _isLiked.value = false _errorStatus.value = emptyList() viewModelScope.launch { Log.d(TAG, "init: productId: $productId") getProductDetails() checkIfInCart() setLike() } } private fun getProductDetails() { viewModelScope.launch { _dataStatus.value = StoreDataStatus.LOADING try { Log.d(TAG, "getting product Data") val res = productsRepository.getProductById(productId) if (res is Success) { _productData.value = res.data _dataStatus.value = StoreDataStatus.DONE } else if (res is Error) { throw Exception("Error getting product") } } catch (e: Exception) { _productData.value = Product() _dataStatus.value = StoreDataStatus.ERROR } } } fun setLike() { viewModelScope.launch { val res = authRepository.getLikesByUserId(currentUserId!!) if (res is Success) { val userLikes = res.data ?: emptyList() _isLiked.value = userLikes.contains(productId) Log.d(TAG, "Setting Like: Success, value = ${_isLiked.value}, ${res.data?.size}") } else { if (res is Error) Log.d(TAG, "Getting Likes: Error Occurred, ${res.exception.message}") } } } fun toggleLikeProduct() { Log.d(TAG, "toggling Like") viewModelScope.launch { val deferredRes = async { if (_isLiked.value == true) { authRepository.removeProductFromLikes(productId, currentUserId!!) } else { authRepository.insertProductToLikes(productId, currentUserId!!) } } val res = deferredRes.await() if (res is Success) { _isLiked.value = !_isLiked.value!! } else{ if(res is Error) Log.d(TAG, "Error toggling like, ${res.exception}") } } } fun isSeller() = sessionManager.isUserSeller() fun checkIfInCart() { viewModelScope.launch { val deferredRes = async { authRepository.getUserData(currentUserId!!) } val userRes = deferredRes.await() if (userRes is Success) { val uData = userRes.data if (uData != null) { val cartList = uData.cart val idx = cartList.indexOfFirst { it.productId == productId } _isItemInCart.value = idx >= 0 Log.d(TAG, "Checking in Cart: Success, value = ${_isItemInCart.value}, ${cartList.size}") } else { _isItemInCart.value = false } } else { _isItemInCart.value = false } } } fun addToCart(size: Int?, color: String?) { val errList = mutableListOf() if (size == null) errList.add(AddItemErrors.ERROR_SIZE) if (color.isNullOrBlank()) errList.add(AddItemErrors.ERROR_COLOR) if (errList.isEmpty()) { val itemId = UUID.randomUUID().toString() val newItem = UserData.CartItem( itemId, productId, productData.value!!.owner, 1, color, size ) insertCartItem(newItem) } } private fun insertCartItem(item: UserData.CartItem) { viewModelScope.launch { _addItemStatus.value = AddObjectStatus.ADDING val deferredRes = async { authRepository.insertCartItemByUserId(item, currentUserId!!) } val res = deferredRes.await() if (res is Success) { Log.d(TAG, "onAddItem: Success") _addItemStatus.value = AddObjectStatus.DONE } else { _addItemStatus.value = AddObjectStatus.ERR_ADD if (res is Error) { Log.d(TAG, "onAddItem: Error, ${res.exception.message}") } } } } } ================================================ FILE: app/src/main/res/drawable/address_account_drawable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/avatar_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/bottom_nav_selector.xml ================================================ ================================================ FILE: app/src/main/res/drawable/btn_gradient.xml ================================================ ================================================ FILE: app/src/main/res/drawable/card_item_selector.xml ================================================ ================================================ FILE: app/src/main/res/drawable/color_radio_normal.xml ================================================ ================================================ FILE: app/src/main/res/drawable/color_radio_selected.xml ================================================ ================================================ FILE: app/src/main/res/drawable/color_radio_selector.xml ================================================ ================================================ FILE: app/src/main/res/drawable/dotted_line_drawable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/heart_icon_drawable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_48.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_add_shopping_cart_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_person_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_shopping_cart_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_cancel_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_chevron_left_48.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_delete_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_edit_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_favorite_filled_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_favorite_outlined_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_filled_check_circle_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_filled_library_books_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_filled_location_on_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_filled_logout_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_filled_person_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_filter_alt_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_menu_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_arrow_back_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_email_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_library_books_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_phone_android_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outlined_home_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outlined_person_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outlined_shopping_cart_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_remove_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_remove_shopping_cart_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_search_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/layout_background_rounded_corners.xml ================================================ ================================================ FILE: app/src/main/res/drawable/liked_heart_drawable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/login_bg_img.xml ================================================ ================================================ FILE: app/src/main/res/drawable/orders_account_drawable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/person_account_drawable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/radio_normal.xml ================================================ ================================================ FILE: app/src/main/res/drawable/radio_selected.xml ================================================ ================================================ FILE: app/src/main/res/drawable/radio_selector.xml ================================================ ================================================ FILE: app/src/main/res/drawable/round_button.xml ================================================ ================================================ FILE: app/src/main/res/drawable/round_outline_rect.xml ================================================ ================================================ FILE: app/src/main/res/drawable/signout_account_drawable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/sl_favourite_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/font/nunito_sans.xml ================================================ ================================================ FILE: app/src/main/res/font/nunito_sans_extrabold.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_launch.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_otp.xml ================================================