Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9d48b76
POCardTokenizationDelegate and propagating of POCardTokenizationEvent
vitalii-vanziak-cko Apr 8, 2025
da4a8a2
Deprecation
vitalii-vanziak-cko Apr 9, 2025
990bcc8
Implement models as POEventDispatcher request/response
vitalii-vanziak-cko Apr 9, 2025
d4725b3
preferredScheme and shouldContinue
vitalii-vanziak-cko Apr 9, 2025
8a3dec2
CustomerActionsService
vitalii-vanziak-cko Apr 10, 2025
8959d88
Re-implemented DefaultCustomerActionsService using coroutines, applie…
vitalii-vanziak-cko Apr 10, 2025
1aa8df4
DefaultCustomerActionsService: removed redundant private modifier fro…
vitalii-vanziak-cko Apr 10, 2025
a49772c
GATEWAY_TOKEN_PREFIX
vitalii-vanziak-cko Apr 10, 2025
b339202
GatewayRequest
vitalii-vanziak-cko Apr 10, 2025
9a9b4d9
gatewayToken
vitalii-vanziak-cko Apr 10, 2025
8f82235
Const names
vitalii-vanziak-cko Apr 10, 2025
b9fe1e6
Code improvements
vitalii-vanziak-cko Apr 10, 2025
5c434aa
action -> customerAction
vitalii-vanziak-cko Apr 11, 2025
e012f13
Refactor invoice auth
vitalii-vanziak-cko Apr 14, 2025
840f195
Return Job from authorizeInvoice(), updated KDoc
vitalii-vanziak-cko Apr 15, 2025
ea0e023
Use new authorize() on DC
vitalii-vanziak-cko Apr 15, 2025
5fd0160
Reimplement assigning of a customer token
vitalii-vanziak-cko Apr 15, 2025
5c8a88d
processTokenizedCard() - delegate, response event, launcher
vitalii-vanziak-cko Apr 15, 2025
7730217
requestToProcessTokenizedCard()
vitalii-vanziak-cko Apr 16, 2025
3cd03fe
Deprecated
vitalii-vanziak-cko Apr 16, 2025
539921e
Update example app
vitalii-vanziak-cko Apr 17, 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
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ import com.processout.example.shared.Constants
import com.processout.example.shared.toMessage
import com.processout.example.ui.screen.MainActivity
import com.processout.example.ui.screen.base.BaseFragment
import com.processout.example.ui.screen.card.payment.CardPaymentUiState.*
import com.processout.example.ui.screen.card.payment.CardPaymentViewModelEvent.LaunchTokenization
import com.processout.sdk.api.ProcessOut
import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest
import com.processout.sdk.api.model.response.POCard
import com.processout.sdk.api.service.PO3DSService
import com.processout.sdk.checkout.threeds.POCheckout3DSService
Expand All @@ -32,7 +31,9 @@ import com.processout.sdk.ui.card.tokenization.POCardTokenizationLauncher
import com.processout.sdk.ui.shared.view.dialog.POAlertDialog
import com.processout.sdk.ui.threeds.PO3DSRedirectCustomTabLauncher
import com.processout.sdk.ui.threeds.POTest3DSService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class CardPaymentFragment : BaseFragment<FragmentCardPaymentBinding>(
FragmentCardPaymentBinding::inflate
Expand All @@ -42,15 +43,18 @@ class CardPaymentFragment : BaseFragment<FragmentCardPaymentBinding>(
CardPaymentViewModel.Factory()
}

private val invoices = ProcessOut.instance.invoices
private val dispatcher = ProcessOut.instance.dispatchers.cardTokenization
private lateinit var launcher: POCardTokenizationLauncher
private lateinit var customTabLauncher: PO3DSRedirectCustomTabLauncher

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launcher = POCardTokenizationLauncher.create(
from = this,
delegate = DefaultCardTokenizationDelegate(
viewModel = viewModel,
invoices = ProcessOut.instance.invoices,
provide3DSService = ::create3DSService
),
callback = ::handle
)
customTabLauncher = PO3DSRedirectCustomTabLauncher.create(from = this)
Expand All @@ -59,44 +63,35 @@ class CardPaymentFragment : BaseFragment<FragmentCardPaymentBinding>(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setOnClickListeners()
viewLifecycleOwner.lifecycleScope.launch {
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.uiState.collect { handle(it) }
}
}
viewLifecycleOwner.lifecycleScope.launch {
dispatcher.processTokenizedCardRequest.collect { request ->
viewModel.onTokenized(request)
}
}
viewLifecycleOwner.lifecycleScope.launch {
invoices.authorizeInvoiceResult.collect { result ->
dispatcher.complete(result)
withContext(Dispatchers.Main.immediate) {
viewModel.events.collect { handle(it) }
}
}
}
}

