diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/AndroidManifest.xml b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/AndroidManifest.xml
index aae0dd93..dc4e1a22 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/AndroidManifest.xml
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/AndroidManifest.xml
@@ -54,6 +54,7 @@
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true"
+ android:launchMode="singleTask"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
tools:ignore="LockedOrientationActivity">
@@ -62,6 +63,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/CheckoutKitApp.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/CheckoutKitApp.kt
index 3a3b5b32..28e3b128 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/CheckoutKitApp.kt
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/CheckoutKitApp.kt
@@ -1,5 +1,6 @@
package com.shopify.checkout_kit_android_demo
+import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
@@ -32,32 +33,52 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
+import com.shopify.checkout_kit_android_demo.cart.CartBootstrapException
+import com.shopify.checkout_kit_android_demo.cart.CartBootstrapLink
import com.shopify.checkout_kit_android_demo.cart.CartViewModel
import com.shopify.checkout_kit_android_demo.cart.data.totalQuantity
+import com.shopify.checkout_kit_android_demo.common.ID
import com.shopify.checkout_kit_android_demo.common.ObserveAsEvents
import com.shopify.checkout_kit_android_demo.common.SnackbarController
+import com.shopify.checkout_kit_android_demo.common.SnackbarEvent
import com.shopify.checkout_kit_android_demo.common.navigation.BottomAppBarWithNavigation
import com.shopify.checkout_kit_android_demo.common.navigation.CheckoutKitNavHost
import com.shopify.checkout_kit_android_demo.common.navigation.Screen
import com.shopify.checkout_kit_android_demo.common.ui.theme.CheckoutKitSampleTheme
import com.shopify.checkout_kit_android_demo.logs.LogsViewModel
+import com.shopify.checkout_kit_android_demo.products.product.data.ProductRepository
import com.shopify.checkout_kit_android_demo.settings.SettingsUiState
import com.shopify.checkout_kit_android_demo.settings.SettingsViewModel
import com.shopify.checkoutkit.ColorScheme
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
+import org.koin.compose.koinInject
@Composable
-fun CheckoutKitApp() {
+fun CheckoutKitApp(
+ cartBootstrapUri: Uri? = null,
+ onCartBootstrapHandled: () -> Unit = {},
+) {
val settingsViewModel = koinViewModel()
val cartViewModel = koinViewModel()
val logsViewModel = koinViewModel()
+ val productRepository = koinInject()
- CheckoutKitAppRoot(settingsViewModel, cartViewModel, logsViewModel)
+ CheckoutKitAppRoot(
+ settingsViewModel = settingsViewModel,
+ cartViewModel = cartViewModel,
+ logsViewModel = logsViewModel,
+ productRepository = productRepository,
+ cartBootstrapUri = cartBootstrapUri,
+ onCartBootstrapHandled = onCartBootstrapHandled,
+ )
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -66,6 +87,9 @@ fun CheckoutKitAppRoot(
settingsViewModel: SettingsViewModel,
cartViewModel: CartViewModel,
logsViewModel: LogsViewModel,
+ productRepository: ProductRepository,
+ cartBootstrapUri: Uri?,
+ onCartBootstrapHandled: () -> Unit,
) {
val useDarkTheme = settingsViewModel.uiState.collectAsState().value
.isDarkTheme(isSystemInDarkTheme())
@@ -76,7 +100,12 @@ fun CheckoutKitAppRoot(
CheckoutKitSampleTheme(darkTheme = useDarkTheme) {
Surface(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("checkout-kit-sample-ready")
+ .semantics {
+ testTagsAsResourceId = true
+ },
) {
val navController = rememberNavController()
var currentScreen by remember { mutableStateOf(Screen.Product) }
@@ -98,6 +127,25 @@ fun CheckoutKitAppRoot(
}
}
+ LaunchedEffect(cartBootstrapUri) {
+ val uri = cartBootstrapUri ?: return@LaunchedEffect
+
+ try {
+ val cartBootstrapLink = CartBootstrapLink.parse(uri) ?: return@LaunchedEffect
+ val variantId = variantIdFor(cartBootstrapLink, productRepository)
+
+ cartViewModel.seedCart(variantId, cartBootstrapLink.quantity) { result ->
+ result.onSuccess {
+ navController.navigate(Screen.Cart.route)
+ }
+ }
+ } catch (e: Exception) {
+ SnackbarController.sendEvent(SnackbarEvent(R.string.cart_bootstrap_failed))
+ } finally {
+ onCartBootstrapHandled()
+ }
+ }
+
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
@@ -117,9 +165,12 @@ fun CheckoutKitAppRoot(
)
},
actions = {
- IconButton(onClick = {
- navController.navigate(Screen.Cart.route)
- }) {
+ IconButton(
+ modifier = Modifier.testTag("cart-tab"),
+ onClick = {
+ navController.navigate(Screen.Cart.route)
+ }
+ ) {
BadgedBox(badge = {
if (totalQuantity > 0) {
Badge(
@@ -164,6 +215,24 @@ fun CheckoutKitAppRoot(
}
}
+private suspend fun variantIdFor(
+ cartBootstrapLink: CartBootstrapLink,
+ productRepository: ProductRepository,
+): ID {
+ cartBootstrapLink.variantId?.let { return it }
+
+ val productIndex = cartBootstrapLink.productIndex
+ ?: throw CartBootstrapException("Missing variantId or productIndex")
+ val product = productRepository
+ .getProducts(numProducts = productIndex + 1, numVariants = 1, cursor = null)
+ .products
+ .getOrNull(productIndex)
+ val variant = product?.variants?.firstOrNull()
+ ?: throw CartBootstrapException("Cart bootstrap product variant was not found")
+
+ return variant.id
+}
+
data class AppBarState(
val actions: @Composable RowScope.() -> Unit = {},
)
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/MainActivity.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/MainActivity.kt
index 74f39805..f0cf9e5a 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/MainActivity.kt
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/MainActivity.kt
@@ -1,6 +1,7 @@
package com.shopify.checkout_kit_android_demo
import android.Manifest
+import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
@@ -13,6 +14,9 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import timber.log.Timber
import timber.log.Timber.DebugTree
@@ -27,10 +31,13 @@ class MainActivity : ComponentActivity() {
private var geolocationPermissionCallback: GeolocationPermissions.Callback? = null
private var geolocationOrigin: String? = null
+ private var cartBootstrapUri by mutableStateOf(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ cartBootstrapUri = intent?.data
+
enableEdgeToEdge()
// Allow debugging the WebView via chrome://inspect
@@ -42,7 +49,10 @@ class MainActivity : ComponentActivity() {
}
setContent {
- CheckoutKitApp()
+ CheckoutKitApp(
+ cartBootstrapUri = cartBootstrapUri,
+ onCartBootstrapHandled = { cartBootstrapUri = null },
+ )
}
requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
@@ -67,6 +77,12 @@ class MainActivity : ComponentActivity() {
}
}
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ setIntent(intent)
+ cartBootstrapUri = intent.data
+ }
+
fun onShowFileChooser(filePathCallback: ValueCallback>, fileChooserParams: FileChooserParams): Boolean {
this.filePathCallback = filePathCallback
if (permissionAlreadyGranted(Manifest.permission.CAMERA)) {
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartBootstrapLink.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartBootstrapLink.kt
new file mode 100644
index 00000000..757295ce
--- /dev/null
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartBootstrapLink.kt
@@ -0,0 +1,53 @@
+package com.shopify.checkout_kit_android_demo.cart
+
+import android.net.Uri
+import com.shopify.checkout_kit_android_demo.common.ID
+
+data class CartBootstrapLink(
+ val variantId: ID?,
+ val productIndex: Int?,
+ val quantity: Int,
+) {
+ companion object {
+ private const val SCHEME = "checkout-kit-android"
+ private const val HOST = "cart"
+
+ fun parse(uri: Uri): CartBootstrapLink? {
+ if (uri.scheme != SCHEME) return null
+
+ if (uri.host != HOST) {
+ throw CartBootstrapException("Unsupported cart bootstrap path")
+ }
+
+ val variantId = uri.getQueryParameter("variantId")?.trim()
+ val productIndexParam = uri.getQueryParameter("productIndex")?.trim()
+ val quantityParam = uri.getQueryParameter("quantity")?.trim() ?: "1"
+ val quantity = quantityParam.toIntOrNull()
+
+ if (quantity == null || quantity < 1) {
+ throw CartBootstrapException("quantity must be a positive integer")
+ }
+
+ if (!variantId.isNullOrEmpty() && !productIndexParam.isNullOrEmpty()) {
+ throw CartBootstrapException("Use variantId or productIndex, not both")
+ }
+
+ if (!variantId.isNullOrEmpty()) {
+ return CartBootstrapLink(variantId = ID(variantId), productIndex = null, quantity = quantity)
+ }
+
+ if (productIndexParam.isNullOrEmpty()) {
+ throw CartBootstrapException("Missing variantId or productIndex")
+ }
+
+ val productIndex = productIndexParam.toIntOrNull()
+ if (productIndex == null || productIndex < 0) {
+ throw CartBootstrapException("productIndex must be a non-negative integer")
+ }
+
+ return CartBootstrapLink(variantId = null, productIndex = productIndex, quantity = quantity)
+ }
+ }
+}
+
+class CartBootstrapException(message: String) : IllegalArgumentException(message)
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartView.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartView.kt
index 5e2b23a1..6326dca4 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartView.kt
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartView.kt
@@ -31,6 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
@@ -231,6 +232,7 @@ private fun CheckoutButton(
modifier = modifier
) {
Button(
+ modifier = Modifier.testTag("checkout-button"),
shape = RectangleShape,
onClick = onClick,
) {
@@ -254,7 +256,7 @@ private fun EmptyCartMessage(
modifier: Modifier,
) {
Box(
- modifier,
+ modifier.testTag("cart-empty-message"),
contentAlignment = Alignment.Center,
) {
Column(
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartViewModel.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartViewModel.kt
index 0482de47..8f5f385c 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartViewModel.kt
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/cart/CartViewModel.kt
@@ -72,6 +72,11 @@ class CartViewModel(
}
}
+ fun seedCart(variantId: ID, quantity: Int, onComplete: OnComplete) {
+ clearCart()
+ addToCart(variantId, quantity, onComplete)
+ }
+
fun modifyLineItem(lineItemId: ID, quantity: Int?) = viewModelScope.launch {
when (val state = _cartState.value) {
is CartState.Cart -> {
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/common/navigation/BottomAppBarWithNavigation.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/common/navigation/BottomAppBarWithNavigation.kt
index f1b3d2f6..231ca550 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/common/navigation/BottomAppBarWithNavigation.kt
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/common/navigation/BottomAppBarWithNavigation.kt
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.semantics.contentDescription
@@ -40,6 +41,7 @@ fun BottomAppBarWithNavigation(
ImageVector.vectorResource(R.drawable.home),
stringResource(id = R.string.navigation_home),
currentScreen,
+ "home-tab",
)
NavigationItem(
navController,
@@ -47,6 +49,7 @@ fun BottomAppBarWithNavigation(
ImageVector.vectorResource(R.drawable.product),
stringResource(id = R.string.navigation_shop),
currentScreen,
+ "products-tab",
)
NavigationItem(
navController,
@@ -54,6 +57,7 @@ fun BottomAppBarWithNavigation(
ImageVector.vectorResource(R.drawable.profile),
stringResource(id = R.string.navigation_log_in),
currentScreen,
+ "settings-tab",
)
}
}
@@ -66,6 +70,7 @@ fun NavigationItem(
icon: ImageVector,
label: String,
currentScreen: Screen,
+ testTag: String,
) {
val isActiveScreen = currentScreen == screen
val color = if (isActiveScreen) {
@@ -77,9 +82,11 @@ fun NavigationItem(
Column {
IconButton(
onClick = { navController.navigate(screen.route) },
- modifier = Modifier.semantics {
- this.contentDescription = "$label icon"
- }
+ modifier = Modifier
+ .testTag(testTag)
+ .semantics {
+ this.contentDescription = "$label icon"
+ }
) {
Icon(imageVector = icon, contentDescription = label, tint = color)
}
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/products/ProductsView.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/products/ProductsView.kt
index ddae6edb..ff9df50a 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/products/ProductsView.kt
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/products/ProductsView.kt
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -97,6 +98,7 @@ fun ProductsView(
Product(
product = product,
imageHeight = if (largeScreen) defaultProductImageHeightLg else defaultProductImageHeight,
+ testTag = "product-${index}-grid-item",
onProductClick = { productId ->
productsViewModel.productClicked(navController, productId)
}
@@ -113,10 +115,12 @@ fun ProductsView(
fun Product(
product: Product,
imageHeight: Dp,
+ testTag: String,
onProductClick: (id: ID) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier
.wrapContentWidth()
+ .testTag(testTag)
.clickable {
onProductClick(product.id)
}) {
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/products/product/AddToCartButton.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/products/product/AddToCartButton.kt
index a8cf783c..a487ef70 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/products/product/AddToCartButton.kt
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkout_kit_android_demo/products/product/AddToCartButton.kt
@@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -28,7 +29,9 @@ fun AddToCartButton(
horizontalAlignment = Alignment.CenterHorizontally,
) {
TextButton(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag("add-to-cart-button"),
enabled = !loading,
onClick = onClick,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onBackground),
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/res/values/strings.xml b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/res/values/strings.xml
index f62c1c74..984da372 100644
--- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/res/values/strings.xml
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/res/values/strings.xml
@@ -46,6 +46,7 @@
Failed to load products
Failed to create cart
Failed to update cart
+ Failed to prepare cart
An error occurred when completing log in
Failed to load customerR