diff --git a/android/src/main/java/com/komoju/android/sdk/ExperimentalStdlibApi.kt b/android/src/main/java/com/komoju/android/sdk/ExperimentalStdlibApi.kt new file mode 100644 index 0000000..3b50fdf --- /dev/null +++ b/android/src/main/java/com/komoju/android/sdk/ExperimentalStdlibApi.kt @@ -0,0 +1,22 @@ +package com.komoju.android.sdk + +/** + * This annotation means that this option is experimental and subject to change in Future. + * Please test your behaviour overall once you update the komoju SDK Version + */ +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FIELD, + AnnotationTarget.LOCAL_VARIABLE, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.TYPEALIAS, +) +annotation class ExperimentalKomojuPaymentApi diff --git a/android/src/main/java/com/komoju/android/sdk/KomojuPaymentActivity.kt b/android/src/main/java/com/komoju/android/sdk/KomojuPaymentActivity.kt index 2230fab..0f76199 100644 --- a/android/src/main/java/com/komoju/android/sdk/KomojuPaymentActivity.kt +++ b/android/src/main/java/com/komoju/android/sdk/KomojuPaymentActivity.kt @@ -32,6 +32,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.SlideTransition +import com.komoju.android.sdk.navigation.PaymentResultScreenModel +import com.komoju.android.sdk.navigation.paymentResultScreenModel import com.komoju.android.sdk.ui.screens.RouterEffect import com.komoju.android.sdk.ui.screens.payment.KomojuPaymentScreen import com.komoju.android.sdk.ui.theme.KomojuMobileSdkTheme @@ -59,6 +61,8 @@ internal class KomojuPaymentActivity : ComponentActivity() { }, ) + private var commonScreenModel: PaymentResultScreenModel? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -92,6 +96,7 @@ internal class KomojuPaymentActivity : ComponentActivity() { Navigator( KomojuPaymentScreen(viewModel.configuration), ) { navigator -> + commonScreenModel = navigator.paymentResultScreenModel() SlideTransition(navigator) RouterEffect(viewModel.router.collectAsStateWithLifecycle(), viewModel::onRouteConsumed) NewIntentEffect(LocalContext.current, viewModel::onNewDeeplink) @@ -120,11 +125,16 @@ internal class KomojuPaymentActivity : ComponentActivity() { } override fun finish() { + setResult( + RESULT_OK, + Intent().apply { + putExtra(KomojuStartPaymentForResultContract.RESULT_KEY, commonScreenModel?.result) + }, + ) lifecycleScope.launch { viewModel.toggleVisibility(false) delay(ANIMATION_DURATION.toLong()) // Let the animation finish super.finish() } - // TODO: Set Result } } diff --git a/android/src/main/java/com/komoju/android/sdk/KomojuPaymentViewModel.kt b/android/src/main/java/com/komoju/android/sdk/KomojuPaymentViewModel.kt index 9ca99af..04ce1c9 100644 --- a/android/src/main/java/com/komoju/android/sdk/KomojuPaymentViewModel.kt +++ b/android/src/main/java/com/komoju/android/sdk/KomojuPaymentViewModel.kt @@ -1,6 +1,5 @@ package com.komoju.android.sdk -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.komoju.android.sdk.ui.screens.KomojuPaymentRoute @@ -40,7 +39,6 @@ internal class KomojuPaymentViewModel(internal val configuration: KomojuSDK.Conf }, ), ) - Log.d("Aman", "handleIntentAction $deeplinkEntity") } } diff --git a/android/src/main/java/com/komoju/android/sdk/KomojuSDK.kt b/android/src/main/java/com/komoju/android/sdk/KomojuSDK.kt index 816ce0e..94d6fb0 100644 --- a/android/src/main/java/com/komoju/android/sdk/KomojuSDK.kt +++ b/android/src/main/java/com/komoju/android/sdk/KomojuSDK.kt @@ -2,6 +2,7 @@ package com.komoju.android.sdk import android.content.Context import android.content.Intent +import android.os.Build import android.os.Parcelable import androidx.activity.result.contract.ActivityResultContract import com.komoju.android.sdk.types.Currency @@ -30,6 +31,7 @@ object KomojuSDK { internal val sessionId: String?, // Unique session ID for payment transaction. internal val redirectURL: String = "", // URL to redirect after payment completion. internal val configurableTheme: ConfigurableTheme, // Custom theme for UI elements. + internal val inlinedProcessing: Boolean, // Flag to enable inlined processing. ) : Parcelable { /** @@ -41,6 +43,7 @@ object KomojuSDK { private var currency: Currency = Currency.JPY // Default currency is Japanese Yen. private var isDebugMode: Boolean = false // Debug mode is off by default. private var configurableTheme: ConfigurableTheme = ConfigurableTheme.default // Custom theme for UI elements. + private var inlinedProcessing: Boolean = false // Inlined processing is off by default. /** Sets the language for the payment. */ fun setLanguage(language: Language) = apply { @@ -57,10 +60,27 @@ object KomojuSDK { this.isDebugMode = isDebugMode } + /** Sets the custom theme for the payment UI. */ fun setConfigurableTheme(configurableTheme: ConfigurableTheme) = apply { this.configurableTheme = configurableTheme } + /** + * WARNING: Experimental API [Try this only if you are sure] Disabled by Default. + * + * This API enables or disables inlined processing. + * If this is enabled then The SDK will try to do processing with minimum amount of screens. + * + * For e.g. + * * If PayPay Payment id captured, it will close the SDK ASAP it verifies the payment. + * * When you will try to pay with Credit Card and Second step verification is not required, SDK will never show the WebView and will handle the callback itself. + * + */ + @ExperimentalKomojuPaymentApi + fun setInlinedProcessing(inlinedProcessing: Boolean) = apply { + this.inlinedProcessing = inlinedProcessing + } + /** * Builds the [Configuration] instance with the provided settings. */ @@ -71,6 +91,7 @@ object KomojuSDK { sessionId = sessionId, isDebugMode = isDebugMode, configurableTheme = configurableTheme, + inlinedProcessing = inlinedProcessing, ) } } @@ -79,7 +100,8 @@ object KomojuSDK { * Data class to hold the result of a payment transaction. * @param isSuccessFul Whether the payment was successful or not. */ - data class PaymentResult(val isSuccessFul: Boolean) + @Parcelize + data class PaymentResult(val isSuccessFul: Boolean) : Parcelable /** * Property that provides the contract to start the payment process @@ -107,6 +129,7 @@ internal class KomojuStartPaymentForResultContract : ActivityResultContract= Build.VERSION_CODES.TIRAMISU -> intent?.getParcelableExtra(RESULT_KEY, KomojuSDK.PaymentResult::class.java) + else -> + @Suppress("DEPRECATION") + intent?.getParcelableExtra(RESULT_KEY) + } ?: KomojuSDK.PaymentResult(isSuccessFul = false) } diff --git a/android/src/main/java/com/komoju/android/sdk/navigation/PaymentResultScreenModel.kt b/android/src/main/java/com/komoju/android/sdk/navigation/PaymentResultScreenModel.kt new file mode 100644 index 0000000..2191c4b --- /dev/null +++ b/android/src/main/java/com/komoju/android/sdk/navigation/PaymentResultScreenModel.kt @@ -0,0 +1,19 @@ +package com.komoju.android.sdk.navigation + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.navigator.Navigator +import com.komoju.android.sdk.KomojuSDK + +internal class PaymentResultScreenModel : ScreenModel { + var result: KomojuSDK.PaymentResult? = null + private set + + fun setResult(result: KomojuSDK.PaymentResult) { + this.result = result + } +} + +@Composable +internal fun Navigator.paymentResultScreenModel() = rememberNavigatorScreenModel(PaymentResultScreenModel::class.simpleName) { PaymentResultScreenModel() } diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/KomojuPaymentRoutes.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/KomojuPaymentRoutes.kt index 2058efc..70dbdce 100644 --- a/android/src/main/java/com/komoju/android/sdk/ui/screens/KomojuPaymentRoutes.kt +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/KomojuPaymentRoutes.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.platform.LocalContext import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.komoju.android.sdk.KomojuSDK +import com.komoju.android.sdk.navigation.paymentResultScreenModel import com.komoju.android.sdk.ui.screens.awating.KonbiniAwaitingPaymentScreen import com.komoju.android.sdk.ui.screens.failed.PaymentFailedScreen import com.komoju.android.sdk.ui.screens.failed.Reason @@ -29,6 +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() } internal sealed interface KomojuPaymentRoute { @@ -63,6 +65,7 @@ internal fun RouterEffect(routerState: State, onHandled: () -> Unit) { val context = LocalContext.current val router = routerState.value val backPressDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + val resultScreenModel = navigator.paymentResultScreenModel() LaunchedEffect(router) { when (router) { is Router.Pop -> if (navigator.pop().not()) backPressDispatcher?.onBackPressed() @@ -78,6 +81,10 @@ internal fun RouterEffect(routerState: State, onHandled: () -> Unit) { is Router.Browser -> context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(router.url))) null -> Unit + is Router.SetPaymentResultAndPop -> { + resultScreenModel.setResult(router.result) + if (navigator.pop().not()) backPressDispatcher?.onBackPressed() + } } onHandled() } diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/failed/PaymentFailedScreen.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/failed/PaymentFailedScreen.kt index 3412cb2..1a6c81e 100644 --- a/android/src/main/java/com/komoju/android/sdk/ui/screens/failed/PaymentFailedScreen.kt +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/failed/PaymentFailedScreen.kt @@ -36,12 +36,6 @@ internal class PaymentFailedScreen(private val route: KomojuPaymentRoute.Payment } } -enum class Reason { - USER_CANCEL, - CREDIT_CARD_ERROR, - OTHER, -} - @Composable private fun Screen.PaymentFailedScreenContent(route: KomojuPaymentRoute.PaymentFailed) { val i18nTexts = LocalI18nTexts.current diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/failed/Reason.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/failed/Reason.kt new file mode 100644 index 0000000..cdc9fb3 --- /dev/null +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/failed/Reason.kt @@ -0,0 +1,35 @@ +package com.komoju.android.sdk.ui.screens.failed + +/** + * Enum class representing the reasons for a payment failure. + * + * This enum provides various constants that describe common reasons why a payment may fail. + * These reasons can be used to handle different error cases in the application and provide + * appropriate feedback to the user. + */ +enum class Reason { + + /** + * Payment was canceled by the user. + * + * This reason indicates that the user deliberately canceled the payment process. + * It typically happens when the user decides not to complete the transaction. + */ + USER_CANCEL, + + /** + * Payment failed due to a credit card error. + * + * This reason indicates that there was an issue with the user's credit card, such as + * an invalid card number, insufficient funds, or the card being declined by the issuer. + */ + CREDIT_CARD_ERROR, + + /** + * Payment failed due to another unspecified reason. + * + * This reason is used when the cause of the failure does not fit into any of the other + * predefined categories. + */ + OTHER, +} diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/success/PaymentSuccessScreen.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/success/PaymentSuccessScreen.kt index 2b34a34..2518c36 100644 --- a/android/src/main/java/com/komoju/android/sdk/ui/screens/success/PaymentSuccessScreen.kt +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/success/PaymentSuccessScreen.kt @@ -1,6 +1,5 @@ package com.komoju.android.sdk.ui.screens.success -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -14,6 +13,7 @@ import androidx.compose.material.icons.rounded.Clear import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -21,11 +21,19 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.komoju.android.sdk.KomojuSDK import com.komoju.android.sdk.R +import com.komoju.android.sdk.navigation.paymentResultScreenModel import com.komoju.android.sdk.ui.composables.PrimaryButton +import com.komoju.android.sdk.ui.screens.RouterEffect import com.komoju.android.sdk.ui.theme.KomojuMobileSdkTheme import com.komoju.android.sdk.ui.theme.LocalI18nTexts +import com.komoju.android.sdk.utils.PreviewScreen internal class PaymentSuccessScreen : Screen { @Composable @@ -35,9 +43,15 @@ internal class PaymentSuccessScreen : Screen { } @Composable -private fun PaymentSuccessScreenContent() { - val onBackPressDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher +private fun Screen.PaymentSuccessScreenContent() { + val screenModel = rememberScreenModel { PaymentSuccessScreenModel() } + RouterEffect(screenModel.router.collectAsStateWithLifecycle(), screenModel::onRouteConsumed) val i18nTexts = LocalI18nTexts.current + val navigator = LocalNavigator.currentOrThrow + val resultScreenModel = navigator.paymentResultScreenModel() + LaunchedEffect(Unit) { + resultScreenModel.setResult(KomojuSDK.PaymentResult(isSuccessFul = true)) + } Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { Icon( @@ -46,7 +60,7 @@ private fun PaymentSuccessScreenContent() { modifier = Modifier .padding(16.dp) .clickable { - onBackPressDispatcher?.onBackPressed() + screenModel.onCloseButtonClicked() }, ) } @@ -62,7 +76,7 @@ private fun PaymentSuccessScreenContent() { .padding(16.dp), text = i18nTexts["BACK_TO_STORE"], ) { - onBackPressDispatcher?.onBackPressed() + screenModel.onBackToStoreButtonClicked() } } } @@ -71,6 +85,6 @@ private fun PaymentSuccessScreenContent() { @Preview private fun PaymentSuccessScreenContentPreview() { KomojuMobileSdkTheme { - PaymentSuccessScreenContent() + PreviewScreen.PaymentSuccessScreenContent() } } diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/success/PaymentSuccessScreenModel.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/success/PaymentSuccessScreenModel.kt new file mode 100644 index 0000000..61c2f88 --- /dev/null +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/success/PaymentSuccessScreenModel.kt @@ -0,0 +1,13 @@ +package com.komoju.android.sdk.ui.screens.success + +import com.komoju.android.sdk.navigation.RouterStateScreenModel + +internal class PaymentSuccessScreenModel : RouterStateScreenModel(Unit) { + fun onCloseButtonClicked() { + mutableRouter.pop() + } + + fun onBackToStoreButtonClicked() { + mutableRouter.pop() + } +} diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/verify/VerifyPaymentScreenModel.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/verify/VerifyPaymentScreenModel.kt index bef9dcd..7cb13c8 100644 --- a/android/src/main/java/com/komoju/android/sdk/ui/screens/verify/VerifyPaymentScreenModel.kt +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/verify/VerifyPaymentScreenModel.kt @@ -7,6 +7,7 @@ import com.komoju.android.sdk.ui.screens.KomojuPaymentRoute import com.komoju.android.sdk.ui.screens.Router import com.komoju.android.sdk.ui.screens.failed.Reason import com.komoju.mobile.sdk.entities.PaymentStatus +import com.komoju.mobile.sdk.entities.PaymentStatus.Companion.isSuccessful import com.komoju.mobile.sdk.remote.apis.KomojuRemoteApi import kotlinx.coroutines.launch @@ -26,9 +27,17 @@ internal class VerifyPaymentScreenModel(private val config: KomojuSDK.Configurat private suspend fun processBySession() { komojuApi.sessions.verifyPaymentBySessionID(config.sessionId.orEmpty()).onSuccess { paymentDetails -> - mutableRouter.value = when (paymentDetails.status) { - PaymentStatus.COMPLETED, PaymentStatus.CAPTURED -> Router.ReplaceAll(KomojuPaymentRoute.PaymentSuccess) - else -> Router.ReplaceAll(KomojuPaymentRoute.PaymentFailed(Reason.OTHER)) + mutableRouter.value = when { + config.inlinedProcessing -> Router.SetPaymentResultAndPop( + KomojuSDK.PaymentResult( + isSuccessFul = paymentDetails.status.isSuccessful(), + ), + ) + + else -> when (paymentDetails.status.isSuccessful()) { + true -> Router.ReplaceAll(KomojuPaymentRoute.PaymentSuccess) + else -> Router.ReplaceAll(KomojuPaymentRoute.PaymentFailed(Reason.OTHER)) + } } }.onFailure { mutableRouter.value = Router.ReplaceAll(KomojuPaymentRoute.PaymentFailed(Reason.OTHER)) diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebChromeClient.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebChromeClient.kt index 05be317..b3206b6 100644 --- a/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebChromeClient.kt +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebChromeClient.kt @@ -1,12 +1,8 @@ package com.komoju.android.sdk.ui.screens.webview -import android.util.Log import android.webkit.ConsoleMessage import com.kevinnzou.web.AccompanistWebChromeClient internal class WebChromeClient : AccompanistWebChromeClient() { - override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { - Log.d("Aman", "WebView Logs" + consoleMessage.message()) - return true - } + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean = true } diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewClient.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewClient.kt index 13e5019..8bbf2d9 100644 --- a/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewClient.kt +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewClient.kt @@ -1,7 +1,6 @@ package com.komoju.android.sdk.ui.screens.webview import android.content.Intent -import android.util.Log import android.webkit.WebResourceRequest import android.webkit.WebView import androidx.core.app.ActivityOptionsCompat @@ -18,7 +17,6 @@ internal class WebViewClient : AccompanistWebViewClient() { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean = view.checkAndOpen(request.url.toString()) private fun WebView.checkAndOpen(url: String): Boolean { - Log.d("Aman", "checking url: $url") try { val uri = url.toUri() if (uri.scheme == resources.getString(R.string.komoju_consumer_app_scheme)) { @@ -34,7 +32,6 @@ internal class WebViewClient : AccompanistWebViewClient() { error("Unsupported scheme for deeplink, load in webView Instead.") } } catch (_: Exception) { - Log.d("Aman", "loading url: $url") loadUrl(url) return false } diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewScreen.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewScreen.kt index 615c53a..fdd718c 100644 --- a/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewScreen.kt +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewScreen.kt @@ -1,7 +1,6 @@ package com.komoju.android.sdk.ui.screens.webview import androidx.activity.compose.BackHandler -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -33,11 +32,14 @@ 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 androidx.lifecycle.compose.collectAsStateWithLifecycle +import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.screen.Screen import com.kevinnzou.web.LoadingState import com.kevinnzou.web.WebView import com.kevinnzou.web.rememberWebViewState import com.komoju.android.sdk.ui.screens.KomojuPaymentRoute +import com.komoju.android.sdk.ui.screens.RouterEffect import com.komoju.android.sdk.ui.theme.LocalConfigurableTheme internal data class WebViewScreen(val route: KomojuPaymentRoute.WebView) : Screen { @@ -48,10 +50,11 @@ internal data class WebViewScreen(val route: KomojuPaymentRoute.WebView) : Scree } @Composable -private fun WebViewScreenContent(route: KomojuPaymentRoute.WebView) { +private fun Screen.WebViewScreenContent(route: KomojuPaymentRoute.WebView) { val state = rememberWebViewState(route.url) var showBackPressDialog by remember { mutableStateOf(false) } - val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + val screenModel = rememberScreenModel { WebViewScreenModel() } + RouterEffect(screenModel.router.collectAsStateWithLifecycle(), screenModel::onRouteConsumed) if (route.canComeBack.not() && showBackPressDialog.not()) { BackHandler { showBackPressDialog = true @@ -72,7 +75,7 @@ private fun WebViewScreenContent(route: KomojuPaymentRoute.WebView) { indication = ripple(bounded = true, radius = 24.dp), interactionSource = remember { MutableInteractionSource() }, onClick = { - onBackPressedDispatcher?.onBackPressed() + screenModel.onBackPressed() }, ) .padding(16.dp), @@ -139,7 +142,7 @@ private fun WebViewScreenContent(route: KomojuPaymentRoute.WebView) { text = "Yes", modifier = Modifier .clickable { - onBackPressedDispatcher?.onBackPressed() + screenModel.onBackPressed() } .padding(16.dp), ) diff --git a/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewScreenModel.kt b/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewScreenModel.kt new file mode 100644 index 0000000..4e62610 --- /dev/null +++ b/android/src/main/java/com/komoju/android/sdk/ui/screens/webview/WebViewScreenModel.kt @@ -0,0 +1,10 @@ +package com.komoju.android.sdk.ui.screens.webview + +import com.komoju.android.sdk.navigation.RouterStateScreenModel + +internal class WebViewScreenModel : RouterStateScreenModel(Unit) { + + fun onBackPressed() { + mutableRouter.pop() + } +} diff --git a/android/src/main/java/com/komoju/android/sdk/utils/PreviewScreen.kt b/android/src/main/java/com/komoju/android/sdk/utils/PreviewScreen.kt new file mode 100644 index 0000000..3777cd2 --- /dev/null +++ b/android/src/main/java/com/komoju/android/sdk/utils/PreviewScreen.kt @@ -0,0 +1,14 @@ +package com.komoju.android.sdk.utils + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.screen.Screen + +/** + * An Empty Screen to Support Android Studio Previews + */ +internal object PreviewScreen : Screen { + private fun readResolve(): Any = PreviewScreen + + @Composable + override fun Content() = Unit +} diff --git a/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeItemDetailScreen.kt b/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeItemDetailScreen.kt index 36702ca..b947bc6 100644 --- a/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeItemDetailScreen.kt +++ b/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeItemDetailScreen.kt @@ -62,6 +62,7 @@ data class FakeItemDetailScreen(private val index: Int) : Screen { val komojuSDKConfiguration by screenModel.komojuSDKConfiguration.collectAsStateWithLifecycle() val komojuPaymentLauncher = rememberLauncherForActivityResult(KomojuSDK.KomojuPaymentResultContract) { screenModel.onKomojuPaymentCompleted() + navigator.push(if (it.isSuccessFul) FakeOrderSuccessScreen() else FakeOrderFailedScreen()) } LaunchedEffect(komojuSDKConfiguration) { val configuration = komojuSDKConfiguration @@ -159,7 +160,7 @@ data class FakeItemDetailScreen(private val index: Int) : Screen { .padding(16.dp), ) { Text( - "Buy this Item", + stringResource(R.string.buy_this_item), ) } } diff --git a/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeOrderFailedScreen.kt b/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeOrderFailedScreen.kt new file mode 100644 index 0000000..1048642 --- /dev/null +++ b/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeOrderFailedScreen.kt @@ -0,0 +1,101 @@ +package com.komoju.android.ui.screens.store + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import com.komoju.android.R +import com.komoju.android.ui.theme.KomojuDarkGreen + +class FakeOrderFailedScreen : Screen { + @Composable + override fun Content() { + FakeOrderFailedScreenContent() + } +} + +@Preview(showSystemUi = true, showBackground = true) +@Composable +private fun FakeOrderFailedScreenContent() { + val navigator = LocalNavigator.current + Column( + Modifier + .padding(16.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .padding(16.dp), + painter = painterResource(R.drawable.ic_sentiment_dissatisfied_24), + tint = Color.Red.copy(alpha = .5f), + contentDescription = null, + ) + + Text("Payment Failed", fontSize = 24.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.padding(16.dp)) + Text( + "Oops! Seems like there was an error placing your order because of a failed payment, please try again or report an issue. If any money is deducted then it will be refunded within 7 days", + fontSize = 16.sp, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.padding(16.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center, + ) { + Column { + Button( + colors = ButtonDefaults.buttonColors(containerColor = KomojuDarkGreen), + onClick = { + navigator?.pop() + }, + modifier = Modifier + .fillMaxWidth(), + ) { + Text( + stringResource(R.string.retry_payment), + ) + } + Spacer(modifier = Modifier.padding(8.dp)) + OutlinedButton( + onClick = { + navigator?.popUntilRoot() + }, + modifier = Modifier + .fillMaxWidth(), + ) { + Text( + stringResource(R.string.close), + ) + } + } + } + Spacer(modifier = Modifier.padding(16.dp)) + } +} diff --git a/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeOrderSuccessScreen.kt b/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeOrderSuccessScreen.kt new file mode 100644 index 0000000..bc85f93 --- /dev/null +++ b/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeOrderSuccessScreen.kt @@ -0,0 +1,102 @@ +package com.komoju.android.ui.screens.store + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import com.komoju.android.R +import com.komoju.android.ui.theme.KomojuDarkGreen +import com.komoju.android.ui.theme.KomojuLightGreen + +class FakeOrderSuccessScreen : Screen { + @Composable + override fun Content() { + FakeOrderConfirmedScreenContent() + } +} + +@Preview(showSystemUi = true, showBackground = true) +@Composable +private fun FakeOrderConfirmedScreenContent() { + val navigator = LocalNavigator.current + Column( + Modifier + .padding(16.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .padding(16.dp), + imageVector = Icons.Default.CheckCircle, + tint = KomojuLightGreen, + contentDescription = null, + ) + + Text(stringResource(R.string.order_confirmed), fontSize = 24.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.padding(16.dp)) + Text( + stringResource(R.string.thank_you_for_your_order), + fontSize = 16.sp, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.padding(16.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center, + ) { + Column { + Button( + colors = ButtonDefaults.buttonColors(containerColor = KomojuDarkGreen), + onClick = { + navigator?.popUntilRoot() + }, + modifier = Modifier + .fillMaxWidth(), + ) { + Text( + stringResource(R.string.continue_shopping), + ) + } + Spacer(modifier = Modifier.padding(8.dp)) + OutlinedButton( + onClick = { + navigator?.popUntilRoot() + }, + modifier = Modifier + .fillMaxWidth(), + ) { + Text( + stringResource(R.string.share), + ) + } + } + } + Spacer(modifier = Modifier.padding(16.dp)) + } +} diff --git a/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeStoreScreenModel.kt b/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeStoreScreenModel.kt index aed417f..da18675 100644 --- a/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeStoreScreenModel.kt +++ b/example-android/src/main/java/com/komoju/android/ui/screens/store/FakeStoreScreenModel.kt @@ -2,6 +2,7 @@ package com.komoju.android.ui.screens.store import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope +import com.komoju.android.sdk.ExperimentalKomojuPaymentApi import com.komoju.android.sdk.KomojuSDK import com.komoju.android.sdk.types.Currency import com.komoju.android.sdk.types.Language @@ -58,6 +59,7 @@ class FakeStoreScreenModel : ScreenModel { ) } + @OptIn(ExperimentalKomojuPaymentApi::class) fun onBuyClicked(item: Item) { screenModelScope.launch { createSession(item)?.let { sessionId -> @@ -67,6 +69,7 @@ class FakeStoreScreenModel : ScreenModel { ).setLanguage(language) .setCurrency(currency) .setConfigurableTheme(komojuConfigurableTheme) + .setInlinedProcessing(true) .build().let { komojuConfig -> _komojuSDKConfiguration.value = komojuConfig } diff --git a/example-android/src/main/res/drawable/ic_sentiment_dissatisfied_24.xml b/example-android/src/main/res/drawable/ic_sentiment_dissatisfied_24.xml new file mode 100644 index 0000000..5d38dc1 --- /dev/null +++ b/example-android/src/main/res/drawable/ic_sentiment_dissatisfied_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/example-android/src/main/res/values-ja/strings.xml b/example-android/src/main/res/values-ja/strings.xml index 376b3fc..62c93bc 100644 --- a/example-android/src/main/res/values-ja/strings.xml +++ b/example-android/src/main/res/values-ja/strings.xml @@ -18,5 +18,12 @@ オフホワイト ブルー ピンク + この商品を購入する + 買い物を続ける + 共有 + 近い + 支払いを再試行する + ご注文ありがとうございます。ご注文は確認されました。発送の詳細は、ご登録のメールアドレスにすぐに送信されます。 + 注文確定 \ No newline at end of file diff --git a/example-android/src/main/res/values/strings.xml b/example-android/src/main/res/values/strings.xml index 5996cc3..1c0068a 100644 --- a/example-android/src/main/res/values/strings.xml +++ b/example-android/src/main/res/values/strings.xml @@ -17,4 +17,11 @@ Off-White Blue Pink + Buy this Item + Continue Shopping + Share + Close + Retry Payment + Thank you for your Order, it has been confirmed, Shipping Details will be shared on your registered email address soon + Order Confirmed \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c7e357..4f2ad83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ nexus-publish = "2.0.0" android-minSdk = "24" android-compileSdk = "34" coreKtx = "1.13.1" -fragmentKtx = "1.8.3" +fragmentKtx = "1.8.4" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" @@ -13,12 +13,12 @@ appcompat = "1.7.0" material = "1.12.0" lifecycleRuntimeKtx = "2.8.6" activityCompose = "1.9.2" -composeBom = "2024.09.02" +composeBom = "2024.09.03" retrofit = "2.11.0" ktor = "2.3.12" coroutines = "1.8.1" datetime = "0.6.1" -uiTooling = "1.7.2" +uiTooling = "1.7.3" composeWebView = "0.33.6" konlinXJson = "1.7.2" voyager = "1.1.0-beta02" diff --git a/shared/src/commonMain/kotlin/com/komoju/mobile/sdk/entities/Payment.kt b/shared/src/commonMain/kotlin/com/komoju/mobile/sdk/entities/Payment.kt index 94cd45b..10abbe0 100644 --- a/shared/src/commonMain/kotlin/com/komoju/mobile/sdk/entities/Payment.kt +++ b/shared/src/commonMain/kotlin/com/komoju/mobile/sdk/entities/Payment.kt @@ -39,5 +39,7 @@ enum class PaymentStatus { companion object { fun fromString(status: String): PaymentStatus = entries.find { it.name == status.uppercase() } ?: throw IllegalArgumentException("Invalid payment status: $status") + + fun PaymentStatus.isSuccessful(): Boolean = this in listOf(COMPLETED, CAPTURED) } }