Skip to content

[Android] extract checkout protocol module#315

Merged
kiftio merged 1 commit into
mainfrom
06-22-split_android_checkout_protocol
Jun 25, 2026
Merged

[Android] extract checkout protocol module#315
kiftio merged 1 commit into
mainfrom
06-22-split_android_checkout_protocol

Conversation

@kiftio

@kiftio kiftio commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Goal

Split Android's low-level Embedded Checkout Protocol implementation out of Checkout Kit into an independently publishable Kotlin module.

This PR is architectural. It makes the protocol layer reusable and separable, while keeping Checkout Kit responsible for the curated Android app-developer API. It sits before the follow-up API-tightening branch that prevents consumers from hooking directly into lower-level communication processing.

Note

This is a breaking change while the Android SDK is still in alpha.

New Architecture

  • :embedded-checkout-protocol is now a plain Kotlin/JVM java-library module at protocol/languages/kotlin/embedded-checkout-protocol.
  • It is published as com.shopify:embedded-checkout-protocol.
  • Its Kotlin package is com.shopify.ucp.embedded.checkout, leaving a clear sibling path for future modules such as com.shopify.ucp.embedded.cart.
  • :lib remains the Android Checkout Kit AAR, published as com.shopify:checkout-kit.
  • :lib depends on :embedded-checkout-protocol with api project(":embedded-checkout-protocol"), so normal Checkout Kit consumers still receive the protocol model/descriptor types transitively.
  • The protocol artifact owns raw/generated protocol concerns: ECP model types, generated EmbeddedCheckoutProtocol.Event wire-name catalog, thin descriptors, JSON-RPC codec helpers, serializers, protocol tests, and its own API baseline.
  • Checkout Kit owns Android host behavior and curation: WebView integration, ec.ready/ack handling, default native behavior, supported event filtering, and the higher-level CheckoutProtocol API that app developers use.

This gives us a natural path to add separate deployables later:

include ":embedded-checkout-protocol"
project(":embedded-checkout-protocol").projectDir = file("protocol/languages/kotlin/embedded-checkout-protocol")

include ":embedded-cart-protocol"
project(":embedded-cart-protocol").projectDir = file("protocol/languages/kotlin/embedded-cart-protocol")

Implementation Notes

  • Moves the protocol module out of platforms/android and into protocol/languages/kotlin.
  • Converts the protocol artifact from an Android library/AAR to a Kotlin/JVM jar, removing the Android manifest and Robolectric-only protocol setup.
  • Keeps the Gradle project path :embedded-checkout-protocol and Maven artifact com.shopify:embedded-checkout-protocol stable.
  • Regenerates protocol models under com.shopify.ucp.embedded.checkout.
  • Generates the low-level EmbeddedCheckoutProtocol.Event catalog from OpenRPC methods, scoped to ec.*; sibling capabilities such as cart can live in their own future modules.
  • Moves thin descriptor and JSON-RPC encode/decode helpers into the protocol artifact so it is useful standalone.
  • Keeps Checkout Kit curation in CheckoutProtocol.kt and WebView/default behavior in EmbeddedCheckoutProtocolBridge.kt.
  • Adds a standalone Kotlin protocol Gradle root at protocol/languages/kotlin.
  • Wires the moved module into Android root/sample Gradle settings and React Native local Android publishing.
  • Centralizes Android/Kotlin dependency and plugin versions in platforms/android/gradle/libs.versions.toml, with shared Java/Kotlin compatibility values in platforms/android/gradle/android-library-versions.gradle; Android SDK levels stay local to the Android AAR build.
  • Versions embedded-checkout-protocol independently from checkout-kit; React Native's nativeSdkVersions.android remains the exact lowercase published com.shopify:checkout-kit SemVer.
  • Updates Android release validation so the React Native package's Android native SDK version must match checkoutKitAndroid from the version catalog.
  • Updates CI/API validation to cover the protocol JVM module through aggregate apiCheck.

Most of the diff is module/file movement, generated model/API baseline movement, and Gradle wiring. Runtime behavior is intended to stay the same in this PR.

Stack Notes

This PR creates the protocol split and standalone artifact.

The next branch in the stack tightens the public Kit API by preventing consumers from hooking directly into lower-level communication processing and steering them toward typed callbacks, for example:

client.on(CheckoutProtocol.start) { checkout ->
    // ...
}

Validation

Latest validation after the package/module move:

dev codegen kotlin
platforms/android/gradlew -p protocol/languages/kotlin :embedded-checkout-protocol:test --console=plain
platforms/android/gradlew -p protocol/languages/kotlin apiCheck --console=plain
dev android test
dev android api check
dev android check
platforms/android/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo :embedded-checkout-protocol:test --console=plain
USE_LOCAL_SDK=1 platforms/react-native/scripts/publish_android_snapshot
dev rn test android
.github/scripts/validate-release-version Android 4.0.0-alpha.1 android/4.0.0-alpha.1
git diff --check

One expected non-failing caveat: Android lint now sees the JVM protocol project as an external dependency from the Android build. That is intentional so the protocol artifact stays Android-free; protocol coverage comes from its tests, detekt, and API checks.

kiftio commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@kiftio kiftio mentioned this pull request Jun 22, 2026
11 tasks
@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch 6 times, most recently from a9f6061 to c0c0897 Compare June 23, 2026 12:29
compileSdk: 36,
minSdk: 23,
minCompileSdk: 35,
javaVersion: "11",

@kiftio kiftio Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common versions for things that are not dependencies/plugins like kotlin & java

androidApplicationGradlePlugin = "9.2.1"
androidLibraryGradlePlugin = "9.1.1"
apollo = "5.0.0"
apolloCache = "1.0.3"

@kiftio kiftio Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

centrally define dependency/plugin versions via a version catalog

we're using detekt, kotlin sdk, kotlinx serialization in both

@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch from c0c0897 to 598cd99 Compare June 23, 2026 13:17

private var listener = CheckoutWebViewListener(NoopCheckoutListener())
private val embeddedCheckoutProtocol = EmbeddedCheckoutProtocol(this)
private val embeddedCheckoutProtocol = EmbeddedCheckoutProtocolBridge(this)

@kiftio kiftio Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just differentiating between the protocol, and how we bridge to it via @JavascriptInterface

and avoiding a potential import alias or fqdn

@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch 3 times, most recently from 8e9f76a to ae4461f Compare June 23, 2026 14:28
}
}

