Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Identity] Support selfie #5149

Merged
merged 1 commit into from
Jun 16, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -6,6 +6,7 @@ import com.stripe.android.camera.framework.image.cropCenter
import com.stripe.android.camera.framework.image.size
import com.stripe.android.camera.framework.util.maxAspectRatioInSize
import com.stripe.android.identity.states.IdentityScanState
import com.stripe.android.identity.utils.roundToMaxDecimals
import org.tensorflow.lite.DataType
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.support.common.ops.NormalizeOp
Expand Down Expand Up @@ -71,7 +72,7 @@ internal class FaceDetectorAnalyzer(
width = (boundingBoxes[0][2] - boundingBoxes[0][0]) / INPUT_WIDTH,
height = (boundingBoxes[0][3] - boundingBoxes[0][1]) / INPUT_HEIGHT,
),
resultScore = score[0]
resultScore = score[0].roundToMaxDecimals(2)
)
}

Expand Down
Expand Up @@ -6,14 +6,14 @@ import com.stripe.android.camera.framework.image.cropCenter
import com.stripe.android.camera.framework.image.size
import com.stripe.android.camera.framework.util.maxAspectRatioInSize
import com.stripe.android.identity.states.IdentityScanState
import com.stripe.android.identity.utils.roundToMaxDecimals
import org.tensorflow.lite.DataType
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.support.common.ops.NormalizeOp
import org.tensorflow.lite.support.image.ImageProcessor
import org.tensorflow.lite.support.image.TensorImage
import org.tensorflow.lite.support.image.ops.ResizeOp
import java.io.File
import kotlin.math.roundToInt

/**
* Analyzer to run IDDetector.
Expand Down Expand Up @@ -88,19 +88,6 @@ internal class IDDetectorAnalyzer(modelFile: File, private val idDetectorMinScor
)
}

/**
* Round a float to max decimals. Backend requires scores uploaded with max 2 decimals.
*
* e.g -
* 3.123f.roundToMaxDecimals(2) = 3.12
* 3.499f.roundToMaxDecimals(2) = 3.5
*/
private fun Float.roundToMaxDecimals(decimals: Int): Float {
var multiplier = 1.0f
repeat(decimals) { multiplier *= 10 }
return (this * multiplier).roundToInt() / multiplier
}

// TODO(ccen): check if we should enable this to track stats
override val statsName: String? = null

Expand Down
Expand Up @@ -19,6 +19,7 @@ import com.stripe.android.identity.networking.models.ClearDataParam
import com.stripe.android.identity.networking.models.CollectedDataParam
import com.stripe.android.identity.networking.models.VerificationPage.Companion.isMissingBiometricConsent
import com.stripe.android.identity.networking.models.VerificationPage.Companion.isUnsupportedClient
import com.stripe.android.identity.networking.models.VerificationPage.Companion.requireSelfie
import com.stripe.android.identity.networking.models.VerificationPageStaticContentConsentPage
import com.stripe.android.identity.utils.navigateToErrorFragmentWithFailedReason
import com.stripe.android.identity.utils.postVerificationPageDataAndMaybeSubmit
Expand Down Expand Up @@ -58,27 +59,6 @@ internal class ConsentFragment(
binding.merchantLogo
)

binding.agree.setOnClickListener {
binding.agree.toggleToLoading()
binding.decline.isEnabled = false
postVerificationPageDataAndNavigate(
CollectedDataParam(
biometricConsent = true
)
)
}
binding.decline.setOnClickListener {
binding.decline.toggleToLoading()
binding.agree.isEnabled = false
postVerificationPageDataAndNavigate(
CollectedDataParam(
biometricConsent = false
)
)
}
binding.progressCircular.setOnClickListener {
setLoadingFinishedUI()
}
return binding.root
}

