Skip to content

Commit b20bd53

Browse files
committed
Checkpoint 2
1 parent 2009528 commit b20bd53

16 files changed

Lines changed: 1022 additions & 689 deletions
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
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 UIKit
26+
import SwiftUI
27+
import React
28+
import ShopifyCheckoutSheetKit
29+
import PassKit
30+
31+
// MARK: - AcceleratedCheckout Components
32+
33+
@available(iOS 17.0, *)
34+
class AcceleratedCheckoutConfiguration {
35+
static let shared = AcceleratedCheckoutConfiguration()
36+
var configuration: ShopifyAcceleratedCheckouts.Configuration?
37+
var wallets: [String] = ["shopPay", "applePay"]
38+
39+
private init() {
40+
setupApplePay()
41+
}
42+
43+
private func setupApplePay() {
44+
// Configure Apple Pay environment
45+
// This should be called during app initialization
46+
if let merchantID = Bundle.main.object(forInfoDictionaryKey: "ApplePayMerchantID") as? String {
47+
// Apple Pay is configured via merchant ID in Info.plist
48+
// The actual Apple Pay setup is handled by the ShopifyAcceleratedCheckouts framework
49+
print("✅ Apple Pay configured with Merchant ID: \(merchantID)")
50+
} else {
51+
print("⚠️ Apple Pay Merchant ID not found in Info.plist. Add 'ApplePayMerchantID' key to enable Apple Pay.")
52+
}
53+
}
54+
}
55+
56+
@objc(RCTAcceleratedCheckoutButtonsManager)
57+
class RCTAcceleratedCheckoutButtonsManager: RCTViewManager {
58+
59+
override func view() -> UIView! {
60+
if #available(iOS 17.0, *) {
61+
return RCTAcceleratedCheckoutButtonsView()
62+
}
63+
64+
// Return an empty view for iOS < 17.0 (silent fallback)
65+
return UIView()
66+
}
67+
68+
override static func requiresMainQueueSetup() -> Bool {
69+
return true
70+
}
71+
72+
override func constantsToExport() -> [AnyHashable : Any]! {
73+
return [:]
74+
}
75+
}
76+
77+
@available(iOS 17.0, *)
78+
class RCTAcceleratedCheckoutButtonsView: UIView {
79+
private var hostingController: UIHostingController<AnyView>?
80+
private var configuration: ShopifyAcceleratedCheckouts.Configuration?
81+
private weak var parentViewController: UIViewController?
82+
83+
@objc var cartId: String? {
84+
didSet {
85+
updateView()
86+
}
87+
}
88+
89+
@objc var variantId: String? {
90+
didSet {
91+
updateView()
92+
}
93+
}
94+
95+
@objc var quantity: NSNumber = 1 {
96+
didSet {
97+
updateView()
98+
}
99+
}
100+
101+
@objc var cornerRadius: NSNumber = 8 {
102+
didSet {
103+
updateView()
104+
}
105+
}
106+
107+
@objc var wallets: [String]? {
108+
didSet {
109+
updateView()
110+
}
111+
}
112+
113+
@objc var onPress: RCTBubblingEventBlock?
114+
@objc var onFail: RCTBubblingEventBlock?
115+
@objc var onComplete: RCTBubblingEventBlock?
116+
@objc var onCancel: RCTBubblingEventBlock?
117+
@objc var onRenderStateChange: RCTBubblingEventBlock?
118+
@objc var onShouldRecoverFromError: RCTDirectEventBlock?
119+
@objc var onWebPixelEvent: RCTBubblingEventBlock?
120+
@objc var onClickLink: RCTBubblingEventBlock?
121+
122+
override init(frame: CGRect) {
123+
super.init(frame: frame)
124+
setupView()
125+
}
126+
127+
required init?(coder: NSCoder) {
128+
super.init(coder: coder)
129+
setupView()
130+
}
131+
132+
private func setupView() {
133+
// Configuration will be set via a static method from the main module
134+
self.configuration = AcceleratedCheckoutConfiguration.shared.configuration
135+
136+
// Find the parent view controller
137+
DispatchQueue.main.async { [weak self] in
138+
self?.parentViewController = self?.findViewController()
139+
}
140+
141+
updateView()
142+
143+
// Listen for configuration updates
144+
NotificationCenter.default.addObserver(
145+
self,
146+
selector: #selector(configurationUpdated),
147+
name: Notification.Name("AcceleratedCheckoutConfigurationUpdated"),
148+
object: nil
149+
)
150+
}
151+
152+
private func findViewController() -> UIViewController? {
153+
var responder: UIResponder? = self
154+
while let nextResponder = responder?.next {
155+
if let viewController = nextResponder as? UIViewController {
156+
return viewController
157+
}
158+
responder = nextResponder
159+
}
160+
return nil
161+
}
162+
163+
@objc private func configurationUpdated() {
164+
self.configuration = AcceleratedCheckoutConfiguration.shared.configuration
165+
updateView()
166+
}
167+
168+
private func createCartButtons(
169+
cartId: String,
170+
wallets: [Wallet],
171+
config: ShopifyAcceleratedCheckouts.Configuration,
172+
applePayConfig: ShopifyAcceleratedCheckouts.ApplePayConfiguration
173+
) -> some View {
174+
AcceleratedCheckoutButtons(cartID: cartId)
175+
.wallets(wallets)
176+
.onComplete { [weak self] event in
177+
self?.handleCheckoutCompleted(event)
178+
}
179+
.onFail { [weak self] error in
180+
self?.handleCheckoutFailed(error)
181+
}
182+
.onCancel { [weak self] in
183+
self?.handleCheckoutCancelled()
184+
}
185+
.onRenderStateChange { [weak self] state in
186+
self?.handleRenderStateChange(state)
187+
}
188+
.onClickLink { [weak self] url in
189+
self?.handleClickLink(url)
190+
}
191+
.onWebPixelEvent { [weak self] event in
192+
self?.handleWebPixelEvent(event)
193+
}
194+
.cornerRadius(CGFloat(cornerRadius.doubleValue))
195+
.environment(config)
196+
.environment(applePayConfig)
197+
}
198+
199+
private func createVariantButtons(
200+
variantId: String,
201+
quantity: Int,
202+
wallets: [Wallet],
203+
config: ShopifyAcceleratedCheckouts.Configuration,
204+
applePayConfig: ShopifyAcceleratedCheckouts.ApplePayConfiguration
205+
) -> some View {
206+
AcceleratedCheckoutButtons(variantID: variantId, quantity: quantity)
207+
.wallets(wallets)
208+
.onComplete { [weak self] event in
209+
self?.handleCheckoutCompleted(event)
210+
}
211+
.onFail { [weak self] error in
212+
self?.handleCheckoutFailed(error)
213+
}
214+
.onCancel { [weak self] in
215+
self?.handleCheckoutCancelled()
216+
}
217+
.onRenderStateChange { [weak self] state in
218+
self?.handleRenderStateChange(state)
219+
}
220+
.onClickLink { [weak self] url in
221+
self?.handleClickLink(url)
222+
}
223+
.onWebPixelEvent { [weak self] event in
224+
self?.handleWebPixelEvent(event)
225+
}
226+
.cornerRadius(CGFloat(cornerRadius.doubleValue))
227+
.environment(config)
228+
.environment(applePayConfig)
229+
}
230+
231+
private func updateView() {
232+
// Make sure we have a configuration before creating the view
233+
guard let config = configuration else {
234+
// If no configuration is set yet, show an empty view
235+
if let hostingController = hostingController {
236+
hostingController.rootView = AnyView(EmptyView())
237+
}
238+
return
239+
}
240+
241+
// Use wallets from props, or fall back to shared configuration, or default to both
242+
let walletsToUse = wallets ?? AcceleratedCheckoutConfiguration.shared.wallets
243+
let shopifyWallets = convertToShopifyWallets(walletsToUse)
244+
245+
// Create Apple Pay configuration
246+
let applePayConfig = ShopifyAcceleratedCheckouts.ApplePayConfiguration(
247+
merchantIdentifier: Bundle.main.object(forInfoDictionaryKey: "ApplePayMerchantID") as? String ?? "merchant.com.shopify",
248+
contactFields: [.email, .phone]
249+
)
250+
251+
let swiftUIView: AnyView
252+
253+
if let cartId = cartId {
254+
swiftUIView = AnyView(createCartButtons(
255+
cartId: cartId,
256+
wallets: shopifyWallets,
257+
config: config,
258+
applePayConfig: applePayConfig
259+
))
260+
} else if let variantId = variantId {
261+
swiftUIView = AnyView(createVariantButtons(
262+
variantId: variantId,
263+
quantity: quantity.intValue,
264+
wallets: shopifyWallets,
265+
config: config,
266+
applePayConfig: applePayConfig
267+
))
268+
} else {
269+
// Empty view if no cart or variant ID is provided
270+
swiftUIView = AnyView(EmptyView())
271+
}
272+
273+
if let hostingController = hostingController {
274+
hostingController.rootView = swiftUIView
275+
} else {
276+
hostingController = UIHostingController(rootView: swiftUIView)
277+
hostingController?.view.backgroundColor = UIColor.clear
278+
279+
// Ensure the hosting view can receive touch events
280+
hostingController?.view.isUserInteractionEnabled = true
281+
282+
if let hostingView = hostingController?.view {
283+
addSubview(hostingView)
284+
hostingView.translatesAutoresizingMaskIntoConstraints = false
285+
NSLayoutConstraint.activate([
286+
hostingView.topAnchor.constraint(equalTo: topAnchor),
287+
hostingView.leadingAnchor.constraint(equalTo: leadingAnchor),
288+
hostingView.trailingAnchor.constraint(equalTo: trailingAnchor),
289+
hostingView.bottomAnchor.constraint(equalTo: bottomAnchor)
290+
])
291+
}
292+
}
293+
294+
// Ensure this view can also receive touch events
295+
self.isUserInteractionEnabled = true
296+
}
297+
298+
private func convertToShopifyWallets(_ walletStrings: [String]) -> [Wallet] {
299+
return walletStrings.compactMap { walletString in
300+
switch walletString {
301+
case "shopPay":
302+
return .shopPay
303+
case "applePay":
304+
return .applePay
305+
default:
306+
return nil
307+
}
308+
}
309+
}
310+
311+
private func handleCheckoutCompleted(_ event: CheckoutCompletedEvent) {
312+
onComplete?(ShopifyEventSerialization.serialize(checkoutCompletedEvent: event))
313+
}
314+
315+
private func handleCheckoutFailed(_ error: CheckoutError) {
316+
onFail?(ShopifyEventSerialization.serialize(checkoutError: error))
317+
}
318+
319+
private func handleCheckoutCancelled() {
320+
onCancel?([:])
321+
}
322+
323+
private func handleRenderStateChange(_ state: RenderState) {
324+
onRenderStateChange?(["state": ShopifyEventSerialization.serialize(renderState: state)])
325+
}
326+
327+
private func handleWebPixelEvent(_ event: PixelEvent) {
328+
onWebPixelEvent?(ShopifyEventSerialization.serialize(pixelEvent: event))
329+
}
330+
331+
private func handleClickLink(_ url: URL) {
332+
onClickLink?([
333+
"url": url.absoluteString
334+
])
335+
}
336+
337+
override func layoutSubviews() {
338+
super.layoutSubviews()
339+
hostingController?.view.frame = bounds
340+
}
341+
342+
override var intrinsicContentSize: CGSize {
343+
// Provide a default size for the button
344+
return CGSize(width: UIView.noIntrinsicMetric, height: 50)
345+
}
346+
}

0 commit comments

Comments
 (0)