Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Spreedly Checkout — Android Example

This sample app demonstrates the [Spreedly Android Checkout SDK](https://github.com/spreedly/checkout-android-sdk) at version **1.2.0** (tag `v1.2.0`).
This sample app demonstrates the [Spreedly Android Checkout SDK](https://github.com/spreedly/checkout-android-sdk) at version **1.3.0** (tag `v1.3.0`).

## Setup

Expand All @@ -20,10 +20,10 @@ gpr.key=YOUR_GITHUB_TOKEN
All SDK modules are resolved from GitHub Packages:

```kotlin
implementation("com.spreedly:checkout-paymentsheet:1.2.0")
implementation("com.spreedly:checkout-braintree-apm:1.2.0")
implementation("com.spreedly:checkout-stripe-apm:1.2.0")
implementation("com.spreedly:checkout-threeds:1.2.0")
implementation("com.spreedly:checkout-paymentsheet:1.3.0")
implementation("com.spreedly:checkout-braintree-apm:1.3.0")
implementation("com.spreedly:checkout-stripe-apm:1.3.0")
implementation("com.spreedly:checkout-threeds:1.3.0")
```

## SDK Documentation
Expand Down
11 changes: 6 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,12 @@ kotlin {

dependencies {
// ✅ Use paymentsheet which includes payments-core and hosted-fields
implementation("com.spreedly:checkout-paymentsheet:1.2.0")
implementation("com.spreedly:checkout-braintree-apm:1.2.0")
implementation("com.spreedly:checkout-stripe-apm:1.2.0")
implementation("com.spreedly:checkout-stripe-radar:1.2.0")
implementation("com.spreedly:checkout-threeds:1.2.0")
implementation("com.spreedly:checkout-paymentsheet:1.3.0")
implementation("com.spreedly:checkout-braintree-apm:1.3.0")
implementation("com.spreedly:checkout-stripe-apm:1.3.0")
implementation("com.spreedly:checkout-stripe-radar:1.3.0")
implementation("com.spreedly:checkout-threeds:1.3.0")
implementation("com.spreedly:checkout-clicktopay:1.3.0")

implementation(libs.kotlinx.serialization.json)
implementation(platform(libs.androidx.compose.bom))
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/spreedly/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.spreedly.example.screens.headlessbankaccount.HeadlessBankAccountViewM
import com.spreedly.example.viewmodel.viewModelWithContext
import com.spreedly.example.screens.bottomsheet.BottomSheetPaymentScreen
import com.spreedly.example.screens.bottomsheet.BottomSheetPaymentViewModel
import com.spreedly.example.screens.clicktopay.ClickToPayPaymentScreen
import com.spreedly.example.screens.braintreepayment.BraintreePaymentScreen
import com.spreedly.example.screens.customcheckout.CheckoutWithAdditionalFieldsScreen
import com.spreedly.example.screens.customizedcheckout.CustomisedCheckoutScreen
Expand Down Expand Up @@ -210,6 +211,11 @@ fun MainNavHost(bottomSheetViewModel: BottomSheetPaymentViewModel) {
)
}

composable("clicktopay_demo") {
ClickToPayPaymentScreen(
onBackClick = { navController.popBackStack() },
)
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.spreedly.example.screens.clicktopay

import com.spreedly.clicktopay.ClickToPaySavedCardsDetectorResult
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withTimeoutOrNull

internal fun deviceRecognitionForDetectorResult(
result: ClickToPaySavedCardsDetectorResult,
): DeviceRecognitionState =
when {
result.hasSavedCards -> DeviceRecognitionState.Recognized
result.failure != null -> DeviceRecognitionState.DetectionFailed
else -> DeviceRecognitionState.NotRecognized
}

internal const val CHECKOUT_INACTIVE_REMOUNT_TIMEOUT_MS = 10_000L
internal const val CHECKOUT_INACTIVE_REMOUNT_POLL_MS = 50L
internal const val SAVED_CARDS_DETECTOR_TEARDOWN_TIMEOUT_MS = 5_000L

internal fun shouldRemountSavedCardsDetector(
deviceRecognition: DeviceRecognitionState,
remountedDetectorForCheckoutGeneration: Int,
checkoutSessionGeneration: Int,
): Boolean =
deviceRecognition != DeviceRecognitionState.UsingDifferentEmail &&
remountedDetectorForCheckoutGeneration != checkoutSessionGeneration

internal suspend fun awaitCheckoutInactiveForRemount(
isCheckoutActive: () -> Boolean,
delayMs: Long = CHECKOUT_INACTIVE_REMOUNT_POLL_MS,
timeoutMs: Long = CHECKOUT_INACTIVE_REMOUNT_TIMEOUT_MS,
delay: suspend (Long) -> Unit,
nowMs: () -> Long = { System.currentTimeMillis() },
): Boolean {
val deadline = nowMs() + timeoutMs
while (isCheckoutActive()) {
if (nowMs() >= deadline) {
return false
}
delay(delayMs)
}
return true
}

internal suspend fun awaitCheckoutInactiveUntilRemount(
isCheckoutActive: () -> Boolean,
delayMs: Long = CHECKOUT_INACTIVE_REMOUNT_POLL_MS,
delay: suspend (Long) -> Unit,
) {
while (isCheckoutActive()) {
delay(delayMs)
}
}

internal class SavedCardsDetectorTearDownAwaiter {
private var inFlight: CompletableDeferred<Unit>? = null
private var disposeSignaledWithoutAwaiter = false

fun begin(): CompletableDeferred<Unit> {
if (disposeSignaledWithoutAwaiter) {
disposeSignaledWithoutAwaiter = false
return CompletableDeferred<Unit>().also { it.complete(Unit) }
}
val deferred = CompletableDeferred<Unit>()
inFlight = deferred
return deferred
}

fun signalDisposed() {
val current = inFlight
if (current != null) {
current.complete(Unit)
inFlight = null
} else {
disposeSignaledWithoutAwaiter = true
}
}

fun clear() {
inFlight = null
disposeSignaledWithoutAwaiter = false
}
}

internal suspend fun awaitSavedCardsDetectorTearDown(
hadActiveDetector: Boolean,
controllerAwaitTearDown: (suspend () -> Boolean)?,
awaiter: SavedCardsDetectorTearDownAwaiter,
timeoutMs: Long = SAVED_CARDS_DETECTOR_TEARDOWN_TIMEOUT_MS,
): Boolean {
if (!hadActiveDetector) {
return true
}
if (controllerAwaitTearDown != null) {
return controllerAwaitTearDown()
}
val fallback = awaiter.begin()
return try {
withTimeoutOrNull(timeoutMs) {
fallback.await()
} != null
} finally {
awaiter.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.spreedly.example.screens.clicktopay

import com.spreedly.clicktopay.ClickToPayTokenizeBilling

data class ClickToPayMerchantPrefill(
val firstName: String = "",
val lastName: String = "",
val phoneCountryCode: String = "",
val phoneNumber: String = "",
val addressLine1: String = "",
val addressLine2: String = "",
val city: String = "",
val state: String = "",
val zip: String = "",
val country: String = "",
val copyBillingToShipping: Boolean = false,
) {
fun makeTokenizeBilling(email: String): ClickToPayTokenizeBilling {
val trimmedEmail = email.trim()
val trimmedPhone = phoneNumber.trim()
val billing =
ClickToPayTokenizeBilling(
email = trimmedEmail.ifBlank { null },
firstName = firstName.nilIfBlank(),
lastName = lastName.nilIfBlank(),
phoneNumber = trimmedPhone.ifBlank { null },
addressLine1 = addressLine1.nilIfBlank(),
addressLine2 = addressLine2.nilIfBlank(),
city = city.nilIfBlank(),
state = state.nilIfBlank(),
zip = zip.nilIfBlank(),
country = country.nilIfBlank(),
)
if (!copyBillingToShipping) {
return billing
}
return billing.copy(
shippingAddressLine1 = billing.addressLine1,
shippingAddressLine2 = billing.addressLine2,
shippingCity = billing.city,
shippingState = billing.state,
shippingZip = billing.zip,
shippingCountry = billing.country,
shippingPhoneNumber = billing.phoneNumber,
)
}
}

private fun String.nilIfBlank(): String? {
val trimmed = trim()
return trimmed.ifBlank { null }
}
Loading