Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
40a6346
CardScannerConfiguration in POCardTokenizationConfiguration
vitalii-vanziak-cko Mar 24, 2025
8d00724
po_icon_camera
vitalii-vanziak-cko Mar 24, 2025
2c82a82
Localizations
vitalii-vanziak-cko Mar 25, 2025
7ef633a
Add scan button to Card Tokenization
vitalii-vanziak-cko Mar 25, 2025
a3502ff
clear focus
vitalii-vanziak-cko Mar 25, 2025
9b1690b
Launch card scanner
vitalii-vanziak-cko Mar 25, 2025
9f21ba5
cardScannerAction
vitalii-vanziak-cko Mar 25, 2025
9a061b8
Update POScannedCard
vitalii-vanziak-cko Mar 25, 2025
7b3c190
Improve card scanner animation
vitalii-vanziak-cko Mar 26, 2025
56ce22d
Delay permission request and camera preview initialization for smooth…
vitalii-vanziak-cko Mar 26, 2025
c7d9805
Fix starting of card scanner from card tokenization
vitalii-vanziak-cko Mar 26, 2025
5ffa6ef
CardScannerInteractor logs
vitalii-vanziak-cko Mar 27, 2025
c1075a8
CardTokenizationInteractor raname metods
vitalii-vanziak-cko Mar 27, 2025
af52aab
Logs
vitalii-vanziak-cko Mar 27, 2025
a71bac6
Extended PODynamicCheckoutConfiguration
vitalii-vanziak-cko Mar 27, 2025
601dd46
Updated DynamicCheckoutInteractorState
vitalii-vanziak-cko Mar 27, 2025
5fd89ad
Revert
vitalii-vanziak-cko Mar 27, 2025
f807e61
Map card scanner config on DC
vitalii-vanziak-cko Mar 27, 2025
5c0cd0c
Move card scanner button in Sections on CardTokenizationScreen
vitalii-vanziak-cko Mar 27, 2025
1d96cf0
Add card scanner button on DC
vitalii-vanziak-cko Mar 27, 2025
123bf2c
Dispatchers.Main.immediate
vitalii-vanziak-cko Mar 27, 2025
aa3b923
Log
vitalii-vanziak-cko Mar 27, 2025
4350ed1
dispatchSideEffects
vitalii-vanziak-cko Mar 27, 2025
a920531
Logs
vitalii-vanziak-cko Mar 27, 2025
8b72e46
DC: launch card scanner and handle result
vitalii-vanziak-cko Mar 27, 2025
fd4851a
Fix initConfiguration() on DC
vitalii-vanziak-cko Mar 27, 2025
5716173
AGP 8.9.1
vitalii-vanziak-cko Mar 27, 2025
4e332c4
Updated libs
vitalii-vanziak-cko Mar 27, 2025
3f01f94
Kotlin 2.1.20
vitalii-vanziak-cko Mar 27, 2025
f5de93b
toUri
vitalii-vanziak-cko Mar 27, 2025
3518ac0
lateinit var configuration: PODynamicCheckoutConfiguration
vitalii-vanziak-cko Mar 27, 2025
f3ee04b
Add OCR metadata to Example app
vitalii-vanziak-cko Mar 27, 2025
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
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

