Skip to content

Commit

Permalink
Inline Payment Processing
Browse files Browse the repository at this point in the history
  • Loading branch information
AmniX committed Oct 9, 2024
1 parent 4187bdc commit 43d3701
Show file tree
Hide file tree
Showing 13 changed files with 450 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.komoju.android.sdk.ui.composables

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CheckCircle
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.komoju.android.sdk.ui.theme.KomojuMobileSdkTheme
import com.komoju.android.sdk.ui.theme.LocalConfigurableTheme
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@Composable
internal fun InlinedPaymentPrimaryButton(text: String, state: InlinedPaymentPrimaryButtonState, modifier: Modifier = Modifier, onClick: () -> Unit) {
val configurableTheme = LocalConfigurableTheme.current
Button(
modifier = modifier,
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color(configurableTheme.primaryButtonColor),
contentColor = Color(configurableTheme.primaryButtonContentColor),
),
shape = RoundedCornerShape(configurableTheme.primaryButtonCornerRadiusInDP.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(38.dp),
contentAlignment = Alignment.Center,
) {
when (state) {
InlinedPaymentPrimaryButtonState.LOADING -> CircularProgressIndicator(strokeWidth = 2.dp, color = LocalContentColor.current, modifier = Modifier.size(24.dp))
InlinedPaymentPrimaryButtonState.IDLE -> Text(modifier = Modifier.padding(8.dp), text = text, style = TextStyle(fontWeight = FontWeight.Bold), maxLines = 1)
InlinedPaymentPrimaryButtonState.SUCCESS -> Icon(Icons.Rounded.CheckCircle, contentDescription = null, modifier = Modifier.size(24.dp))
InlinedPaymentPrimaryButtonState.ERROR -> Icon(Icons.Rounded.Close, contentDescription = null, modifier = Modifier.size(24.dp))
}
}
}
}

enum class InlinedPaymentPrimaryButtonState {
LOADING,
IDLE,
SUCCESS,
ERROR,
}

@Composable
fun rememberInlinedPaymentPrimaryButtonState(default: InlinedPaymentPrimaryButtonState = InlinedPaymentPrimaryButtonState.IDLE): MutableState<InlinedPaymentPrimaryButtonState> =
rememberSaveable { mutableStateOf(default) }