private fun handle(result: ProcessOutActivityResult<POCard>) {
val uiState = viewModel.uiState.value
val invoiceId = if (uiState is Authorizing) uiState.uiModel.invoiceId else null
viewModel.reset()
enableControls(isEnabled = true)
result.onSuccess { card ->
val invoiceId = viewModel.state.value.invoiceId
showAlert(getString(R.string.authorize_invoice_success_format, invoiceId, card.id))
}.onFailure { showAlert(it.toMessage()) }
}.onFailure {
showAlert(it.toMessage())
}
}

private fun handle(uiState: CardPaymentUiState) {
handleControls(uiState)
when (uiState) {
is Submitted -> launchCardTokenization()
is Tokenized -> authorizeInvoice(uiState.uiModel)
is Failure -> showAlert(uiState.failure.toMessage())
else -> {}
private fun handle(event: CardPaymentViewModelEvent) {
when (event) {
LaunchTokenization -> {
enableControls(isEnabled = false)
launchTokenization()
}
}
}

private fun launchCardTokenization() {
viewModel.onTokenizing()
private fun launchTokenization() {
launcher.launch(
POCardTokenizationConfiguration(
cardScanner = CardScannerConfiguration(),
Expand All @@ -105,19 +100,6 @@ class CardPaymentFragment : BaseFragment<FragmentCardPaymentBinding>(
)
}

private fun authorizeInvoice(uiModel: CardPaymentUiModel) {
invoices.authorizeInvoice(
request = POInvoiceAuthorizationRequest(
invoiceId = uiModel.invoiceId,
source = uiModel.cardId,
saveSource = uiModel.saveCard,
clientSecret = uiModel.clientSecret
),
threeDSService = create3DSService()
)
viewModel.onAuthorizing()
}

private fun create3DSService(): PO3DSService {
val selected3DSService = with(binding.threedsServiceRadioGroup) {
findViewById<RadioButton>(checkedRadioButtonId).text.toString()
Expand Down Expand Up @@ -169,18 +151,10 @@ class CardPaymentFragment : BaseFragment<FragmentCardPaymentBinding>(

private fun onSubmitClick() {
with(binding) {
val details = InvoiceDetails(
viewModel.submit(
amount = amountInput.text.toString(),
currency = currencyInput.text.toString()
)
viewModel.submit(details)
}
}

private fun handleControls(uiState: CardPaymentUiState) {
when (uiState) {
Initial, is Failure -> enableControls(true)
else -> enableControls(false)
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.processout.example.shared.Constants
import com.processout.example.ui.screen.card.payment.CardPaymentUiState.*
import com.processout.example.ui.screen.card.payment.CardPaymentViewModelEvent.LaunchTokenization
import com.processout.sdk.api.ProcessOut
import com.processout.sdk.api.model.request.POCardTokenizationProcessingRequest
import com.processout.sdk.api.model.request.POCreateCustomerRequest
import com.processout.sdk.api.model.request.POCreateInvoiceRequest
import com.processout.sdk.api.model.response.POCustomer
import com.processout.sdk.api.model.response.POInvoice
import com.processout.sdk.api.service.POCustomerTokensService
import com.processout.sdk.api.service.POInvoicesService
import com.processout.sdk.core.getOrNull
import com.processout.sdk.core.onFailure
import com.processout.sdk.core.onSuccess
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.UUID

Expand All @@ -36,40 +37,41 @@ class CardPaymentViewModel(
}
}

private val _uiState = MutableStateFlow<CardPaymentUiState>(Initial)
val uiState = _uiState.asStateFlow()
private val _state = MutableStateFlow(CardPaymentViewModelState())
val state = _state.asStateFlow()

private var customerId: String? = null
private val _events = Channel<CardPaymentViewModelEvent>()
val events = _events.receiveAsFlow()

fun submit(details: InvoiceDetails) {
_uiState.value = Submitting
fun submit(amount: String, currency: String) {
_state.update {
it.copy(
amount = amount,
currency = currency
)
}
viewModelScope.launch {
if (customerId == null) {
customerId = createCustomer()?.id
createInvoice(details)
} else {
createInvoice(details)
}
_events.send(LaunchTokenization)
}
}

private suspend fun createInvoice(details: InvoiceDetails) =
invoices.createInvoice(
suspend fun createInvoice(): POInvoice? {
val state = _state.value
if (state.customerId == null) {
_state.update { it.copy(customerId = createCustomer()?.id) }
}
val invoice = invoices.createInvoice(
POCreateInvoiceRequest(
name = UUID.randomUUID().toString(),
amount = details.amount,
currency = details.currency,
customerId = customerId,
amount = state.amount,
currency = state.currency,
customerId = state.customerId,
returnUrl = Constants.RETURN_URL
)
).onSuccess { invoice ->
_uiState.value = Submitted(
CardPaymentUiModel(
invoiceId = invoice.id,
clientSecret = invoice.clientSecret
)
)
}.onFailure { _uiState.value = Failure(it) }
).getOrNull()
_state.update { it.copy(invoiceId = invoice?.id) }
return invoice
}

private suspend fun createCustomer(): POCustomer? =
customerTokens.createCustomer(
Expand All @@ -79,42 +81,4 @@ class CardPaymentViewModel(
email = "test@email.com"
)
).getOrNull()

fun onTokenizing() {
val uiState = _uiState.value
if (uiState is Submitted) {
_uiState.value = Tokenizing(uiState.uiModel)
}
}

fun onTokenized(request: POCardTokenizationProcessingRequest) {
val uiState = _uiState.value
if (uiState is Tokenizing) {
_uiState.value = Tokenized(
uiState.uiModel.copy(
cardId = request.card.id,
saveCard = request.saveCard
)
)
}
if (uiState is Authorizing) {
_uiState.value = Tokenized(
uiState.uiModel.copy(
cardId = request.card.id,
saveCard = request.saveCard
)
)
}
}

fun onAuthorizing() {
val uiState = _uiState.value
if (uiState is Tokenized) {
_uiState.value = Authorizing(uiState.uiModel)
}
}

fun reset() {
_uiState.value = Initial
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.processout.example.ui.screen.card.payment

data class CardPaymentViewModelState(
val amount: String = String(),
val currency: String = String(),
val invoiceId: String? = null,
val customerId: String? = null
)

sealed interface CardPaymentViewModelEvent {
data object LaunchTokenization : CardPaymentViewModelEvent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@file:Suppress("FoldInitializerAndIfToElvis")

package com.processout.example.ui.screen.card.payment

import com.processout.sdk.api.model.request.POInvoiceAuthorizationRequest
import com.processout.sdk.api.model.response.POCard
import com.processout.sdk.api.service.PO3DSService
import com.processout.sdk.api.service.POInvoicesService
import com.processout.sdk.core.POFailure.Code.Generic
import com.processout.sdk.core.ProcessOutResult
import com.processout.sdk.ui.card.tokenization.POCardTokenizationDelegate

class DefaultCardTokenizationDelegate(
private val viewModel: CardPaymentViewModel,
private val invoices: POInvoicesService,
private val provide3DSService: () -> PO3DSService
) : POCardTokenizationDelegate {

override suspend fun processTokenizedCard(
card: POCard,
saveCard: Boolean
): ProcessOutResult<Any> {
val invoice = viewModel.createInvoice()
if (invoice == null) {
return ProcessOutResult.Failure(
code = Generic(),
message = "Failed to create an invoice."
)
}
return invoices.authorize(
request = POInvoiceAuthorizationRequest(
invoiceId = invoice.id,
source = card.id,
saveSource = saveCard,
clientSecret = invoice.clientSecret
),
threeDSService = provide3DSService()
)
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("OVERRIDE_DEPRECATION")

package com.processout.sdk.api.dispatcher

import com.processout.sdk.api.dispatcher.card.tokenization.POCardTokenizationEventDispatcher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface POEventDispatchers {
val nativeAlternativePaymentMethod: PONativeAlternativePaymentMethodEventDispatcher

/** Dispatcher that allows to handle events during card tokenization. */
@Deprecated(message = "Use API with POCardTokenizationDelegate instead.")
val cardTokenization: POCardTokenizationEventDispatcher

/** Dispatcher that allows to handle events during card updates. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.SharedFlow
/**
* Dispatcher that allows to handle events during card tokenization.
*/
@Deprecated(message = "Use API with POCardTokenizationDelegate instead.")
interface POCardTokenizationEventDispatcher {

/** Allows to subscribe for card tokenization lifecycle events. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.processout.sdk.api.model.request

import com.processout.sdk.api.dispatcher.POEventDispatcher
import com.processout.sdk.api.model.response.POCard
import java.util.UUID

/**
* Request to process tokenized card (authorize invoice or assign customer token).
*
* @param[card] Tokenized card.
* @param[saveCard] Indicates whether the user has chosen to save the card for future payments.
* @param[uuid] Unique identifier of request.
*/
data class POCardTokenizationProcessingRequest(
val card: POCard,
val saveCard: Boolean
)
val saveCard: Boolean,
override val uuid: UUID = UUID.randomUUID()
) : POEventDispatcher.Request
Loading