buildscript {
ext {
androidGradlePluginVersion = '8.9.0'
kotlinVersion = '2.1.10'
kspVersion = '2.1.10-1.0.29'
androidGradlePluginVersion = '8.9.1'
kotlinVersion = '2.1.20'
kspVersion = '2.1.20-1.0.32'
dokkaVersion = '1.9.20'
androidxNavigationVersion = '2.8.6'
androidxNavigationVersion = '2.8.9'
nexusPublishPluginVersion = '2.0.0'
}
dependencies {
Expand Down Expand Up @@ -38,14 +38,14 @@ ext {

androidxCoreVersion = '1.15.0'
androidxAppCompatVersion = '1.7.0'
androidxConstraintLayoutVersion = '2.2.0'
androidxActivityVersion = '1.10.0'
androidxFragmentVersion = '1.8.5'
androidxConstraintLayoutVersion = '2.2.1'
androidxActivityVersion = '1.10.1'
androidxFragmentVersion = '1.8.6'
androidxLifecycleVersion = '2.8.7'
androidxRecyclerViewVersion = '1.4.0'
androidxSwipeRefreshLayoutVersion = '1.1.0'
androidxBrowserVersion = '1.8.0'
androidxCameraVersion = '1.4.1'
androidxCameraVersion = '1.4.2'

androidxComposeBOMVersion = '2025.03.00'
composeGooglePayButtonVersion = '1.0.0'
Expand Down
4 changes: 4 additions & 0 deletions example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="ocr" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import com.processout.sdk.core.ProcessOutActivityResult
import com.processout.sdk.core.onFailure
import com.processout.sdk.core.onSuccess
import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration
import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.Button
import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.CardScannerConfiguration
import com.processout.sdk.ui.card.tokenization.POCardTokenizationLauncher
import com.processout.sdk.ui.shared.view.dialog.POAlertDialog
import com.processout.sdk.ui.threeds.PO3DSRedirectCustomTabLauncher
Expand Down Expand Up @@ -99,8 +99,8 @@ class CardPaymentFragment : BaseFragment<FragmentCardPaymentBinding>(
viewModel.onTokenizing()
launcher.launch(
POCardTokenizationConfiguration(
savingAllowed = true,
submitButton = Button()
cardScanner = CardScannerConfiguration(),
savingAllowed = true
)
)
}
Expand Down
1 change: 1 addition & 0 deletions sdk/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<string name="po_card_tokenization_title">إضافة بطاقة جديدة</string>
<string name="po_card_tokenization_button_submit">إرسال</string>
<string name="po_card_tokenization_button_cancel">إلغاء</string>
<string name="po_card_tokenization_button_scan">امسح البطاقة</string>
<string name="po_card_tokenization_preferred_scheme">النظام المفضل للبطاقة</string>
<string name="po_card_tokenization_card_details_cardholder_placeholder">اسم حامل البطاقة</string>
<string name="po_card_tokenization_card_details_cvc_placeholder">كود التحقق CVV</string>
Expand Down
1 change: 1 addition & 0 deletions sdk/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<string name="po_card_tokenization_title">Ajouter une nouvelle carte</string>
<string name="po_card_tokenization_button_submit">Envoyer</string>
<string name="po_card_tokenization_button_cancel">Annuler</string>
<string name="po_card_tokenization_button_scan">Scanner la carte</string>
<string name="po_card_tokenization_preferred_scheme">Réseau de Carte Préféré</string>
<string name="po_card_tokenization_card_details_cardholder_placeholder">Nom du porteur de carte</string>
<string name="po_card_tokenization_card_details_cvc_placeholder">CVV</string>
Expand Down
1 change: 1 addition & 0 deletions sdk/src/main/res/values-pl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<string name="po_card_tokenization_title">Dodaj nową kartę</string>
<string name="po_card_tokenization_button_submit">Zatwierdź</string>
<string name="po_card_tokenization_button_cancel">Anuluj</string>
<string name="po_card_tokenization_button_scan">Skanowanie karty</string>
<string name="po_card_tokenization_preferred_scheme">Preferowany Schemat</string>
<string name="po_card_tokenization_card_details_cardholder_placeholder">Imię i nazwisko na karcie</string>
<string name="po_card_tokenization_card_details_cvc_placeholder">CVC</string>
Expand Down
1 change: 1 addition & 0 deletions sdk/src/main/res/values-pt/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<string name="po_card_tokenization_title">Adicionar novo cartão</string>
<string name="po_card_tokenization_button_submit">Enviar</string>
<string name="po_card_tokenization_button_cancel">Cancelar</string>
<string name="po_card_tokenization_button_scan">Digitalizar cartão</string>
<string name="po_card_tokenization_preferred_scheme">Rede de pagamento preferencial</string>
<string name="po_card_tokenization_card_details_cardholder_placeholder">Nome no cartão</string>
<string name="po_card_tokenization_card_details_cvc_placeholder">CVC</string>
Expand Down
1 change: 1 addition & 0 deletions sdk/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<string name="po_card_tokenization_title">Add New Card</string>
<string name="po_card_tokenization_button_submit">Submit</string>
<string name="po_card_tokenization_button_cancel">Cancel</string>
<string name="po_card_tokenization_button_scan">Scan card</string>
<string name="po_card_tokenization_preferred_scheme">Preferred Scheme</string>
<string name="po_card_tokenization_card_details_cardholder_placeholder">Cardholder Name</string>
<string name="po_card_tokenization_card_details_cvc_placeholder">CVC</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.processout.sdk.ui.card.scanner.CardScannerSideEffect.CameraPermission
import com.processout.sdk.ui.card.scanner.recognition.CardRecognitionSession
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
Expand All @@ -21,6 +22,10 @@ internal class CardScannerInteractor(
private val cardRecognitionSession: CardRecognitionSession
) : BaseInteractor() {

private companion object {
const val INIT_DELAY_MS = 500L
}

private val _completion = MutableStateFlow<CardScannerCompletion>(Awaiting)
val completion = _completion.asStateFlow()

Expand All @@ -31,30 +36,29 @@ internal class CardScannerInteractor(
val sideEffects = _sideEffects.receiveAsFlow()

init {
POLogger.info("Starting card scanner.")
collectRecognizedCards()
interactorScope.launch {
delay(INIT_DELAY_MS)
_sideEffects.send(CameraPermissionRequest)
}
}

private fun initState() = CardScannerInteractorState(
currentCard = null,
isTorchEnabled = false
isCameraPermissionGranted = false,
isTorchEnabled = false,
currentCard = null
)

fun onEvent(event: CardScannerEvent) {
when (event) {
is CameraPermissionResult -> handle(event)
is ImageAnalysis -> interactorScope.launch {
cardRecognitionSession.recognize(event.imageProxy)
}
is TorchToggle -> _state.update { it.copy(isTorchEnabled = event.isEnabled) }
is CameraPermissionResult -> if (!event.isGranted) {
cancel(
ProcessOutResult.Failure(
code = Generic(),
message = "Camera permission is not granted."
)
)
is TorchToggle -> {
POLogger.debug("Torch toggle: ${event.isEnabled}.")
_state.update { it.copy(isTorchEnabled = event.isEnabled) }
}
is Cancel -> cancel(
ProcessOutResult.Failure(
Expand All @@ -66,6 +70,20 @@ internal class CardScannerInteractor(
}
}

private fun handle(event: CameraPermissionResult) {
_state.update { it.copy(isCameraPermissionGranted = event.isGranted) }
if (event.isGranted) {
POLogger.info("Started: camera permission is granted.")
} else {
cancel(
ProcessOutResult.Failure(
code = Generic(),
message = "Camera permission is not granted."
)
)
}
}

private fun collectRecognizedCards() {
interactorScope.launch(Dispatchers.Main.immediate) {
cardRecognitionSession.currentCard.collect { card ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.processout.sdk.ui.card.scanner
import com.processout.sdk.ui.card.scanner.recognition.POScannedCard

internal data class CardScannerInteractorState(
val currentCard: POScannedCard?,
val isTorchEnabled: Boolean
val isCameraPermissionGranted: Boolean,
val isTorchEnabled: Boolean,
val currentCard: POScannedCard?
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import androidx.camera.view.CameraController.IMAGE_ANALYSIS
import androidx.camera.view.CameraController.IMAGE_CAPTURE
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.animation.*
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
Expand Down Expand Up @@ -133,10 +136,9 @@ internal fun CardScannerScreen(
style = style.description.textStyle
)
CameraPreview(
isTorchEnabled = state.torchAction.checked,
currentCard = state.currentCard,
state = state,
onEvent = onEvent,
cameraPreviewStyle = style.cameraPreview,
style = style.cameraPreview,
cardStyle = style.card,
modifier = Modifier
.fillMaxWidth()
Expand All @@ -161,30 +163,21 @@ internal fun CardScannerScreen(

@Composable
private fun CameraPreview(
isTorchEnabled: Boolean,
currentCard: POScannedCard?,
state: CardScannerViewModelState,
onEvent: (CardScannerEvent) -> Unit,
cameraPreviewStyle: CameraPreviewStyle,
style: CameraPreviewStyle,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
offsetSize: Dp = spacing.extraLarge
) {
val cameraController = rememberLifecycleCameraController(
onAnalyze = { imageProxy ->
onEvent(ImageAnalysis(imageProxy))
}
)
LaunchedEffect(isTorchEnabled) {
cameraController.enableTorch(isTorchEnabled)
}
Box(
modifier = modifier
.border(
width = cameraPreviewStyle.border.width,
color = cameraPreviewStyle.border.color,
shape = cameraPreviewStyle.shape
width = style.border.width,
color = style.border.color,
shape = style.shape
)
.clip(cameraPreviewStyle.shape)
.clip(style.shape)
.drawWithContent {
val offsetSizePx = offsetSize.toPx()
val cardSize = androidx.compose.ui.geometry.Size(
Expand All @@ -196,7 +189,7 @@ private fun CameraPreview(
val cornerRadius = CornerRadius(cornerRadiusSizePx, cornerRadiusSizePx)
drawContent()
drawWithLayer {
drawRect(color = cameraPreviewStyle.overlayColor)
drawRect(color = style.overlayColor)
drawRoundRect(
size = cardSize,
topLeft = topLeftOffset,
Expand All @@ -214,20 +207,32 @@ private fun CameraPreview(
)
}
) {
AndroidView(
modifier = modifier.clip(cameraPreviewStyle.shape),
factory = {
PreviewView(it).apply {
controller = cameraController
clipToOutline = true
scaleType = PreviewView.ScaleType.FILL_START
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
if (state.isCameraPermissionGranted) {
val cameraController = rememberLifecycleCameraController(
onAnalyze = { imageProxy ->
onEvent(ImageAnalysis(imageProxy))
}
},
onRelease = { cameraController.unbind() }
)
)
val isTorchEnabled = state.torchAction.checked
LaunchedEffect(isTorchEnabled) {
cameraController.enableTorch(isTorchEnabled)
}
AndroidView(
modifier = modifier,
factory = {
PreviewView(it).apply {
controller = cameraController
scaleType = PreviewView.ScaleType.FILL_START
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
},
onRelease = { cameraController.unbind() }
)
} else {
Box(modifier.background(Color.Black))
}
ScannedCard(
card = currentCard,
card = state.currentCard,
style = cardStyle
)
}
Expand All @@ -253,23 +258,12 @@ private fun ScannedCard(
modifier = Modifier.requiredHeightIn(min = 90.dp),
verticalArrangement = Arrangement.spacedBy(space = 10.dp)
) {
AnimatedContent(
targetState = card?.number,
transitionSpec = {
fadeIn(
animationSpec = tween(durationMillis = AnimationDurationMillis)
) togetherWith fadeOut(
animationSpec = tween(durationMillis = AnimationDurationMillis)
)
}
) { number ->
POTextAutoSize(
text = number ?: String(),
modifier = Modifier.fillMaxWidth(),
color = style.number.color,
style = style.number.textStyle
)
}
POTextAutoSize(
text = card?.number ?: String(),
modifier = Modifier.fillMaxWidth(),
color = style.number.color,
style = style.number.textStyle
)
Row {
POText(
text = card?.cardholderName ?: String(),
Expand Down Expand Up @@ -437,5 +431,5 @@ internal object CardScannerScreen {
/** Height to width ratio of a card by ISO/IEC 7810 standard. */
val CardHeightToWidthRatio = 0.63f

val AnimationDurationMillis = 250
val AnimationDurationMillis = 300
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ internal class CardScannerViewModel(
description = description ?: app.getString(R.string.po_card_scanner_description),
currentCard = state.currentCard,
torchAction = torchAction(state.isTorchEnabled),
cancelAction = cancelButton?.toAction()
cancelAction = cancelButton?.toAction(),
isCameraPermissionGranted = state.isCameraPermissionGranted
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ internal data class CardScannerViewModelState(
val description: String,
val currentCard: POScannedCard?,
val torchAction: POActionState,
val cancelAction: POActionState?
val cancelAction: POActionState?,
val isCameraPermissionGranted: Boolean
)
Loading