@Composable
@Preview(showBackground = true, showSystemUi = true)
private fun PaymentButtonPreview() {
var state by rememberInlinedPaymentPrimaryButtonState()
val coroutineScope = rememberCoroutineScope()
KomojuMobileSdkTheme {
InlinedPaymentPrimaryButton(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
state = state,
text = "Pay \$100.00",
) {
if (state == InlinedPaymentPrimaryButtonState.IDLE) {
coroutineScope.launch {
state = InlinedPaymentPrimaryButtonState.LOADING
delay(2.seconds)
state = InlinedPaymentPrimaryButtonState.SUCCESS
delay(2.seconds)
state = InlinedPaymentPrimaryButtonState.ERROR
delay(2.seconds)
state = InlinedPaymentPrimaryButtonState.IDLE
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.komoju.android.sdk.ui.composables

import android.annotation.SuppressLint
import android.graphics.Color
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import com.kevinnzou.web.AccompanistWebViewClient
import com.kevinnzou.web.WebView
import com.kevinnzou.web.rememberWebViewState
import com.komoju.android.sdk.R

@SuppressLint("SetJavaScriptEnabled")
@Composable
internal fun InlinedWebView(modifier: Modifier, url: String, onDone: (String) -> Unit, onChallengePresented: () -> Unit, onCloseButtonClicked: () -> Unit) {
val state = rememberWebViewState(url)
Column(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(54.dp),
) {
Text(
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
.align(Alignment.CenterVertically),
text = state.pageTitle.orEmpty(),
fontSize = 20.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Image(
imageVector = Icons.Rounded.Close,
contentDescription = "Close Payment Sheet",
modifier = Modifier
.clickable(
indication = ripple(bounded = true, radius = 24.dp),
interactionSource = remember { MutableInteractionSource() },
onClick = {
onCloseButtonClicked()
},
)
.padding(16.dp),
)
}
WebView(
modifier = Modifier.weight(1f),
state = state,
onCreated = { nativeWebView ->
nativeWebView.clipToOutline = true
nativeWebView.setBackgroundColor(Color.TRANSPARENT)
nativeWebView.settings.apply {
domStorageEnabled = true
javaScriptEnabled = true
}
},
captureBackPresses = false,
client = remember { InlinedWebViewClient(onDone, onChallengePresented) },
)
}
}

private class InlinedWebViewClient(private val onDeeplinkCaptured: (String) -> Unit, private val onChallengePresented: () -> Unit) : AccompanistWebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = view.checkAndOpen(url)
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean = view.checkAndOpen(request.url.toString())

private fun WebView.checkAndOpen(url: String): Boolean {
try {
val uri = url.toUri()
if (uri.scheme == resources.getString(R.string.komoju_consumer_app_scheme)) {
onDeeplinkCaptured(url)
return true
} else {
error("Unsupported scheme for deeplink, load in webView Instead.")
}
} catch (_: Exception) {
loadUrl(url)
return false
}
}

override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
Log.d("Aman", request?.url.toString())
if (request?.url.toString().startsWith("https://acs-challenge.testlab.3dsecure.cloud")) {
onChallengePresented()
}
return super.shouldInterceptRequest(view, request)
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.komoju.android.sdk.ui.composables

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
Expand All @@ -28,7 +31,14 @@ internal fun PrimaryButton(text: String, modifier: Modifier = Modifier, onClick:
),
shape = RoundedCornerShape(configurableTheme.primaryButtonCornerRadiusInDP.dp),
) {
Text(modifier = Modifier.padding(8.dp), text = text, style = TextStyle(fontWeight = FontWeight.Bold), maxLines = 1)
Box(
modifier = Modifier
.fillMaxWidth()
.height(38.dp),
contentAlignment = Alignment.Center,
) {
Text(modifier = Modifier.padding(8.dp), text = text, style = TextStyle(fontWeight = FontWeight.Bold), maxLines = 1)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal sealed class Router {
data class ReplaceAll(val route: KomojuPaymentRoute) : Router()
data class Handle(val url: String) : Router()
data class Browser(val url: String) : Router()
data class SetPaymentResultAndPop(val result: KomojuSDK.PaymentResult) : Router()
data class SetPaymentResultAndPop(val result: KomojuSDK.PaymentResult = KomojuSDK.PaymentResult(false)) : Router()
}

internal sealed interface KomojuPaymentRoute {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.komoju.android.sdk.ui.screens.payment

import com.komoju.android.sdk.ui.screens.failed.Reason
import com.komoju.mobile.sdk.entities.PaymentStatus
import com.komoju.mobile.sdk.remote.apis.KomojuRemoteApi

internal suspend fun KomojuRemoteApi.verifyTokenAndProcessPayment(
sessionId: String,
token: String,
amount: String,
currency: String,
onError: (Reason) -> Unit,
onSuccess: suspend (PaymentStatus) -> Unit,
) {
tokens.verifySecureToken(token).onSuccess { isVerifiedByToken ->
if (isVerifiedByToken) {
payByToken(sessionId, token, amount, currency, onError, onSuccess)
} else {
onError(Reason.CREDIT_CARD_ERROR)
}
}.onFailure {
onError(Reason.CREDIT_CARD_ERROR)
}
}

private suspend fun KomojuRemoteApi.payByToken(
sessionId: String,
token: String,
amount: String,
currency: String,
onError: (Reason) -> Unit,
onSuccess: suspend (PaymentStatus) -> Unit,
) {
sessions.pay(sessionId, token, amount, currency).onSuccess { response ->
if (response.status == PaymentStatus.CAPTURED) {
processBySession(sessionId, onSuccess, onError)
} else {
onError(Reason.CREDIT_CARD_ERROR)
}
}.onFailure {
onError(Reason.CREDIT_CARD_ERROR)
}
}

private suspend fun KomojuRemoteApi.processBySession(sessionId: String, onSuccess: suspend (PaymentStatus) -> Unit, onError: (Reason) -> Unit) {
sessions.verifyPaymentBySessionID(sessionId).onSuccess { paymentDetails ->
onSuccess(paymentDetails.status)
}.onFailure {
onError(Reason.OTHER)
}
}
Loading

0 comments on commit 43d3701

Please sign in to comment.