Skip to content

Commit

Permalink
Demo application no includes support for tokenized payments with both…
Browse files Browse the repository at this point in the history
… CIT and MIT payment type, and either charge or authorization hold (for both payment types).
  • Loading branch information
mliikanen committed Jul 4, 2023
1 parent 02fea04 commit 5fd3658
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 37 deletions.
37 changes: 23 additions & 14 deletions demo-app/src/main/java/fi/paytrail/demo/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package fi.paytrail.demo

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibility
Expand Down Expand Up @@ -37,17 +36,23 @@ import fi.paytrail.paymentsdk.model.PaytrailPaymentState.State.PAYMENT_OK
import fi.paytrail.paymentsdk.tokenization.AddCardForm
import fi.paytrail.paymentsdk.tokenization.PayWithTokenizationId
import fi.paytrail.paymentsdk.tokenization.TokenPaymentChargeType
import fi.paytrail.paymentsdk.tokenization.TokenPaymentType
import fi.paytrail.paymentsdk.tokenization.model.AddCardRequest
import fi.paytrail.paymentsdk.tokenization.model.AddCardResult
import fi.paytrail.sdk.apiclient.models.Callbacks
import kotlinx.coroutines.launch
import javax.inject.Inject

private const val NAV_ARG_TOKENIZATION_ID = "tokenizationId"
private const val NAV_ARG_PAYMENT_TYPE = "paymentType"
private const val NAV_ARG_CHARGE_TYPE = "chargeType"

