Skip to content

Commit

Permalink
[Experiment] Webview Trick to bypass 3DS (#27)
Browse files Browse the repository at this point in the history
* Inlined Payment Processing

* Inlined Payment Processing

* Version Upgrade
  • Loading branch information
AmniX authored Oct 17, 2024
1 parent 7cc7084 commit 5333771
Show file tree
Hide file tree
Showing 12 changed files with 444 additions and 29 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,110 @@
package com.komoju.android.sdk.ui.composables

import android.annotation.SuppressLint
import android.graphics.Color
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? {
if (request?.url.toString().contains("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 @@ -23,7 +23,7 @@ internal sealed class Router {
data class Push(val route: KomojuPaymentRoute) : Router()
data class Replace(val route: KomojuPaymentRoute) : Router()
data class ReplaceAll(val route: KomojuPaymentRoute) : 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 5333771

Please sign in to comment.