Skip to content

Commit 409b422

Browse files
authored
Reorganise common code that will be used by Accelerated Checkouts (#242)
* Add image optimization and debug logging utilities to sample app - Add getOptimizedImageUrl utility for Shopify CDN image optimization - Add debugLog and createDebugLogger utilities with conditional development logging These utilities improve sample app performance and debugging capabilities and can be used independently of any feature development. * Add useShopifyEventHandlers hook for checkout event handling - Provides standardized event handlers with debug logging - Includes handlers for press, fail, complete, cancel, render state changes, pixel events, and link clicks - Integrates with existing cart context for order completion cleanup - Can be used independently of any specific checkout features * Use serializations + checkout event handlers in sample app * Improve simulator discoverability * Graphql transforms for thumbnails * Fix linting * Update snapshot * Move logger inside useMemo * Make ShopifyEventSerialization explicitly internal * Add explicit .ruby-version file, regenerate Gemfile.lock
1 parent 5688d65 commit 409b422

19 files changed

+397
-170
lines changed

.ruby-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.3.6
1+
3.1.2
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
24+
import Foundation
25+
import ShopifyCheckoutSheetKit
26+
27+
/**
28+
* Shared event serialization utilities for converting ShopifyCheckoutSheetKit events
29+
* to React Native compatible dictionaries.
30+
*/
31+
internal enum ShopifyEventSerialization {
32+
/**
33+
* Encodes a Codable object to a JSON dictionary for React Native bridge.
34+
*/
35+
static func encodeToJSON(from value: Codable) -> [String: Any] {
36+
let encoder = JSONEncoder()
37+
38+
do {
39+
let jsonData = try encoder.encode(value)
40+
if let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] {
41+
return jsonObject
42+
}
43+
} catch {
44+
print("Error encoding to JSON object: \(error)")
45+
}
46+
return [:]
47+
}
48+
49+
/**
50+
* Converts a JSON string to a dictionary.
51+
*/
52+
static func stringToJSON(from value: String?) -> [String: Any]? {
53+
guard let data = value?.data(using: .utf8, allowLossyConversion: false) else { return [:] }
54+
do {
55+
return try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
56+
} catch {
57+
print("Failed to convert string to JSON: \(error)", value ?? "nil")
58+
return [:]
59+
}
60+
}
61+
62+
/**
63+
* Converts a CheckoutCompletedEvent to a React Native compatible dictionary.
64+
*/
65+
static func serialize(checkoutCompletedEvent event: CheckoutCompletedEvent) -> [String: Any] {
66+
return encodeToJSON(from: event)
67+
}
68+
69+
/**
70+
* Converts a PixelEvent to a React Native compatible dictionary.
71+
*/
72+
static func serialize(pixelEvent event: PixelEvent) -> [String: Any] {
73+
switch event {
74+
case let .standardEvent(standardEvent):
75+
let encoded = encodeToJSON(from: standardEvent)
76+
return [
77+
"context": encoded["context"] ?? NSNull(),
78+
"data": encoded["data"] ?? NSNull(),
79+
"id": encoded["id"] ?? NSNull(),
80+
"name": encoded["name"] ?? NSNull(),
81+
"timestamp": encoded["timestamp"] ?? NSNull(),
82+
"type": "STANDARD"
83+
]
84+
85+
case let .customEvent(customEvent):
86+
return [
87+
"context": encodeToJSON(from: customEvent.context),
88+
"customData": stringToJSON(from: customEvent.customData) ?? NSNull(),
89+
"id": customEvent.id,
90+
"name": customEvent.name,
91+
"timestamp": customEvent.timestamp,
92+
"type": "CUSTOM"
93+
]
94+
}
95+
}
96+
97+
static func serialize(clickEvent url: URL) -> [String: URL] {
98+
return ["url": url]
99+
}
100+
101+
/**
102+
* Converts a CheckoutError to a React Native compatible dictionary.
103+
* Handles all specific error types with proper type information.
104+
*/
105+
static func serialize(checkoutError error: CheckoutError) -> [String: Any] {
106+
switch error {
107+
case let .checkoutExpired(message, code, recoverable):
108+
return [
109+
"__typename": "CheckoutExpiredError",
110+
"message": message,
111+
"code": code.rawValue,
112+
"recoverable": recoverable
113+
]
114+
115+
case let .checkoutUnavailable(message, code, recoverable):
116+
switch code {
117+
case let .clientError(clientErrorCode):
118+
return [
119+
"__typename": "CheckoutClientError",
120+
"message": message,
121+
"code": clientErrorCode.rawValue,
122+
"recoverable": recoverable
123+
]
124+
case let .httpError(statusCode):
125+
return [
126+
"__typename": "CheckoutHTTPError",
127+
"message": message,
128+
"code": "http_error",
129+
"statusCode": statusCode,
130+
"recoverable": recoverable
131+
]
132+
}
133+
134+
case let .configurationError(message, code, recoverable):
135+
return [
136+
"__typename": "ConfigurationError",
137+
"message": message,
138+
"code": code.rawValue,
139+
"recoverable": recoverable
140+
]
141+
142+
case let .sdkError(underlying, recoverable):
143+
return [
144+
"__typename": "InternalError",
145+
"code": "unknown",
146+
"message": underlying.localizedDescription,
147+
"recoverable": recoverable
148+
]
149+
150+
@unknown default:
151+
return [
152+
"__typename": "UnknownError",
153+
"code": "unknown",
154+
"message": error.localizedDescription,
155+
"recoverable": error.isRecoverable
156+
]
157+
}
158+
}
159+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
24+
import UIKit
25+
26+
// MARK: - UIColor Extensions
27+
28+
extension UIColor {
29+
convenience init(hex: String) {
30+
let hexString: String = hex.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
31+
let start = hexString.index(hexString.startIndex, offsetBy: hexString.hasPrefix("#") ? 1 : 0)
32+
let hexColor = String(hexString[start...])
33+
34+
let scanner = Scanner(string: hexColor)
35+
var hexNumber: UInt64 = 0
36+
37+
if scanner.scanHexInt64(&hexNumber) {
38+
let red = (hexNumber & 0xFF0000) >> 16
39+
let green = (hexNumber & 0x00FF00) >> 8
40+
let blue = hexNumber & 0x0000FF
41+
42+
self.init(
43+
red: CGFloat(red) / 0xFF,
44+
green: CGFloat(green) / 0xFF,
45+
blue: CGFloat(blue) / 0xFF,
46+
alpha: 1
47+
)
48+
} else {
49+
self.init(red: 0, green: 0, blue: 0, alpha: 1)
50+
}
51+
}
52+
}

modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.swift

Lines changed: 4 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
6262

6363
func checkoutDidComplete(event: CheckoutCompletedEvent) {
6464
if hasListeners {
65-
sendEvent(withName: "completed", body: encodeToJSON(from: event))
65+
sendEvent(withName: "completed", body: ShopifyEventSerialization.serialize(checkoutCompletedEvent: event))
6666
}
6767
}
6868

@@ -73,66 +73,12 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
7373
func checkoutDidFail(error: ShopifyCheckoutSheetKit.CheckoutError) {
7474
guard hasListeners else { return }
7575

76-
if case let .checkoutExpired(message, code, recoverable) = error {
77-
sendEvent(withName: "error", body: [
78-
"__typename": "CheckoutExpiredError",
79-
"message": message,
80-
"code": code.rawValue,
81-
"recoverable": recoverable
82-
])
83-
} else if case let .checkoutUnavailable(message, code, recoverable) = error {
84-
switch code {
85-
case let .clientError(clientErrorCode):
86-
sendEvent(withName: "error", body: [
87-
"__typename": "CheckoutClientError",
88-
"message": message,
89-
"code": clientErrorCode.rawValue,
90-
"recoverable": recoverable
91-
])
92-
case let .httpError(statusCode):
93-
sendEvent(withName: "error", body: [
94-
"__typename": "CheckoutHTTPError",
95-
"message": message,
96-
"code": "http_error",
97-
"statusCode": statusCode,
98-
"recoverable": recoverable
99-
])
100-
}
101-
} else if case let .configurationError(message, code, recoverable) = error {
102-
sendEvent(withName: "error", body: [
103-
"__typename": "ConfigurationError",
104-
"message": message,
105-
"code": code.rawValue,
106-
"recoverable": recoverable
107-
])
108-
} else if case let .sdkError(underlying, recoverable) = error {
109-
var errorMessage = "\(underlying.localizedDescription)"
110-
sendEvent(withName: "error", body: [
111-
"__typename": "InternalError",
112-
"code": "unknown",
113-
"message": errorMessage,
114-
"recoverable": recoverable
115-
])
116-
} else {
117-
sendEvent(withName: "error", body: [
118-
"__typename": "UnknownError",
119-
"code": "unknown",
120-
"message": error.localizedDescription,
121-
"recoverable": error.isRecoverable
122-
])
123-
}
76+
sendEvent(withName: "error", body: ShopifyEventSerialization.serialize(checkoutError: error))
12477
}
12578

12679
func checkoutDidEmitWebPixelEvent(event: ShopifyCheckoutSheetKit.PixelEvent) {
12780
if hasListeners {
128-
var genericEvent: [String: Any]
129-
switch event {
130-
case let .standardEvent(standardEvent):
131-
genericEvent = mapToGenericEvent(standardEvent: standardEvent)
132-
case let .customEvent(customEvent):
133-
genericEvent = mapToGenericEvent(customEvent: customEvent)
134-
}
135-
sendEvent(withName: "pixel", body: genericEvent)
81+
sendEvent(withName: "pixel", body: ShopifyEventSerialization.serialize(pixelEvent: event))
13682
}
13783
}
13884

@@ -255,94 +201,9 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
255201
"colorScheme": ShopifyCheckoutSheetKit.configuration.colorScheme.rawValue,
256202
"tintColor": ShopifyCheckoutSheetKit.configuration.tintColor,
257203
"backgroundColor": ShopifyCheckoutSheetKit.configuration.backgroundColor,
258-
"closeButtonColor": ShopifyCheckoutSheetKit.configuration.closeButtonTintColor
204+
"closeButtonColor": ShopifyCheckoutSheetKit.configuration.closeButtonTintColor
259205
]
260206

