Skip to content

Commit

Permalink
Fix Offsite Payments with Live Env (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
AmniX authored Oct 9, 2024
1 parent 734eb5e commit 30cb5ec
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 42 deletions.
1 change: 1 addition & 0 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ dependencies {
implementation(libs.voyager.navigator)
implementation(libs.voyager.screenModel)
implementation(libs.voyager.transitions)
implementation(libs.androidx.browser)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
package com.komoju.android.sdk.ui.screens

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.komoju.android.sdk.KomojuSDK
Expand All @@ -28,8 +23,6 @@ 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 Handle(val url: String) : Router()
data class Browser(val url: String) : Router()
data class SetPaymentResultAndPop(val result: KomojuSDK.PaymentResult) : Router()
}

Expand Down Expand Up @@ -62,7 +55,6 @@ internal sealed interface KomojuPaymentRoute {
@Composable
internal fun RouterEffect(routerState: State<Router?>, onHandled: () -> Unit) {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val router = routerState.value
val backPressDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val resultScreenModel = navigator.paymentResultScreenModel()
Expand All @@ -74,12 +66,7 @@ internal fun RouterEffect(routerState: State<Router?>, onHandled: () -> Unit) {
is Router.Push -> navigator.push(router.route.screen)
is Router.Replace -> navigator.replace(router.route.screen)
is Router.ReplaceAll -> navigator.replaceAll(router.route.screen)
is Router.Handle -> when (router.url.canOpenAnApp(context)) {
true -> context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(router.url)))
false -> navigator.push(KomojuPaymentRoute.WebView(router.url).screen)
}

is Router.Browser -> context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(router.url)))
null -> Unit
is Router.SetPaymentResultAndPop -> {
resultScreenModel.setResult(router.result)
Expand All @@ -89,7 +76,3 @@ internal fun RouterEffect(routerState: State<Router?>, onHandled: () -> Unit) {
onHandled()
}
}

internal fun String.canOpenAnApp(context: Context): Boolean = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(this@canOpenAnApp)
}.resolveActivity(context.packageManager) != null
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.komoju.android.sdk.ui.screens.payment

import android.os.Parcelable
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand Down Expand Up @@ -30,6 +31,7 @@ import com.komoju.android.sdk.ui.screens.payment.composables.PaymentMethodForm
import com.komoju.android.sdk.ui.screens.payment.composables.PaymentMethodsRow
import com.komoju.android.sdk.ui.screens.payment.composables.PaymentSheetHandle
import com.komoju.android.sdk.ui.theme.LocalI18nTexts
import com.komoju.android.sdk.utils.OffsiteCustomTabResultContract
import kotlinx.parcelize.Parcelize

