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

[BIG-PR] Refactor Android-Specific Code to Kotlin Multiplatform Support #70

Merged
merged 11 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 6 additions & 16 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,30 +52,20 @@ android {
}

dependencies {
api(project(":shared"))
implementation(project(":shared"))
implementation(libs.core.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.activity.compose)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.activity.compose)
implementation(platform(libs.compose.bom))
implementation(libs.ui)
implementation(libs.ui.graphics)
implementation(libs.ui.tooling.preview)
implementation(libs.material3)
implementation(libs.compose.webview)
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)
implementation(libs.ui.graphics)
implementation(libs.material3)
implementation(platform(libs.compose.bom))
implementation(libs.ui)
debugImplementation(libs.androidx.ui.tooling)
}

Expand Down
176 changes: 176 additions & 0 deletions android/src/main/java/com/komoju/android/sdk/KomojuAndroidSDK.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package com.komoju.android.sdk

import android.os.Parcelable
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.ui.graphics.toArgb
import com.komoju.android.sdk.annotations.ExperimentalKomojuPaymentApi
import com.komoju.android.sdk.types.Currency
import com.komoju.android.sdk.types.Language
import com.komoju.mobile.sdk.KomojuMobileSDKConfiguration
import com.komoju.mobile.sdk.KomojuMobileSDKPaymentResult
import com.komoju.mobile.sdk.canProcessPayment
import com.komoju.mobile.sdk.ui.theme.DefaultConfigurableTheme
import com.komoju.mobile.sdk.ui.theme.toColor
import kotlinx.parcelize.Parcelize
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import com.komoju.mobile.sdk.ui.theme.ConfigurableTheme as CoreConfigurableTheme

