From 86f0aa47dc95d2337224cbda0713f75eac3c151b Mon Sep 17 00:00:00 2001 From: AdityaGaikwad Date: Mon, 29 Jul 2024 21:06:32 +0530 Subject: [PATCH] Completed address Adding Uis and api integration. Worked on getting callbacks for url change in PaymentActivity. Integrated sub-category api and its ui with tablyout Integrated product listing api and cached data in ShopViewModel for every super-category, category and sub-category. Created ProductFragment to showcase product details: Added viewpager and its adapter to show all images of a product in a sliding manner. --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 3 +- .../com/woka/networking/RetrofitHelper.kt | 2 +- .../woka/onboard/fragments/OnboardFragment.kt | 2 +- .../main/java/com/woka/shop/ShopApiService.kt | 4 + .../main/java/com/woka/shop/ShopRepository.kt | 20 + .../shop/adapters/ProductImagesAdapter.kt | 32 ++ .../woka/shop/adapters/ShopProductAdapter.kt | 54 +++ .../productlisting/ProductListingResponse.kt | 6 + .../models/productlisting/ShopMasterDetail.kt | 10 + .../shop/models/productlisting/ShopProduct.kt | 18 + .../com/woka/shop/viewmodels/ShopViewModel.kt | 88 +++- .../com/woka/shop/views/AddressActivity.kt | 31 +- .../com/woka/shop/views/PaymentActivity.kt | 20 + .../fragments/address/AddAddressFragment.kt | 187 +++++++++ .../fragments/address/PinCodeFragment.kt | 22 +- .../fragments/cart/ParentAddressFragment.kt | 38 +- .../views/fragments/shop/ProductFragment.kt | 66 +++ .../views/fragments/shop/ShopFragment2.kt | 2 +- .../views/fragments/shop/ShopFragment3.kt | 135 ++++-- .../main/java/com/woka/utils/Extensions.kt | 38 ++ .../main/java/com/woka/utils/PagingData.kt | 6 + .../webseries/viewmodel/WebSeriesViewModel.kt | 6 +- app/src/main/res/drawable/ic_cart_filled.xml | 5 + app/src/main/res/drawable/season_tab_bg.xml | 4 +- .../main/res/drawable/sub_category_tab_bg.xml | 18 + .../main/res/layout/fragment_add_address.xml | 394 ++++++++++++++++++ .../res/layout/fragment_parent_address.xml | 3 +- app/src/main/res/layout/fragment_product.xml | 208 +++++++++ app/src/main/res/layout/fragment_shop3.xml | 251 ++++++++--- app/src/main/res/layout/fragment_web_show.xml | 3 +- .../res/layout/product_image_view_holder.xml | 6 + .../main/res/layout/product_view_holder.xml | 73 ++++ .../layout/summary_cart_item_view_holder.xml | 4 +- app/src/main/res/values/strings.xml | 28 +- gradle.properties | 2 +- 36 files changed, 1670 insertions(+), 121 deletions(-) create mode 100644 app/src/main/java/com/woka/shop/adapters/ProductImagesAdapter.kt create mode 100644 app/src/main/java/com/woka/shop/adapters/ShopProductAdapter.kt create mode 100644 app/src/main/java/com/woka/shop/models/productlisting/ProductListingResponse.kt create mode 100644 app/src/main/java/com/woka/shop/models/productlisting/ShopMasterDetail.kt create mode 100644 app/src/main/java/com/woka/shop/models/productlisting/ShopProduct.kt create mode 100644 app/src/main/java/com/woka/shop/views/fragments/address/AddAddressFragment.kt create mode 100644 app/src/main/java/com/woka/shop/views/fragments/shop/ProductFragment.kt create mode 100644 app/src/main/java/com/woka/utils/PagingData.kt create mode 100644 app/src/main/res/drawable/ic_cart_filled.xml create mode 100644 app/src/main/res/drawable/sub_category_tab_bg.xml create mode 100644 app/src/main/res/layout/fragment_add_address.xml create mode 100644 app/src/main/res/layout/fragment_product.xml create mode 100644 app/src/main/res/layout/product_image_view_holder.xml create mode 100644 app/src/main/res/layout/product_view_holder.xml diff --git a/app/build.gradle b/app/build.gradle index b2c81b5..a2cb117 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,6 +52,8 @@ ext.jwPlayerVersion = '4.17.0' ext.exoplayerVersion = '1.3.1' dependencies { + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.32' + implementation "com.facebook.shimmer:shimmer:0.5.0" implementation "com.airbnb.android:lottie:6.4.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c20acc2..a53eff0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,8 @@ + android:screenOrientation="portrait" + android:windowSoftInputMode="adjustPan" /> handleApiResponse(response: Response>): ApiResult { + private fun handleApiResponse(response: Response>): ApiResult { if (response.isSuccessful) { val body = response.body() ?: return ApiResult.Error(errorMessage = "Empty Response") return when (body.success) { diff --git a/app/src/main/java/com/woka/onboard/fragments/OnboardFragment.kt b/app/src/main/java/com/woka/onboard/fragments/OnboardFragment.kt index fe4f655..a2e130d 100644 --- a/app/src/main/java/com/woka/onboard/fragments/OnboardFragment.kt +++ b/app/src/main/java/com/woka/onboard/fragments/OnboardFragment.kt @@ -96,7 +96,7 @@ class OnboardFragment : Fragment() { private fun initViews() { binding.apply { viewPager.adapter = OnboardingAdapter() - TabLayoutMediator(tabLayout, viewPager){tab, position -> }.attach() + TabLayoutMediator(tabLayout, viewPager){_, _ -> }.attach() } } diff --git a/app/src/main/java/com/woka/shop/ShopApiService.kt b/app/src/main/java/com/woka/shop/ShopApiService.kt index f5402c7..0cfb16b 100644 --- a/app/src/main/java/com/woka/shop/ShopApiService.kt +++ b/app/src/main/java/com/woka/shop/ShopApiService.kt @@ -11,6 +11,7 @@ import com.woka.shop.models.couponlisting.CouponsResponse import com.woka.shop.models.createorder.CreateOrderRequestData import com.woka.shop.models.createorder.CreateOrderResponse import com.woka.shop.models.edd.EDDResponse +import com.woka.shop.models.productlisting.ProductListingResponse import com.woka.shop.models.subcategorylisting.SubCategoryResponse import com.woka.shop.models.superlisting.SuperCategoryResponse import okhttp3.FormBody @@ -31,6 +32,9 @@ interface ShopApiService { @POST("sub_category_listing") suspend fun subcategoryListing(@Body formBody: FormBody): Response> + @POST("v2/shop_product_listing") + suspend fun productListing(@Body formBody: FormBody): Response> + // cart @GET("cart_listing") suspend fun cartListing(): Response> diff --git a/app/src/main/java/com/woka/shop/ShopRepository.kt b/app/src/main/java/com/woka/shop/ShopRepository.kt index 9e6358d..d5f3e87 100644 --- a/app/src/main/java/com/woka/shop/ShopRepository.kt +++ b/app/src/main/java/com/woka/shop/ShopRepository.kt @@ -13,6 +13,7 @@ import com.woka.shop.models.couponlisting.CouponsResponse import com.woka.shop.models.createorder.CreateOrderRequestData import com.woka.shop.models.createorder.CreateOrderResponse import com.woka.shop.models.edd.EDDResponse +import com.woka.shop.models.productlisting.ProductListingResponse import com.woka.shop.models.subcategorylisting.SubCategoryResponse import com.woka.shop.models.superlisting.SuperCategoryResponse import okhttp3.FormBody @@ -51,6 +52,25 @@ object ShopRepository { } } + suspend fun productListing(categoryId: String, subcategoryId: String?, pageNo: String, count: String): ApiResult { + return handleApiCall { + val bodyBuilder = FormBody.Builder() + bodyBuilder.add("category_id", categoryId) + + subcategoryId?.let { + bodyBuilder.add("sub_category_id", it) + } + + bodyBuilder.add("api_version", "v2") + bodyBuilder.add("start", pageNo) + bodyBuilder.add("limit", count) + + apiService.productListing( + bodyBuilder.build() + ) + } + } + // cart listing with loose caching private var cartResponse: CartResponse? = null diff --git a/app/src/main/java/com/woka/shop/adapters/ProductImagesAdapter.kt b/app/src/main/java/com/woka/shop/adapters/ProductImagesAdapter.kt new file mode 100644 index 0000000..36eb340 --- /dev/null +++ b/app/src/main/java/com/woka/shop/adapters/ProductImagesAdapter.kt @@ -0,0 +1,32 @@ +package com.woka.shop.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.bumptech.glide.Glide +import com.woka.databinding.ProductImageViewHolderBinding + +class ProductImagesAdapter( + private val imageList: ArrayList +) : RecyclerView.Adapter(){ + + inner class ImageViewHolder(val binding: ProductImageViewHolderBinding): ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { + return ImageViewHolder( + ProductImageViewHolderBinding.inflate( + LayoutInflater.from(parent.context), + parent, false + ) + ) + } + + override fun getItemCount(): Int = imageList.size + + override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { + Glide.with(holder.binding.root.context) + .load(imageList[holder.absoluteAdapterPosition]) + .into(holder.binding.root) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/adapters/ShopProductAdapter.kt b/app/src/main/java/com/woka/shop/adapters/ShopProductAdapter.kt new file mode 100644 index 0000000..a908668 --- /dev/null +++ b/app/src/main/java/com/woka/shop/adapters/ShopProductAdapter.kt @@ -0,0 +1,54 @@ +package com.woka.shop.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.woka.databinding.ProductViewHolderBinding +import com.woka.shop.models.productlisting.ShopProduct +import java.util.concurrent.Executors + +class ShopProductAdapter: ListAdapter(ASYNC_DIFF_UTIL) { + companion object{ + private val DIFF_UTIL = object :DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: ShopProduct, newItem: ShopProduct): Boolean = oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: ShopProduct, newItem: ShopProduct): Boolean = oldItem == newItem + } + + private val ASYNC_DIFF_UTIL = AsyncDifferConfig.Builder(DIFF_UTIL) + .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) + .build() + } + + inner class ProductViewHolder(val binding: ProductViewHolderBinding): ViewHolder(binding.root) + + var onProductClicked: ((ShopProduct) -> Unit)? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder { + return ProductViewHolder( + ProductViewHolderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ProductViewHolder, position: Int) { + val product = getItem(holder.absoluteAdapterPosition) + + with(holder.binding){ + if (product.shop_image?.isNotEmpty() == true) + image.loadImage(product.shop_image.first()) + + title.text = product.product_name + price.text = "${product.product_price}" + + root.setOnClickListener { + onProductClicked?.invoke(product) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/productlisting/ProductListingResponse.kt b/app/src/main/java/com/woka/shop/models/productlisting/ProductListingResponse.kt new file mode 100644 index 0000000..ef928f5 --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/productlisting/ProductListingResponse.kt @@ -0,0 +1,6 @@ +package com.woka.shop.models.productlisting + +data class ProductListingResponse( + val result: List?, + val total_records: Int? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/productlisting/ShopMasterDetail.kt b/app/src/main/java/com/woka/shop/models/productlisting/ShopMasterDetail.kt new file mode 100644 index 0000000..223b55c --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/productlisting/ShopMasterDetail.kt @@ -0,0 +1,10 @@ +package com.woka.shop.models.productlisting + +data class ShopMasterDetail( + val description_english: String?, + val description_hindi: String?, + val id: Int?, + val product_id: Int?, + val product_name_english: String?, + val product_name_hindi: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/models/productlisting/ShopProduct.kt b/app/src/main/java/com/woka/shop/models/productlisting/ShopProduct.kt new file mode 100644 index 0000000..e3e95d7 --- /dev/null +++ b/app/src/main/java/com/woka/shop/models/productlisting/ShopProduct.kt @@ -0,0 +1,18 @@ +package com.woka.shop.models.productlisting + +data class ShopProduct( + val added_to_cart: Boolean?, + val category_master_id: Int?, + val id: Int?, + val product_name: String?, + val product_price: String?, + val product_thumbnail: String?, + val remain_stock_quantity: Int?, + val shop_image: List?, + val shop_master_detail: ShopMasterDetail?, + val sku_id: String?, + val stock_status: String?, + val sub_category_master_id: Int?, + val tax_category: Any?, + val tax_value: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/viewmodels/ShopViewModel.kt b/app/src/main/java/com/woka/shop/viewmodels/ShopViewModel.kt index 5897d39..5709861 100644 --- a/app/src/main/java/com/woka/shop/viewmodels/ShopViewModel.kt +++ b/app/src/main/java/com/woka/shop/viewmodels/ShopViewModel.kt @@ -7,8 +7,10 @@ import androidx.lifecycle.viewModelScope import com.woka.networking.ApiResult import com.woka.shop.ShopRepository import com.woka.shop.models.categorylisting.CategoryData +import com.woka.shop.models.productlisting.ShopProduct import com.woka.shop.models.subcategorylisting.SubCategoryData import com.woka.shop.models.superlisting.SuperCategory +import com.woka.utils.PagingData import kotlinx.coroutines.launch class ShopViewModel: ViewModel() { @@ -72,11 +74,12 @@ class ShopViewModel: ViewModel() { val subcategoryLiveData: LiveData>> get() = _subcategoryLiveData - private val subcategoryDataMap = HashMap>() + val subcategoryDataMap = HashMap>() - fun loadSubCategories(categoryId: String){ - if (subcategoryDataMap.containsKey(categoryId)){ - _subcategoryLiveData.postValue(ApiResult.Success(subcategoryDataMap[categoryId])) + fun loadSubCategories(superCategoryId: String, categoryId: String){ + val key = "${superCategoryId}_$categoryId" + if (subcategoryDataMap.containsKey(key)){ + _subcategoryLiveData.postValue(ApiResult.Success(subcategoryDataMap[key])) return } @@ -87,7 +90,9 @@ class ShopViewModel: ViewModel() { is ApiResult.Loading -> {} is ApiResult.Success -> { response.data?.result?.filterNotNull()?.toMutableList()?.let { - subcategoryDataMap[categoryId] = it + // adding a all tag at start + it.add(0, SubCategoryData(null, null, "All", null)) + subcategoryDataMap[key] = it _subcategoryLiveData.postValue(ApiResult.Success(it)) } } @@ -98,4 +103,77 @@ class ShopViewModel: ViewModel() { fun clearSubCategoryLiveData(){ _subcategoryLiveData.postValue(ApiResult.Loading()) } + + // product listing + private val _productListingLiveData = MutableLiveData>>() + val productListingLiveData: LiveData>> + get() = _productListingLiveData + + var productPagingData = HashMap() + + // product data for every super-category, category and sub-category + private var productDataMap = HashMap>() + + fun loadProducts(superCategoryId: String, categoryId: String, subCategoryId: String?) { + val key = "${superCategoryId}_${categoryId}_$subCategoryId" + if (productDataMap.containsKey(key) && productDataMap[key]?.isNotEmpty() == true) { + _productListingLiveData.postValue(ApiResult.Success(productDataMap[key])) + } else { + loadMoreEpisodes(superCategoryId, categoryId, subCategoryId) + } + } + + fun loadMoreEpisodes(superCategoryId: String, categoryId: String, subCategoryId: String?) { + viewModelScope.launch { + _productListingLiveData.postValue(ApiResult.Loading()) + + val key = "${superCategoryId}_${categoryId}_$subCategoryId" + + val pagingData = if (productPagingData.containsKey(key)) { + productPagingData[key]!! + } else { + val pagingData = PagingData() + productPagingData[key] = pagingData + pagingData + } + + when (val response = repository.productListing( + categoryId, + subCategoryId, + "${pagingData.nextPageToLoad}", + "${pagingData.quantityPerPage}" + )) { + is ApiResult.Error -> { + _productListingLiveData.postValue( + ApiResult.Error( + response.errorMessage, + response.error + ) + ) + } + + is ApiResult.Loading -> { + _productListingLiveData.postValue(ApiResult.Loading()) + } + + is ApiResult.Success -> { + response.data?.let { data -> + data.result?.filterNotNull()?.let { newList -> + val currentList = productDataMap.getOrDefault(key, ArrayList()) + currentList.addAll(newList) + productDataMap[key] = currentList + + pagingData.lastPage = productDataMap[key]?.size == data.total_records + pagingData.nextPageToLoad++ + _productListingLiveData.postValue(ApiResult.Success(productDataMap[key])) + } + } + } + } + } + } + + fun clearProductListingLiveData() { + _productListingLiveData.postValue(ApiResult.Loading()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/AddressActivity.kt b/app/src/main/java/com/woka/shop/views/AddressActivity.kt index 511ceba..20ed4f3 100644 --- a/app/src/main/java/com/woka/shop/views/AddressActivity.kt +++ b/app/src/main/java/com/woka/shop/views/AddressActivity.kt @@ -11,6 +11,11 @@ import com.woka.utils.WokaBaseActivity class AddressActivity : WokaBaseActivity() { + companion object { + const val EXTRA_PRODUCT_IDS = "extra_product_ids" + const val EXTRA_COUPON_CODE = "extra_coupon_code" + } + private lateinit var binding: ActivityAddressBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -24,10 +29,32 @@ class AddressActivity : WokaBaseActivity() { insets } - binding.toolbar.title.text = getString(R.string.address_details) + initViews() + + clickEvents() supportFragmentManager.beginTransaction() - .add(R.id.fcv_address, PinCodeFragment.newInstance()) + .add( + R.id.fcv_address, + PinCodeFragment.newInstance( + intent.getIntegerArrayListExtra(EXTRA_PRODUCT_IDS), + intent.getStringExtra(EXTRA_COUPON_CODE) + ) + ) .commit() } + + private fun initViews() { + binding.apply { + toolbar.title.text = getString(R.string.address_details) + } + } + + private fun clickEvents() { + binding.apply { + toolbar.backBtn.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/PaymentActivity.kt b/app/src/main/java/com/woka/shop/views/PaymentActivity.kt index 1fea1b8..188b003 100644 --- a/app/src/main/java/com/woka/shop/views/PaymentActivity.kt +++ b/app/src/main/java/com/woka/shop/views/PaymentActivity.kt @@ -5,15 +5,19 @@ import android.graphics.Color import android.os.Build import android.os.Bundle import android.webkit.WebSettings +import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.enableEdgeToEdge import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope import com.woka.R import com.woka.databinding.ActivityPaymentBinding import com.woka.utils.WokaBaseActivity import com.woka.utils.lightStatusBar import com.woka.utils.toast +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class PaymentActivity : WokaBaseActivity() { @@ -69,6 +73,22 @@ class PaymentActivity : WokaBaseActivity() { binding.webView.setWebViewClient(WebViewClient()) + binding.webView.webViewClient = object : WebViewClient(){ + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + if (url?.startsWith("https://secure.ccavenue.com/") == true){ + if (url.contains("cancelTransaction")){ + lifecycleScope.launch { + delay(2000) + runOnUiThread { + finish() + } + } + } + } + } + } + binding.webView.loadUrl(it) } } \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/fragments/address/AddAddressFragment.kt b/app/src/main/java/com/woka/shop/views/fragments/address/AddAddressFragment.kt new file mode 100644 index 0000000..89f8669 --- /dev/null +++ b/app/src/main/java/com/woka/shop/views/fragments/address/AddAddressFragment.kt @@ -0,0 +1,187 @@ +package com.woka.shop.views.fragments.address + +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.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.woka.R +import com.woka.databinding.FragmentAddAddressBinding +import com.woka.networking.ApiResult +import com.woka.shop.models.addaddress.AddAddressRequestData +import com.woka.shop.viewmodels.AddressViewModel +import com.woka.shop.viewmodels.CartViewModel +import com.woka.shop.views.PaymentActivity +import com.woka.utils.ProgressView +import com.woka.utils.setAsPhoneInput +import com.woka.utils.toast +import kotlinx.coroutines.launch + +class AddAddressFragment private constructor( + private val pinCode: String, + private val productIds: ArrayList?, + private val couponCode: String? +) : Fragment() { + + private lateinit var binding: FragmentAddAddressBinding + private lateinit var viewModel: AddressViewModel + private lateinit var cartViewModel: CartViewModel + private lateinit var progressView: ProgressView + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentAddAddressBinding.inflate(inflater, container, false) + viewModel = ViewModelProvider(requireActivity())[AddressViewModel::class.java] + cartViewModel = ViewModelProvider(requireActivity())[CartViewModel::class.java] + progressView = ProgressView(requireContext(), getString(R.string.please_wait)) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initViews() + + clickEvents() + + setObservers() + + } + + private fun clickEvents() { + binding.apply { + submit.setOnClickListener { + if (allOkay()){ + addAddress() + } + } + } + } + + private fun addAddress(){ + lifecycleScope.launch { + binding.apply { + progressView.show() + + val requestData = AddAddressRequestData( + addressName.text.toString(), + "${addressLine1.text}, ${addressLine2.text}", + city.text.toString(), + state.text.toString(), + pinCode, + phone.text.toString() + ) + + when (val response = viewModel.addAddress(requestData)){ + is ApiResult.Error -> { + progressView.hide() + toast(response.errorMessage) + } + is ApiResult.Loading -> {} + is ApiResult.Success -> { + progressView.hide() + + if (productIds != null){ + cartViewModel.createOrder( + productIds, + couponCode, + "${response.data?.id}" + ) + } + } + } + } + } + } + + private fun allOkay(): Boolean { + var allOkay = true + + binding.apply { + if (addressName.text.trim().isEmpty()){ + addressName.error = getString(R.string.please_enter_name) + allOkay = false + } + + if (addressLine1.text.trim().length < 15){ + addressLine1.error = getString(R.string.at_least_15_characters) + allOkay = false + } + + if (addressLine2.text.length < 15){ + addressLine2.error = getString(R.string.at_least_15_characters) + allOkay = false + } + + if (city.text.trim().length < 2){ + city.error = getString(R.string.at_least_2_characters) + allOkay = false + } + + if (state.text.trim().length < 2){ + state.error = getString(R.string.at_least_2_characters) + allOkay = false + } + + if (country.text.trim().length < 2){ + country.error = getString(R.string.at_least_2_characters) + allOkay = false + } + + if (phone.text.trim().length < 10){ + phone.error = getString(R.string.enter_10_digit_phone) + allOkay = false + } + } + + return allOkay + } + + private fun initViews() { + binding.apply { + phone.setAsPhoneInput() + + pincode.setText(pinCode) + } + } + + private fun setObservers(){ + cartViewModel.createOrderLiveData.observe(viewLifecycleOwner){ + when(it){ + is ApiResult.Error -> { + progressView.hide() + toast(it.errorMessage) + } + is ApiResult.Loading -> { + progressView.show() + } + is ApiResult.Success -> { + progressView.hide() + + it.data?.url?.let {paymentUrl -> + activity?.let {activity -> + activity.startActivity( + Intent(activity, PaymentActivity::class.java).apply { + putExtra(PaymentActivity.EXTRA_PAYMENT_LINK, paymentUrl) + } + ) + } + } + } + null -> {} + } + } + } + + companion object { + @JvmStatic + fun newInstance(pinCode: String, + productIds: ArrayList?, + couponCode: String?) = AddAddressFragment(pinCode, productIds, couponCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/fragments/address/PinCodeFragment.kt b/app/src/main/java/com/woka/shop/views/fragments/address/PinCodeFragment.kt index 0ea4db4..33fed94 100644 --- a/app/src/main/java/com/woka/shop/views/fragments/address/PinCodeFragment.kt +++ b/app/src/main/java/com/woka/shop/views/fragments/address/PinCodeFragment.kt @@ -1,11 +1,11 @@ package com.woka.shop.views.fragments.address import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.woka.R @@ -13,10 +13,13 @@ import com.woka.databinding.FragmentPinCodeBinding import com.woka.networking.ApiResult import com.woka.shop.viewmodels.AddressViewModel import com.woka.utils.ProgressView -import com.woka.utils.toast import kotlinx.coroutines.launch +import java.util.ArrayList -class PinCodeFragment private constructor(): Fragment() { +class PinCodeFragment private constructor( + private val productIds: ArrayList?, + private val couponCode: String? +): Fragment() { private lateinit var binding: FragmentPinCodeBinding private lateinit var progressView: ProgressView @@ -47,11 +50,15 @@ class PinCodeFragment private constructor(): Fragment() { binding.apply { btn.setOnClickListener { if (selectedPinCode.length == 6){ - toast(selectedPinCode) + parentFragmentManager.beginTransaction() + .add(R.id.fcv_address, AddAddressFragment.newInstance(selectedPinCode, productIds, couponCode)) + .addToBackStack(null) + .commit() + return@setOnClickListener } - if (pincode.text.length < 6){ + if (pincode.text.length != 6){ pincode.error = getString(R.string.enter_a_valid_pincode) return@setOnClickListener } @@ -73,7 +80,6 @@ class PinCodeFragment private constructor(): Fragment() { selectedPinCode = pinCode btn.text = getString(R.string.proceed) - } } } @@ -94,6 +100,8 @@ class PinCodeFragment private constructor(): Fragment() { companion object { @JvmStatic - fun newInstance() = PinCodeFragment() + fun newInstance( + productIds: ArrayList?, + couponCode: String?) = PinCodeFragment(productIds, couponCode) } } \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/fragments/cart/ParentAddressFragment.kt b/app/src/main/java/com/woka/shop/views/fragments/cart/ParentAddressFragment.kt index bef3278..f85abe6 100644 --- a/app/src/main/java/com/woka/shop/views/fragments/cart/ParentAddressFragment.kt +++ b/app/src/main/java/com/woka/shop/views/fragments/cart/ParentAddressFragment.kt @@ -14,6 +14,8 @@ import com.woka.shop.adapters.ParentAddressAdapter import com.woka.shop.models.cartlisting.CartResponse import com.woka.shop.viewmodels.CartViewModel import com.woka.shop.views.AddressActivity +import com.woka.shop.views.AddressActivity.Companion.EXTRA_COUPON_CODE +import com.woka.shop.views.AddressActivity.Companion.EXTRA_PRODUCT_IDS import com.woka.shop.views.PaymentActivity import com.woka.shop.views.PaymentActivity.Companion.EXTRA_PAYMENT_LINK import com.woka.utils.ProgressView @@ -21,6 +23,7 @@ import com.woka.utils.hide import com.woka.utils.setVisibility import com.woka.utils.show import com.woka.utils.toast +import java.util.ArrayList class ParentAddressFragment : Fragment() { @@ -67,17 +70,7 @@ class ParentAddressFragment : Fragment() { private fun clickEvents(){ binding.apply { selectAddress.setOnClickListener { - val productIds = ArrayList() - - if (viewModel.cartListLiveData.value is ApiResult.Success){ - (viewModel.cartListLiveData.value as ApiResult.Success).data?.result?.let { - for (cartItem in it){ - cartItem?.id?.let {id -> - productIds.add(id) - } - } - } - } + val productIds = getProductIds() if (productIds.isEmpty()){ toast(getString(R.string.no_products_added_to_cart)) @@ -94,12 +87,33 @@ class ParentAddressFragment : Fragment() { addNewAddress.setOnClickListener { activity?.let { - it.startActivity(Intent(it, AddressActivity::class.java)) + it.startActivity( + Intent(it, AddressActivity::class.java).apply { + putIntegerArrayListExtra(EXTRA_PRODUCT_IDS, getProductIds()) + putExtra(EXTRA_COUPON_CODE, viewModel.appliedCoupon) + } + ) } } } } + private fun getProductIds(): ArrayList { + val productIds = ArrayList() + + if (viewModel.cartListLiveData.value is ApiResult.Success){ + (viewModel.cartListLiveData.value as ApiResult.Success).data?.result?.let { + for (cartItem in it){ + cartItem?.id?.let {id -> + productIds.add(id) + } + } + } + } + + return productIds + } + private fun setObservers() { adapter.addressSelectListener = { addressId -> binding.selectAddress.show() diff --git a/app/src/main/java/com/woka/shop/views/fragments/shop/ProductFragment.kt b/app/src/main/java/com/woka/shop/views/fragments/shop/ProductFragment.kt new file mode 100644 index 0000000..86c9e2e --- /dev/null +++ b/app/src/main/java/com/woka/shop/views/fragments/shop/ProductFragment.kt @@ -0,0 +1,66 @@ +package com.woka.shop.views.fragments.shop + +import android.os.Bundle +import android.text.Html +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.tabs.TabLayoutMediator +import com.woka.R +import com.woka.databinding.FragmentProductBinding +import com.woka.shop.adapters.ProductImagesAdapter +import com.woka.shop.models.productlisting.ShopProduct + +class ProductFragment private constructor( + private val shopProduct: ShopProduct, + private val category: String +): Fragment() { + + companion object { + @JvmStatic + fun newInstance(shopProduct: ShopProduct, categoryName: String) = ProductFragment(shopProduct, categoryName) + } + + private lateinit var binding: FragmentProductBinding + private lateinit var imageAdapter: ProductImagesAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentProductBinding.inflate(inflater, container, false) + val imageList = ArrayList() + shopProduct.shop_image?.filterNotNull()?.let { + imageList.addAll(it) + } + imageAdapter = ProductImagesAdapter(imageList) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initViews() + + } + + private fun initViews() { + binding.apply { + + vpImages.adapter = imageAdapter + + TabLayoutMediator(tabLayout, vpImages){_, _ -> }.attach() + + title.text = shopProduct.product_name + categoryName.text = category + skuId.text = shopProduct.sku_id + price.text = shopProduct.product_price + + description.text = Html.fromHtml( + shopProduct.shop_master_detail?.description_english, + Html.FROM_HTML_MODE_LEGACY + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/woka/shop/views/fragments/shop/ShopFragment2.kt b/app/src/main/java/com/woka/shop/views/fragments/shop/ShopFragment2.kt index 812e515..7c81051 100644 --- a/app/src/main/java/com/woka/shop/views/fragments/shop/ShopFragment2.kt +++ b/app/src/main/java/com/woka/shop/views/fragments/shop/ShopFragment2.kt @@ -62,7 +62,7 @@ class ShopFragment2 private constructor(private val superCategoryId: String): Fr adapter.onCategoryClickListener = { parentFragmentManager.beginTransaction() - .replace(R.id.fcv_shop, ShopFragment3.newInstance("${it.id}")) + .replace(R.id.fcv_shop, ShopFragment3.newInstance(superCategoryId, "${it.id}", "${it.title}")) .addToBackStack(null) .commitAllowingStateLoss() } diff --git a/app/src/main/java/com/woka/shop/views/fragments/shop/ShopFragment3.kt b/app/src/main/java/com/woka/shop/views/fragments/shop/ShopFragment3.kt index f74d520..5efe07d 100644 --- a/app/src/main/java/com/woka/shop/views/fragments/shop/ShopFragment3.kt +++ b/app/src/main/java/com/woka/shop/views/fragments/shop/ShopFragment3.kt @@ -6,19 +6,33 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import com.google.android.material.tabs.TabLayout +import com.woka.R import com.woka.databinding.FragmentShop3Binding import com.woka.networking.ApiResult -import com.woka.shop.adapters.CategoryAdapter -import com.woka.shop.models.BaseCategory +import com.woka.shop.adapters.ShopProductAdapter import com.woka.shop.viewmodels.ShopViewModel import com.woka.utils.hide +import com.woka.utils.setVisibility import com.woka.utils.show -class ShopFragment3 private constructor(private val categoryId: String): Fragment() { +class ShopFragment3 private constructor( + private val superCategoryId: String, + private val categoryId: String, + private val categoryName: String +) : Fragment(), TabLayout.OnTabSelectedListener { + + companion object { + @JvmStatic + fun newInstance(superCategoryId: String, categoryId: String, categoryName: String) = + ShopFragment3(superCategoryId, categoryId, categoryName) + } private lateinit var binding: FragmentShop3Binding private lateinit var viewModel: ShopViewModel - private lateinit var adapter: CategoryAdapter + private lateinit var productAdapter: ShopProductAdapter + + private var loadMoreProducts = false override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -26,7 +40,7 @@ class ShopFragment3 private constructor(private val categoryId: String): Fragmen ): View { binding = FragmentShop3Binding.inflate(inflater, container, false) viewModel = ViewModelProvider(requireActivity())[ShopViewModel::class.java] - adapter = CategoryAdapter() + productAdapter = ShopProductAdapter() return binding.root } @@ -39,28 +53,53 @@ class ShopFragment3 private constructor(private val categoryId: String): Fragmen setObservers() - viewModel.loadSubCategories(categoryId) + viewModel.loadSubCategories(superCategoryId, categoryId) } override fun onDestroyView() { super.onDestroyView() viewModel.clearSubCategoryLiveData() + viewModel.clearProductListingLiveData() } private fun initViews() { binding.apply { - rvCategory.adapter = adapter + categoryTabs.addOnTabSelectedListener(this@ShopFragment3) + + rvProducts.adapter = productAdapter + rvProducts.itemAnimator = null } } private fun clickEvents() { binding.apply { retryButton.setOnClickListener { - viewModel.loadSubCategories(categoryId) + viewModel.loadSubCategories(superCategoryId, categoryId) } - adapter.onCategoryClickListener = { + prodLoadMoreBtn.setOnClickListener { + loadMoreProducts = true + val subCategoryId = categoryTabs.getTabAt(categoryTabs.selectedTabPosition)?.tag?.toString() + viewModel.loadMoreEpisodes(superCategoryId, categoryId, subCategoryId) + } + productAdapter.onProductClicked = { + parentFragmentManager.beginTransaction() + .add(R.id.fcv_shop, ProductFragment.newInstance(it, categoryName)) + .addToBackStack(null) + .commit() + } + } + } + + private fun loadProducts() { + val selectedTabPos = binding.categoryTabs.selectedTabPosition + if (selectedTabPos >= 0) { + viewModel.subcategoryDataMap["${superCategoryId}_$categoryId"]?.let { subCategories -> + if (selectedTabPos < subCategories.size) { + loadMoreProducts = false + viewModel.loadProducts(superCategoryId, categoryId, subCategories[selectedTabPos].id?.toString()) + } } } } @@ -70,31 +109,74 @@ class ShopFragment3 private constructor(private val categoryId: String): Fragmen viewModel.subcategoryLiveData.observe(viewLifecycleOwner) { when (it) { is ApiResult.Error -> { - rvCategory.hide() - shimmer.hide() + categoryTabs.hide() + categoryShimmer.hide() errorView.show() } is ApiResult.Loading -> { - rvCategory.hide() - shimmer.show() + categoryTabs.hide() + categoryShimmer.show() errorView.hide() } is ApiResult.Success -> { it.data?.let { categoryList -> - adapter.submitList(categoryList.map { category -> - BaseCategory( - category.id, - category.sub_category_thumbnail, - category.sub_category_name + categoryTabs.show() + categoryShimmer.hide() + errorView.hide() + for (category in categoryList) { + binding.categoryTabs.addTab( + binding.categoryTabs.newTab() + .setText("${category.sub_category_name}") + .setTag(category.id) ) } - ) { - rvCategory.show() + } + } + } + } - shimmer.hide() - errorView.hide() + viewModel.productListingLiveData.observe(viewLifecycleOwner){ + when (it) { + is ApiResult.Error -> { + binding.productShimmer.hide() + if (loadMoreProducts) { + // load more + binding.prodLoadMoreBtn.text = getString(R.string.retry) + binding.prodLoadMoreBtn.show() + } else { + // first time load + binding.rvProducts.hide() + binding.productShimmer.hide() + binding.prodLoadMoreBtn.hide() + } + } + + is ApiResult.Loading -> { + binding.productShimmer.show() + binding.prodLoadMoreBtn.hide() + binding.rvProducts.setVisibility(loadMoreProducts) + } + + is ApiResult.Success -> { + it.data?.let { productList -> + binding.rvProducts.show() + binding.productShimmer.hide() + + binding.prodLoadMoreBtn.text = getString(R.string.load_more) + val subCategoryId = categoryTabs.getTabAt(categoryTabs.selectedTabPosition)?.tag + binding.prodLoadMoreBtn.setVisibility(viewModel.productPagingData["${superCategoryId}_${categoryId}_$subCategoryId"]?.lastPage == false) + + if (loadMoreProducts) { + // loaded more data + productAdapter.notifyItemRangeInserted( + productAdapter.currentList.size, + productList.size + ) + } else { + // new category data load + productAdapter.submitList(productList) } } } @@ -103,8 +185,11 @@ class ShopFragment3 private constructor(private val categoryId: String): Fragmen } } - companion object { - @JvmStatic - fun newInstance(categoryId: String) = ShopFragment3(categoryId) + override fun onTabSelected(p0: TabLayout.Tab?) { + loadProducts() } + + override fun onTabUnselected(p0: TabLayout.Tab?) {} + override fun onTabReselected(p0: TabLayout.Tab?) {} + } \ No newline at end of file diff --git a/app/src/main/java/com/woka/utils/Extensions.kt b/app/src/main/java/com/woka/utils/Extensions.kt index 51faaed..63f6e71 100644 --- a/app/src/main/java/com/woka/utils/Extensions.kt +++ b/app/src/main/java/com/woka/utils/Extensions.kt @@ -7,14 +7,19 @@ import android.content.Context import android.content.res.Configuration import android.net.ConnectivityManager import android.net.NetworkCapabilities +import android.text.InputFilter +import android.text.Spanned import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.view.Window import android.view.inputmethod.InputMethodManager +import android.widget.EditText import android.widget.Toast import androidx.core.view.WindowCompat import androidx.fragment.app.Fragment +import com.google.i18n.phonenumbers.PhoneNumberUtil +import com.google.i18n.phonenumbers.Phonenumber import com.woka.WokaApp.Companion.userPrefs import java.util.Locale import kotlin.math.ceil @@ -100,4 +105,37 @@ fun Context.isNetworkConnected(): Boolean { val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) +} + +fun EditText.setAsPhoneInput(){ + // phone number formatting + // to deal with input pasting in the edit text + + val phoneFilter = InputFilter { charSequence: CharSequence, _: Int, _: Int, _: Spanned?, _: Int, _: Int -> + var phoneNumberStr = charSequence.toString() + try { + val phoneNumberUtil: PhoneNumberUtil = PhoneNumberUtil.getInstance() + val phoneNumber: Phonenumber.PhoneNumber = phoneNumberUtil.parse(charSequence, "US") + phoneNumberStr = java.lang.String.valueOf(phoneNumber.nationalNumber) + } catch (e: Exception) { + // nothing + } + + if (phoneNumberStr.length > 10) { + // pasted number length is greater than 10 + return@InputFilter phoneNumberStr.substring(0, 10) + } + val totalPhoneNumber: String = text.toString() + phoneNumberStr + + error = null + + if (totalPhoneNumber.length > 10) { + // max length should be 10 + return@InputFilter "" + } else { + return@InputFilter phoneNumberStr + } + } + + filters = arrayOf(phoneFilter) } \ No newline at end of file diff --git a/app/src/main/java/com/woka/utils/PagingData.kt b/app/src/main/java/com/woka/utils/PagingData.kt new file mode 100644 index 0000000..290f091 --- /dev/null +++ b/app/src/main/java/com/woka/utils/PagingData.kt @@ -0,0 +1,6 @@ +package com.woka.utils + +data class PagingData( + var nextPageToLoad: Int = 0, var quantityPerPage: Int = 6, + var lastPage: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/woka/webseries/viewmodel/WebSeriesViewModel.kt b/app/src/main/java/com/woka/webseries/viewmodel/WebSeriesViewModel.kt index f30f8ae..4c40224 100644 --- a/app/src/main/java/com/woka/webseries/viewmodel/WebSeriesViewModel.kt +++ b/app/src/main/java/com/woka/webseries/viewmodel/WebSeriesViewModel.kt @@ -8,6 +8,7 @@ import com.jwplayer.pub.api.media.playlists.PlaylistItem import com.woka.modules.categorymodels.CategoriesResponse import com.woka.networking.ApiResult import com.woka.players.models.VideoPlayList +import com.woka.utils.PagingData import com.woka.webseries.WebSeriesRepository import com.woka.webseries.models.ContinueEpisodeResponse import com.woka.webseries.models.ShowData @@ -20,11 +21,6 @@ class WebSeriesViewModel : ViewModel() { private val repository = WebSeriesRepository - data class PagingData( - var nextPageToLoad: Int = 0, var quantityPerPage: Int = 6, - var lastPage: Boolean = false - ) - // categories listing private val _showCategoryLiveData = MutableLiveData>() diff --git a/app/src/main/res/drawable/ic_cart_filled.xml b/app/src/main/res/drawable/ic_cart_filled.xml new file mode 100644 index 0000000..d866aef --- /dev/null +++ b/app/src/main/res/drawable/ic_cart_filled.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/season_tab_bg.xml b/app/src/main/res/drawable/season_tab_bg.xml index 2027107..a441223 100644 --- a/app/src/main/res/drawable/season_tab_bg.xml +++ b/app/src/main/res/drawable/season_tab_bg.xml @@ -2,14 +2,14 @@ - + - + diff --git a/app/src/main/res/drawable/sub_category_tab_bg.xml b/app/src/main/res/drawable/sub_category_tab_bg.xml new file mode 100644 index 0000000..1d46d6d --- /dev/null +++ b/app/src/main/res/drawable/sub_category_tab_bg.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_address.xml b/app/src/main/res/layout/fragment_add_address.xml new file mode 100644 index 0000000..f7843ca --- /dev/null +++ b/app/src/main/res/layout/fragment_add_address.xml @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +