Skip to content

Commit c0feec4

Browse files
feat: add all protocol events
1 parent da83ecb commit c0feec4

10 files changed

Lines changed: 322 additions & 30 deletions

File tree

platforms/react-native/__mocks__/react-native.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,14 @@ const UIManager = {
5656
if (name === 'RCTAcceleratedCheckoutButtons') {
5757
return {
5858
Constants: {
59-
checkoutProtocolEventTypes: ['ec.start'],
59+
checkoutProtocolEventTypes: [
60+
'ec.complete',
61+
'ec.error',
62+
'ec.line_items.change',
63+
'ec.messages.change',
64+
'ec.start',
65+
'ec.totals.change',
66+
],
6067
},
6168
};
6269
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,36 @@ object ProtocolRelay {
3838
var client = CheckoutProtocol.Client()
3939
for (method in subscribedMethods) {
4040
when (method) {
41+
CheckoutProtocol.complete.method -> {
42+
client = client.on(CheckoutProtocol.complete) { checkout ->
43+
forwardEnvelope(method, checkout, dispatch)
44+
}
45+
}
46+
CheckoutProtocol.error.method -> {
47+
client = client.on(CheckoutProtocol.error) { error ->
48+
forwardEnvelope(method, error, dispatch)
49+
}
50+
}
51+
CheckoutProtocol.lineItemsChange.method -> {
52+
client = client.on(CheckoutProtocol.lineItemsChange) { checkout ->
53+
forwardEnvelope(method, checkout, dispatch)
54+
}
55+
}
56+
CheckoutProtocol.messagesChange.method -> {
57+
client = client.on(CheckoutProtocol.messagesChange) { checkout ->
58+
forwardEnvelope(method, checkout, dispatch)
59+
}
60+
}
4161
CheckoutProtocol.start.method -> {
4262
client = client.on(CheckoutProtocol.start) { checkout ->
4363
forwardEnvelope(method, checkout, dispatch)
4464
}
4565
}
66+
CheckoutProtocol.totalsChange.method -> {
67+
client = client.on(CheckoutProtocol.totalsChange) { checkout ->
68+
forwardEnvelope(method, checkout, dispatch)
69+
}
70+
}
4671
}
4772
}
4873
return client

platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/test/java/com/shopify/reactnative/checkoutkit/ProtocolRelayTest.kt

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,56 @@ class ProtocolRelayTest {
8181
assertThat(firstItem["imageUrl"]?.jsonPrimitive?.content).isEqualTo("https://example.com/image.png")
8282
}
8383

84+
@Test
85+
fun `relay dispatches envelope for every public checkout state event`() {
86+
val methods = listOf(
87+
"ec.complete",
88+
"ec.line_items.change",
89+
"ec.messages.change",
90+
"ec.start",
91+
"ec.totals.change",
92+
)
93+
94+
for (method in methods) {
95+
var captured: String? = null
96+
val client = ProtocolRelay.makeClient(
97+
listOf(method),
98+
DispatchCallback { json -> captured = json },
99+
)
100+
101+
client.process(checkoutNotificationFixture(method))
102+
shadowOf(Looper.getMainLooper()).runToEndOfTasks()
103+
104+
val json = captured
105+
assertThat(json).isNotNull()
106+
val parsed = Json.parseToJsonElement(json!!).jsonObject
107+
assertThat(parsed["type"]?.jsonPrimitive?.content).isEqualTo(method)
108+
assertThat(parsed["payload"]!!.jsonObject["id"]?.jsonPrimitive?.content).isEqualTo("checkout-123")
109+
}
110+
}
111+
112+
@Test
113+
fun `relay dispatches envelope on ec error`() {
114+
var captured: String? = null
115+
val client = ProtocolRelay.makeClient(
116+
listOf("ec.error"),
117+
DispatchCallback { json -> captured = json },
118+
)
119+
120+
client.process(ecErrorNotificationFixture)
121+
shadowOf(Looper.getMainLooper()).runToEndOfTasks()
122+
123+
val json = captured
124+
assertThat(json).isNotNull()
125+
val parsed = Json.parseToJsonElement(json!!).jsonObject
126+
assertThat(parsed["type"]?.jsonPrimitive?.content).isEqualTo("ec.error")
127+
128+
val payload = parsed["payload"]!!.jsonObject
129+
assertThat(payload["messages"]!!.jsonArray[0].jsonObject["content"]?.jsonPrimitive?.content)
130+
.isEqualTo("Something went wrong")
131+
assertThat(payload["ucp"]!!.jsonObject["status"]?.jsonPrimitive?.content).isEqualTo("error")
132+
}
133+
84134
@Test
85135
fun `relay ignores methods not in subscribed list`() {
86136
var captured: String? = null
@@ -118,6 +168,11 @@ private data class SnakePayload(
118168
@SerialName("line_items") val lineItems: List<String>,
119169
)
120170

171+
private fun checkoutNotificationFixture(method: String) = ecStartNotificationFixture.replace(
172+
"\"method\": \"ec.start\"",
173+
"\"method\": \"$method\"",
174+
)
175+
121176
private val ecStartNotificationFixture = """
122177
{
123178
"jsonrpc": "2.0",
@@ -156,3 +211,25 @@ private val ecStartNotificationFixture = """
156211
}
157212
}
158213
""".trimIndent()
214+
215+
private val ecErrorNotificationFixture = """
216+
{
217+
"jsonrpc": "2.0",
218+
"method": "ec.error",
219+
"params": {
220+
"error": {
221+
"ucp": {
222+
"version": "2026-04-08",
223+
"status": "error"
224+
},
225+
"messages": [
226+
{
227+
"type": "error",
228+
"content": "Something went wrong",
229+
"severity": "recoverable"
230+
}
231+
]
232+
}
233+
}
234+
}
235+
""".trimIndent()

platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ProtocolRelay.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ import Foundation
2929
#endif
3030

3131
let supportedProtocolRelayMethods = [
32-
CheckoutProtocol.start.method
32+
CheckoutProtocol.complete.method,
33+
CheckoutProtocol.error.method,
34+
CheckoutProtocol.lineItemsChange.method,
35+
CheckoutProtocol.messagesChange.method,
36+
CheckoutProtocol.start.method,
37+
CheckoutProtocol.totalsChange.method
3338
]
3439

3540
func makeRelayClient(
@@ -40,10 +45,30 @@ func makeRelayClient(
4045

4146
for method in subscribedMethods {
4247
switch method {
48+
case CheckoutProtocol.complete.method:
49+
client = client.on(CheckoutProtocol.complete) { checkout in
50+
forwardEnvelope(type: method, payload: checkout, dispatch: dispatch)
51+
}
52+
case CheckoutProtocol.error.method:
53+
client = client.on(CheckoutProtocol.error) { error in
54+
forwardEnvelope(type: method, payload: error, dispatch: dispatch)
55+
}
56+
case CheckoutProtocol.lineItemsChange.method:
57+
client = client.on(CheckoutProtocol.lineItemsChange) { checkout in
58+
forwardEnvelope(type: method, payload: checkout, dispatch: dispatch)
59+
}
60+
case CheckoutProtocol.messagesChange.method:
61+
client = client.on(CheckoutProtocol.messagesChange) { checkout in
62+
forwardEnvelope(type: method, payload: checkout, dispatch: dispatch)
63+
}
4364
case CheckoutProtocol.start.method:
4465
client = client.on(CheckoutProtocol.start) { checkout in
4566
forwardEnvelope(type: method, payload: checkout, dispatch: dispatch)
4667
}
68+
case CheckoutProtocol.totalsChange.method:
69+
client = client.on(CheckoutProtocol.totalsChange) { checkout in
70+
forwardEnvelope(type: method, payload: checkout, dispatch: dispatch)
71+
}
4772
default:
4873
continue
4974
}

platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/Tests/ProtocolRelayTests.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,53 @@ struct ProtocolRelayTests {
6767
#expect(firstItem["imageUrl"] as? String == "https://example.com/image.png")
6868
}
6969

70+
@MainActor
71+
@Test func relayDispatchesEnvelopeForEveryPublicCheckoutStateEvent() async throws {
72+
let methods = [
73+
"ec.complete",
74+
"ec.line_items.change",
75+
"ec.messages.change",
76+
"ec.start",
77+
"ec.totals.change"
78+
]
79+
80+
for method in methods {
81+
var captured: String?
82+
let client = makeRelayClient(
83+
subscribedMethods: [method],
84+
dispatch: { json in captured = json }
85+
)
86+
87+
_ = await client.process(checkoutNotificationFixture(method: method))
88+
89+
let json = try #require(captured)
90+
let parsed = try #require(JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any])
91+
#expect(parsed["type"] as? String == method)
92+
let payload = try #require(parsed["payload"] as? [String: Any])
93+
#expect(payload["id"] as? String == "checkout-123")
94+
}
95+
}
96+
97+
@MainActor
98+
@Test func relayDispatchesEnvelopeOnEcError() async throws {
99+
var captured: String?
100+
let client = makeRelayClient(
101+
subscribedMethods: ["ec.error"],
102+
dispatch: { json in captured = json }
103+
)
104+
105+
_ = await client.process(ecErrorNotificationFixture)
106+
107+
let json = try #require(captured)
108+
let parsed = try #require(JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any])
109+
#expect(parsed["type"] as? String == "ec.error")
110+
let payload = try #require(parsed["payload"] as? [String: Any])
111+
let messages = try #require(payload["messages"] as? [[String: Any]])
112+
#expect(messages.first?["content"] as? String == "Something went wrong")
113+
let ucp = try #require(payload["ucp"] as? [String: Any])
114+
#expect(ucp["status"] as? String == "error")
115+
}
116+
70117
@MainActor
71118
@Test func relayIgnoresMethodsNotInSubscribedList() async throws {
72119
var captured: String?
@@ -91,6 +138,13 @@ private struct SnakePayload: Codable {
91138
}
92139
}
93140

141+
private func checkoutNotificationFixture(method: String) -> String {
142+
ecStartNotificationFixture.replacingOccurrences(
143+
of: "\"method\": \"ec.start\"",
144+
with: "\"method\": \"\(method)\""
145+
)
146+
}
147+
94148
private let ecStartNotificationFixture = #"""
95149
{
96150
"jsonrpc": "2.0",
@@ -129,3 +183,25 @@ private let ecStartNotificationFixture = #"""
129183
}
130184
}
131185
"""#
186+
187+
private let ecErrorNotificationFixture = #"""
188+
{
189+
"jsonrpc": "2.0",
190+
"method": "ec.error",
191+
"params": {
192+
"error": {
193+
"ucp": {
194+
"version": "2026-04-08",
195+
"status": "error"
196+
},
197+
"messages": [
198+
{
199+
"type": "error",
200+
"content": "Something went wrong",
201+
"severity": "recoverable"
202+
}
203+
]
204+
}
205+
}
206+
}
207+
"""#

platforms/react-native/modules/@shopify/checkout-kit-react-native/src/components/AcceleratedCheckoutButtons.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ interface CommonAcceleratedCheckoutButtonsProps {
115115
/**
116116
* Checkout Protocol event handlers scoped to this button instance.
117117
*
118-
* Currently supports CheckoutProtocol.start.
118+
* Supports all public Checkout Protocol notification events.
119119
*/
120120
events?: ProtocolHandlers;
121121

@@ -437,10 +437,11 @@ function routeProtocolDispatchEnvelope(
437437
return;
438438
}
439439

440-
const handler = (events as Record<
441-
string,
442-
((payload: unknown) => void) | undefined
443-
> | undefined)?.[envelope.type];
440+
const handler = (
441+
events as
442+
| Record<string, ((payload: unknown) => void) | undefined>
443+
| undefined
444+
)?.[envelope.type];
444445

445446
if (handler == null) {
446447
return;

platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO
2323

2424
import type {CheckoutException} from './errors';
2525
import type {ProtocolHandlers} from './protocol';
26-
export type {Checkout, CheckoutProtocolPayloads, ProtocolHandlers} from './protocol';
26+
export type {
27+
Checkout,
28+
CheckoutProtocolPayloads,
29+
ErrorResponse,
30+
ProtocolHandlers,
31+
} from './protocol';
2732

2833
export type Maybe<T> = T | undefined;
2934

platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {CheckoutProtocol} from './protocol';
6464
import type {
6565
Checkout,
6666
CheckoutProtocolPayloads,
67+
ErrorResponse,
6768
ProtocolHandlers,
6869
} from './protocol';
6970

@@ -152,12 +153,14 @@ class ShopifyCheckout implements ShopifyCheckoutKit {
152153
.map(([method]) => method);
153154

154155
if (dispatcher) {
155-
this.dispatchSubscription = RNShopifyCheckoutKit.onDispatch(envelopeJson => {
156-
dispatcher(envelopeJson);
157-
if (isTerminalDispatchEnvelope(envelopeJson)) {
158-
this.releaseDispatchSubscription();
159-
}
160-
});
156+
this.dispatchSubscription = RNShopifyCheckoutKit.onDispatch(
157+
envelopeJson => {
158+
dispatcher(envelopeJson);
159+
if (isTerminalDispatchEnvelope(envelopeJson)) {
160+
this.releaseDispatchSubscription();
161+
}
162+
},
163+
);
161164
}
162165

163166
RNShopifyCheckoutKit.present(checkoutUrl, subscribedMethods);
@@ -393,10 +396,12 @@ class ShopifyCheckout implements ShopifyCheckoutKit {
393396
const protocolHandler =
394397
protocol == null
395398
? undefined
396-
: (protocol as Record<
397-
string,
398-
((payload: unknown) => void) | undefined
399-
>)[type];
399+
: (
400+
protocol as Record<
401+
string,
402+
((payload: unknown) => void) | undefined
403+
>
404+
)[type];
400405

401406
if (protocolHandler) {
402407
if (!isPlainObject(payload)) {
@@ -669,6 +674,7 @@ export type {
669674
CheckoutException,
670675
CheckoutProtocolPayloads,
671676
Configuration,
677+
ErrorResponse,
672678
Features,
673679
GeolocationRequestEvent,
674680
PresentCallbacks,

0 commit comments

Comments
 (0)