Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experiment] Webview Trick to bypass 3DS #27

Merged
merged 3 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -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
Loading