<repo_name>SARATH365/shopping-android-app<file_sep>/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<UserData.CartItem>,
	products: List<Product>, userLikes: List<String>
) : RecyclerView.Adapter<CartItemAdapter.ViewHolder>() {

	lateinit var onClickListener: OnClickListener
	var data: List<UserData.CartItem> = items
	var proList: List<Product> = products
	var likesList: List<String> = 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_sep>/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_sep>/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<Boolean>
	suspend fun removeProductFromLikes(productId: String, userId: String): Result<Boolean>
	suspend fun insertAddress(newAddress: UserData.Address, userId: String): Result<Boolean>
	suspend fun updateAddress(newAddress: UserData.Address, userId: String): Result<Boolean>
	suspend fun deleteAddressById(addressId: String, userId: String): Result<Boolean>
	suspend fun insertCartItemByUserId(cartItem: UserData.CartItem, userId: String): Result<Boolean>
	suspend fun updateCartItemByUserId(cartItem: UserData.CartItem, userId: String): Result<Boolean>
	suspend fun deleteCartItemByUserId(itemId: String, userId: String): Result<Boolean>
	suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String): Result<Boolean>
	suspend fun setStatusOfOrder(orderId: String, userId: String, status: String): Result<Boolean>
	suspend fun getOrdersByUserId(userId: String): Result<List<UserData.OrderItem>?>
	suspend fun getAddressesByUserId(userId: String): Result<List<UserData.Address>?>
	suspend fun getLikesByUserId(userId: String): Result<List<String>?>
	suspend fun getUserData(userId: String): Result<UserData?>
	fun getFirebaseAuth(): FirebaseAuth
	fun signInWithPhoneAuthCredential(
		credential: PhoneAuthCredential,
		isUserLoggedIn: MutableLiveData<Boolean>,
		context: Context
	)

	fun isRememberMeOn(): Boolean
}
<file_sep>/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<ProductDetailsFragment>
	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<ShoppingApplication>()

	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",
		"<EMAIL>",
		"dh94328hd",
		ArrayList(),
		ArrayList(),
		ArrayList(),
		"CUSTOMER"
	)
	private val userSeller = UserData(
		"user1234selller",
		"<NAME>",
		"+919999988888",
		"<EMAIL>",
		"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_sep>/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<UserData?>

	fun updateEmailsAndMobiles(email: String, mobile: String) {}

	suspend fun getEmailsAndMobiles(): EmailMobileData? {
		return null
	}

	suspend fun getUserByMobileAndPassword(
		mobile: String,
		password: String
	): MutableList<UserData> {
		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<List<UserData.OrderItem>?>

	suspend fun getAddressesByUserId(userId: String): Result<List<UserData.Address>?>

	suspend fun getLikesByUserId(userId: String): Result<List<String>?>
}<file_sep>/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<AddEditProductViewModel>()
	private val focusChangeListener = MyOnFocusChangeListener()

	// arguments
	private var isEdit by Delegates.notNull<Boolean>()
	private lateinit var catName: String
	private lateinit var productId: String

	private var sizeList = mutableSetOf<Int>()
	private var colorsList = mutableSetOf<String>()
	private var imgList = mutableListOf<Uri>()

	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<Uri>
			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<Int>? = 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<String>? = 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_sep>/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_sep>/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.android.parcel.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<String> = ArrayList(),
	@TypeConverters(ObjectListTypeConvertor::class)
	var addresses: List<Address> = ArrayList(),
	@TypeConverters(ObjectListTypeConvertor::class)
	var cart: List<CartItem> = ArrayList(),
	@TypeConverters(ObjectListTypeConvertor::class)
	var orders: List<OrderItem> = ArrayList(),
	var userType: String = UserType.CUSTOMER.name
) : Parcelable {
	fun toHashMap(): HashMap<String, Any> {
		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<CartItem> = ArrayList(),
		var itemsPrices: Map<String, Double> = 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<String, Any> {
			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<String, String> {
			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<String, Any> {
			val hashMap = hashMapOf<String, Any>()
			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_sep>/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",
			"<EMAIL>",
			"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",
			"<EMAIL>",
			"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_sep>/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"
}


<file_sep>/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<HomeFragment>
	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<ShoppingApplication>()

	private val userCustomer = UserData(
		"sdjm43892yfh948ehod",
		"Vishal",
		"+919999988888",
		"<EMAIL>",
		"dh94328hd",
		ArrayList(),
		ArrayList(),
		ArrayList(),
		"CUSTOMER"
	)
	private val userSeller = UserData(
		"user1234selller",
		"<NAME>",
		"+919999988888",
		"<EMAIL>",
		"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<ProductAdapter.ItemViewHolder>(
					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<ProductAdapter.ItemViewHolder>(
					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_sep>/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<UserData?> =
		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<List<UserData.OrderItem>?> =
		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<List<UserData.Address>?> =
		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<List<String>?> =
		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_sep>/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<String, String> {
	val result = mutableMapOf<String, String>()
	val isoCountries = Locale.getISOCountries()
	val countriesList = isoCountries.map { isoCountry ->
		result[isoCountry] = Locale("", isoCountry).displayCountry
	}
	return result
}<file_sep>/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 <T : ViewModel?> create(modelClass: Class<T>): 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<AddItemErrors>) {
		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_sep>/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",
		"<EMAIL>",
		"dh94328hd",
		ArrayList(),
		ArrayList(),
		ArrayList()
	)

	@get:Rule
	var instantTaskExecutorRule = InstantTaskExecutorRule()

	@Before
	fun setUp() {
		val context = ApplicationProvider.getApplicationContext<ShoppingApplication>()
		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_sep>/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",
		"<EMAIL>",
		"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_sep>/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<Any>, userLikes: List<String>, private val context: Context) :
	RecyclerView.Adapter<RecyclerView.ViewHolder>() {

	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_sep>/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_sep>/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<FragmentSignupBinding>() {

	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, <PASSWORD>, 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_sep>/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<LoginFragment>
	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("<PASSWORD>")
	}

	@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("<PASSWORD>")
		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("<PASSWORD>")
		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_sep>/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 itd 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) |

## Built With
- Kotlin
- Firebase
- Room
- Material
- Glide



---

<p align="center"> Made with :blue_heart: by <a href="https://github.com/i-vishi"><NAME></a></p><file_sep>/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<String, Product> = LinkedHashMap()
	private val imagesStorage = mutableListOf<String>()
	private val observableProducts = MutableLiveData<Result<List<Product>>>()

	override suspend fun refreshProducts(): StoreDataStatus {
		observableProducts.value = Success(productsServiceData.values.toList())
		return StoreDataStatus.DONE
	}

	override fun observeProducts(): LiveData<Result<List<Product>>?> {
		runBlocking { refreshProducts() }
		return observableProducts
	}

	override fun observeProductsByOwner(ownerId: String): LiveData<Result<List<Product>>?> {
		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<List<Product>> {
		productsServiceData.values.let { pros ->
			val res = pros.filter { it.owner == ownerId }
			return Success(res)
		}
	}

	override suspend fun getProductById(productId: String, forceUpdate: Boolean): Result<Product> {
		productsServiceData[productId]?.let {
			return Success(it)
		}
		return Error(Exception("Product Not Found!"))
	}

	override suspend fun insertProduct(newProduct: Product): Result<Boolean> {
		productsServiceData[newProduct.productId] = newProduct
		return Success(true)
	}

	override suspend fun insertImages(imgList: List<Uri>): List<String> {
		val result = mutableListOf<String>()
		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<Boolean> {
		productsServiceData[product.productId] = product
		return Success(true)
	}

	override suspend fun updateImages(newList: List<Uri>, oldList: List<String>): List<String> {
		val urlList = mutableListOf<String>()
		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<Boolean> {
		productsServiceData.remove(productId)
		refreshProducts()
		return Success(true)
	}
}<file_sep>/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<String> {
		return value.split(",").map { it }
	}

	@TypeConverter
	fun fromStringListToString(value: List<String>): String {
		return value.joinToString(separator = ",")
	}

	@TypeConverter
	fun fromStringToIntegerList(value: String): List<Int> {
		return value.split(",").map { it.toInt() }
	}

	@TypeConverter
	fun fromIntegerListToString(value: List<Int>): String {
		return value.joinToString(separator = ",")
	}
}<file_sep>/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<View?>): Matcher<View?> {
	return object : BoundedMatcher<View?, RecyclerView>(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<View>? {
		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_sep>/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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<List<UserData.Address>?> {
		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<List<String>?> {
		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<UserData?> {
		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<Boolean>,
		context: Context
	) {
		// no implementation
	}

	override fun isRememberMeOn(): Boolean {
		// no implementation
		return true
	}
}<file_sep>/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<FragmentLoginBinding>() {
	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_sep>/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<String> {
		return listOf("UPI", "Debit Card", "Cash On Delivery")
	}
}<file_sep>/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_sep>/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<String>()
	val selectedCategory: LiveData<String> get() = _selectedCategory

	private val _productId = MutableLiveData<String>()
	val productId: LiveData<String> get() = _productId

	private val _isEdit = MutableLiveData<Boolean>()
	val isEdit: LiveData<Boolean> get() = _isEdit

	private val _errorStatus = MutableLiveData<AddProductViewErrors>()
	val errorStatus: LiveData<AddProductViewErrors> get() = _errorStatus

	private val _dataStatus = MutableLiveData<StoreDataStatus>()
	val dataStatus: LiveData<StoreDataStatus> get() = _dataStatus

	private val _addProductErrors = MutableLiveData<AddProductErrors?>()
	val addProductErrors: LiveData<AddProductErrors?> get() = _addProductErrors

	private val _productData = MutableLiveData<Product>()
	val productData: LiveData<Product> get() = _productData

	@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
	val newProductData = MutableLiveData<Product>()

	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<Int>,
		colors: List<String>,
		imgList: List<Uri>,
	) {
		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<Uri>) {
		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<Uri>) {
		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_sep>/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.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 <T : ViewModel?> create(modelClass: Class<T>): 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)

			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
				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_sep>/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 }

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 <T> 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 <T> 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_sep>/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.android.parcel.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<Int> = ArrayList(),
	var availableColors: List<String> = ArrayList(),
	var images: List<String> = ArrayList(),
	var rating: Double = 0.0
) : Parcelable {
	fun toHashMap(): HashMap<String, Any> {
		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_sep>/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_sep>/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_sep>/app/src/test/java/com/vishalgaur/shoppingapp/UtilsTest.kt
package com.vishalgaur.shoppingapp

import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertEquals
import org.junit.Test

class UtilsTest {
	@Test
	fun checkEmail_empty_returnsFalse() {
		val email = ""
		val result = isEmailValid(email)

		assertEquals(result, false)
	}

	@Test
	fun checkEmail_invalid_returnsFalse() {
		val email1 = "vishalgaur"
		val email2 = "vishalgaur.com"
		val email3 = "<EMAIL>"
		val result1 = isEmailValid(email1)
		val result2 = isEmailValid(email2)
		val result3 = isEmailValid(email3)

		assertEquals(result1, false)
		assertEquals(result2, false)
		assertEquals(result3, false)
	}

	@Test
	fun checkEmail_valid_returnsTrue() {
		val email1 = "    <EMAIL>"
		val email2 = "<EMAIL>     "
		val email3 = "<EMAIL>"
		val result1 = isEmailValid(email1)
		val result2 = isEmailValid(email2)
		val result3 = isEmailValid(email3)

		assertEquals(result1, true)
		assertEquals(result2, true)
		assertEquals(result3, true)
	}

	@Test
	fun checkPhone_empty_returnsFalse() {
		val phone = ""
		val result = isPhoneValid(phone)

		assertEquals(result, false)
	}

	@Test
	fun checkPhone_invalid_returnsFalse() {
		val phone1 = "1968743574694865"
		val phone2 = "  1111  "
		val phone3 = "2454678910"
		val result1 = isPhoneValid(phone1)
		val result2 = isPhoneValid(phone2)
		val result3 = isPhoneValid(phone3)

		assertEquals(result1, false)
		assertEquals(result2, false)
		assertEquals(result3, false)
	}

	@Test
	fun checkPhone_valid_returnsTrue() {
		val phone1 = "9876543210"
		val phone2 = "    6985741526"
		val phone3 = "8989895858   "
		val result1 = isPhoneValid(phone1)
		val result2 = isPhoneValid(phone2)
		val result3 = isPhoneValid(phone3)

		assertEquals(result1, true)
		assertEquals(result2, true)
		assertEquals(result3, true)
	}

	@Test
	fun getRandomString_hasExpectedLength() {
		val result1 = getRandomString(10, "odweuih", 10)
		val result2 = getRandomString(10, "", 5)
		assertThat(result1.length, `is`(27))
		assertThat(result2.length, `is`(15))
	}

	@Test
	fun getProductId_hasExpectedSubStrings() {
		val result = getProductId("soewifnc9we48yf0", "shoes")
		val subs = result.split("-")
		assert(subs.size >= 3)
	}

	@Test
	fun getOfferPercentage_zeroZero_returnsZero() {
		val result = getOfferPercentage(0.0, 0.0)
		assertThat(result, `is`(0))
	}

	@Test
	fun getOfferPercentage_highLow_returnsZero() {
		val result = getOfferPercentage(90.0, 100.0)
		assertThat(result, `is`(0))
	}

	@Test
	fun getOfferPercentage_lowHigh_returnsOfferInt() {
		val result1 = getOfferPercentage(100.0, 90.5)
		val result2 = getOfferPercentage(100.0, 90.75)
		val result3 = getOfferPercentage(100.0, 90.25)
		assertThat(result1, `is`(10))
		assertThat(result2, `is`(9))
		assertThat(result3, `is`(10))
	}

	@Test
	fun getOfferPercentage_equal_returnsZero() {
		val result = getOfferPercentage(90.5, 90.5)
		assertThat(result, `is`(0))
	}
}<file_sep>/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<Result<List<Product>>?>()

	private fun storageRef() = firebaseStorage.reference
	private fun productsCollectionRef() = firebaseDb.collection(PRODUCT_COLLECTION)

	override suspend fun refreshProducts() {
		observableProducts.value = getAllProducts()
	}

	override fun observeProducts(): LiveData<Result<List<Product>>?> {
		return observableProducts
	}

	override suspend fun getAllProducts(): Result<List<Product>> {
		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<Product> {
		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_sep>/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",
		"<EMAIL>",
		"12345",
		emptyList(),
		emptyList(),
		emptyList(),
		"SELLER",
	)
	private val userCustomer = UserData(
		"dwoeihwjklvn48329752",
		"<NAME>",
		"+919090909090",
		"<EMAIL>",
		"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("<EMAIL>", "+919999988888")
		runOnUiThread {
			runBlockingTest {
				val result =
					authRepository.checkEmailAndMobile("<EMAIL>", "+919685", context)
				assertThat(result, `is`(SignUpErrors.SERR))
			}
		}
	}

	@Test
	fun checkEmailAndMobile_existingMobile_returnsError() {
		authRemoteDataSource.updateEmailsAndMobiles("<EMAIL>", "+919999988888")
		runOnUiThread {
			runBlockingTest {
				val result =
					authRepository.checkEmailAndMobile("<EMAIL>", "+919999988888", context)
				assertThat(result, `is`(SignUpErrors.SERR))
			}
		}
	}

	@Test
	fun checkEmailAndMobile_existingMobileAndEmail_returnsError() {
		authRemoteDataSource.updateEmailsAndMobiles("<EMAIL>", "+919999988888")
		runOnUiThread {
			runBlockingTest {
				val result =
					authRepository.checkEmailAndMobile("<EMAIL>", "+919999988888", context)
				assertThat(result, `is`(SignUpErrors.SERR))
			}
		}
	}

	@Test
	fun checkEmailAndMobile_newData_returnsError() {
		authRemoteDataSource.updateEmailsAndMobiles("<EMAIL>", "+919999988888")
		runOnUiThread {
			runBlockingTest {
				val result =
					authRepository.checkEmailAndMobile(
						"<EMAIL>",
						"+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_sep>/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<Boolean>()
	val isEdit: LiveData<Boolean> get() = _isEdit

	private val _addressId = MutableLiveData<String>()
	val addressId: LiveData<String> get() = _addressId

	private val _dataStatus = MutableLiveData<StoreDataStatus>()
	val dataStatus: LiveData<StoreDataStatus> get() = _dataStatus

	private val _errorStatus = MutableLiveData<List<AddAddressViewErrors>>()
	val errorStatus: LiveData<List<AddAddressViewErrors>> get() = _errorStatus

	private val _addAddressStatus = MutableLiveData<AddObjectStatus?>()
	val addAddressStatus: LiveData<AddObjectStatus?> get() = _addAddressStatus

	private val _addressData = MutableLiveData<UserData.Address>()
	val addressData: LiveData<UserData.Address> get() = _addressData

	@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
	val newAddressData = MutableLiveData<UserData.Address>()

	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<AddAddressViewErrors>()
		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_sep>/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<UserData.CartItem>?) {
		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<PriceCardAdapter.ViewHolder>() {

		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_sep>/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_sep>/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<UserData?> {
		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<List<UserData.OrderItem>?> {
		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<List<UserData.Address>?> {
		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<List<String>?> {
		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<UserData> =
		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<String, MutableList<UserData.CartItem>> = 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<String, Double>()
				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<UserData.CartItem>())
		}
	}

	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_sep>/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 = "<EMAIL>"
		val pwd1 = "<PASSWORD>"
		val pwd2 = "<PASSWORD>"
		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 = "<NAME>"
		val mobile = "9988665555"
		val email = "<EMAIL>"
		val pwd1 = "<PASSWORD>"
		val pwd2 = "<PASSWORD>"
		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 = "<NAME>"
		val mobile = "9988665555"
		val email = "<EMAIL>"
		val pwd1 = "<PASSWORD>"
		val pwd2 = "<PASSWORD>"
		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 = "<NAME>"
		val mobile = "9988665fng55"
		val email = "<EMAIL>"
		val pwd1 = "<PASSWORD>"
		val pwd2 = "<PASSWORD>"
		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 = "<NAME>"
		val mobile = "9988665fng55"
		val email = "<EMAIL>"
		val pwd1 = "<PASSWORD>"
		val pwd2 = "<PASSWORD>"
		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 = "   <NAME>"
		val mobile = "   9988665755"
		val email = "<EMAIL>    "
		val pwd1 = "<PASSWORD>"
		val pwd2 = "<PASSWORD>"
		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_sep>/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_sep>/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_sep>/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.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<OTPStatus>()
	val otpStatus: LiveData<OTPStatus> get() = _otpStatus

	private val _isOTPSent = MutableLiveData<Boolean>()
	val isOTPSent: LiveData<Boolean> 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
	}

	private val callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {

		override fun onVerificationCompleted(credential: PhoneAuthCredential) {
			Log.d(TAG, "onVerificationCompleted:$credential")
			authRepository.signInWithPhoneAuthCredential(credential, isUserLoggedIn, application.applicationContext)
		}

		override fun onVerificationFailed(e: FirebaseException) {
			Log.w(TAG, "onVerificationFailed", e)

			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
		}
	}

	private fun verifyPhoneWithCode(verificationId: String, code: String, isUserLoggedIn: MutableLiveData<Boolean>) {
		try {
			val credential = PhoneAuthProvider.getCredential(verificationId, code)
			authRepository.signInWithPhoneAuthCredential(credential, isUserLoggedIn, getApplication<ShoppingApplication>().applicationContext)
		} catch (e: Exception) {
			Log.d(TAG, "onVerifyWithCode: Exception Occurred: ${e.message}")
		}
	}
}<file_sep>/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<Product>? = mutableListOf()) :
	ProductDataSource {
	private val imagesStorage = mutableListOf<String>()
	override fun observeProducts(): LiveData<Result<List<Product>>?> {
		products?.let { pros ->
			val res = MutableLiveData(pros)
			return Transformations.map(res) {
				Success(it.toList())
			}
		}
		return MutableLiveData(Error(Exception()))
	}

	override fun observeProductsByOwner(ownerId: String): LiveData<Result<List<Product>>?> {
		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<List<Product>> {
		products?.let {
			return Success(it)
		}
		return Error(Exception("Products Not Found"))
	}

	override suspend fun refreshProducts() {
		// No implementation
	}

	override suspend fun getProductById(productId: String): Result<Product> {
		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<List<Product>> {
		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<Product>) {
		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_sep>/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_sep>/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<UserData.Address> {
		if (data.isNullOrBlank()) {
			return emptyList()
		}
		val listType = object : TypeToken<List<UserData.Address>>() {}.type
		val gson = Gson()
		return gson.fromJson(data, listType)
	}

	@TypeConverter
	fun addressObjectListToString(addressList: List<UserData.Address>): String {
		if (addressList.isEmpty()) {
			return ""
		}
		val gson = Gson()
		val listType = object : TypeToken<List<UserData.Address>>() {}.type
		return gson.toJson(addressList, listType)
	}

	@TypeConverter
	fun stringToCartObjectList(data: String?): List<UserData.CartItem> {
		if (data.isNullOrBlank()) {
			return emptyList()
		}
		val listType = object : TypeToken<List<UserData.CartItem>>() {}.type
		val gson = Gson()
		return gson.fromJson(data, listType)
	}

	@TypeConverter
	fun cartObjectListToString(cartList: List<UserData.CartItem>): String {
		if (cartList.isEmpty()) {
			return ""
		}
		val gson = Gson()
		val listType = object : TypeToken<List<UserData.CartItem>>() {}.type
		return gson.toJson(cartList, listType)
	}

	@TypeConverter
	fun stringToOrderObjectList(data: String?): List<UserData.OrderItem> {
		if (data.isNullOrBlank()) {
			return emptyList()
		}
		val listType = object : TypeToken<List<UserData.OrderItem>>() {}.type
		val gson = Gson()
		return gson.fromJson(data, listType)
	}

	@TypeConverter
	fun orderObjectListToString(orderList: List<UserData.OrderItem>): String {
		if (orderList.isEmpty()) {
			return ""
		}
		val gson = Gson()
		val listType = object : TypeToken<List<UserData.OrderItem>>() {}.type
		return gson.toJson(orderList, listType)
	}
}<file_sep>/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<SignupFragment>
	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("<NAME>  ")
	}

	@Test
	fun userCanEnterMobile() {
		insertInMobileEditText("8976527465")
	}

	@Test
	fun userCanEnterEmail() {
		insertInEmailEditText("  <EMAIL>")
	}

	@Test
	fun userCanEnterPassword() {
		insertInPwdEditText("<PASSWORD>")
	}

	@Test
	fun userCanEnterInCnfPassword() {
		insertInCnfPwdEditText("<PASSWORD>")
	}

	@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("<NAME> ")
		insertInMobileEditText("8976527465  ")
		insertInEmailEditText("  <EMAIL>")
		insertInPwdEditText("<PASSWORD>")
		insertInCnfPwdEditText("<PASSWORD>")
		clickTermsSwitch()
		clickSignUpButton()

		onView(withId(R.id.signup_email_edit_text)).check(matches(hasErrorText(`is`(EMAIL_ERROR_TEXT))))
	}

	@Test
	fun onSignUp_invalidMobile_showsMobileError() {
		insertInNameEditText("<NAME> ")
		insertInMobileEditText("86527465  ")
		insertInEmailEditText("  <EMAIL>")
		insertInPwdEditText("<PASSWORD>")
		insertInCnfPwdEditText("<PASSWORD>")
		clickTermsSwitch()
		clickSignUpButton()

		onView(withId(R.id.signup_mobile_edit_text)).check(matches(hasErrorText(`is`(MOB_ERROR_TEXT))))
	}

	@Test
	fun onSignUp_notAcceptedTerms_showsError() {
		insertInNameEditText("<NAME> ")
		insertInMobileEditText("8652744565  ")
		insertInEmailEditText("  <EMAIL>")
		insertInPwdEditText("<PASSWORD>")
		insertInCnfPwdEditText("<PASSWORD>")
		clickSignUpButton()

		onView(withId(R.id.signup_error_text_view)).check(matches(withText("Accept the Terms.")))
	}

	@Test
	fun onSignUp_notSamePasswords_showsError() {
		insertInNameEditText("<NAME> ")
		insertInMobileEditText("8652744565  ")
		insertInEmailEditText("  <EMAIL>")
		insertInPwdEditText("<PASSWORD>")
		insertInCnfPwdEditText("<PASSWORD>")
		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("<NAME> ")
		insertInMobileEditText("8652744565  ")
		insertInEmailEditText("  <EMAIL>")
		insertInPwdEditText("<PASSWORD>")
		insertInCnfPwdEditText("<PASSWORD>")
		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_sep>/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<UserData.Address>,
	private val isSelect: Boolean
) :
	RecyclerView.Adapter<AddressAdapter.ViewHolder>() {

	lateinit var onClickListener: OnClickListener
	var data: List<UserData.Address> = 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_sep>/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<List<Product>>()
	val products: LiveData<List<Product>> get() = _products

	private lateinit var _allProducts: MutableLiveData<List<Product>>
	val allProducts: LiveData<List<Product>> get() = _allProducts

	private var _userProducts = MutableLiveData<List<Product>>()
	val userProducts: LiveData<List<Product>> get() = _userProducts

	private var _userOrders = MutableLiveData<List<UserData.OrderItem>>()
	val userOrders: LiveData<List<UserData.OrderItem>> get() = _userOrders

	private var _userAddresses = MutableLiveData<List<UserData.Address>>()
	val userAddresses: LiveData<List<UserData.Address>> get() = _userAddresses

	private var _selectedOrder = MutableLiveData<UserData.OrderItem?>()
	val selectedOrder: LiveData<UserData.OrderItem?> get() = _selectedOrder

	private var _orderProducts = MutableLiveData<List<Product>>()
	val orderProducts: LiveData<List<Product>> get() = _orderProducts

	private var _likedProducts = MutableLiveData<List<Product>>()
	val likedProducts: LiveData<List<Product>> get() = _likedProducts

	private var _userLikes = MutableLiveData<List<String>>()
	val userLikes: LiveData<List<String>> get() = _userLikes

	private var _filterCategory = MutableLiveData("All")
	val filterCategory: LiveData<String> get() = _filterCategory

	private val _storeDataStatus = MutableLiveData<StoreDataStatus>()
	val storeDataStatus: LiveData<StoreDataStatus> get() = _storeDataStatus

	private val _dataStatus = MutableLiveData<StoreDataStatus>()
	val dataStatus: LiveData<StoreDataStatus> get() = _dataStatus

	private val _userData = MutableLiveData<UserData?>()
	val userData: LiveData<UserData?> 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<List<Product>>
		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<Product> = 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<List<Product>?>?): LiveData<List<Product>> {
		val res = MutableLiveData<List<Product>>()
		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<List<Product>>
		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_sep>/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<UserData>()
	val userData: LiveData<UserData> get() = _userData

	private val _signErrorStatus = MutableLiveData<SignUpErrors?>()
	val signErrorStatus: LiveData<SignUpErrors?> get() = _signErrorStatus

	private val _errorStatus = MutableLiveData<SignUpViewErrors>()
	val errorStatus: LiveData<SignUpViewErrors> get() = _errorStatus

	private val _errorStatusLoginFragment = MutableLiveData<LoginViewErrors>()
	val errorStatusLoginFragment: LiveData<LoginViewErrors> get() = _errorStatusLoginFragment

	private val _loginErrorStatus = MutableLiveData<LogInErrors?>()
	val loginErrorStatus: LiveData<LogInErrors?> 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<ShoppingApplication>().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_sep>/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_sep>/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_sep>/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<Product>, private val context: Context) :
	RecyclerView.Adapter<LikedProductAdapter.ViewHolder>() {

	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_sep>/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<Result<List<Product>>?> {
		return try {
			Transformations.map(productsDao.observeProducts()) {
				Success(it)
			}
		} catch (e: Exception) {
			Transformations.map(MutableLiveData(e)) {
				Error(e)
			}
		}
	}

	override fun observeProductsByOwner(ownerId: String): LiveData<Result<List<Product>>?> {
		return try {
			Transformations.map(productsDao.observeProductsByOwner(ownerId)) {
				Success(it)
			}
		} catch (e: Exception) {
			Transformations.map(MutableLiveData(e)) {
				Error(e)
			}
		}
	}

	override suspend fun getAllProducts(): Result<List<Product>> = withContext(ioDispatcher) {
		return@withContext try {
			Success(productsDao.getAllProducts())
		} catch (e: Exception) {
			Error(e)
		}
	}

	override suspend fun getAllProductsByOwner(ownerId: String): Result<List<Product>> =
		withContext(ioDispatcher) {
			return@withContext try {
				Success(productsDao.getProductsByOwnerId(ownerId))
			} catch (e: Exception) {
				Error(e)
			}
		}

	override suspend fun getProductById(productId: String): Result<Product> =
		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<Product>) = 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_sep>/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<Result<List<Product>>?>

	suspend fun getAllProducts(): Result<List<Product>>

	suspend fun refreshProducts() {}

	suspend fun getProductById(productId: String): Result<Product>

	suspend fun insertProduct(newProduct: Product)

	suspend fun updateProduct(proData: Product)

	fun observeProductsByOwner(ownerId: String): LiveData<Result<List<Product>>?> {
		return MutableLiveData()
	}

	suspend fun getAllProductsByOwner(ownerId: String): Result<List<Product>> {
		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<Product>) {}
}<file_sep>/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_sep>/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<Product>)

	@Query("SELECT * FROM products")
	suspend fun getAllProducts(): List<Product>

	@Query("SELECT * FROM products")
	fun observeProducts(): LiveData<List<Product>>

	@Query("SELECT * FROM products WHERE owner = :ownerId")
	fun observeProductsByOwner(ownerId: String): LiveData<List<Product>>

	@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<Product>

	@Query("DELETE FROM products WHERE productId = :proId")
	suspend fun deleteProductById(proId: String): Int

	@Query("DELETE FROM products")
	suspend fun deleteAllProducts()
}<file_sep>/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<String>) :
	RecyclerView.Adapter<PayByAdapter.ViewHolder>() {

	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_sep>/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<String>) :
	RecyclerView.Adapter<ProductImagesAdapter.ViewHolder>() {

	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_sep>/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",
		"<EMAIL>",
		"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<ShoppingApplication>()
		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_sep>/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<UserData.CartItem>?) {
		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<String> = 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<UserData.CartItem>): Int {
		var totalCount = 0
		cartItems.forEach {
			totalCount += it.quantity
		}
		return totalCount
	}

	private fun getItemsPriceTotal(
		priceList: Map<String, Double>,
		cartItems: List<UserData.CartItem>
	): Double {
		var totalPrice = 0.0
		priceList.forEach { (itemId, price) ->
			totalPrice += price * (cartItems.find { it.itemId == itemId }?.quantity ?: 1)
		}
		return totalPrice
	}
}<file_sep>/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<AddEditAddressViewModel>()

	private var isEdit by Delegates.notNull<Boolean>()
	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<AddAddressViewErrors>) {
		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_sep>/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<UserData.OrderItem>, private val context: Context) :
	RecyclerView.Adapter<OrdersAdapter.ViewHolder>() {

	lateinit var onClickListener: OnClickListener
	var data: List<UserData.OrderItem> = 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_sep>/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<VBinding : ViewBinding> : 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_sep>/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
		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)
				}
			}
		}
		return sErr
	}

	override suspend fun checkLogin(mobile: String, password: String): UserData? {
		Log.d(TAG, "on Login: checking mobile and password")
		val queryResult =
			authRemoteDataSource.getUserByMobileAndPassword(mobile, password)
		return if (queryResult.size > 0) {
			queryResult[0]
		} else {
			null
		}
	}

	override fun signInWithPhoneAuthCredential(
		credential: PhoneAuthCredential,
		isUserLoggedIn: MutableLiveData<Boolean>, context: Context
	) {
		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)
					}

				}
			}
	}

	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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<Boolean> {
		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<List<UserData.OrderItem>?> {
		return userLocalDataSource.getOrdersByUserId(userId)
	}

	override suspend fun getAddressesByUserId(userId: String): Result<List<UserData.Address>?> {
		return userLocalDataSource.getAddressesByUserId(userId)
	}

	override suspend fun getLikesByUserId(userId: String): Result<List<String>?> {
		return userLocalDataSource.getLikesByUserId(userId)
	}

	override suspend fun getUserData(userId: String): Result<UserData?> {
		return userLocalDataSource.getUserById(userId)
	}

}<file_sep>/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<UserData?> {
		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<UserData> {
		val res = mutableListOf<UserData>()
		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<List<UserData.Address>?> {
		uData?.let {
			if (it.userId == userId) {
				return Success(it.addresses)
			}
		}
		return Error(Exception("User Not Found"))
	}

	override suspend fun getLikesByUserId(userId: String): Result<List<String>?> {
		uData?.let {
			if (it.userId == userId) {
				return Success(it.likes)
			}
		}
		return Error(Exception("User Not Found"))
	}
}<file_sep>/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<Uri>) :
	RecyclerView.Adapter<AddProductImagesAdapter.ViewHolder>() {

	private var data: MutableList<Uri> = images as MutableList<Uri>

	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_sep>/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<Result<List<Product>>?> {
		return productsLocalSource.observeProducts()
	}

	override fun observeProductsByOwner(ownerId: String): LiveData<Result<List<Product>>?> {
		return productsLocalSource.observeProductsByOwner(ownerId)
	}

	override suspend fun getAllProductsByOwner(ownerId: String): Result<List<Product>> {
		return productsLocalSource.getAllProductsByOwner(ownerId)
	}

	override suspend fun getProductById(productId: String, forceUpdate: Boolean): Result<Product> {
		if (forceUpdate) {
			updateProductFromRemoteSource(productId)
		}
		return productsLocalSource.getProductById(productId)
	}

	override suspend fun insertProduct(newProduct: Product): Result<Boolean> {
		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<Uri>): List<String> {
		var urlList = mutableListOf<String>()
		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<Boolean> {
		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<Uri>, oldList: List<String>): List<String> {
		var urlList = mutableListOf<String>()
		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<Boolean> {
		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_sep>/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<Product?>()
	val productData: LiveData<Product?> get() = _productData

	private val _dataStatus = MutableLiveData<StoreDataStatus>()
	val dataStatus: LiveData<StoreDataStatus> get() = _dataStatus

	private val _errorStatus = MutableLiveData<List<AddItemErrors>>()
	val errorStatus: LiveData<List<AddItemErrors>> get() = _errorStatus

	private val _addItemStatus = MutableLiveData<AddObjectStatus?>()
	val addItemStatus: LiveData<AddObjectStatus?> get() = _addItemStatus

	private val _isLiked = MutableLiveData<Boolean>()
	val isLiked: LiveData<Boolean> get() = _isLiked

	private val _isItemInCart = MutableLiveData<Boolean>()
	val isItemInCart: LiveData<Boolean> 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<AddItemErrors>()
		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_sep>/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<Product>?) {
		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<String>,
		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<Product>, adsList: List<Int>): List<Any> {
		val itemsList = mutableListOf<Any>()
		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<Int> {
		return listOf(R.drawable.ad_ex_2, R.drawable.ad_ex_1, R.drawable.ad_ex_3)
	}
}<file_sep>/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_sep>/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<String, String?> {
		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_sep>/app/src/main/java/com/vishalgaur/shoppingapp/data/utils/EmailMobileData.kt
package com.vishalgaur.shoppingapp.data.utils

data class EmailMobileData(
	val emails: ArrayList<String> = ArrayList(),
	val mobiles: ArrayList<String> = ArrayList()
)<file_sep>/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",
		"<NAME>",
		"+919999988888",
		"<EMAIL>",
		"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<Int>()
		val colors = emptyList<String>()
		val imgList = emptyList<Uri>()

		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_sep>/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<Result<List<Product>>?>
	fun observeProductsByOwner(ownerId: String): LiveData<Result<List<Product>>?>
	suspend fun getAllProductsByOwner(ownerId: String): Result<List<Product>>
	suspend fun getProductById(productId: String, forceUpdate: Boolean = false): Result<Product>
	suspend fun insertProduct(newProduct: Product): Result<Boolean>
	suspend fun insertImages(imgList: List<Uri>): List<String>
	suspend fun updateProduct(product: Product): Result<Boolean>
	suspend fun updateImages(newList: List<Uri>, oldList: List<String>): List<String>
	suspend fun deleteProductById(productId: String): Result<Boolean>
}<file_sep>/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_sep>/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_sep>/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_sep>/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<List<UserData.Address>>()
	val userAddresses: LiveData<List<UserData.Address>> get() = _userAddresses

	private val _userLikes = MutableLiveData<List<String>>()
	val userLikes: LiveData<List<String>> get() = _userLikes

	private val _cartItems = MutableLiveData<List<UserData.CartItem>>()
	val cartItems: LiveData<List<UserData.CartItem>> get() = _cartItems

	private val _priceList = MutableLiveData<Map<String, Double>>()
	val priceList: LiveData<Map<String, Double>> get() = _priceList

	private val _cartProducts = MutableLiveData<List<Product>>()
	val cartProducts: LiveData<List<Product>> get() = _cartProducts

	private val _dataStatus = MutableLiveData<StoreDataStatus>()
	val dataStatus: LiveData<StoreDataStatus> get() = _dataStatus

	private val _orderStatus = MutableLiveData<StoreDataStatus>()
	val orderStatus: LiveData<StoreDataStatus> get() = _orderStatus

	private val _selectedAddress = MutableLiveData<String>()
	private val _selectedPaymentMethod = MutableLiveData<String>()
	private val newOrderData = MutableLiveData<UserData.OrderItem>()

	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<UserData.CartItem>
			_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<UserData.CartItem>
			_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<String, Double>()
			val proList = mutableListOf<Product>()
			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_sep>/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()
}