Skip to content

Commit bce2e51

Browse files
feat: add first protocol event
1 parent ef1aa23 commit bce2e51

28 files changed

Lines changed: 1614 additions & 21 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ apollo-ios-cli
3131

3232
# Android / Gradle
3333
.gradle/
34+
.kotlin/
3435
build/
3536
captures/
3637
.externalNativeBuild

dev.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,28 @@ commands:
267267
build:
268268
desc: Build the @shopify/checkout-kit-react-native module
269269
run: cd platforms/react-native && pnpm module build
270+
test:
271+
desc: Run React Native module tests (JS + iOS + Android)
272+
long_desc: |
273+
Runs unit tests across all three React Native targets:
274+
- JS: Jest tests in `platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/`
275+
- iOS: Swift Package tests at `platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/`
276+
- Android: Gradle JVM tests for `:shopify_checkout-kit-react-native` (requires a local Maven publish of `:lib`)
277+
run: |
278+
set -e
279+
cd platforms/react-native && pnpm test
280+
cd modules/@shopify/checkout-kit-react-native/ios && swift test
281+
cd ../../../../sample/android && USE_LOCAL_SDK=1 ./gradlew :shopify_checkout-kit-react-native:test
282+
subcommands:
283+
js:
284+
desc: Run JS unit tests via jest
285+
run: cd platforms/react-native && pnpm test
286+
ios:
287+
desc: Run native iOS unit tests (Swift Package at modules/.../ios)
288+
run: cd platforms/react-native/modules/@shopify/checkout-kit-react-native/ios && swift test
289+
android:
290+
desc: Run native Android unit tests for the RN module (uses local Maven publish of :lib)
291+
run: cd platforms/react-native/sample/android && USE_LOCAL_SDK=1 ./gradlew :shopify_checkout-kit-react-native:test
270292
lint:
271293
desc: Run all React Native lint checks (Swift, module, sample)
272294
aliases: [style]

platforms/react-native/modules/@shopify/checkout-kit-react-native/RNShopifyCheckoutKit.podspec

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ Pod::Spec.new do |s|
1414
s.source = { :git => "https://github.com/Shopify/checkout-kit.git", :tag => "#{s.version}" }
1515

1616
s.source_files = "ios/*.{h,m,mm,swift}"
17+
# `ios/Package.swift` is the manifest for the nested SwiftPM test package
18+
# (CasingTransform / ProtocolRelay unit tests). It imports `PackageDescription`
19+
# which only exists in the SwiftPM toolchain, so it must not be compiled by
20+
# CocoaPods/Xcode when the RN module is consumed from an iOS app.
21+
s.exclude_files = "ios/Package.swift"
1722

1823
s.dependency "React-Core"
1924

platforms/react-native/modules/@shopify/checkout-kit-react-native/android/build.gradle

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
buildscript {
2+
ext.kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : "2.1.20"
3+
24
repositories {
35
google()
46
mavenCentral()
57
}
68

79
dependencies {
810
classpath "com.android.tools.build:gradle:8.11.0"
11+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
12+
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
913
}
1014
}
1115

1216
apply plugin: "com.android.library"
1317
apply plugin: "com.facebook.react"
18+
apply plugin: "org.jetbrains.kotlin.android"
19+
apply plugin: "org.jetbrains.kotlin.plugin.serialization"
1420

1521
def getExtOrIntegerDefault(name) {
1622
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties[name]).toInteger()
@@ -73,8 +79,17 @@ android {
7379
sourceCompatibility JavaVersion.VERSION_1_8
7480
targetCompatibility JavaVersion.VERSION_1_8
7581
}
82+
83+
kotlinOptions {
84+
jvmTarget = "1.8"
85+
}
86+
87+
testOptions {
88+
unitTests.includeAndroidResources = true
89+
}
7690
}
7791