261207
resolve(config)
262208
}
263-
264-
// MARK: - Private
265-
266-
private func stringToJSON(from value: String?) -> [String: Any]? {
267-
guard let data = value?.data(using: .utf8, allowLossyConversion: false) else { return [:] }
268-
do {
269-
return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
270-
} catch {
271-
print("Failed to convert string to JSON: \(error)", value)
272-
return [:]
273-
}
274-
}
275-
276-
private func encodeToJSON(from value: Codable) -> [String: Any] {
277-
let encoder = JSONEncoder()
278-
279-
do {
280-
let jsonData = try encoder.encode(value)
281-
if let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] {
282-
return jsonObject
283-
}
284-
} catch {
285-
print("Error encoding to JSON object: \(error)")
286-
}
287-
return [:]
288-
}
289-
290-
private func mapToGenericEvent(standardEvent: StandardEvent) -> [String: Any] {
291-
let encoded = encodeToJSON(from: standardEvent)
292-
return [
293-
"context": encoded["context"],
294-
"data": encoded["data"],
295-
"id": encoded["id"],
296-
"name": encoded["name"],
297-
"timestamp": encoded["timestamp"],
298-
"type": "STANDARD"
299-
] as [String: Any]
300-
}
301-
302-
private func mapToGenericEvent(customEvent: CustomEvent) -> [String: Any] {
303-
do {
304-
return try decodeAndMap(event: customEvent)
305-
} catch {
306-
print("[debug] Failed to map custom event: \(error)")
307-
}
308-
309-
return [:]
310-
}
311-
312-
private func decodeAndMap(event: CustomEvent, decoder _: JSONDecoder = JSONDecoder()) throws -> [String: Any] {
313-
return [
314-
"context": encodeToJSON(from: event.context),
315-
"customData": stringToJSON(from: event.customData),
316-
"id": event.id,
317-
"name": event.name,
318-
"timestamp": event.timestamp,
319-
"type": "CUSTOM"
320-
] as [String: Any]
321-
}
322-
}
323-
324-
extension UIColor {
325-
convenience init(hex: String) {
326-
let hexString: String = hex.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
327-
let start = hexString.index(hexString.startIndex, offsetBy: hexString.hasPrefix("#") ? 1 : 0)
328-
let hexColor = String(hexString[start...])
329-
330-
let scanner = Scanner(string: hexColor)
331-
var hexNumber: UInt64 = 0
332-
333-
if scanner.scanHexInt64(&hexNumber) {
334-
let red = (hexNumber & 0xFF0000) >> 16
335-
let green = (hexNumber & 0x00FF00) >> 8
336-
let blue = hexNumber & 0x0000FF
337-
338-
self.init(
339-
red: CGFloat(red) / 0xFF,
340-
green: CGFloat(green) / 0xFF,
341-
blue: CGFloat(blue) / 0xFF,
342-
alpha: 1
343-
)
344-
} else {
345-
self.init(red: 0, green: 0, blue: 0, alpha: 1)
346-
}
347-
}
348209
}

modules/@shopify/checkout-sheet-kit/package.snapshot.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"ios/ShopifyCheckoutSheetKit-Bridging-Header.h",
1313
"ios/ShopifyCheckoutSheetKit.mm",
1414
"ios/ShopifyCheckoutSheetKit.swift",
15+
"ios/ShopifyCheckoutSheetKit+EventSerialization.swift",
16+
"ios/ShopifyCheckoutSheetKit+Extensions.swift",
1517
"lib/commonjs/context.js",
1618
"lib/commonjs/context.js.map",
1719
"lib/commonjs/errors.d.js",

0 commit comments

Comments
 (0)