From 43c9f876c0d94a866b65d03610c80258fc2832de Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Sep 2025 19:14:08 +0300 Subject: [PATCH 01/14] [MS-1163] Implementation of the External Credential selection screen --- feature/external-credential/build.gradle.kts | 2 + .../ExternalCredentialControllerFragment.kt | 27 +++ .../controller/ExternalCredentialState.kt | 13 ++ .../controller/ExternalCredentialViewModel.kt | 37 +++++ .../ExternalCredentialSelectFragment.kt | 154 ++++++++++++++++++ .../ExternalCredentialSelectViewModel.kt | 29 ++++ .../view/ExternalCredentialTypeAdapter.kt | 47 ++++++ .../src/main/res/drawable/ghana_id_card.webp | Bin 0 -> 5048 bytes .../main/res/drawable/ghana_nhis_card.webp | Bin 0 -> 6138 bytes .../src/main/res/drawable/qr_code.webp | Bin 0 -> 3818 bytes .../main/res/layout-land/item_document.xml | 41 +++++ .../res/layout/dialog_skip_scan_confirm.xml | 65 ++++++++ .../fragment_external_credential_select.xml | 52 +++++- .../src/main/res/layout/item_document.xml | 41 +++++ .../graph_external_credential_internal.xml | 9 +- .../resources/src/main/res/values/strings.xml | 13 ++ 16 files changed, 519 insertions(+), 11 deletions(-) create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialState.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectViewModel.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/view/ExternalCredentialTypeAdapter.kt create mode 100644 feature/external-credential/src/main/res/drawable/ghana_id_card.webp create mode 100644 feature/external-credential/src/main/res/drawable/ghana_nhis_card.webp create mode 100644 feature/external-credential/src/main/res/drawable/qr_code.webp create mode 100644 feature/external-credential/src/main/res/layout-land/item_document.xml create mode 100644 feature/external-credential/src/main/res/layout/dialog_skip_scan_confirm.xml create mode 100644 feature/external-credential/src/main/res/layout/item_document.xml diff --git a/feature/external-credential/build.gradle.kts b/feature/external-credential/build.gradle.kts index 2db2841b64..61fa3b3d79 100644 --- a/feature/external-credential/build.gradle.kts +++ b/feature/external-credential/build.gradle.kts @@ -8,5 +8,7 @@ android { } dependencies { + implementation(project(":infra:config-store")) + implementation(project(":infra:config-sync")) implementation(project(":feature:exit-form")) } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt index e95d488dc3..c377a49fc7 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt @@ -2,7 +2,9 @@ package com.simprints.feature.externalcredential.screens.controller import android.os.Bundle import android.view.View +import androidx.activity.addCallback import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -19,6 +21,7 @@ import kotlin.getValue @AndroidEntryPoint internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment_external_credential_controller) { private val args: ExternalCredentialControllerFragmentArgs by navArgs() + private val viewModel: ExternalCredentialViewModel by activityViewModels() private val hostFragment: Fragment? get() = childFragmentManager.findFragmentById(R.id.external_credential_host_fragment) @@ -48,5 +51,29 @@ internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment } } internalNavController?.setGraph(R.navigation.graph_external_credential_internal) + + initObservers() + initListeners() + } + + private fun initObservers() { + viewModel.stateLiveData.observe(viewLifecycleOwner) { + } + } + + private fun initListeners() { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + when (internalNavController?.currentDestination?.id) { + R.id.externalCredentialSelectFragment -> { + // Exit form navigation + findNavController().navigateSafely( + this@ExternalCredentialControllerFragment, + R.id.action_global_refusalFragment, + ) + } + + else -> internalNavController?.popBackStack() + } + } } } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialState.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialState.kt new file mode 100644 index 0000000000..609900800f --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialState.kt @@ -0,0 +1,13 @@ +package com.simprints.feature.externalcredential.screens.controller + +import com.simprints.core.domain.externalcredential.ExternalCredentialType + +internal data class ExternalCredentialState( + val selectedType: ExternalCredentialType? +) { + companion object { + val EMPTY = ExternalCredentialState( + selectedType = null + ) + } +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt new file mode 100644 index 0000000000..1cb2f215dd --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt @@ -0,0 +1,37 @@ +package com.simprints.feature.externalcredential.screens.controller + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import com.simprints.infra.resources.R as IDR + +@HiltViewModel +internal class ExternalCredentialViewModel @Inject internal constructor( + +) : ViewModel() { + + private var state: ExternalCredentialState = ExternalCredentialState.EMPTY + set(value) { + field = value + _stateLiveData.postValue(value) + } + private val _stateLiveData = MutableLiveData() + val stateLiveData: LiveData = _stateLiveData + + fun setSelectedExternalCredentialType(selectedType: ExternalCredentialType?) { + updateState { it.copy(selectedType = selectedType) } + } + + private fun updateState(state: (ExternalCredentialState) -> ExternalCredentialState) { + this.state = state(this.state) + } + + fun mapTypeToStringResource(type: ExternalCredentialType) = when(type) { + ExternalCredentialType.NHISCard -> IDR.string.mfid_type_nhis_card + ExternalCredentialType.GhanaIdCard -> IDR.string.mfid_type_ghana_id_card + ExternalCredentialType.QRCode -> IDR.string.mfid_type_qr_code + } +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt index ffd14f1e37..65235eac9e 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt @@ -1,9 +1,163 @@ package com.simprints.feature.externalcredential.screens.select +import android.app.Dialog +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.TextView import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.feature.externalcredential.R +import com.simprints.feature.externalcredential.databinding.FragmentExternalCredentialSelectBinding +import com.simprints.feature.externalcredential.screens.controller.ExternalCredentialViewModel +import com.simprints.feature.externalcredential.screens.select.view.ExternalCredentialTypeAdapter +import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ORCHESTRATION +import com.simprints.infra.logging.Simber +import com.simprints.infra.uibase.navigation.navigateSafely +import com.simprints.infra.uibase.view.applySystemBarInsets +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint +import kotlin.getValue +import com.simprints.infra.resources.R as IDR @AndroidEntryPoint internal class ExternalCredentialSelectFragment : Fragment(R.layout.fragment_external_credential_select) { + + private val mainViewModel by viewModels() + private val viewModel by viewModels() + private val binding by viewBinding(FragmentExternalCredentialSelectBinding::bind) + + private var dialog: Dialog? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + applySystemBarInsets(view) + Simber.i("ExternalCredentialSelectFragment started", tag = ORCHESTRATION) + + observeChanges() + viewModel.loadExternalCredentials() + } + + override fun onDestroy() { + dismissDialog() + super.onDestroy() + } + + private fun dismissDialog() { + dialog?.dismiss() + dialog = null + } + + private fun initListeners(types: List) { + binding.skipScanning.setOnClickListener { + displaySkipScanningConfirmationDialog( + credentialTypes = types, + onConfirm = { + dismissDialog() + findNavController().navigateSafely( + this, + ExternalCredentialSelectFragmentDirections.actionExternalCredentialSelectFragmentToExternalCredentialSkip(), + ) + }, + onCancel = ::dismissDialog + ) + } + } + + private fun initViews(types: List) { + binding.title.text = when (types.size) { + 1 -> { + val documentType = getString(mainViewModel.mapTypeToStringResource(types.first())) + getString(IDR.string.mfid_scanner_selection_title_specific).format(documentType) + } + + else -> getString(IDR.string.mfid_scanner_selection_title_generic) + } + } + + private fun observeChanges() { + viewModel.externalCredentialTypes.observe(viewLifecycleOwner) { externalCredentialTypes -> + updateSelectedCredentialType(null) + fillRecyclerView(externalCredentialTypes) + initViews(externalCredentialTypes) + initListeners(externalCredentialTypes) + } + } + + private fun fillRecyclerView(types: List) { + with(binding.documentsRecyclerView) { + layoutManager = LinearLayoutManager(requireContext()) + adapter = ExternalCredentialTypeAdapter(types) { selectedType -> + updateSelectedCredentialType(selectedType) + navigateToScanner(selectedType) + } + } + } + + private fun updateSelectedCredentialType(type: ExternalCredentialType?) { + mainViewModel.setSelectedExternalCredentialType(type) + } + + private fun navigateToScanner(type: ExternalCredentialType) { + when (type) { + ExternalCredentialType.NHISCard -> startOcr() + ExternalCredentialType.GhanaIdCard -> startOcr() + ExternalCredentialType.QRCode -> startQrScan() + } + } + + private fun startQrScan() { + findNavController().navigateSafely( + this, + ExternalCredentialSelectFragmentDirections.actionExternalCredentialSelectFragmentToExternalCredentialScanQr(), + ) + } + + private fun displaySkipScanningConfirmationDialog( + credentialTypes: List, + onConfirm: () -> Unit, + onCancel: () -> Unit + ) { + dialog?.dismiss() + dialog = BottomSheetDialog(requireContext()) + val view = layoutInflater.inflate(R.layout.dialog_skip_scan_confirm, null) + val bodyText = view.findViewById(R.id.skipDialogBodyText) + val cancelButton = view.findViewById