
Unified type-safe API integrating Google Pay and Apple Pay, with reactive capability detection, Compose UI payment components, serializable tokens, robust error handling and thread-safe, production-ready state management.
Quickstart โข Installation โข Samples โข Documentation โข
A Kotlin Multiplatform payment library with Compose Multiplatform UI components for Google Pay and Apple Pay across Android, iOS, and Web. One API, shared types, reactive availability detection, and platform-native payment buttons.
Get started with KPayment in 5 minutes:
// Create a payment manager
val manager = rememberMobilePaymentManager(config)
// Check availability
val isReady by manager.observeAvailability(currentNativePaymentProvider())
.collectAsState(initial = false)
// Launch payment
val launcher = rememberNativePaymentLauncher { result ->
when (result) {
is PaymentResult.Success -> handleSuccess(result.token)
is PaymentResult.Error -> handleError(result)
is PaymentResult.Cancelled -> handleCancellation()
}
}
PaymentButton(enabled = isReady, onClick = { launcher.launch("10.00") })Next Steps:
| Platform | Google Pay | Apple Pay | Compose UI |
|---|---|---|---|
| Android | โ | โ | โ |
| iOS | โ | โ | โ |
| Web (JS) | โ | โ * | โ |
| WASM | โ | โ * | โ |
*Apple Pay on Web requires Safari and a merchant validation endpoint.
Android:
iOS:
Web:
Build Environment:
Flow and StateFlow
val config = MobilePaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID"
),
applePayMobile = ApplePayMobileConfig(
merchantId = "merchant.com.yourcompany.app",
base = ApplePayBaseConfig(merchantName = "Your Store")
)
)
@Composable
fun PaymentScreen() {
val manager = rememberMobilePaymentManager(config)
val isReady by manager.observeAvailability(currentNativePaymentProvider())
.collectAsState(initial = false)
PaymentManagerProvider(manager) {
val launcher = rememberNativePaymentLauncher { result ->
when (result) {
is PaymentResult.Success -> println("Token: ${result.token}")
is PaymentResult.Error -> println("Error: ${result.message}")
is PaymentResult.Cancelled -> println("Cancelled")
}
}
PaymentButton(
theme = NativePaymentTheme.Dark,
type = NativePaymentType.Pay,
enabled = isReady,
radius = 12.dp,
onClick = { launcher.launch("10.00") }
)
}
}val config = WebPaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID"
),
applePayWeb = ApplePayWebConfig(
base = ApplePayBaseConfig(merchantName = "Your Store"),
merchantValidationEndpoint = "https://example.com/apple-pay/validate",
baseUrl = "https://example.com",
domain = "example.com"
)
)
@Composable
fun PaymentScreen() {
val manager = rememberWebPaymentManager(config)
PaymentManagerProvider(manager) {
val googlePay = rememberGooglePayWebLauncher { result ->
// Handle result
}
val applePay = rememberApplePayWebLauncher { result ->
// Handle result
}
Button(onClick = { googlePay.launch("10.00") }) {
Text("Pay with Google Pay")
}
Button(onClick = { applePay.launch("10.00") }) {
Text("Pay with Apple Pay")
}
}
}// Reactive: Observe availability changes (recommended)
val isReady by manager.observeAvailability(PaymentProvider.GooglePay)
.collectAsState(initial = false)
Button(enabled = isReady, onClick = { launcher.launch("10.00") }) {
Text("Pay with Google Pay")
}
// Explicit check when needed
val capabilities = manager.checkCapabilities()
if (capabilities.canPayWith(PaymentProvider.GooglePay)) {
launcher.launch("10.00")
}when (val result = paymentResult) {
is PaymentResult.Success -> {
// Send token to your backend
processPayment(result.token)
}
is PaymentResult.Error -> {
when (result.reason) {
PaymentErrorReason.NetworkError -> showNetworkError()
PaymentErrorReason.NotAvailable -> showAlternativePayment()
PaymentErrorReason.Timeout -> retryPayment()
else -> showGenericError(result.message)
}
}
is PaymentResult.Cancelled -> {
// User cancelled - no action needed
}
}See full examples in Quick Start and comprehensive guides below.
Ensure Maven Central is in your repository list (usually already configured):
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}Add to your libs.versions.toml:
[versions]
kpayment = "0.1.0"
[libraries]
kpayment-core = { module = "com.kttipay:kpayment-core", version.ref = "kpayment" }
kpayment-mobile = { module = "com.kttipay:kpayment-mobile", version.ref = "kpayment" }
kpayment-web = { module = "com.kttipay:kpayment-web", version.ref = "kpayment" }Then in your build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kpayment.core)
}
androidMain.dependencies {
implementation(libs.kpayment.mobile)
}
iosMain.dependencies {
implementation(libs.kpayment.mobile)
}
jsMain.dependencies {
implementation(libs.kpayment.web)
}
wasmJsMain.dependencies {
implementation(libs.kpayment.web)
}
}
}Add the KPayment dependencies directly to your build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.kttipay:kpayment-core:0.1.0")
}
androidMain.dependencies {
implementation("com.kttipay:kpayment-mobile:0.1.0")
}
iosMain.dependencies {
implementation("com.kttipay:kpayment-mobile:0.1.0")
}
jsMain.dependencies {
implementation("com.kttipay:kpayment-web:0.1.0")
}
wasmJsMain.dependencies {
implementation("com.kttipay:kpayment-web:0.1.0")
}
}
}No additional configuration required. Google Play Services Wallet is included as a dependency.
Add Apple Pay Capability:
Configure Merchant ID:
merchant.com.yourcompany.yourapp
For Apple Pay on Web:
See Apple Pay on the Web documentation
Amounts are decimal strings (for example, "10.00").
val config = MobilePaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID"
),
applePayMobile = null
)
@Composable
fun PaymentScreen() {
val manager = rememberMobilePaymentManager(config)
val isReady by manager.observeAvailability(currentNativePaymentProvider())
.collectAsState(initial = false)
PaymentManagerProvider(manager) {
val launcher = rememberNativePaymentLauncher { result ->
// handle PaymentResult.Success, PaymentResult.Error, PaymentResult.Cancelled
}
PaymentButton(
theme = NativePaymentTheme.Dark,
type = NativePaymentType.Pay,
enabled = isReady,
radius = 12.dp,
onClick = { launcher.launch("10.00") }
)
}
}val config = MobilePaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = null,
applePayMobile = ApplePayMobileConfig(
merchantId = "merchant.com.yourcompany.app",
base = ApplePayBaseConfig(merchantName = "Your Store")
)
)
@Composable
fun PaymentScreen() {
val manager = rememberMobilePaymentManager(config)
val isReady by manager.observeAvailability(currentNativePaymentProvider())
.collectAsState(initial = false)
PaymentManagerProvider(manager) {
val launcher = rememberNativePaymentLauncher { result ->
// handle PaymentResult.Success, PaymentResult.Error, PaymentResult.Cancelled
}
PaymentButton(
theme = NativePaymentTheme.Dark,
type = NativePaymentType.Pay,
enabled = isReady,
radius = 12.dp,
onClick = { launcher.launch("10.00") }
)
}
}Use createWebPaymentManager(config) outside Compose if you are not using Compose UI.
val config = WebPaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID"
),
applePayWeb = ApplePayWebConfig(
base = ApplePayBaseConfig(merchantName = "Your Store"),
merchantValidationEndpoint = "https://example.com/apple-pay/validate",
baseUrl = "https://example.com",
domain = "example.com"
)
)
@Composable
fun PaymentScreen() {
val manager = rememberWebPaymentManager(config)
PaymentManagerProvider(manager) {
val googlePay = rememberGooglePayWebLauncher { result ->
when (result) {
is PaymentResult.Success -> println("Token: ${result.token}")
is PaymentResult.Error -> println("Error: ${result.message}")
is PaymentResult.Cancelled -> println("Cancelled")
}
}
val applePay = rememberApplePayWebLauncher { result ->
when (result) {
is PaymentResult.Success -> println("Token: ${result.token}")
is PaymentResult.Error -> println("Error: ${result.message}")
is PaymentResult.Cancelled -> println("Cancelled")
}
}
Button(onClick = { googlePay.launch("10.00") }) {
Text("Pay with Google Pay")
}
Button(onClick = { applePay.launch("10.00") }) {
Text("Pay with Apple Pay")
}
}
}val googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID",
allowedCardNetworks = setOf(
GooglePayCardNetwork.VISA,
GooglePayCardNetwork.MASTERCARD
),
allowedAuthMethods = GooglePayAuthMethod.DEFAULT,
currencyCode = "AUD",
countryCode = "AU"
)val googlePay = GooglePayConfig(
allowCreditCards = true,
assuranceDetailsRequired = true
)allowCreditCards: Set to true to allow credit card transactions (default: false)assuranceDetailsRequired: Set to true to request additional cardholder verification (default: false)val appleBase = ApplePayBaseConfig(
merchantName = "Your Store",
supportedNetworks = setOf(
ApplePayNetwork.VISA,
ApplePayNetwork.MASTERCARD
),
merchantCapabilities = setOf(
ApplePayMerchantCapability.CAPABILITY_3DS,
ApplePayMerchantCapability.CAPABILITY_DEBIT
),
currencyCode = "AUD",
countryCode = "AU"
)
val applePayMobile = ApplePayMobileConfig(
merchantId = "merchant.com.yourcompany.app",
base = appleBase
)
val applePayWeb = ApplePayWebConfig(
base = appleBase,
merchantValidationEndpoint = "https://example.com/apple-pay/validate",
baseUrl = "https://example.com",
domain = "example.com"
)// Reactive: Observe availability changes (most common)
val isReady by manager.observeAvailability(PaymentProvider.GooglePay)
.collectAsState(initial = false)
Button(
enabled = isReady,
onClick = { launcher.launch("10.00") }
) {
Text("Pay with Google Pay")
}
// Explicit check when needed
val capabilities = manager.checkCapabilities()
if (capabilities.canPayWith(PaymentProvider.GooglePay)) {
launcher.launch("10.00")
}
// Handle detailed capability status
val capabilities = manager.checkCapabilities()
when (capabilities.googlePay) {
CapabilityStatus.Ready -> { /* show button */ }
CapabilityStatus.NotConfigured -> { /* missing config */ }
CapabilityStatus.NotSupported -> { /* platform not supported */ }
CapabilityStatus.Checking -> { /* loading */ }
is CapabilityStatus.Error -> { /* handle error: ${error.reason} */ }
}when (val result = paymentResult) {
is PaymentResult.Success -> {
val token = result.token
// Send token to your backend for processing
}
is PaymentResult.Error -> {
when (result.reason) {
PaymentErrorReason.Timeout -> {}
PaymentErrorReason.NetworkError -> {}
PaymentErrorReason.DeveloperError -> {}
PaymentErrorReason.InternalError -> {}
PaymentErrorReason.NotAvailable -> {}
PaymentErrorReason.AlreadyInProgress -> {}
PaymentErrorReason.SignInRequired -> {}
PaymentErrorReason.ApiNotConnected -> {}
PaymentErrorReason.ConnectionSuspendedDuringCall -> {}
PaymentErrorReason.Interrupted -> {}
PaymentErrorReason.Unknown -> {}
}
}
is PaymentResult.Cancelled -> {}
}The launcher exposes isProcessing: StateFlow<Boolean> to track whether a payment is in progress:
val launcher = rememberNativePaymentLauncher { result -> /* handle */ }
val isProcessing by launcher.isProcessing.collectAsState()
PaymentButton(
enabled = isReady && !isProcessing,
onClick = { launcher.launch("10.00") }
)| Method | Type | Description |
|---|---|---|
config |
Property | The payment configuration |
checkCapabilities() |
suspend |
Check current payment capabilities from platform SDKs |
observeCapabilities() |
Flow<PaymentCapabilities> |
Reactively observe full payment capabilities |
observeAvailability(provider) |
Flow<Boolean> |
Reactively observe specific provider availability |
// Reactive UI for full capabilities
val capabilities by manager.observeCapabilities()
.collectAsState(initial = PaymentCapabilities.initial)
when (capabilities.googlePay) {
CapabilityStatus.Ready -> ShowPaymentButton()
CapabilityStatus.Checking -> ShowLoading()
else -> ShowNotAvailable()
}
// Reactive UI for single provider (convenience)
val isReady by manager.observeAvailability(PaymentProvider.GooglePay)
.collectAsState(initial = false)
Button(enabled = isReady, onClick = { launcher.launch("10.00") }) {
Text("Pay")
}
// Explicit capability check
val capabilities = manager.checkCapabilities()
if (capabilities.canPayWith(PaymentProvider.GooglePay)) {
launcher.launch("10.00")
}All payment results come through the PaymentResult sealed class:
when (result) {
is PaymentResult.Success -> sendTokenToBackend(result.token)
is PaymentResult.Error -> handleError(result.reason, result.message)
is PaymentResult.Cancelled -> { /* user cancelled, no action needed */ }
}| Reason | When | Suggested Action |
|---|---|---|
Timeout |
Request timed out | Retry after a delay |
NetworkError |
No connectivity | Check network, retry |
DeveloperError |
Bad config or invalid amount | Fix configuration |
InternalError |
Platform SDK failure | Retry with backoff |
NotAvailable |
Payment method unavailable | Show alternative |
AlreadyInProgress |
Duplicate launch | Disable button via launcher.isProcessing
|
SignInRequired |
User not signed in | Prompt sign-in |
ApiNotConnected |
SDK not initialized | Re-initialize manager |
ConnectionSuspendedDuringCall |
App backgrounded mid-call | Retry on resume |
Interrupted |
Operation interrupted | Show retry dialog |
Unknown |
Unclassified error | Log and show generic message |
observeAvailability() before showing the payment button.launcher.isProcessing to disable the button while a payment is in flight.Cancelled is not an error โ don't show error UI for it.KPayment is organized into three main modules:
KPayment/
โโโ payment-core/ Shared interfaces, models, and types
โโโ payment-mobile/ Android + iOS implementations
โโโ payment-web/ Web (JS/WASM) implementations
Shared abstractions used by all platforms:
PaymentManager - unified API for capability checks and configurationGooglePayConfig, ApplePayBaseConfig, MobilePaymentConfig, WebPaymentConfig)PaymentResult) and tokens (GooglePayToken, ApplePayToken)StateFlow and Flow
See payment-core/README.md for a focused overview.
Android + iOS implementation with Compose helpers:
createMobilePaymentManager(...)rememberMobilePaymentManager(...)PaymentButton and rememberNativePaymentLauncher(...)
See payment-mobile/README.md for setup and platform-specific details.
Web implementation for JS/Wasm:
createWebPaymentManager(...)rememberWebPaymentManager(...)rememberGooglePayWebLauncher(...) and rememberApplePayWebLauncher(...)
See payment-web/README.md for setup and platform-specific details.
KPayment includes an optional logging system for debugging:
KPaymentLogger.enabled = true
KPaymentLogger.callback = object : KPaymentLogCallback {
override fun onLog(event: LogEvent) {
println("[${event.tag}] ${event.message}")
}
}By default, logging is disabled and will not interfere with your app's logging.
The library includes comprehensive unit tests:
./gradlew payment-core:jvmTest # Core logic tests
./gradlew payment-web:jsTest # Web platform tests
./gradlew payment-mobile:iosSimulatorArm64Test # iOS tests
./gradlew payment-mobile:testAndroid # Android testsTest coverage includes:
KPayment includes complete sample applications demonstrating integration on all platforms:
sampleMobile - Android + iOS KMP sample with:
sampleWeb - Web sample (JS/WASM) with:
Sample configurations:
sampleMobile/src/commonMain/kotlin/com/kttipay/kpayment/config/PaymentConfig.ktsampleWeb/src/webMain/kotlin/com/kttipay/kpayment/PaymentConfig.ktRunning Samples:
# Android
./gradlew sampleMobile:installDebug
# iOS
cd iosApp && pod install
open iosApp.xcworkspace
# Web
./gradlew sampleWeb:jsBrowserDevelopmentRunWe welcome contributions to KPayment! Here's how you can help:
Found a bug or have a feature request? Please open an issue on GitHub Issues with:
git checkout -b feature/amazing-feature)./gradlew check
git clone https://github.com/kttipay/KPayment.git
cd KPayment
./gradlew buildRequirements:
Copyright 2025 KTTipay
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
See LICENSE file for the full license text.
Built with โค๏ธ using Kotlin Multiplatform
Quickstart โข Installation โข Samples โข Documentation โข
A Kotlin Multiplatform payment library with Compose Multiplatform UI components for Google Pay and Apple Pay across Android, iOS, and Web. One API, shared types, reactive availability detection, and platform-native payment buttons.
Get started with KPayment in 5 minutes:
// Create a payment manager
val manager = rememberMobilePaymentManager(config)
// Check availability
val isReady by manager.observeAvailability(currentNativePaymentProvider())
.collectAsState(initial = false)
// Launch payment
val launcher = rememberNativePaymentLauncher { result ->
when (result) {
is PaymentResult.Success -> handleSuccess(result.token)
is PaymentResult.Error -> handleError(result)
is PaymentResult.Cancelled -> handleCancellation()
}
}
PaymentButton(enabled = isReady, onClick = { launcher.launch("10.00") })Next Steps:
| Platform | Google Pay | Apple Pay | Compose UI |
|---|---|---|---|
| Android | โ | โ | โ |
| iOS | โ | โ | โ |
| Web (JS) | โ | โ * | โ |
| WASM | โ | โ * | โ |
*Apple Pay on Web requires Safari and a merchant validation endpoint.
Android:
iOS:
Web:
Build Environment:
Flow and StateFlow
val config = MobilePaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID"
),
applePayMobile = ApplePayMobileConfig(
merchantId = "merchant.com.yourcompany.app",
base = ApplePayBaseConfig(merchantName = "Your Store")
)
)
@Composable
fun PaymentScreen() {
val manager = rememberMobilePaymentManager(config)
val isReady by manager.observeAvailability(currentNativePaymentProvider())
.collectAsState(initial = false)
PaymentManagerProvider(manager) {
val launcher = rememberNativePaymentLauncher { result ->
when (result) {
is PaymentResult.Success -> println("Token: ${result.token}")
is PaymentResult.Error -> println("Error: ${result.message}")
is PaymentResult.Cancelled -> println("Cancelled")
}
}
PaymentButton(
theme = NativePaymentTheme.Dark,
type = NativePaymentType.Pay,
enabled = isReady,
radius = 12.dp,
onClick = { launcher.launch("10.00") }
)
}
}val config = WebPaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID"
),
applePayWeb = ApplePayWebConfig(
base = ApplePayBaseConfig(merchantName = "Your Store"),
merchantValidationEndpoint = "https://example.com/apple-pay/validate",
baseUrl = "https://example.com",
domain = "example.com"
)
)
@Composable
fun PaymentScreen() {
val manager = rememberWebPaymentManager(config)
PaymentManagerProvider(manager) {
val googlePay = rememberGooglePayWebLauncher { result ->
// Handle result
}
val applePay = rememberApplePayWebLauncher { result ->
// Handle result
}
Button(onClick = { googlePay.launch("10.00") }) {
Text("Pay with Google Pay")
}
Button(onClick = { applePay.launch("10.00") }) {
Text("Pay with Apple Pay")
}
}
}// Reactive: Observe availability changes (recommended)
val isReady by manager.observeAvailability(PaymentProvider.GooglePay)
.collectAsState(initial = false)
Button(enabled = isReady, onClick = { launcher.launch("10.00") }) {
Text("Pay with Google Pay")
}
// Explicit check when needed
val capabilities = manager.checkCapabilities()
if (capabilities.canPayWith(PaymentProvider.GooglePay)) {
launcher.launch("10.00")
}when (val result = paymentResult) {
is PaymentResult.Success -> {
// Send token to your backend
processPayment(result.token)
}
is PaymentResult.Error -> {
when (result.reason) {
PaymentErrorReason.NetworkError -> showNetworkError()
PaymentErrorReason.NotAvailable -> showAlternativePayment()
PaymentErrorReason.Timeout -> retryPayment()
else -> showGenericError(result.message)
}
}
is PaymentResult.Cancelled -> {
// User cancelled - no action needed
}
}See full examples in Quick Start and comprehensive guides below.
Ensure Maven Central is in your repository list (usually already configured):
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}Add to your libs.versions.toml:
[versions]
kpayment = "0.1.0"
[libraries]
kpayment-core = { module = "com.kttipay:kpayment-core", version.ref = "kpayment" }
kpayment-mobile = { module = "com.kttipay:kpayment-mobile", version.ref = "kpayment" }
kpayment-web = { module = "com.kttipay:kpayment-web", version.ref = "kpayment" }Then in your build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kpayment.core)
}
androidMain.dependencies {
implementation(libs.kpayment.mobile)
}
iosMain.dependencies {
implementation(libs.kpayment.mobile)
}
jsMain.dependencies {
implementation(libs.kpayment.web)
}
wasmJsMain.dependencies {
implementation(libs.kpayment.web)
}
}
}Add the KPayment dependencies directly to your build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.kttipay:kpayment-core:0.1.0")
}
androidMain.dependencies {
implementation("com.kttipay:kpayment-mobile:0.1.0")
}
iosMain.dependencies {
implementation("com.kttipay:kpayment-mobile:0.1.0")
}
jsMain.dependencies {
implementation("com.kttipay:kpayment-web:0.1.0")
}
wasmJsMain.dependencies {
implementation("com.kttipay:kpayment-web:0.1.0")
}
}
}No additional configuration required. Google Play Services Wallet is included as a dependency.
Add Apple Pay Capability:
Configure Merchant ID:
merchant.com.yourcompany.yourapp
For Apple Pay on Web:
See Apple Pay on the Web documentation
Amounts are decimal strings (for example, "10.00").
val config = MobilePaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID"
),
applePayMobile = null
)
@Composable
fun PaymentScreen() {
val manager = rememberMobilePaymentManager(config)
val isReady by manager.observeAvailability(currentNativePaymentProvider())
.collectAsState(initial = false)
PaymentManagerProvider(manager) {
val launcher = rememberNativePaymentLauncher { result ->
// handle PaymentResult.Success, PaymentResult.Error, PaymentResult.Cancelled
}
PaymentButton(
theme = NativePaymentTheme.Dark,
type = NativePaymentType.Pay,
enabled = isReady,
radius = 12.dp,
onClick = { launcher.launch("10.00") }
)
}
}val config = MobilePaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = null,
applePayMobile = ApplePayMobileConfig(
merchantId = "merchant.com.yourcompany.app",
base = ApplePayBaseConfig(merchantName = "Your Store")
)
)
@Composable
fun PaymentScreen() {
val manager = rememberMobilePaymentManager(config)
val isReady by manager.observeAvailability(currentNativePaymentProvider())
.collectAsState(initial = false)
PaymentManagerProvider(manager) {
val launcher = rememberNativePaymentLauncher { result ->
// handle PaymentResult.Success, PaymentResult.Error, PaymentResult.Cancelled
}
PaymentButton(
theme = NativePaymentTheme.Dark,
type = NativePaymentType.Pay,
enabled = isReady,
radius = 12.dp,
onClick = { launcher.launch("10.00") }
)
}
}Use createWebPaymentManager(config) outside Compose if you are not using Compose UI.
val config = WebPaymentConfig(
environment = PaymentEnvironment.Development,
googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID"
),
applePayWeb = ApplePayWebConfig(
base = ApplePayBaseConfig(merchantName = "Your Store"),
merchantValidationEndpoint = "https://example.com/apple-pay/validate",
baseUrl = "https://example.com",
domain = "example.com"
)
)
@Composable
fun PaymentScreen() {
val manager = rememberWebPaymentManager(config)
PaymentManagerProvider(manager) {
val googlePay = rememberGooglePayWebLauncher { result ->
when (result) {
is PaymentResult.Success -> println("Token: ${result.token}")
is PaymentResult.Error -> println("Error: ${result.message}")
is PaymentResult.Cancelled -> println("Cancelled")
}
}
val applePay = rememberApplePayWebLauncher { result ->
when (result) {
is PaymentResult.Success -> println("Token: ${result.token}")
is PaymentResult.Error -> println("Error: ${result.message}")
is PaymentResult.Cancelled -> println("Cancelled")
}
}
Button(onClick = { googlePay.launch("10.00") }) {
Text("Pay with Google Pay")
}
Button(onClick = { applePay.launch("10.00") }) {
Text("Pay with Apple Pay")
}
}
}val googlePay = GooglePayConfig(
merchantId = "YOUR_MERCHANT_ID",
merchantName = "Your Store",
gateway = "stripe",
gatewayMerchantId = "YOUR_GATEWAY_ID",
allowedCardNetworks = setOf(
GooglePayCardNetwork.VISA,
GooglePayCardNetwork.MASTERCARD
),
allowedAuthMethods = GooglePayAuthMethod.DEFAULT,
currencyCode = "AUD",
countryCode = "AU"
)val googlePay = GooglePayConfig(
allowCreditCards = true,
assuranceDetailsRequired = true
)allowCreditCards: Set to true to allow credit card transactions (default: false)assuranceDetailsRequired: Set to true to request additional cardholder verification (default: false)val appleBase = ApplePayBaseConfig(
merchantName = "Your Store",
supportedNetworks = setOf(
ApplePayNetwork.VISA,
ApplePayNetwork.MASTERCARD
),
merchantCapabilities = setOf(
ApplePayMerchantCapability.CAPABILITY_3DS,
ApplePayMerchantCapability.CAPABILITY_DEBIT
),
currencyCode = "AUD",
countryCode = "AU"
)
val applePayMobile = ApplePayMobileConfig(
merchantId = "merchant.com.yourcompany.app",
base = appleBase
)
val applePayWeb = ApplePayWebConfig(
base = appleBase,
merchantValidationEndpoint = "https://example.com/apple-pay/validate",
baseUrl = "https://example.com",
domain = "example.com"
)// Reactive: Observe availability changes (most common)
val isReady by manager.observeAvailability(PaymentProvider.GooglePay)
.collectAsState(initial = false)
Button(
enabled = isReady,
onClick = { launcher.launch("10.00") }
) {
Text("Pay with Google Pay")
}
// Explicit check when needed
val capabilities = manager.checkCapabilities()
if (capabilities.canPayWith(PaymentProvider.GooglePay)) {
launcher.launch("10.00")
}
// Handle detailed capability status
val capabilities = manager.checkCapabilities()
when (capabilities.googlePay) {
CapabilityStatus.Ready -> { /* show button */ }
CapabilityStatus.NotConfigured -> { /* missing config */ }
CapabilityStatus.NotSupported -> { /* platform not supported */ }
CapabilityStatus.Checking -> { /* loading */ }
is CapabilityStatus.Error -> { /* handle error: ${error.reason} */ }
}when (val result = paymentResult) {
is PaymentResult.Success -> {
val token = result.token
// Send token to your backend for processing
}
is PaymentResult.Error -> {
when (result.reason) {
PaymentErrorReason.Timeout -> {}
PaymentErrorReason.NetworkError -> {}
PaymentErrorReason.DeveloperError -> {}
PaymentErrorReason.InternalError -> {}
PaymentErrorReason.NotAvailable -> {}
PaymentErrorReason.AlreadyInProgress -> {}
PaymentErrorReason.SignInRequired -> {}
PaymentErrorReason.ApiNotConnected -> {}
PaymentErrorReason.ConnectionSuspendedDuringCall -> {}
PaymentErrorReason.Interrupted -> {}
PaymentErrorReason.Unknown -> {}
}
}
is PaymentResult.Cancelled -> {}
}The launcher exposes isProcessing: StateFlow<Boolean> to track whether a payment is in progress:
val launcher = rememberNativePaymentLauncher { result -> /* handle */ }
val isProcessing by launcher.isProcessing.collectAsState()
PaymentButton(
enabled = isReady && !isProcessing,
onClick = { launcher.launch("10.00") }
)| Method | Type | Description |
|---|---|---|
config |
Property | The payment configuration |
checkCapabilities() |
suspend |
Check current payment capabilities from platform SDKs |
observeCapabilities() |
Flow<PaymentCapabilities> |
Reactively observe full payment capabilities |
observeAvailability(provider) |
Flow<Boolean> |
Reactively observe specific provider availability |
// Reactive UI for full capabilities
val capabilities by manager.observeCapabilities()
.collectAsState(initial = PaymentCapabilities.initial)
when (capabilities.googlePay) {
CapabilityStatus.Ready -> ShowPaymentButton()
CapabilityStatus.Checking -> ShowLoading()
else -> ShowNotAvailable()
}
// Reactive UI for single provider (convenience)
val isReady by manager.observeAvailability(PaymentProvider.GooglePay)
.collectAsState(initial = false)
Button(enabled = isReady, onClick = { launcher.launch("10.00") }) {
Text("Pay")
}
// Explicit capability check
val capabilities = manager.checkCapabilities()
if (capabilities.canPayWith(PaymentProvider.GooglePay)) {
launcher.launch("10.00")
}All payment results come through the PaymentResult sealed class:
when (result) {
is PaymentResult.Success -> sendTokenToBackend(result.token)
is PaymentResult.Error -> handleError(result.reason, result.message)
is PaymentResult.Cancelled -> { /* user cancelled, no action needed */ }
}| Reason | When | Suggested Action |
|---|---|---|
Timeout |
Request timed out | Retry after a delay |
NetworkError |
No connectivity | Check network, retry |
DeveloperError |
Bad config or invalid amount | Fix configuration |
InternalError |
Platform SDK failure | Retry with backoff |
NotAvailable |
Payment method unavailable | Show alternative |
AlreadyInProgress |
Duplicate launch | Disable button via launcher.isProcessing
|
SignInRequired |
User not signed in | Prompt sign-in |
ApiNotConnected |
SDK not initialized | Re-initialize manager |
ConnectionSuspendedDuringCall |
App backgrounded mid-call | Retry on resume |
Interrupted |
Operation interrupted | Show retry dialog |
Unknown |
Unclassified error | Log and show generic message |
observeAvailability() before showing the payment button.launcher.isProcessing to disable the button while a payment is in flight.Cancelled is not an error โ don't show error UI for it.KPayment is organized into three main modules:
KPayment/
โโโ payment-core/ Shared interfaces, models, and types
โโโ payment-mobile/ Android + iOS implementations
โโโ payment-web/ Web (JS/WASM) implementations
Shared abstractions used by all platforms:
PaymentManager - unified API for capability checks and configurationGooglePayConfig, ApplePayBaseConfig, MobilePaymentConfig, WebPaymentConfig)PaymentResult) and tokens (GooglePayToken, ApplePayToken)StateFlow and Flow
See payment-core/README.md for a focused overview.
Android + iOS implementation with Compose helpers:
createMobilePaymentManager(...)rememberMobilePaymentManager(...)PaymentButton and rememberNativePaymentLauncher(...)
See payment-mobile/README.md for setup and platform-specific details.
Web implementation for JS/Wasm:
createWebPaymentManager(...)rememberWebPaymentManager(...)rememberGooglePayWebLauncher(...) and rememberApplePayWebLauncher(...)
See payment-web/README.md for setup and platform-specific details.
KPayment includes an optional logging system for debugging:
KPaymentLogger.enabled = true
KPaymentLogger.callback = object : KPaymentLogCallback {
override fun onLog(event: LogEvent) {
println("[${event.tag}] ${event.message}")
}
}By default, logging is disabled and will not interfere with your app's logging.
The library includes comprehensive unit tests:
./gradlew payment-core:jvmTest # Core logic tests
./gradlew payment-web:jsTest # Web platform tests
./gradlew payment-mobile:iosSimulatorArm64Test # iOS tests
./gradlew payment-mobile:testAndroid # Android testsTest coverage includes:
KPayment includes complete sample applications demonstrating integration on all platforms:
sampleMobile - Android + iOS KMP sample with:
sampleWeb - Web sample (JS/WASM) with:
Sample configurations:
sampleMobile/src/commonMain/kotlin/com/kttipay/kpayment/config/PaymentConfig.ktsampleWeb/src/webMain/kotlin/com/kttipay/kpayment/PaymentConfig.ktRunning Samples:
# Android
./gradlew sampleMobile:installDebug
# iOS
cd iosApp && pod install
open iosApp.xcworkspace
# Web
./gradlew sampleWeb:jsBrowserDevelopmentRunWe welcome contributions to KPayment! Here's how you can help:
Found a bug or have a feature request? Please open an issue on GitHub Issues with:
git checkout -b feature/amazing-feature)./gradlew check
git clone https://github.com/kttipay/KPayment.git
cd KPayment
./gradlew buildRequirements:
Copyright 2025 KTTipay
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
See LICENSE file for the full license text.
Built with โค๏ธ using Kotlin Multiplatform