Expand All @@ -92,7 +72,10 @@ internal class ConsentFragment(
fallbackUrlLauncher.launchFallbackUrl(verificationPage.fallbackUrl)
} else if (verificationPage.isMissingBiometricConsent()) {
setLoadingFinishedUI()
bindViewData(verificationPage.biometricConsent)
bindViewData(
verificationPage.biometricConsent,
verificationPage.requireSelfie()
)
} else {
navigateToDocSelection()
}
Expand All @@ -116,12 +99,15 @@ internal class ConsentFragment(
/**
* Post VerificationPageData with the type and navigate base on its result.
*/
private fun postVerificationPageDataAndNavigate(collectedDataParam: CollectedDataParam) {
private fun postVerificationPageDataAndNavigate(
collectedDataParam: CollectedDataParam,
requireSelfie: Boolean
) {
lifecycleScope.launch {
postVerificationPageDataAndMaybeSubmit(
identityViewModel,
collectedDataParam,
ClearDataParam.CONSENT_TO_DOC_SELECT,
if (requireSelfie) ClearDataParam.CONSENT_TO_DOC_SELECT_WITH_SELFIE else ClearDataParam.CONSENT_TO_DOC_SELECT,
fromFragment = R.id.consentFragment,
notSubmitBlock = {
navigateToDocSelection()
Expand All @@ -134,7 +120,10 @@ internal class ConsentFragment(
findNavController().navigate(R.id.action_consentFragment_to_docSelectionFragment)
}

private fun bindViewData(consentPage: VerificationPageStaticContentConsentPage) {
private fun bindViewData(
consentPage: VerificationPageStaticContentConsentPage,
requireSelfie: Boolean
) {
binding.titleText.text = consentPage.title

consentPage.privacyPolicy?.let {
Expand All @@ -158,6 +147,30 @@ internal class ConsentFragment(
binding.body.setHtmlString(consentPage.body)
binding.agree.setText(consentPage.acceptButtonText)
binding.decline.setText(consentPage.declineButtonText)

binding.agree.setOnClickListener {
binding.agree.toggleToLoading()
binding.decline.isEnabled = false
postVerificationPageDataAndNavigate(
CollectedDataParam(
biometricConsent = true
),
requireSelfie
)
}
binding.decline.setOnClickListener {
binding.decline.toggleToLoading()
binding.agree.isEnabled = false
postVerificationPageDataAndNavigate(
CollectedDataParam(
biometricConsent = false
),
requireSelfie
)
}
binding.progressCircular.setOnClickListener {
setLoadingFinishedUI()
}
}

private fun setLoadingFinishedUI() {
Expand Down
Expand Up @@ -21,6 +21,7 @@ import com.stripe.android.identity.networking.Status
import com.stripe.android.identity.networking.models.ClearDataParam
import com.stripe.android.identity.networking.models.CollectedDataParam
import com.stripe.android.identity.networking.models.CollectedDataParam.Type
import com.stripe.android.identity.networking.models.VerificationPage.Companion.requireSelfie
import com.stripe.android.identity.states.IdentityScanState
import com.stripe.android.identity.utils.navigateToDefaultErrorFragment
import com.stripe.android.identity.utils.navigateToUploadFragment
Expand Down Expand Up @@ -59,19 +60,23 @@ internal class DocSelectionFragment(
binding.title.text = verificationPage.documentSelect.title
when (verificationPage.documentSelect.idDocumentTypeAllowlist.count()) {
0 -> {
toggleMultiSelectionUI()
toggleMultiSelectionUI(requireSelfie = verificationPage.requireSelfie())
}
1 -> {
verificationPage.documentSelect.let { documentSelect ->
toggleSingleSelectionUI(
documentSelect.idDocumentTypeAllowlist.entries.first().key,
documentSelect.buttonText,
documentSelect.body,
verificationPage.requireSelfie()
)
}
}
else -> {
toggleMultiSelectionUI(verificationPage.documentSelect.idDocumentTypeAllowlist)
toggleMultiSelectionUI(
verificationPage.documentSelect.idDocumentTypeAllowlist,
verificationPage.requireSelfie()
)
}
}
},
Expand All @@ -91,7 +96,10 @@ internal class DocSelectionFragment(
* Toggle UI to show multiple selection types. If idDocumentTypeAllowlist from server is null,
* show all three types with default values.
*/
private fun toggleMultiSelectionUI(idDocumentTypeAllowlist: Map<String, String>? = null) {
private fun toggleMultiSelectionUI(
idDocumentTypeAllowlist: Map<String, String>? = null,
requireSelfie: Boolean
) {
binding.multiSelectionContent.visibility = View.VISIBLE
binding.singleSelectionContent.visibility = View.GONE
idDocumentTypeAllowlist?.let {
Expand All @@ -105,7 +113,7 @@ internal class DocSelectionFragment(
binding.dl.isClickable = false
binding.id.isClickable = false
binding.passportIndicator.visibility = View.VISIBLE
postVerificationPageDataAndNavigate(Type.PASSPORT)
postVerificationPageDataAndNavigate(Type.PASSPORT, requireSelfie)
}
binding.passportSeparator.visibility = View.VISIBLE
}
Expand All @@ -117,7 +125,7 @@ internal class DocSelectionFragment(
binding.passport.isClickable = false
binding.id.isClickable = false
binding.dlIndicator.visibility = View.VISIBLE
postVerificationPageDataAndNavigate(Type.DRIVINGLICENSE)
postVerificationPageDataAndNavigate(Type.DRIVINGLICENSE, requireSelfie)
}
binding.dlSeparator.visibility = View.VISIBLE
}
Expand All @@ -129,7 +137,7 @@ internal class DocSelectionFragment(
binding.passport.isClickable = false
binding.dl.isClickable = false
binding.idIndicator.visibility = View.VISIBLE
postVerificationPageDataAndNavigate(Type.IDCARD)
postVerificationPageDataAndNavigate(Type.IDCARD, requireSelfie)
}
binding.idSeparator.visibility = View.VISIBLE
}
Expand All @@ -151,7 +159,8 @@ internal class DocSelectionFragment(
private fun toggleSingleSelectionUI(
allowedType: String,
buttonText: String,
bodyText: String?
bodyText: String?,
requireSelfie: Boolean
) {
binding.multiSelectionContent.visibility = View.GONE
binding.singleSelectionContent.visibility = View.VISIBLE
Expand All @@ -162,21 +171,21 @@ internal class DocSelectionFragment(
binding.singleSelectionBody.text = bodyText
binding.singleSelectionContinue.setOnClickListener {
binding.singleSelectionContinue.toggleToLoading()
postVerificationPageDataAndNavigate(Type.PASSPORT)
postVerificationPageDataAndNavigate(Type.PASSPORT, requireSelfie)
}
}
DRIVING_LICENSE_KEY -> {
binding.singleSelectionBody.text = bodyText
binding.singleSelectionContinue.setOnClickListener {
binding.singleSelectionContinue.toggleToLoading()
postVerificationPageDataAndNavigate(Type.DRIVINGLICENSE)
postVerificationPageDataAndNavigate(Type.DRIVINGLICENSE, requireSelfie)
}
}
ID_CARD_KEY -> {
binding.singleSelectionBody.text = bodyText
binding.singleSelectionContinue.setOnClickListener {
binding.singleSelectionContinue.toggleToLoading()
postVerificationPageDataAndNavigate(Type.IDCARD)
postVerificationPageDataAndNavigate(Type.IDCARD, requireSelfie)
}
}
else -> {
Expand All @@ -188,12 +197,13 @@ internal class DocSelectionFragment(
/**
* Post VerificationPageData with the type and navigate base on its result.
*/
private fun postVerificationPageDataAndNavigate(type: Type) {
private fun postVerificationPageDataAndNavigate(type: Type, requireSelfie: Boolean) {
lifecycleScope.launch {
postVerificationPageDataAndMaybeSubmit(
identityViewModel = identityViewModel,
collectedDataParam = CollectedDataParam(idDocumentType = type),
clearDataParam = ClearDataParam.DOC_SELECT_TO_UPLOAD,
clearDataParam =
if (requireSelfie) ClearDataParam.DOC_SELECT_TO_UPLOAD_WITH_SELFIE else ClearDataParam.DOC_SELECT_TO_UPLOAD,
fromFragment = R.id.docSelectionFragment,
notSubmitBlock = {
cameraPermissionEnsureable.ensureCameraPermission(
Expand Down
Expand Up @@ -21,6 +21,7 @@ import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory
import com.stripe.android.identity.databinding.IdentityDocumentScanFragmentBinding
import com.stripe.android.identity.networking.models.ClearDataParam
import com.stripe.android.identity.networking.models.CollectedDataParam
import com.stripe.android.identity.networking.models.VerificationPage.Companion.requireSelfie
import com.stripe.android.identity.states.IdentityScanState
import com.stripe.android.identity.ui.LoadingButton
import com.stripe.android.identity.utils.navigateToDefaultErrorFragment
Expand Down Expand Up @@ -166,25 +167,37 @@ internal abstract class IdentityDocumentScanFragment(
onSuccess = { verificationPage ->
lifecycleScope.launch {
runCatching {
postVerificationPageDataAndMaybeSubmit(
identityViewModel = identityViewModel,
collectedDataParam =
CollectedDataParam.createFromUploadedResultsForAutoCapture(
type = type,
frontHighResResult = requireNotNull(it.frontHighResResult.data),
frontLowResResult = requireNotNull(it.frontLowResResult.data),
backHighResResult = requireNotNull(it.backHighResResult.data),
backLowResResult = requireNotNull(it.backLowResResult.data)
),
clearDataParam = ClearDataParam.UPLOAD_TO_CONFIRM,
fromFragment = fragmentId,
notSubmitBlock =
verificationPage.selfieCapture?.let {
{
findNavController().navigate(R.id.action_global_selfieFragment)
}
if (verificationPage.requireSelfie()) {
postVerificationPageDataAndMaybeSubmit(
identityViewModel = identityViewModel,
collectedDataParam =
CollectedDataParam.createFromUploadedResultsForAutoCapture(
type = type,
frontHighResResult = requireNotNull(it.frontHighResResult.data),
frontLowResResult = requireNotNull(it.frontLowResResult.data),
backHighResResult = requireNotNull(it.backHighResResult.data),
backLowResResult = requireNotNull(it.backLowResResult.data)
),
clearDataParam = ClearDataParam.UPLOAD_TO_SELFIE,
fromFragment = fragmentId
) {
findNavController().navigate(R.id.action_global_selfieFragment)
}
)
} else {
postVerificationPageDataAndMaybeSubmit(
identityViewModel = identityViewModel,
collectedDataParam =
CollectedDataParam.createFromUploadedResultsForAutoCapture(
type = type,
frontHighResResult = requireNotNull(it.frontHighResResult.data),
frontLowResResult = requireNotNull(it.frontLowResResult.data),
backHighResResult = requireNotNull(it.backHighResResult.data),
backLowResResult = requireNotNull(it.backLowResResult.data)
),
clearDataParam = ClearDataParam.UPLOAD_TO_CONFIRM,
fromFragment = fragmentId
)
}
}.onFailure { throwable ->
Log.e(
TAG,
Expand Down