private fun CheckoutProtocol.Client.processForTest(message: String): String? {

@kiftio kiftio Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite ugly.. but it's test-only and it lets us keep process() internal and existing test coverage

"Models.kt",
);
await generateKotlin(specDir, target);
await run("node", [path.join(PROTOCOL_DIR, "scripts", "generate_kotlin_catalog.mjs")]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto gen the catalog

...commonSchemaSources(specDir),
"--package",
"com.shopify.checkoutkit",
"com.shopify.ucp.embedded.checkout",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought was that this would work well with a future com.shopify.ucp.embedded.cart package

val default = RecordingClient(response = DEFAULT_RESPONSE)
fun `always run after merchant runs both notification handlers`() {
var merchantHandled = false
var defaultHandled = false

@kiftio kiftio Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We removed CheckoutCommunicationClient, which RecordingClient implemented and moved the tests over to testing the public .on() syntax


@Serializable
private data class WindowOpenErrorDto(
private data class WindowOpenResultDto(

@kiftio kiftio Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is private to kit

delegationDescriptor takes one response serializer and WindowOpenResultDto represents the common UCP response envelope for both success and rejected outcomes.

@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch from ae4461f to 8bb18b2 Compare June 23, 2026 15:18
@@ -10,6 +10,8 @@ import kotlinx.serialization.encoding.*
*/
@Serializable
public data class Checkout (
public val attribution: Map<String, String>? = null,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these changes just came from running generate on kotlin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected - Westin updated the specs but hadn't merged new generations, I had the same for swift

@kiftio kiftio marked this pull request as ready for review June 23, 2026 15:32
@kiftio kiftio requested a review from a team as a code owner June 23, 2026 15:32
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

React Native — Coverage Report

Lines Statements Branches Functions
Coverage: 92%
91.66% (319/348) 87.86% (181/206) 100% (82/82)

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

Package Size

Platform Artifact Base Head Delta
React Native npm tarball 90.9 KiB 90.8 KiB -104 B
Android release AAR 613.4 KiB 167.9 KiB -445.5 KiB

Measured from the PR base SHA and PR head SHA. This comment reports package artifact sizes only; it is not a final app binary-size report.

@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch 4 times, most recently from c0f407f to f420c55 Compare June 23, 2026 16:32
id 'org.jetbrains.kotlin.plugin.serialization' version '2.3.21' apply false
id 'io.gitlab.arturbosch.detekt' version '1.23.8' apply false
id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.18.1'
alias(libs.plugins.android.library) apply false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use plugins defined in the version catalog

@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch from 0dcfa34 to d284c1c Compare June 24, 2026 09:37

public val windowOpen: DelegationDescriptor<WindowOpenRequest, WindowOpenResult> = EmbeddedCheckoutProtocol.windowOpen.map(
decode = { request ->
request.url.toString().let(Uri::parse).let(::WindowOpenRequest)

@kiftio kiftio Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

windowOpen stays Kit-wrapped so Checkout Kit owns the Android-facing delegation shape and can convert protocol URI to Android Uri. Notification payloads such as Checkout and ErrorResponse are currently protocol generated types.

That allows us to convert to an android Uri and matches up with the current swift shape where Kit owns these types.

We may however want to just expose window.open protocol types via callbacks directly.. We can discuss separately

if (descriptor !in supportedNotificationDescriptors) return this
val entry = NotificationHandler(
decode = { params -> descriptor.decode(params) },
invoke = { payload ->

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invoke -> invoking the on callback

invoke = { payload -> (payload as? P)?.let { handler(it) } },
if (descriptor !in supportedNotificationDescriptors) return this
val entry = NotificationHandler(
decode = { params -> descriptor.decode(params) },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delegate decoding to the protocol descriptor

)

private fun encodeWindowOpenResult(result: WindowOpenResult): JsonObject = when (result) {
private fun encodeWindowOpenResult(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

converting the kit type to a ECP type to respond.

Fully qualified domain names is a result of both protocol and kit having a WindowOpenResult


/** Called by [EmbeddedCheckoutProtocol] for every delegated EC message. */
override fun process(message: String): String? =
internal fun process(message: String): String? =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hide this low-level function, so on is the public API

@@ -118,16 +117,6 @@ if (shopifySdkVersion == null || shopifySdkVersion.trim().isEmpty()) {
def shopifySdkArtifact = "com.shopify:checkout-kit:$shopifySdkVersion"

repositories {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hitting issues here, so I've basically just kept this in the sample build.gradle

protected Map<String, Object> getTypedExportedConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put("version", ShopifyCheckoutKit.version);
constants.put("version", com.shopify.checkoutkit.BuildConfig.SDK_VERSION);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we changed ShopifyCheckoutKit.version to ShopifyCheckoutKit.VERSIONto match constant conventions

But, the publshed version still has the old case, so CI (which builds against published) can fail.

This basically bypasses that and uses a BuildConfig field instead for resolving version

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I understand why CI would fail here
If the published version has .version then I'd expect that to work here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is probably a bit circular now.. I think this was updated to ShopifyCheckoutKit.VERSION in one iteration, and CI failed.

And then this was to resolve it so that it doesn't matter either way. But for this PR we could actually undo this if preferred

detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_formatting_version"
detektPlugins libs.detekt.formatting

api project(':embedded-checkout-protocol')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing protocol here, so it'll be a transitive depdendency of kit

@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch from d284c1c to 691cd9c Compare June 24, 2026 10:53
@kiftio kiftio changed the title Split Android checkout protocol module [Android] extract checkout protocol module Jun 24, 2026
@kiftio kiftio mentioned this pull request Jun 24, 2026
11 tasks
@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch from 691cd9c to aba9849 Compare June 24, 2026 11:30
Comment on lines +74 to +76
- name: Check public Kotlin protocol API baseline
run: ../../protocol/languages/kotlin/gradlew -p ../../protocol/languages/kotlin :embedded-checkout-protocol:apiCheck --console=plain

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this basically a version check or does it do more?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a full BCV API check, so we get an api file to allow us to catch embedded-checkout-protocol API drift over time, like with the Checkout Kit one.

@markmur markmur left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! ✨

Comment on lines +30 to +35
public val start: NotificationDescriptor<Checkout> = EmbeddedCheckoutProtocol.start
public val complete: NotificationDescriptor<Checkout> = EmbeddedCheckoutProtocol.complete
public val messagesChange: NotificationDescriptor<Checkout> = EmbeddedCheckoutProtocol.messagesChange
public val lineItemsChange: NotificationDescriptor<Checkout> = EmbeddedCheckoutProtocol.lineItemsChange
public val totalsChange: NotificationDescriptor<Checkout> = EmbeddedCheckoutProtocol.totalsChange
public val error: NotificationDescriptor<ErrorResponse> = EmbeddedCheckoutProtocol.error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder that we are missing ec.fulfillment.change across all 3 platforms

@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch from aba9849 to a55876b Compare June 24, 2026 14:23

@tiagocandido tiagocandido left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff!

Assisted-By: devx/6488e3d0-f47f-4171-a1c4-d2432b2a653c
@kiftio kiftio force-pushed the 06-22-split_android_checkout_protocol branch from a55876b to ac7d044 Compare June 25, 2026 08:39

kiftio commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Jun 25, 9:26 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 25, 9:26 AM UTC: @kiftio merged this pull request with Graphite.

@kiftio kiftio merged commit 3b713e8 into main Jun 25, 2026
33 checks passed
@kiftio kiftio deleted the 06-22-split_android_checkout_protocol branch June 25, 2026 09:26
@kiftio kiftio mentioned this pull request Jun 25, 2026
13 tasks
@markmur markmur added the #gsd:50662 Rebase Checkout Kit on UCP label Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:50662 Rebase Checkout Kit on UCP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants