diff --git a/feature/shares/.gitignore b/feature/shares/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/feature/shares/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/shares/README.md b/feature/shares/README.md new file mode 100644 index 0000000000..11dd15087f --- /dev/null +++ b/feature/shares/README.md @@ -0,0 +1,3 @@ +# :feature:shares module +## Dependency graph +// need to create --> ![Dependency graph](../../docs/images/graphs/dep_graph_feature_shares.svg) \ No newline at end of file diff --git a/feature/shares/build.gradle.kts b/feature/shares/build.gradle.kts new file mode 100644 index 0000000000..60244c3e63 --- /dev/null +++ b/feature/shares/build.gradle.kts @@ -0,0 +1,46 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("mifos.cmp.feature") +} + +android { + namespace = "com.mifos.androidclient.features.shares" +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.serialization.json) + implementation(libs.jb.lifecycleViewmodel) + api(projects.core.model) + api(projects.core.common) + } + + androidMain.dependencies { + implementation(libs.androidx.core.ktx) + } + } +} +dependencies { + implementation(projects.cmpShared) + implementation(projects.core.ui) + implementation(projects.core.domain) + implementation(projects.core.designsystem) + implementation(projects.core.datastore) + implementation(projects.core.network) + implementation(projects.core.database) + implementation(libs.androidx.navigation.fragment) + implementation(libs.androidx.navigation.ui) + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) + implementation(libs.kotlinx.serialization.json) + implementation(libs.jb.lifecycleViewmodel) + implementation(libs.jb.lifecycleruntime) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.recyclerview) + implementation(libs.google.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext) + androidTestImplementation(libs.espresso.core) +} \ No newline at end of file diff --git a/feature/shares/consumer-rules.pro b/feature/shares/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/feature/shares/proguard-rules.pro b/feature/shares/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/feature/shares/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/shares/src/androidMain/AndroidManifest.xml b/feature/shares/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..a71b693fa9 --- /dev/null +++ b/feature/shares/src/androidMain/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/data/models/Charge.kt b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/data/models/Charge.kt new file mode 100644 index 0000000000..0e1bd63520 --- /dev/null +++ b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/data/models/Charge.kt @@ -0,0 +1,12 @@ +package com.mifos.androidclient.features.shares.data.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Charge( + val title: String, + val type: String, + val collectedOn: String, + val amount: String +) : Parcelable \ No newline at end of file diff --git a/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/data/models/ShareAccountData.kt b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/data/models/ShareAccountData.kt new file mode 100644 index 0000000000..9d33c5cec5 --- /dev/null +++ b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/data/models/ShareAccountData.kt @@ -0,0 +1,20 @@ +package com.mifos.androidclient.features.shares.data.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ShareAccountData( + val productName: String, + val externalId: String, + val submittedDate: String, + val currency: String, + val currentPrice: String, + val totalNumberOfShares: String, + val defaultSavingsAccount: String, + val applicationDate: String, + val allowDividends: Boolean, + val minimumActivePeriod: String, + val lockInPeriod: String, + val charges: List +) : Parcelable \ No newline at end of file diff --git a/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ChargesAdapter.kt b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ChargesAdapter.kt new file mode 100644 index 0000000000..f6b13553fb --- /dev/null +++ b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ChargesAdapter.kt @@ -0,0 +1,41 @@ +package com.mifos.androidclient.features.shares.preview + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.mifos.androidclient.databinding.ItemChargeBinding +import com.mifos.androidclient.features.shares.data.models.Charge + +class ChargesAdapter : ListAdapter(ChargeDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChargeViewHolder { + val binding = ItemChargeBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ChargeViewHolder(binding) + } + + override fun onBindViewHolder(holder: ChargeViewHolder, position: Int) { + val charge = getItem(position) + holder.bind(charge) + } + + class ChargeViewHolder(private val binding: ItemChargeBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(charge: Charge) { + binding.chargeTitleTv.text = charge.title + binding.chargeAmountTv.text = charge.amount + } + } +} + +class ChargeDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Charge, newItem: Charge): Boolean { + return oldItem.title == newItem.title && oldItem.amount == newItem.amount + } + + override fun areContentsTheSame(oldItem: Charge, newItem: Charge): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ShareAccountCreationViewModel.kt b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ShareAccountCreationViewModel.kt new file mode 100644 index 0000000000..76e2d86a7e --- /dev/null +++ b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ShareAccountCreationViewModel.kt @@ -0,0 +1,46 @@ +package com.mifos.androidclient.features.shares.preview + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifos.androidclient.features.shares.data.models.Charge +import com.mifos.androidclient.features.shares.data.models.ShareAccountData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class ShareAccountCreationViewModel : ViewModel() { + + private val _shareAccountData = MutableStateFlow(null) + val shareAccountData: StateFlow = _shareAccountData.asStateFlow() + + fun updateShareAccountData(data: ShareAccountData) { + _shareAccountData.value = data + } + + fun populateWithMockData() { + val mockData = ShareAccountData( + productName = "Wallet", + externalId = "33333", + submittedDate = "09 June 2025", + currency = "USD", + currentPrice = "$10,000", + totalNumberOfShares = "10", + defaultSavingsAccount = "Nobi", + applicationDate = "26-09-2025", + allowDividends = true, + minimumActivePeriod = "2 Months", + lockInPeriod = "6 Weeks", + charges = listOf( + Charge("Savings Administration", "Flat", "10-06-2025", "$2"), + Charge("Account Opening Fee", "Flat", "26-09-2025", "$5") + ) + ) + _shareAccountData.value = mockData + } + + fun submitNewShareAccount() { + viewModelScope.launch { + } + } +} diff --git a/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ShareAccountPreviewFragment.kt b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ShareAccountPreviewFragment.kt new file mode 100644 index 0000000000..659cb96b7c --- /dev/null +++ b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ShareAccountPreviewFragment.kt @@ -0,0 +1,88 @@ +package com.mifos.androidclient.features.shares.preview + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.mifos.androidclient.databinding.FragmentShareAccountPreviewBinding +import com.mifos.androidclient.features.shares.data.models.ShareAccountData + +class ShareAccountPreviewFragment : Fragment() { + + private var _binding: FragmentShareAccountPreviewBinding? = null + private val binding get() = _binding!! + private val viewModel: ShareAccountCreationViewModel by activityViewModels() + private val chargesAdapter = ChargesAdapter() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentShareAccountPreviewBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // For isolated testing, populate the ViewModel with mock data. + // Remove this line once the previous steps are implemented and passing data. + viewModel.populateWithMockData() + + binding.chargesRv.apply { + layoutManager = LinearLayoutManager(context) + adapter = chargesAdapter + } + + viewModel.shareAccountData.observe(viewLifecycleOwner) { data -> + data?.let { + updateUI(it) + chargesAdapter.submitList(it.charges) + } + } + + binding.backBtn.setOnClickListener { + findNavController().popBackStack() + } + + binding.nextBtn.setOnClickListener { + if (viewModel.shareAccountData.value != null) { + viewModel.submitNewShareAccount() + } else { + Toast.makeText(context, "Data is missing. Please go back to previous steps.", Toast.LENGTH_SHORT).show() + } + } + + binding.viewChargesBtn.setOnClickListener { + val charges = viewModel.shareAccountData.value?.charges ?: emptyList() + ViewChargesBottomSheetFragment.newInstance(charges).show( + childFragmentManager, "ViewChargesModal" + ) + } + } + + private fun updateUI(data: ShareAccountData) { + binding.productNameTv.text = "Product Name: ${data.productName}" + binding.externalIdTv.text = "External ID: ${data.externalId}" + binding.submittedDateTv.text = "Submitted Date: ${data.submittedDate}" + + binding.currencyTv.text = "Currency: ${data.currency}" + binding.currentPriceTv.text = "Current Price: ${data.currentPrice}" + binding.totalSharesTv.text = "Total Shares: ${data.totalNumberOfShares}" + binding.defaultSavingsAccountTv.text = "Default Savings Account: ${data.defaultSavingsAccount}" + binding.applicationDateTv.text = "Application Date: ${data.applicationDate}" + binding.allowDividendsTv.text = "Allow Dividends: ${if (data.allowDividends) "Yes" else "No"}" + binding.minimumActivePeriodTv.text = "Minimum Active Period: ${data.minimumActivePeriod}" + binding.lockInPeriodTv.text = "Lock-in Period: ${data.lockInPeriod}" + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ViewChargesBottomSheetFragment.kt b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ViewChargesBottomSheetFragment.kt new file mode 100644 index 0000000000..e0b364a41f --- /dev/null +++ b/feature/shares/src/androidMain/kotlin/com/mifos/feature/shares/preview/ViewChargesBottomSheetFragment.kt @@ -0,0 +1,56 @@ +package com.mifos.androidclient.features.shares.preview + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.mifos.androidclient.databinding.BottomSheetViewChargesBinding +import com.mifos.androidclient.features.shares.data.models.Charge + +class ViewChargesBottomSheetFragment : BottomSheetDialogFragment() { + + private var _binding: BottomSheetViewChargesBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = BottomSheetViewChargesBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val charges = arguments?.getParcelableArrayList(CHARGES_KEY) ?: emptyList() + val adapter = ChargesAdapter() + binding.chargesListRv.apply { + layoutManager = LinearLayoutManager(context) + this.adapter = adapter + } + adapter.submitList(charges) + + binding.cancelBtn.setOnClickListener { + dismiss() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + const val CHARGES_KEY = "charges_key" + + fun newInstance(charges: List): ViewChargesBottomSheetFragment { + return ViewChargesBottomSheetFragment().apply { + arguments = bundleOf(CHARGES_KEY to ArrayList(charges)) + } + } + } +} \ No newline at end of file diff --git a/feature/shares/src/androidMain/res/layout/bottom_sheet_view_charges.xml b/feature/shares/src/androidMain/res/layout/bottom_sheet_view_charges.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/feature/shares/src/androidMain/res/layout/fragment_share_account_preview.xml b/feature/shares/src/androidMain/res/layout/fragment_share_account_preview.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/feature/shares/src/androidMain/res/layout/item_charge.xml b/feature/shares/src/androidMain/res/layout/item_charge.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/feature/shares/src/androidMain/res/values/feature_shares_strings.xml b/feature/shares/src/androidMain/res/values/feature_shares_strings.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/settings.gradle.kts b/settings.gradle.kts index dff5de1de1..21b3469039 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -96,3 +96,4 @@ include(":feature:search") include(":feature:settings") //include(":feature:passcode") include(":feature:search") +include(":feature:shares")