Skip to content

Commit

Permalink
Deeplink Payments (#11)
Browse files Browse the repository at this point in the history
* Deeplink Payments

* Android Lint fixups
  • Loading branch information
AmniX authored Sep 19, 2024
1 parent 1953338 commit 2e1b0c7
Show file tree
Hide file tree
Showing 30 changed files with 344 additions and 151 deletions.
1 change: 0 additions & 1 deletion android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ dependencies {
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.activity.compose)
implementation(libs.navigation.compose)
implementation(platform(libs.compose.bom))
implementation(libs.ui)
implementation(libs.ui.graphics)
Expand Down
17 changes: 15 additions & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<application>
<activity
android:name=".KomojuPaymentActivity"
android:exported="true"
android:launchMode="singleInstance"
android:theme="@style/Komoju.Transparent.Activity" />
android:theme="@style/Komoju.Transparent.Activity">
<intent-filter
android:autoVerify="true"
tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="@string/komoju_consumer_app_scheme" />
</intent-filter>
</activity>
</application>
</manifest>
Empty file.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.degica.komoju.android.sdk

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
Expand All @@ -18,35 +19,30 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.IntentCompat
import androidx.lifecycle.ViewModel
import androidx.core.util.Consumer
import androidx.lifecycle.lifecycleScope
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.SlideTransition
import com.degica.komoju.android.sdk.ui.screens.RouterEffect
import com.degica.komoju.android.sdk.ui.screens.payment.KomojuPaymentScreen
import com.degica.komoju.android.sdk.ui.theme.KomojuMobileSdkTheme
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

private const val ANIMATION_DURATION = 500

internal class KomojuPaymentViewModel : ViewModel() {
private val _isVisible = MutableStateFlow(false)
val isVisible = _isVisible.asStateFlow()

fun toggleVisiblity(value: Boolean) {
_isVisible.value = value
}
}

internal class KomojuPaymentActivity : ComponentActivity() {
private val viewModel by viewModels<KomojuPaymentViewModel>()

Expand All @@ -65,6 +61,7 @@ internal class KomojuPaymentActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
val isVisible by viewModel.isVisible.collectAsState()
val router by viewModel.router.collectAsState()
val animatedAlpha by animateFloatAsState(
targetValue = if (isVisible) .3f else .0f,
label = "scrim_alpha_animation",
Expand Down Expand Up @@ -95,6 +92,8 @@ internal class KomojuPaymentActivity : ComponentActivity() {
KomojuPaymentScreen(configuration),
) { navigator ->
SlideTransition(navigator)
RouterEffect(router, viewModel::onRouteConsumed)
NewIntentEffect(LocalContext.current, viewModel::onNewDeeplink)
}
}
}
Expand All @@ -107,8 +106,16 @@ internal class KomojuPaymentActivity : ComponentActivity() {
}
}

private fun handleIntentAction(intent: Intent, navigator: Navigator) {
// Handle onNewIntent
@Composable
fun NewIntentEffect(context: Context, onNewDeeplink: (String) -> Unit) {
LaunchedEffect(Unit) {
callbackFlow {
val componentActivity = context as ComponentActivity
val consumer = Consumer<Intent> { trySend(it) }
componentActivity.addOnNewIntentListener(consumer)
awaitClose { componentActivity.removeOnNewIntentListener(consumer) }
}.collectLatest { onNewDeeplink(it.data?.toString().orEmpty()) }
}
}

override fun finish() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.degica.komoju.android.sdk

import android.util.Log
import androidx.lifecycle.ViewModel
import com.degica.komoju.android.sdk.ui.screens.Router
import com.degica.komoju.android.sdk.utils.DeeplinkEntity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

internal class KomojuPaymentViewModel : ViewModel() {

private val _isVisible = MutableStateFlow(false)
val isVisible = _isVisible.asStateFlow()

private val _router = MutableStateFlow<Router?>(null)
val router = _router.asStateFlow()

fun toggleVisiblity(value: Boolean) {
_isVisible.value = value
}

fun onRouteConsumed() {
_router.value = null
}

fun onNewDeeplink(deeplink: String) {
val deeplinkEntity = DeeplinkEntity.from(deeplink)
Log.d("Aman", "handleIntentAction $deeplinkEntity")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,18 @@ class KomojuSDK(private val configuration: Configuration) {

companion object {
internal const val CONFIGURATION_KEY: String = "KomojuSDK.Configuration"
fun show(context: Context, configuration: Configuration, onCompleted: () -> Unit) {
fun show(context: Context, configuration: Configuration) {
context.preChecks()
val intent = android.content.Intent(context, KomojuPaymentActivity::class.java)
intent.putExtra(CONFIGURATION_KEY, configuration)
context.startActivity(intent)
}

private fun Context.preChecks() {
if (resources.getString(R.string.komoju_consumer_app_scheme) == "this-should-not-be-the-case") {
error("Please set komoju_consumer_app_scheme in strings.xml with your app scheme")
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.degica.komoju.android.sdk.ui.screens

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.degica.komoju.android.sdk.KomojuSDK
import com.degica.komoju.android.sdk.ui.screens.status.PaymentStatusScreen
import com.degica.komoju.android.sdk.ui.screens.awating.KonbiniAwaitingPaymentScreen
import com.degica.komoju.android.sdk.ui.screens.webview.WebViewScreen
import com.degica.komoju.mobile.sdk.entities.Payment

Expand All @@ -15,31 +19,41 @@ 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()
}

internal sealed interface KomojuPaymentRoute {
data class Status(val configuration: KomojuSDK.Configuration, val payment: Payment) : KomojuPaymentRoute
data class KonbiniAwaitingPayment(val configuration: KomojuSDK.Configuration, val payment: Payment) : KomojuPaymentRoute
data class WebView(val url: String, val canComeBack: Boolean = false) : KomojuPaymentRoute

val screen
get() = when (this) {
is WebView -> WebViewScreen(this)
is Status -> PaymentStatusScreen(this)
is KonbiniAwaitingPayment -> KonbiniAwaitingPaymentScreen(this)
}
}

@Composable
internal fun RouterEffect(router: Router?, onHandled: () -> Unit) {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
LaunchedEffect(router) {
when (router) {
is Router.Pop -> navigator.pop()
is Router.PopToRoot -> navigator.popUntilRoot()
is Router.Push -> navigator.push(router.route.screen)
is Router.Replace -> navigator.replace(router.route.screen)
is Router.ReplaceAll -> navigator.replaceAll(router.route.screen)
null -> Unit
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)
}
else -> 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,4 +1,4 @@
package com.degica.komoju.android.sdk.ui.screens.status
package com.degica.komoju.android.sdk.ui.screens.awating

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
Expand Down Expand Up @@ -48,10 +48,10 @@ import com.degica.komoju.android.sdk.utils.AmountUtils
import com.degica.komoju.mobile.sdk.entities.Payment
import com.degica.komoju.mobile.sdk.entities.PaymentStatus

internal data class PaymentStatusScreen(val route: KomojuPaymentRoute.Status) : Screen {
internal data class KonbiniAwaitingPaymentScreen(val route: KomojuPaymentRoute.KonbiniAwaitingPayment) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { PaymentStatusScreenModel(route.configuration, route.payment) }
val screenModel = rememberScreenModel { KonbiniAwaitingPaymentScreenModel(route.configuration, route.payment) }
val uiState by screenModel.state.collectAsState()
val router by screenModel.router.collectAsState()
RouterEffect(router, screenModel::onRouteConsumed)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.degica.komoju.android.sdk.ui.screens.status
package com.degica.komoju.android.sdk.ui.screens.awating

import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
Expand All @@ -12,8 +12,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

internal class PaymentStatusScreenModel(private val config: KomojuSDK.Configuration, private val payment: Payment? = null) :
StateScreenModel<PaymentStatusUiState>(PaymentStatusUiState(payment)) {
internal class KonbiniAwaitingPaymentScreenModel(private val config: KomojuSDK.Configuration, private val payment: Payment? = null) :
StateScreenModel<KonbiniAwaitingPaymentUiState>(KonbiniAwaitingPaymentUiState(payment)) {
private val komojuApi = KomojuRemoteApi(config.publishableKey, config.language.languageCode)
private val _router = MutableStateFlow<Router?>(null)
val router = _router.asStateFlow()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.degica.komoju.android.sdk.ui.screens.awating

import com.degica.komoju.mobile.sdk.entities.Payment

internal data class KonbiniAwaitingPaymentUiState(val payment: Payment?, val isLoading: Boolean = false)
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,16 @@ internal class KomojuPaymentScreenModel(private val config: KomojuSDK.Configurat

private fun Payment.handle() {
when (this) {
is Payment.Konbini -> {
_router.value = Router.Replace(KomojuPaymentRoute.Status(config, payment = this))
}

is Payment.Konbini -> _router.value = Router.Replace(KomojuPaymentRoute.KonbiniAwaitingPayment(config, payment = this))
is Payment.PayPay -> _router.value = Router.Handle(url = redirectURL)
else -> Unit
}
}

private fun PaymentMethod.validate() = when (this) {
is PaymentMethod.CreditCard -> state.value.creditCardDisplayData.validate()
is PaymentMethod.Konbini -> state.value.konbiniDisplayData.validate(state.value.commonDisplayData)
is PaymentMethod.PayPay -> true // No input required
else -> false
}

Expand Down Expand Up @@ -172,7 +171,7 @@ internal class KomojuPaymentScreenModel(private val config: KomojuSDK.Configurat
is PaymentMethod.Other -> TODO()
is PaymentMethod.Paidy -> TODO()
is PaymentMethod.PayEasy -> TODO()
is PaymentMethod.PayPay -> TODO()
is PaymentMethod.PayPay -> PaymentRequest.PayPay(paymentMethod = this)
is PaymentMethod.RakutenPay -> TODO()
is PaymentMethod.WebMoney -> TODO()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.degica.komoju.android.sdk.R
import com.degica.komoju.android.sdk.types.Language
import com.degica.komoju.android.sdk.ui.theme.Gray200
Expand Down Expand Up @@ -47,16 +48,16 @@ internal fun KonbiniBrandsRow(konbiniBrands: List<KonbiniBrand>, selectedKonbini
private fun KonbiniBrand(konbiniBrand: KonbiniBrand, isSelected: Boolean, onSelected: () -> Unit) {
Column(
modifier = Modifier
.defaultMinSize(minWidth = 136.dp)
.defaultMinSize(minWidth = 120.dp)
.padding(4.dp)
.clip(RoundedCornerShape(8.dp))
.border(1.dp, if (isSelected) KomojuDarkGreen else Gray200, RoundedCornerShape(8.dp))
.clickable(onClick = onSelected)
.padding(16.dp),
.padding(8.dp),
) {
Image(painter = painterResource(konbiniBrand.displayIcon), contentDescription = "${konbiniBrand.key} icon", modifier = Modifier.size(32.dp))
Spacer(modifier = Modifier.height(4.dp))
Text(LocalI18nTextsProvider.current[konbiniBrand.key])
Text(LocalI18nTextsProvider.current[konbiniBrand.key], fontSize = 14.sp)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ internal fun PaymentMethodsRow(paymentMethods: List<PaymentMethod>, selectedPaym
private fun PaymentMethodComposable(paymentMethod: PaymentMethod, isSelected: Boolean, onSelected: () -> Unit) {
Column(
modifier = Modifier
.defaultMinSize(minWidth = 148.dp, minHeight = 112.dp)
.padding(8.dp)
.defaultMinSize(minWidth = 120.dp)
.padding(4.dp)
.clip(RoundedCornerShape(8.dp))
.border(1.dp, if (isSelected) KomojuDarkGreen else Gray200, RoundedCornerShape(8.dp))
.clickable(onClick = onSelected)
.padding(16.dp),
.padding(12.dp),
) {
Image(painter = painterResource(paymentMethod.displayIcon), contentDescription = "${paymentMethod.displayName} icon", modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(4.dp))
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.degica.komoju.android.sdk.ui.screens.success

import androidx.compose.runtime.Composable
import cafe.adriel.voyager.core.screen.Screen

internal class PaymentSuccessScreen : Screen {
@Composable
override fun Content() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.degica.komoju.android.sdk.ui.screens.webview

import com.kevinnzou.web.AccompanistWebChromeClient

internal class WebChromeClient : AccompanistWebChromeClient()
Loading

0 comments on commit 2e1b0c7

Please sign in to comment.