92+
7893
repositories {
7994
mavenLocal()
8095
mavenCentral()
@@ -97,6 +112,11 @@ dependencies {
97112

98113
implementation(shopifySdkArtifact)
99114
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.5")
115+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
100116
debugImplementation(shopifySdkArtifact)
117+
118+
testImplementation "junit:junit:4.13.2"
119+
testImplementation "org.assertj:assertj-core:3.27.7"
120+
testImplementation "org.robolectric:robolectric:4.16.1"
101121
}
102122

platforms/react-native/modules/@shopify/checkout-kit-react-native/android/gradle.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ targetSdkVersion=35
33
compileSdkVersion=36
44
ndkVersion=23.1.7779620
55
buildToolsVersion = "35.0.0"
6+
7+
# Opt out of the React Native Gradle plugin's JdkConfiguratorUtils, which otherwise
8+
# silently rewrites compileOptions to 17 and pins the Kotlin JVM toolchain to 17 for
9+
# every com.android.library it sees. We mirror :lib's pinned JVM 1.8 contract instead.
10+
react.internal.disableJavaVersionAlignment=true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright 2023-present, Shopify Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
package com.shopify.reactnative.checkoutkit
24+
25+
import kotlinx.serialization.json.Json
26+
import kotlinx.serialization.json.JsonArray
27+
import kotlinx.serialization.json.JsonElement
28+
import kotlinx.serialization.json.JsonObject
29+
import kotlinx.serialization.json.decodeFromJsonElement
30+
import kotlinx.serialization.json.encodeToJsonElement
31+
32+
/**
33+
* Bridges typed snake_case payloads (per @SerialName annotations on the native models)
34+
* with camelCase JSON expected by JavaScript consumers.
35+
*/
36+
internal object CasingTransform {
37+
38+
private const val CAMEL_TO_SNAKE_BUFFER_PADDING: Int = 4
39+
40+
internal val json: Json = Json { ignoreUnknownKeys = true }
41+
42+
fun snakeToCamel(s: String): String {
43+
if (s.isEmpty() || !s.contains('_')) return s
44+
val builder = StringBuilder(s.length)
45+
var upperNext = false
46+
for (ch in s) {
47+
if (ch == '_') {
48+
upperNext = true
49+
} else if (upperNext) {
50+
builder.append(ch.uppercaseChar())
51+
upperNext = false
52+
} else {
53+
builder.append(ch)
54+
}
55+
}
56+
return builder.toString()
57+
}
58+
59+
fun camelToSnake(s: String): String {
60+
if (s.isEmpty()) return s
61+
val builder = StringBuilder(s.length + CAMEL_TO_SNAKE_BUFFER_PADDING)
62+
for (ch in s) {
63+
if (ch.isUpperCase()) {
64+
builder.append('_').append(ch.lowercaseChar())
65+
} else {
66+
builder.append(ch)
67+
}
68+
}
69+
return builder.toString()
70+
}
71+
72+
fun transformKeys(element: JsonElement, fn: (String) -> String): JsonElement = when (element) {
73+
is JsonObject -> JsonObject(element.entries.associate { (key, value) -> fn(key) to transformKeys(value, fn) })
74+
is JsonArray -> JsonArray(element.map { transformKeys(it, fn) })
75+
else -> element
76+
}
77+
78+
inline fun <reified T> encodeForJS(payload: T): String {
79+
val element = json.encodeToJsonElement(payload)
80+
val transformed = transformKeys(element, ::snakeToCamel)
81+
return json.encodeToString(JsonElement.serializer(), transformed)
82+
}
83+
84+
inline fun <reified T> decodeFromJS(json: String): T {
85+
val element = Json.parseToJsonElement(json)
86+
val transformed = transformKeys(element, ::camelToSnake)
87+
return CasingTransform.json.decodeFromJsonElement(transformed)
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright 2023-present, Shopify Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
package com.shopify.reactnative.checkoutkit
24+
25+
import kotlinx.serialization.Serializable
26+
27+
@Serializable
28+
internal data class DispatchEnvelope<P>(
29+
val type: String,
30+
val payload: P,
31+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright 2023-present, Shopify Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
package com.shopify.reactnative.checkoutkit
24+
25+
import com.shopify.checkoutkit.CheckoutProtocol
26+
27+
fun interface DispatchCallback {
28+
fun invoke(json: String)
29+
}
30+
31+
object ProtocolRelay {
32+
33+
@JvmStatic
34+
fun makeClient(
35+
subscribedMethods: List<String>,
36+
dispatch: DispatchCallback,
37+
): CheckoutProtocol.Client {
38+
var client = CheckoutProtocol.Client()
39+
for (method in subscribedMethods) {
40+
when (method) {
41+
CheckoutProtocol.start.method -> {
42+
client = client.on(CheckoutProtocol.start) { checkout ->
43+
forwardEnvelope(method, checkout, dispatch)
44+
}
45+
}
46+
}
47+
}
48+
return client
49+
}
50+
51+
private inline fun <reified P> forwardEnvelope(
52+
type: String,
53+
payload: P,
54+
dispatch: DispatchCallback,
55+
) {
56+
try {
57+
dispatch.invoke(CasingTransform.encodeForJS(DispatchEnvelope(type, payload)))
58+
} catch (e: Throwable) {
59+
// dispatch failures are swallowed — there is no native consumer for them
60+
}
61+
}
62+
}

platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ of this software and associated documentation files (the "Software"), to deal
3939
import com.shopify.checkoutkit.NativeShopifyCheckoutKitSpec;
4040
import com.shopify.checkoutkit.*;
4141

42+
import java.util.ArrayList;
4243
import java.util.HashMap;
44+
import java.util.List;
4345
import java.util.Map;
4446
import java.util.Objects;
4547

@@ -85,13 +87,29 @@ public void removeListeners(double count) {
8587
}
8688

8789
@ReactMethod
88-
public void present(String checkoutURL, @Nullable Callback dispatch) {
90+
public void present(String checkoutURL, ReadableArray subscribedMethods, @Nullable Callback dispatch) {
8991
Activity currentActivity = getCurrentActivity();
9092
if (currentActivity instanceof ComponentActivity) {
9193
checkoutListener = new CustomCheckoutListener(dispatch);
94+
95+
List<String> methods = new ArrayList<>();
96+
for (int i = 0; i < subscribedMethods.size(); i++) {
97+
String method = subscribedMethods.getString(i);
98+
if (method != null) {
99+
methods.add(method);
100+
}
101+
}
102+
CheckoutProtocol.Client client = ProtocolRelay.makeClient(
103+
methods,
104+
json -> {
105+
if (dispatch != null) {
106+
dispatch.invoke(json);
107+
}
108+
});
109+
92110
currentActivity.runOnUiThread(() -> {
93111
checkoutSheet = ShopifyCheckoutKit.present(checkoutURL, (ComponentActivity) currentActivity,
94-
checkoutListener);
112+
checkoutListener, client);
95113
});
96114
}
97115
}

0 commit comments

Comments
 (0)