object KomojuAndroidSDK {
val activityResultContract: ActivityResultContract<Configuration, PaymentResult>
get() = KomojuStartPaymentForResultContract()

/**
* Configuration class to hold parameters required for payment processing.
*/
@Parcelize
data class Configuration(
val language: Language, // Language setting for the payment UI.
val currency: Currency, // Currency used in the transaction.
val publishableKey: String?, // Public API key for Komoju integration.
val isDebugMode: Boolean, // Debug mode flag for logging and testing.
val sessionId: String?, // Unique session ID for payment transaction.
val redirectURL: String, // URL to redirect after payment completion.
val appScheme: String, // App schema for deep links.
val configurableTheme: ConfigurableTheme, // Custom theme for UI elements.
val inlinedProcessing: Boolean, // Flag to enable inlined processing.
) : Parcelable {

/**
* Builder class for creating a [Configuration] instance.
* Offers a flexible way to set optional parameters.
*/
class Builder(private val publishableKey: String, private val sessionId: String) {
private var language: Language = Language.JAPANESE // Default language is Japanese.
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 {
this.language = language
}

/** Sets the currency for the transaction. */
fun setCurrency(currency: Currency) = apply {
this.currency = currency
}

/** Enables or disables debug mode. */
fun setDebugMode(isDebugMode: Boolean) = apply {
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.
*/
fun build(): Configuration = Configuration(
language = language,
currency = currency,
publishableKey = publishableKey,
sessionId = sessionId,
isDebugMode = isDebugMode,
configurableTheme = configurableTheme,
inlinedProcessing = inlinedProcessing,
appScheme = "",
redirectURL = "",
)
}
}

@Parcelize
data class ConfigurableTheme(
val primaryColorInt: Int,
val primaryContentColorInt: Int,
val loaderColorInt: Int,
val primaryShapeCornerRadiusInDp: Int,
) : Parcelable {
companion object {
val default: ConfigurableTheme = com.komoju.mobile.sdk.ui.theme.ConfigurableTheme.default.toAndroidSDKConfigurableTheme()
}
}

@Parcelize
data class PaymentResult(val isSuccessFul: Boolean) : Parcelable
}

internal fun KomojuAndroidSDK.ConfigurableTheme.toKomojuConfigurableTheme(): CoreConfigurableTheme = DefaultConfigurableTheme(
primaryColor = primaryColorInt.toLong(),
primaryContentColor = primaryContentColorInt.toLong(),
loaderColor = loaderColorInt.toLong(),
primaryShapeCornerRadiusInDp = this.primaryShapeCornerRadiusInDp,
)

internal fun CoreConfigurableTheme.toAndroidSDKConfigurableTheme(): KomojuAndroidSDK.ConfigurableTheme =
KomojuAndroidSDK.ConfigurableTheme(
primaryColorInt = primaryColor.toColor().toArgb(),
primaryContentColorInt = primaryContentColor.toColor().toArgb(),
loaderColorInt = loaderColor.toColor().toArgb(),
primaryShapeCornerRadiusInDp = primaryShapeCornerRadiusInDp,
)


/**
* Extension function to check if the current configuration is valid for processing a payment.
* @return True if the configuration is non-null and contains both publishableKey and sessionId.
*/
@OptIn(ExperimentalContracts::class)
fun KomojuAndroidSDK.Configuration?.canProcessPayment(): Boolean {
contract {
returns(true) implies (this@canProcessPayment != null)
}
return this?.toMobileConfiguration().canProcessPayment()
}

internal fun KomojuMobileSDKConfiguration.toAndroidConfiguration(): KomojuAndroidSDK.Configuration = KomojuAndroidSDK.Configuration(
language = Language.parse(this.language),
currency = Currency.parse(this.currency),
publishableKey = this.publishableKey,
isDebugMode = this.isDebugMode,
sessionId = this.sessionId,
redirectURL = this.redirectURL,
configurableTheme = this.configurableTheme.toAndroidSDKConfigurableTheme(),
inlinedProcessing = this.inlinedProcessing,
appScheme = this.appScheme,
)

internal fun KomojuAndroidSDK.Configuration.toMobileConfiguration(): KomojuMobileSDKConfiguration = KomojuMobileSDKConfiguration(
language = this.language.languageCode,
currency = this.currency.currencyCode,
publishableKey = this.publishableKey,
isDebugMode = this.isDebugMode,
sessionId = this.sessionId,
redirectURL = this.redirectURL,
configurableTheme = this.configurableTheme.toKomojuConfigurableTheme(),
inlinedProcessing = this.inlinedProcessing,
appScheme = this.appScheme,
)


internal fun KomojuMobileSDKPaymentResult.toParcelable(): KomojuAndroidSDK.PaymentResult = KomojuAndroidSDK.PaymentResult(this.isSuccessFul)

internal fun KomojuAndroidSDK.PaymentResult.toPaymentResult(): KomojuMobileSDKPaymentResult = KomojuMobileSDKPaymentResult(this.isSuccessFul)

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
Expand All @@ -30,13 +29,10 @@ import androidx.core.content.IntentCompat
import androidx.core.util.Consumer
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
import com.komoju.mobile.sdk.navigation.PaymentResultScreenModel
import com.komoju.mobile.sdk.navigation.paymentResultScreenModel
import com.komoju.mobile.sdk.ui.screens.KomojuPaymentEntryPoint
import com.komoju.mobile.sdk.ui.screens.RouterEffect
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.callbackFlow
Expand All @@ -55,7 +51,7 @@ internal class KomojuPaymentActivity : ComponentActivity() {
/* name = */
KomojuStartPaymentForResultContract.CONFIGURATION_KEY,
/* clazz = */
KomojuSDK.Configuration::class.java,
KomojuAndroidSDK.Configuration::class.java,
) ?: error("komoju sdk configuration is null"),
)
},
Expand Down Expand Up @@ -87,21 +83,10 @@ internal class KomojuPaymentActivity : ComponentActivity() {
.navigationBarsPadding(),
contentAlignment = Alignment.BottomCenter,
) {
KomojuMobileSdkTheme(viewModel.configuration) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(.9f),
) {
Navigator(
KomojuPaymentScreen(viewModel.configuration),
) { navigator ->
commonScreenModel = navigator.paymentResultScreenModel()
SlideTransition(navigator)
RouterEffect(viewModel.router.collectAsStateWithLifecycle(), viewModel::onRouteConsumed)
NewIntentEffect(LocalContext.current, viewModel::onNewDeeplink)
}
}
KomojuPaymentEntryPoint(viewModel.configuration) { navigator ->
RouterEffect(viewModel.router.collectAsStateWithLifecycle(), viewModel::onRouteConsumed)
commonScreenModel = navigator.paymentResultScreenModel()
NewIntentEffect(LocalContext.current, viewModel::onNewDeeplink)
}
}
}
Expand All @@ -128,7 +113,7 @@ internal class KomojuPaymentActivity : ComponentActivity() {
setResult(
RESULT_OK,
Intent().apply {
putExtra(KomojuStartPaymentForResultContract.RESULT_KEY, commonScreenModel?.result)
putExtra(KomojuStartPaymentForResultContract.RESULT_KEY, commonScreenModel?.result?.toParcelable())
},
)
lifecycleScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package com.komoju.android.sdk

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.komoju.android.sdk.ui.screens.KomojuPaymentRoute
import com.komoju.android.sdk.ui.screens.Router
import com.komoju.android.sdk.utils.DeeplinkEntity
import com.komoju.mobile.sdk.KomojuMobileSDKConfiguration
import com.komoju.mobile.sdk.ui.screens.KomojuPaymentRoute
import com.komoju.mobile.sdk.ui.screens.Router
import com.komoju.mobile.sdk.utils.DeeplinkEntity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

internal class KomojuPaymentViewModel(internal val configuration: KomojuSDK.Configuration) : ViewModel() {
internal class KomojuPaymentViewModel(internal val configuration: KomojuMobileSDKConfiguration) : ViewModel() {

private val _isVisible = MutableStateFlow(false)
val isVisible = _isVisible.asStateFlow()
Expand All @@ -35,16 +36,18 @@ internal class KomojuPaymentViewModel(internal val configuration: KomojuSDK.Conf
amount = deeplinkEntity.amount,
currency = deeplinkEntity.currency,
)

DeeplinkEntity.Verify.BySessionId -> KomojuPaymentRoute.ProcessPayment.ProcessType.Session
},
),
)
}
}

internal class KomojuPaymentViewModelFactory(private val configuration: KomojuSDK.Configuration) : ViewModelProvider.NewInstanceFactory() {
internal class KomojuPaymentViewModelFactory(private val configuration: KomojuAndroidSDK.Configuration) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return KomojuPaymentViewModel(configuration) as T
return KomojuPaymentViewModel(configuration.toMobileConfiguration()) as T
}
}
Loading