@Parcelize
Expand All @@ -40,9 +42,20 @@ internal data class KomojuPaymentScreen(private val sdkConfiguration: KomojuSDK.
override fun Content() {
val screenViewModel = rememberScreenModel { KomojuPaymentScreenModel(sdkConfiguration) }
val uiState by screenViewModel.state.collectAsStateWithLifecycle()
val offSitePaymentURL by screenViewModel.offSitePaymentURL.collectAsStateWithLifecycle()
val offsitePaymentLauncher = rememberLauncherForActivityResult(OffsiteCustomTabResultContract()) {
screenViewModel.onOffsitePaymentResult()
}
LaunchedEffect(sdkConfiguration.sessionId) {
screenViewModel.init()
}
LaunchedEffect(offSitePaymentURL) {
val url = offSitePaymentURL
if (url != null) {
offsitePaymentLauncher.launch(url)
screenViewModel.onOffSitePaymentURLConsumed()
}
}
RouterEffect(screenViewModel.router.collectAsStateWithLifecycle(), screenViewModel::onRouteConsumed)
Box {
if (uiState.session != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ import com.komoju.mobile.sdk.entities.SecureTokenResponse.Status.OK
import com.komoju.mobile.sdk.entities.SecureTokenResponse.Status.SKIPPED
import com.komoju.mobile.sdk.entities.SecureTokenResponse.Status.UNKNOWN
import com.komoju.mobile.sdk.remote.apis.KomojuRemoteApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

internal class KomojuPaymentScreenModel(private val config: KomojuSDK.Configuration) : RouterStateScreenModel<KomojuPaymentUIState>(KomojuPaymentUIState()) {
private val komojuApi: KomojuRemoteApi = KomojuRemoteApi(config.publishableKey, config.language.languageCode)
private val _offSitePaymentURL = MutableStateFlow<String?>(null)
val offSitePaymentURL = _offSitePaymentURL.asStateFlow()

fun init() {
val sessionId = config.sessionId
Expand Down Expand Up @@ -116,6 +120,7 @@ internal class KomojuPaymentScreenModel(private val config: KomojuSDK.Configurat
processType = KomojuPaymentRoute.ProcessPayment.ProcessType.PayByToken(it.id, request.amount, request.currency),
),
)

NEEDS_VERIFY -> mutableRouter.value = Router.ReplaceAll(KomojuPaymentRoute.WebView(url = it.authURL, isJavaScriptEnabled = true))
ERRORED, UNKNOWN -> mutableRouter.value = Router.ReplaceAll(KomojuPaymentRoute.PaymentFailed(Reason.CREDIT_CARD_ERROR))
}
Expand All @@ -128,7 +133,7 @@ internal class KomojuPaymentScreenModel(private val config: KomojuSDK.Configurat
private fun Payment.handle() {
when (this) {
is Payment.Konbini -> mutableRouter.value = Router.Replace(KomojuPaymentRoute.KonbiniAwaitingPayment(config, payment = this))
is Payment.PayPay -> mutableRouter.value = Router.Handle(url = redirectURL)
is Payment.PayPay -> _offSitePaymentURL.value = redirectURL
else -> Unit
}
}
Expand Down Expand Up @@ -217,4 +222,17 @@ internal class KomojuPaymentScreenModel(private val config: KomojuSDK.Configurat
fun onCloseClicked() {
mutableRouter.pop()
}

fun onOffsitePaymentResult() {
mutableRouter.value = Router.ReplaceAll(
KomojuPaymentRoute.ProcessPayment(
configuration = config,
processType = KomojuPaymentRoute.ProcessPayment.ProcessType.Session,
),
)
}

fun onOffSitePaymentURLConsumed() {
_offSitePaymentURL.value = null
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.komoju.android.sdk.ui.screens.webview

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.core.app.ActivityOptionsCompat
Expand All @@ -19,21 +21,32 @@ internal class WebViewClient : AccompanistWebViewClient() {
private fun WebView.checkAndOpen(url: String): Boolean {
try {
val uri = url.toUri()
if (uri.scheme == resources.getString(R.string.komoju_consumer_app_scheme)) {
startActivity(
context,
Intent(context, KomojuPaymentActivity::class.java).apply {
data = uri
},
ActivityOptionsCompat.makeBasic().toBundle(),
)
return true
} else {
val handled = uri.handle(context)
if (handled.not()) {
error("Unsupported scheme for deeplink, load in webView Instead.")
} else {
return handled
}
} catch (_: Exception) {
loadUrl(url)
return false
}
}
}

private fun Uri.handle(context: Context): Boolean = openKomojuSDKIfAvailable(context)

private fun Uri.openKomojuSDKIfAvailable(context: Context): Boolean {
if (scheme == context.resources.getString(R.string.komoju_consumer_app_scheme)) {
startActivity(
context,
Intent(context, KomojuPaymentActivity::class.java).apply {
data = this@openKomojuSDKIfAvailable
},
ActivityOptionsCompat.makeBasic().toBundle(),
)
return true
} else {
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.komoju.android.sdk.types.Currency

internal object AmountUtils {
fun formatToDecimal(currency: Currency, amount: String): String {
if (amount.isBlank()) return ""
val locale = currency.toLocale()
return NumberFormat.getCurrencyInstance(locale).apply {
this.maximumFractionDigits = 2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.komoju.android.sdk.utils

import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.ACTIVITY_HEIGHT_FIXED
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT

internal class OffsiteCustomTabResultContract : ActivityResultContract<String, Int>() {
override fun createIntent(context: Context, input: String): Intent {
val height = context.resources.displayMetrics.heightPixels
val builder = CustomTabsIntent.Builder()
.setColorScheme(COLOR_SCHEME_LIGHT)
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(Color.WHITE)
.setNavigationBarColor(Color.WHITE)
.setSecondaryToolbarColor(Color.WHITE)
.setNavigationBarDividerColor(Color.BLACK)
.build(),
)
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
.setInitialActivityHeightPx(height, ACTIVITY_HEIGHT_FIXED)
.setCloseButtonPosition(CustomTabsIntent.CLOSE_BUTTON_POSITION_END)
.setToolbarCornerRadiusDp(10)
val customTabsIntent = builder.build().intent
customTabsIntent.setData(Uri.parse(input))
return customTabsIntent
}

override fun parseResult(resultCode: Int, intent: Intent?): Int = resultCode
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
agp = "8.7.0"
browser = "1.8.0"
kotlin = "2.0.20"
nexus-publish = "2.0.0"
android-minSdk = "24"
Expand Down Expand Up @@ -27,6 +28,7 @@ nmcp = "0.0.7"
dokka = "1.9.20"

[libraries]
androidx-browser = { module = "androidx.browser:browser", version.ref = "browser" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
nexus-publish = { module = "io.github.gradle-nexus.publish-plugin:io.github.gradle-nexus.publish-plugin.gradle.plugin", version.ref = "nexus-publish" }
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
brands = (paymentMethod.brands as? JsonArray)?.map { it.toString() }.orEmpty(),
displayName = i18nTexts[paymentMethodType],
Expand All @@ -26,7 +26,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
customerFee = paymentMethod.customerFee ?: 0,
displayName = i18nTexts[paymentMethodType],
Expand All @@ -51,7 +51,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
isOffsite = paymentMethod.offsite ?: false,
displayName = i18nTexts[paymentMethodType],
Expand All @@ -61,7 +61,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
isOffsite = paymentMethod.offsite ?: false,
displayName = i18nTexts[paymentMethodType],
Expand All @@ -71,7 +71,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
customerFee = paymentMethod.customerFee ?: 0,
displayName = i18nTexts[paymentMethodType],
Expand All @@ -81,7 +81,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
customerFee = paymentMethod.customerFee ?: 0,
displayName = i18nTexts[paymentMethodType],
Expand All @@ -91,7 +91,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
displayName = i18nTexts[paymentMethodType],
)
Expand All @@ -100,7 +100,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
displayName = i18nTexts[paymentMethodType],
)
Expand All @@ -109,7 +109,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
displayName = i18nTexts[paymentMethodType],
)
Expand All @@ -118,7 +118,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
isOffsite = paymentMethod.offsite ?: false,
displayName = i18nTexts[paymentMethodType],
Expand All @@ -128,7 +128,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
isOffsite = paymentMethod.offsite ?: false,
displayName = i18nTexts[paymentMethodType],
Expand All @@ -138,7 +138,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
isOffsite = paymentMethod.offsite ?: false,
displayName = i18nTexts[paymentMethodType],
Expand All @@ -148,7 +148,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
isOffsite = paymentMethod.offsite ?: false,
secondIcon = paymentMethod.secondIcon.orEmpty(),
Expand All @@ -159,7 +159,7 @@ internal object SessionMapper {
hashedGateway = paymentMethod.hashedGateway.orEmpty(),
exchangeRate = paymentMethod.exchangeRate ?: 1.0,
currency = paymentMethod.currency.orEmpty(),
amount = paymentMethod.amount.orEmpty(),
amount = (response.amount ?: 0).toString(),
additionalFields = paymentMethod.additionalFields?.filterNotNull().orEmpty(),
displayName = i18nTexts[paymentMethodType],
)
Expand Down

0 comments on commit 30cb5ec

Please sign in to comment.