private const val NAV_SHOPPING_CART = "shopping_cart"
private const val NAV_PAYMENT = "payment"
private const val NAV_CARDS = "cards"
private const val NAV_ADD_CARD = "cards/tokenize"
private const val NAV_PAY_WITH_TOKENIZATION_ID = "cards/charge/{tokenizationId}"
private const val NAV_PAY_WITH_TOKENIZATION_ID =
"cards/{$NAV_ARG_TOKENIZATION_ID}/{$NAV_ARG_PAYMENT_TYPE}/{$NAV_ARG_CHARGE_TYPE}"

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
Expand Down Expand Up @@ -100,10 +105,6 @@ class MainActivity : ComponentActivity() {
// If you want transaction ID,
}
}
Log.i(
"MainActivity",
"state: ${state.state} Transaction ID ${state.finalRedirectRequest?.transactionId}",
)
},
)
}
Expand Down Expand Up @@ -140,12 +141,12 @@ class MainActivity : ComponentActivity() {
TokenizedCreditCards(
modifier = Modifier.fillMaxSize(),
viewModel = hiltViewModel(),
payWithCardAction = { tokenizationId ->
payWithCardAction = { tokenizationId: String, paymentType: TokenPaymentType, chargeType: TokenPaymentChargeType ->
navController.navigate(
NAV_PAY_WITH_TOKENIZATION_ID.replace(
"{tokenizationId}",
tokenizationId,
),
NAV_PAY_WITH_TOKENIZATION_ID
.replace("{$NAV_ARG_TOKENIZATION_ID}", tokenizationId)
.replace("{$NAV_ARG_PAYMENT_TYPE}", paymentType.name)
.replace("{$NAV_ARG_CHARGE_TYPE}", chargeType.name),
)
},
addCardAction = { navController.navigate(NAV_ADD_CARD) },
Expand All @@ -155,16 +156,24 @@ class MainActivity : ComponentActivity() {
composable(
route = NAV_PAY_WITH_TOKENIZATION_ID,
arguments = listOf(
navArgument("tokenizationId") { type = NavType.StringType },
navArgument(NAV_ARG_TOKENIZATION_ID) { type = NavType.StringType },
navArgument(NAV_ARG_PAYMENT_TYPE) { type = NavType.StringType },
navArgument(NAV_ARG_CHARGE_TYPE) { type = NavType.StringType },
),
) {
val tokenizationId = it.arguments!!.getString("tokenizationId")!!
val args = it.arguments!!
val tokenizationId = args.getString(NAV_ARG_TOKENIZATION_ID)!!
val chargeType =
TokenPaymentChargeType.valueOf(args.getString(NAV_ARG_CHARGE_TYPE)!!)
val paymentType =
TokenPaymentType.valueOf(args.getString(NAV_ARG_PAYMENT_TYPE)!!)
PayWithTokenizationId(
modifier = Modifier.fillMaxSize(),
paymentRequest = shoppingCartRepository.cartAsPaymentRequest(),
tokenizationId = tokenizationId,
onPaymentStateChanged = onPaymentStateChanged,
chargeType = TokenPaymentChargeType.CHARGE,
paymentType = paymentType,
chargeType = chargeType,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fi.paytrail.demo.tokenization

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -13,34 +14,57 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fi.paytrail.demo.R
import fi.paytrail.paymentsdk.RequestStatus
import fi.paytrail.paymentsdk.tokenization.TokenPaymentChargeType
import fi.paytrail.paymentsdk.tokenization.TokenPaymentType
import fi.paytrail.sdk.apiclient.models.TokenizationRequestResponse
import kotlinx.coroutines.flow.Flow

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TokenizedCreditCards(
modifier: Modifier,
viewModel: ManageCardsViewModel,
payWithCardAction: (String) -> Unit,
viewModel: TokenizedCreditCardsViewModel,
payWithCardAction: (String, TokenPaymentType, TokenPaymentChargeType) -> Unit,
addCardAction: () -> Unit,
) {
val cards: List<Pair<String, Flow<RequestStatus<TokenizedCreditCard>>>> = viewModel.cards
.collectAsState(initial = emptyList())
.value

val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)
val sheetTokenizationId =
viewModel.actionsMenuTokenizationId.collectAsState(initial = null).value
LaunchedEffect(sheetTokenizationId) {
if (sheetTokenizationId.isNullOrEmpty()) bottomSheetState.hide() else bottomSheetState.show()
}

Column(modifier) {
Box(
modifier = Modifier
Expand All @@ -50,15 +74,133 @@ fun TokenizedCreditCards(
if (cards.isNotEmpty()) {
CreditCardListing(
cards = cards,
onCardClick = { tokenizationId, _ -> payWithCardAction(tokenizationId) },
onCardLongClick = { tokenizationId, _ -> viewModel.removeCard(tokenizationId) },
onCardClick = { tokenizationId, _ ->
payWithCardAction(
tokenizationId,
TokenPaymentType.CIT,
TokenPaymentChargeType.CHARGE,
)
},
onCardLongClick = { tokenizationId, _ ->
viewModel.showCardActions(
tokenizationId,
)
},
)
} else {
NoSavedCards()
}
}

// TODO: Show most recent auth hold & allow commit/revoke

ManageCreditCardsBottomPanel(addCardAction = addCardAction)

if (bottomSheetState.currentValue != SheetValue.Hidden) {
CardActionsBottomSheet(
viewModel = viewModel,
payWithCardAction = payWithCardAction,
bottomSheetState = bottomSheetState,
sheetTokenizationId = sheetTokenizationId,
)
}
}
}

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun CardActionsBottomSheet(
viewModel: TokenizedCreditCardsViewModel,
payWithCardAction: (String, TokenPaymentType, TokenPaymentChargeType) -> Unit,
bottomSheetState: SheetState,
sheetTokenizationId: String?,
) {
ModalBottomSheet(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = viewModel::hideCardActions,
sheetState = bottomSheetState,
shape = RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp),
scrimColor = Color.Black.copy(alpha = 0.4f),
) {

Column(modifier = Modifier.fillMaxWidth()) {
Divider()
BottomSheetAction(
text = stringResource(R.string.card_action_pay_cit_charge),
onClick = {
payWithCardAction(
sheetTokenizationId!!,
TokenPaymentType.CIT,
TokenPaymentChargeType.CHARGE,
)
},
)
Divider()
BottomSheetAction(
text = stringResource(R.string.card_action_pay_cit_auth_hold),
onClick = {
payWithCardAction(
sheetTokenizationId!!,
TokenPaymentType.CIT,
TokenPaymentChargeType.AUTH_HOLD,
)
},
)
Divider()
BottomSheetAction(
text = stringResource(R.string.card_action_pay_mit_charge),
onClick = {
payWithCardAction(
sheetTokenizationId!!,
TokenPaymentType.MIT,
TokenPaymentChargeType.CHARGE,
)
},
)
Divider()
BottomSheetAction(
text = stringResource(R.string.card_action_pay_mit_auth_hold),
onClick = {
payWithCardAction(
sheetTokenizationId!!,
TokenPaymentType.MIT,
TokenPaymentChargeType.AUTH_HOLD,
)
},
)
Divider()
BottomSheetAction(
text = stringResource(R.string.card_action_remove_card),
onClick = { viewModel.removeCard(sheetTokenizationId!!) },
)
Divider()

Spacer(modifier = Modifier.height(32.dp))
}
}
}

@Preview
@Composable
fun BottomSheetAction(
modifier: Modifier = Modifier,
text: String = "Action",
onClick: () -> Unit = {},
) {
Box(
modifier = modifier
.clickable(onClick = onClick)
.fillMaxWidth()
.padding(16.dp),
) {
Text(
text = text,
fontSize = 16.sp,
textAlign = TextAlign.Start,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.labelLarge,
)
}
}

Expand All @@ -80,6 +222,7 @@ private fun CreditCardListing(
) {
item("card-header-prompt") {
Text(stringResource(R.string.manage_cards_prompt))
Spacer(modifier = Modifier.height(16.dp))
}

cards.forEachIndexed { index, cardRequestFlow ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package fi.paytrail.demo.tokenization
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import fi.paytrail.demo.repository.ShoppingCartRepository
import fi.paytrail.paymentsdk.RequestStatus
import fi.paytrail.sdk.apiclient.models.TokenizationRequestResponse
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
Expand All @@ -19,17 +19,19 @@ data class TokenizedCreditCard(
)

@HiltViewModel
class ManageCardsViewModel @Inject constructor(
class TokenizedCreditCardsViewModel @Inject constructor(
private val cardsRepository: SavedCardsRepository,
private val shoppingCartRepository: ShoppingCartRepository,
) : ViewModel() {

private val tokenizationIds = cardsRepository.observeSavedTokenizationIDs()
val actionsMenuTokenizationId = MutableStateFlow<String?>(null)

// TODO: Cache card information (in repository) so adding/removing cards does not
// trigger reloads.
val cards: Flow<List<Pair<String, Flow<RequestStatus<TokenizedCreditCard>>>>> =
tokenizationIds.map { ids ->
ids.map { tokenizationId ->
val requestFlow = getTokenizationResult(tokenizationId)
val requestFlow = getTokenizedCardDetails(tokenizationId)
tokenizationId to requestFlow.map { requestStatus ->
requestStatus.map { tokenizationResponse ->
TokenizedCreditCard(tokenizationId, tokenizationResponse)
Expand All @@ -42,14 +44,25 @@ class ManageCardsViewModel @Inject constructor(
replay = 1,
)

private fun getTokenizationResult(tokenizationId: String) =
private fun getTokenizedCardDetails(tokenizationId: String) =
cardsRepository.getToken(tokenizationId)
.shareIn(
viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
replay = 1,
) as Flow<RequestStatus<TokenizationRequestResponse>>

fun removeCard(tokenizationId: String) =
fun removeCard(tokenizationId: String) {
hideCardActions()
viewModelScope.launch { cardsRepository.removeTokenizationId(tokenizationId) }
}

fun showCardActions(tokenizationId: String) {
actionsMenuTokenizationId.value = tokenizationId
}

fun hideCardActions() {
actionsMenuTokenizationId.value = null
}

}
7 changes: 6 additions & 1 deletion demo-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@
<string name="payment_status_view_status">Payment status: %1$s</string>
<string name="payment_status_view_transaction_id">Transaction id: %1$s</string>
<string name="manage_cards_button_add_card">Add card</string>
<string name="manage_cards_prompt">Select a card to pay with, or add a new card. Long-press to remove a saved card.</string>
<string name="manage_cards_prompt">Click on a card to create CIT payment. Long-press for more actions.</string>
<string name="card_action_pay_cit_charge">Pay (CIT / Charge)</string>
<string name="card_action_pay_cit_auth_hold">Pay (CIT / Auth Hold)</string>
<string name="card_action_pay_mit_charge">Pay (MIT / Charge)</string>
<string name="card_action_pay_mit_auth_hold">Pay (MIT / Auth Hold)</string>
<string name="card_action_remove_card">Remove Card</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ enum class TokenPaymentChargeType {
CHARGE,
}

enum class TokenPaymentType(s: String) {
MIT("mit"),
CIT("cit"),
enum class TokenPaymentType {
MIT,
CIT,
}

@Composable
Expand Down
Loading

0 comments on commit 5fd3658

Please sign in to comment.