Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
feature:alert
feature:exit-form
feature:select-subject-age-group
feature:external-credential
reportsId: feature1

feature-unit-tests2:
Expand Down
2 changes: 2 additions & 0 deletions feature/external-credential/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ android {
}

dependencies {
implementation(project(":infra:config-store"))
implementation(project(":infra:config-sync"))
implementation(project(":feature:exit-form"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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(ExternalCredentialState.EMPTY)
val stateLiveData: LiveData<ExternalCredentialState> = _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
}
}
Original file line number Diff line number Diff line change
@@ -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<ExternalCredentialViewModel>()
private val viewModel by viewModels<ExternalCredentialSelectViewModel>()
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<ExternalCredentialType>) {
binding.skipScanning.setOnClickListener {
displaySkipScanningConfirmationDialog(
credentialTypes = types,
onConfirm = {
dismissDialog()
findNavController().navigateSafely(
this,
ExternalCredentialSelectFragmentDirections.actionExternalCredentialSelectFragmentToExternalCredentialSkip(),
)
},
onCancel = ::dismissDialog
)
}
}

private fun initViews(types: List<ExternalCredentialType>) {
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<ExternalCredentialType>) {
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<ExternalCredentialType>,
onConfirm: () -> Unit,
onCancel: () -> Unit
) {
dialog?.dismiss()
dialog = BottomSheetDialog(requireContext())
val view = layoutInflater.inflate(R.layout.dialog_skip_scan_confirm, null)
val bodyText = view.findViewById<TextView>(R.id.skipDialogBodyText)
val cancelButton = view.findViewById<Button>(R.id.buttonCancel)
val confirmButton = view.findViewById<Button>(R.id.buttonSkip)

bodyText.text = when (credentialTypes.size) {
1 -> {
val documentType = getString(mainViewModel.mapTypeToStringResource(credentialTypes.first()))
getString(IDR.string.mfid_dialog_skip_scan_body_specific).format(documentType)
}

else -> getString(IDR.string.mfid_dialog_skip_scan_body_generic)
}

confirmButton.setOnClickListener { onConfirm() }
cancelButton.setOnClickListener { onCancel() }

dialog?.setContentView(view)
dialog?.setCancelable(true)
dialog?.show()
(dialog as? BottomSheetDialog)?.apply {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.isDraggable = false
}

}

private fun startOcr() {
// TODO [MS-1163] add OCR parameters to navigation once the OCR fragment is implemented
findNavController().navigateSafely(
this,
ExternalCredentialSelectFragmentDirections.actionExternalCredentialSelectFragmentToExternalCredentialScanOcr(),
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.simprints.feature.externalcredential.screens.select

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.simprints.core.domain.externalcredential.ExternalCredentialType
import com.simprints.infra.config.sync.ConfigManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
internal class ExternalCredentialSelectViewModel @Inject internal constructor(
private val configManager: ConfigManager,
) : ViewModel() {

val externalCredentialTypes: LiveData<List<ExternalCredentialType>>
get() = _externalCredentialTypes
private val _externalCredentialTypes = MutableLiveData<List<ExternalCredentialType>>()

fun loadExternalCredentials() {
viewModelScope.launch {
val config = configManager.getProjectConfiguration()
val allowedExternalCredentials = config.multifactorId?.allowedExternalCredentials.orEmpty()
_externalCredentialTypes.postValue(allowedExternalCredentials)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.simprints.feature.externalcredential.screens.select.view

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.simprints.core.ExcludedFromGeneratedTestCoverageReports
import com.simprints.core.domain.externalcredential.ExternalCredentialType
import com.simprints.feature.externalcredential.R
import com.simprints.feature.externalcredential.databinding.ItemDocumentBinding
import com.simprints.infra.resources.R as IDR

@ExcludedFromGeneratedTestCoverageReports("UI classes are not unit tested")
internal class ExternalCredentialTypeAdapter(
private val items: List<ExternalCredentialType>,
private val onClick: (ExternalCredentialType) -> Unit = {}
) : RecyclerView.Adapter<ExternalCredentialTypeAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder = ViewHolder(ItemDocumentBinding.inflate(LayoutInflater.from(parent.context), parent, false))

override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) = holder.bind(items[position])

override fun getItemCount(): Int = items.size

inner class ViewHolder(private val binding: ItemDocumentBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(credentialType: ExternalCredentialType) {
val c = binding.root.context
val text = c.getString(IDR.string.mfid_action_scan_document) + " " + when (credentialType) {
ExternalCredentialType.NHISCard -> c.getString(IDR.string.mfid_type_nhis_card)
ExternalCredentialType.GhanaIdCard -> c.getString(IDR.string.mfid_type_ghana_id_card)
ExternalCredentialType.QRCode -> c.getString(IDR.string.mfid_type_qr_code)
}
val image = when (credentialType) {
ExternalCredentialType.NHISCard -> R.drawable.ghana_nhis_card
ExternalCredentialType.GhanaIdCard -> R.drawable.ghana_id_card
ExternalCredentialType.QRCode -> R.drawable.qr_code
}
binding.documentText.text = text
binding.documentImage.setImageResource(image)
binding.root.setOnClickListener { onClick(credentialType) }
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/simprints_white"
android:elevation="@dimen/cardview_default_elevation"
app:cardUseCompatPadding="true">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/documentImage"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="4dp"
android:scaleType="centerCrop"
android:src="@drawable/ghana_nhis_card"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,16:10"
app:layout_constraintEnd_toStartOf="@id/documentText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3" />

<TextView
android:id="@+id/documentText"
style="@style/Text.Body2"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/documentImage"
app:layout_constraintTop_toTopOf="parent"
tools:text="Scan this document" />

</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
Loading
Loading