@o2vend/theme-cli 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. package/test-theme/widgets/testimonial-carousel.liquid +709 -0
@@ -0,0 +1,4511 @@
1
+ {% layout 'layout/theme' %}
2
+ {% comment %}
3
+ O2VEND Default Theme - Checkout Template
4
+ Checkout process with payment and shipping options
5
+ {% endcomment %}
6
+
7
+ <!-- Checkout Page -->
8
+ {% hook 'checkout_before' %}
9
+ <section class="checkout-page">
10
+ <div class="checkout-container">
11
+ {% comment %} <div class="checkout-header">
12
+ <h1 class="checkout-title">Checkout</h1>
13
+ </div> {% endcomment %}
14
+
15
+ <!-- Price Change Warning Banner -->
16
+ {% if checkout.priceChangeDetected or checkout.priceChangeMetadata.detected %}
17
+ <div class="price-change-banner" id="price-change-banner" data-price-change-detected="true">
18
+ <div class="price-change-banner-content">
19
+ <div class="price-change-icon">
20
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
21
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
22
+ <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
23
+ <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
24
+ </svg>
25
+ </div>
26
+ <div class="price-change-message">
27
+ <h3 class="price-change-title">Prices Have Changed</h3>
28
+ <p class="price-change-description">
29
+ {% if checkout.priceChangeMetadata.totalChange > 0 %}
30
+ Some prices have increased. Please review your order before completing checkout.
31
+ {% elsif checkout.priceChangeMetadata.totalChange < 0 %}
32
+ Great news! Some prices have decreased. Your order total has been updated.
33
+ {% else %}
34
+ Some prices have changed. Please review your order.
35
+ {% endif %}
36
+ </p>
37
+ {% if checkout.warnings and checkout.warnings.size > 0 %}
38
+ <ul class="price-change-warnings-list">
39
+ {% for warning in checkout.warnings %}
40
+ <li>{{ warning }}</li>
41
+ {% endfor %}
42
+ </ul>
43
+ {% endif %}
44
+ </div>
45
+ <div class="price-change-actions">
46
+ <button type="button" class="btn btn-outline btn-sm" id="refresh-checkout-prices">
47
+ Refresh Prices
48
+ </button>
49
+ <button type="button" class="btn btn-ghost btn-sm" id="dismiss-price-change-banner">
50
+ Dismiss
51
+ </button>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ {% endif %}
56
+
57
+ <!-- Price Change Details -->
58
+ {% if checkout.priceChangeDetails or checkout.priceChangeMetadata.itemsChanged > 0 %}
59
+ <div class="price-change-details" id="price-change-details">
60
+ <h3 class="price-change-details-title">Price Changes</h3>
61
+ <div class="price-change-items-list">
62
+ {% if checkout.lineItems %}
63
+ {% for item in checkout.lineItems %}
64
+ {% assign itemId = item.id | default: item.variantId | default: forloop.index %}
65
+ {% assign changeDetail = checkout.priceChangeDetails[itemId] %}
66
+ {% if changeDetail or checkout.priceChangeMetadata.detected %}
67
+ <div class="price-change-item" data-item-id="{{ itemId }}">
68
+ <div class="price-change-item-info">
69
+ <h4 class="price-change-item-title">{{ item.title | default: 'Product' }}</h4>
70
+ {% if item.sku %}
71
+ <p class="price-change-item-sku">SKU: {{ item.sku }}</p>
72
+ {% endif %}
73
+ </div>
74
+ <div class="price-change-item-prices">
75
+ {% if changeDetail.oldPrice or item.originalPrice %}
76
+ <span class="price-change-old-price">
77
+ {{ changeDetail.oldPrice | default: item.originalPrice | money_with_settings: shop.settings }}
78
+ </span>
79
+ {% endif %}
80
+ <span class="price-change-arrow">→</span>
81
+ <span class="price-change-new-price">
82
+ {{ changeDetail.newPrice | default: item.price | money_with_settings: shop.settings }}
83
+ </span>
84
+ {% if changeDetail.percentageChange %}
85
+ <span class="price-change-percentage {% if changeDetail.percentageChange > 0 %}price-increase{% else %}price-decrease{% endif %}">
86
+ ({{ changeDetail.percentageChange | round: 1 }}%)
87
+ </span>
88
+ {% endif %}
89
+ </div>
90
+ </div>
91
+ {% endif %}
92
+ {% endfor %}
93
+ {% endif %}
94
+ </div>
95
+ </div>
96
+ {% endif %}
97
+
98
+ <form class="checkout-form" id="checkout-form" method="POST" action="/webstoreapi/checkouts">
99
+ <div class="checkout-layout">
100
+ <!-- Left Column: Checkout Form -->
101
+ <div class="checkout-main">
102
+
103
+ <!-- Shipping Address Section -->
104
+ {% hook 'checkout_shipping_before' %}
105
+ <div class="checkout-section">
106
+ <h2 class="checkout-section-title">Shipping Address</h2>
107
+
108
+ {% hook 'checkout_shipping_address_before' %}
109
+ <div class="checkout-section-content">
110
+ <div class="form-grid">
111
+ <div class="form-group">
112
+ <label for="shipping-first-name" class="form-label">First Name</label>
113
+ <input
114
+ type="text"
115
+ id="shipping-first-name"
116
+ name="shippingFirstName"
117
+ class="form-input"
118
+ value="{{ checkout.shippingAddress.firstName | default: '' | escape }}"
119
+ required>
120
+ </div>
121
+
122
+ <div class="form-group">
123
+ <label for="shipping-last-name" class="form-label">Last Name</label>
124
+ <input
125
+ type="text"
126
+ id="shipping-last-name"
127
+ name="shippingLastName"
128
+ class="form-input"
129
+ value="{{ checkout.shippingAddress.lastName | default: '' | escape }}">
130
+ </div>
131
+
132
+ <div class="form-group form-group-full">
133
+ <label for="shipping-address" class="form-label">Address</label>
134
+ <input
135
+ type="text"
136
+ id="shipping-address"
137
+ name="shippingAddress"
138
+ class="form-input"
139
+ value="{{ checkout.shippingAddress.address1 | default: '' | escape }}"
140
+ required>
141
+ </div>
142
+
143
+ <div class="form-group">
144
+ <label for="shipping-city" class="form-label">City</label>
145
+ <input
146
+ type="text"
147
+ id="shipping-city"
148
+ name="shippingCity"
149
+ class="form-input"
150
+ value="{{ checkout.shippingAddress.city | default: '' | escape }}"
151
+ required>
152
+ </div>
153
+
154
+ <div class="form-group">
155
+ <label for="shipping-country" class="form-label">Country</label>
156
+ <select id="shipping-country" name="shippingCountry" class="form-input form-select" required>
157
+ <option value="">Select a country</option>
158
+ {% if countries and countries.size > 0 %}
159
+ {% for country in countries %}
160
+ {% assign countryId = country.id | default: country.countryId %}
161
+ {% assign countryCode = country.code2 | default: country.code | default: country.countryCode | default: country.name %}
162
+ <option value="{{ countryId }}"
163
+ data-country-id="{{ countryId }}"
164
+ data-country-code2="{{ country.code2 | default: country.code }}"
165
+ {% if checkout.shippingAddress.countryId == countryId or checkout.shippingAddress.country == countryCode or checkout.shippingAddress.country == country.code2 or checkout.shippingAddress.country == country.code %}selected{% endif %}>
166
+ {{ country.name }}
167
+ </option>
168
+ {% endfor %}
169
+ {% else %}
170
+ <option value="US">United States</option>
171
+ <option value="CA">Canada</option>
172
+ <option value="GB">United Kingdom</option>
173
+ <option value="AU">Australia</option>
174
+ <option value="IN">India</option>
175
+ {% endif %}
176
+ </select>
177
+ </div>
178
+
179
+ <div class="form-group">
180
+ <label for="shipping-state" class="form-label">State</label>
181
+ <select id="shipping-state" name="shippingState" class="form-input form-select" required>
182
+ <option value="">Select a state</option>
183
+ </select>
184
+ <input type="text" id="shipping-state-text" name="shippingState" class="form-input" style="display: none;" placeholder="Enter state/province">
185
+ </div>
186
+
187
+ <div class="form-group">
188
+ <label for="shipping-zip" class="form-label">ZIP Code</label>
189
+ <input
190
+ type="text"
191
+ id="shipping-zip"
192
+ name="shippingZip"
193
+ class="form-input"
194
+ value="{{ checkout.shippingAddress.zip | default: '' | escape }}"
195
+ required>
196
+ </div>
197
+
198
+ <div class="form-group">
199
+ <label for="shipping-phone" class="form-label">Phone</label>
200
+ <input
201
+ type="tel"
202
+ id="shipping-phone"
203
+ name="shippingPhone"
204
+ class="form-input"
205
+ value="{{ checkout.shippingAddress.phone | default: '' | escape }}">
206
+ </div>
207
+ </div>
208
+ </div>
209
+ {% hook 'checkout_shipping_address_after' %}
210
+ </div>
211
+ {% hook 'checkout_shipping_after' %}
212
+
213
+ <!-- Shipping Methods Section -->
214
+ {% hook 'checkout_shipping_methods_before' %}
215
+ <div class="checkout-section" id="shipping-methods-section" style="display: none;">
216
+ <h2 class="checkout-section-title">Shipping Method</h2>
217
+ <div class="checkout-section-content">
218
+ <div id="shipping-methods-container">
219
+ <p class="shipping-methods-loading">Loading shipping options...</p>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ {% hook 'checkout_shipping_methods_after' %}
224
+
225
+ <!-- Billing Address Section -->
226
+ {% hook 'checkout_billing_before' %}
227
+ <div class="checkout-section">
228
+ <h2 class="checkout-section-title">Billing Address</h2>
229
+ <div class="checkout-section-content">
230
+ <div class="form-group">
231
+ <label class="checkbox-label">
232
+ <input type="checkbox" id="same-as-shipping" checked>
233
+ <span>Same as shipping address</span>
234
+ </label>
235
+ </div>
236
+
237
+ <div id="billing-address-fields" style="display: none;">
238
+ <div class="form-grid">
239
+ <div class="form-group">
240
+ <label for="billing-first-name" class="form-label">First Name</label>
241
+ <input
242
+ type="text"
243
+ id="billing-first-name"
244
+ name="billingFirstName"
245
+ class="form-input"
246
+ value="{{ checkout.billingAddress.firstName | default: '' | escape }}">
247
+ </div>
248
+
249
+ <div class="form-group">
250
+ <label for="billing-last-name" class="form-label">Last Name</label>
251
+ <input
252
+ type="text"
253
+ id="billing-last-name"
254
+ name="billingLastName"
255
+ class="form-input"
256
+ value="{{ checkout.billingAddress.lastName | default: '' | escape }}">
257
+ </div>
258
+
259
+ <div class="form-group form-group-full">
260
+ <label for="billing-address" class="form-label">Address</label>
261
+ <input
262
+ type="text"
263
+ id="billing-address"
264
+ name="billingAddress"
265
+ class="form-input"
266
+ value="{{ checkout.billingAddress.address1 | default: '' | escape }}">
267
+ </div>
268
+
269
+ <div class="form-group">
270
+ <label for="billing-city" class="form-label">City</label>
271
+ <input
272
+ type="text"
273
+ id="billing-city"
274
+ name="billingCity"
275
+ class="form-input"
276
+ value="{{ checkout.billingAddress.city | default: '' | escape }}">
277
+ </div>
278
+
279
+ <div class="form-group">
280
+ <label for="billing-country" class="form-label">Country</label>
281
+ <select id="billing-country" name="billingCountry" class="form-input form-select">
282
+ <option value="">Select a country</option>
283
+ {% if countries and countries.size > 0 %}
284
+ {% for country in countries %}
285
+ {% assign countryId = country.id | default: country.countryId %}
286
+ {% assign countryCode = country.code2 | default: country.code | default: country.countryCode | default: country.name %}
287
+ <option value="{{ countryId }}"
288
+ data-country-id="{{ countryId }}"
289
+ data-country-code2="{{ country.code2 | default: country.code }}"
290
+ {% if checkout.billingAddress.countryId == countryId or checkout.billingAddress.country == countryCode or checkout.billingAddress.country == country.code2 or checkout.billingAddress.country == country.code %}selected{% endif %}>
291
+ {{ country.name }}
292
+ </option>
293
+ {% endfor %}
294
+ {% else %}
295
+ <option value="US">United States</option>
296
+ <option value="CA">Canada</option>
297
+ <option value="GB">United Kingdom</option>
298
+ <option value="AU">Australia</option>
299
+ <option value="IN">India</option>
300
+ {% endif %}
301
+ </select>
302
+ </div>
303
+
304
+ <div class="form-group">
305
+ <label for="billing-state" class="form-label">State</label>
306
+ <select id="billing-state" name="billingState" class="form-input form-select">
307
+ <option value="">Select a state</option>
308
+ </select>
309
+ </div>
310
+
311
+ <div class="form-group">
312
+ <label for="billing-zip" class="form-label">ZIP Code</label>
313
+ <input
314
+ type="text"
315
+ id="billing-zip"
316
+ name="billingZip"
317
+ class="form-input"
318
+ value="{{ checkout.billingAddress.zip | default: '' | escape }}">
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ {% hook 'checkout_billing_after' %}
325
+
326
+ <!-- Payment Section -->
327
+ {% hook 'checkout_payment_before' %}
328
+ <div class="checkout-section">
329
+ <h2 class="checkout-section-title">Payment</h2>
330
+
331
+ {% hook 'checkout_payment_methods_before' %}
332
+ <div class="checkout-section-content">
333
+ {% if checkout.paymentMethods and checkout.paymentMethods.size > 0 %}
334
+ <div class="payment-methods">
335
+ <!-- Cash on Delivery -->
336
+ {% if checkout.paymentMethodsGrouped.cod.size > 0 %}
337
+ <div class="payment-method-group">
338
+ <h3 class="payment-method-group-title">Cash on Delivery</h3>
339
+ {% for paymentMethod in checkout.paymentMethodsGrouped.cod %}
340
+ <div class="payment-method payment-method-cod">
341
+ <label class="payment-method-label">
342
+ <input
343
+ type="radio"
344
+ name="paymentMethod"
345
+ value="{{ paymentMethod.id }}"
346
+ {% if forloop.first and checkout.paymentMethodsGrouped.paymentGateway.size == 0 %}checked{% endif %}
347
+ data-payment-type="CoD"
348
+ data-payment-id="{{ paymentMethod.id }}"
349
+ data-payment-fee="{{ paymentMethod.config.PaymentFee | default: 0 }}"
350
+ >
351
+ <div class="payment-method-content">
352
+ <span class="payment-method-name">{{ paymentMethod.name | default: 'Cash on Delivery' }}</span>
353
+ {% if paymentMethod.description %}
354
+ <span class="payment-method-description">{{ paymentMethod.description }}</span>
355
+ {% endif %}
356
+ {% if paymentMethod.config %}
357
+ <div class="payment-method-config">
358
+ {% if paymentMethod.config.DisplayText %}
359
+ <span class="config-text">{{ paymentMethod.config.DisplayText }}</span>
360
+ {% endif %}
361
+ {% if paymentMethod.config.PaymentFee and paymentMethod.config.PaymentFee > 0 %}
362
+ <span class="config-fee">Fee: {{ paymentMethod.config.PaymentFee | money_with_settings: shop.settings }}</span>
363
+ {% endif %}
364
+ {% if paymentMethod.config.MinOrderValue %}
365
+ <span class="config-min">Min order: {{ paymentMethod.config.MinOrderValue | money_with_settings: shop.settings }}</span>
366
+ {% endif %}
367
+ {% if paymentMethod.config.MaxOrderValue %}
368
+ <span class="config-max">Max order: {{ paymentMethod.config.MaxOrderValue | money_with_settings: shop.settings }}</span>
369
+ {% endif %}
370
+ </div>
371
+ {% endif %}
372
+ </div>
373
+ </label>
374
+ </div>
375
+ {% endfor %}
376
+ </div>
377
+ {% endif %}
378
+
379
+ <!-- Payment Gateways -->
380
+ {% if checkout.paymentMethodsGrouped.paymentGateway.size > 0 %}
381
+ <div class="payment-method-group">
382
+ <h3 class="payment-method-group-title">Online Payment</h3>
383
+ {% for paymentMethod in checkout.paymentMethodsGrouped.paymentGateway %}
384
+ <div class="payment-method payment-method-gateway" data-gateway-id="{{ paymentMethod.id }}">
385
+ <label class="payment-method-label">
386
+ <input
387
+ type="radio"
388
+ name="paymentMethod"
389
+ value="{{ paymentMethod.id }}"
390
+ {% if forloop.first and checkout.paymentMethodsGrouped.cod.size == 0 %}checked{% endif %}
391
+ data-payment-type="PaymentGateway"
392
+ data-payment-id="{{ paymentMethod.id }}"
393
+ data-payment-fee="{{ paymentMethod.config.PaymentFee | default: 0 }}"
394
+ >
395
+ <div class="payment-method-content">
396
+ <span class="payment-method-name">{{ paymentMethod.name | default: paymentMethod.id }}</span>
397
+ {% if paymentMethod.description %}
398
+ <span class="payment-method-description">{{ paymentMethod.description }}</span>
399
+ {% endif %}
400
+ {% if paymentMethod.config and paymentMethod.config.DisplayText %}
401
+ <span class="config-text">{{ paymentMethod.config.DisplayText }}</span>
402
+ {% endif %}
403
+ </div>
404
+ </label>
405
+ <!-- Payment gateway plugin snippet will be rendered here -->
406
+ {% hook 'payment_gateway_form' paymentMethod.id %}
407
+ </div>
408
+ {% endfor %}
409
+ </div>
410
+ {% endif %}
411
+
412
+ <!-- Store Credit -->
413
+ {% if checkout.paymentMethodsGrouped.creditNote.size > 0 %}
414
+ <div class="payment-method-group">
415
+ <h3 class="payment-method-group-title">Store Credit</h3>
416
+ {% for paymentMethod in checkout.paymentMethodsGrouped.creditNote %}
417
+ <div class="payment-method payment-method-credit">
418
+ <label class="payment-method-label">
419
+ <input
420
+ type="radio"
421
+ name="paymentMethod"
422
+ value="{{ paymentMethod.id }}"
423
+ data-payment-type="CreditNote"
424
+ data-payment-id="{{ paymentMethod.id }}"
425
+ data-payment-fee="{{ paymentMethod.config.PaymentFee | default: 0 }}"
426
+ >
427
+ <div class="payment-method-content">
428
+ <span class="payment-method-name">{{ paymentMethod.name | default: 'Store Credit' }}</span>
429
+ {% if paymentMethod.availableBalance %}
430
+ <span class="payment-method-balance">Available: {{ paymentMethod.availableBalance | money_with_settings: shop.settings }}</span>
431
+ {% endif %}
432
+ </div>
433
+ </label>
434
+ </div>
435
+ {% endfor %}
436
+ </div>
437
+ {% endif %}
438
+
439
+ <!-- Loyalty Points -->
440
+ {% if checkout.paymentMethodsGrouped.loyaltyPoints.size > 0 %}
441
+ <div class="payment-method-group">
442
+ <h3 class="payment-method-group-title">Loyalty Points</h3>
443
+ {% for paymentMethod in checkout.paymentMethodsGrouped.loyaltyPoints %}
444
+ <div class="payment-method payment-method-loyalty">
445
+ <label class="payment-method-label">
446
+ <input
447
+ type="radio"
448
+ name="paymentMethod"
449
+ value="{{ paymentMethod.id }}"
450
+ data-payment-type="LoyaltyPoints"
451
+ data-payment-id="{{ paymentMethod.id }}"
452
+ data-payment-fee="{{ paymentMethod.config.PaymentFee | default: 0 }}"
453
+ >
454
+ <div class="payment-method-content">
455
+ <span class="payment-method-name">{{ paymentMethod.name | default: 'Loyalty Points' }}</span>
456
+ {% if paymentMethod.availableBalance %}
457
+ <span class="payment-method-balance">Available: {{ paymentMethod.availableBalance }} points</span>
458
+ {% endif %}
459
+ </div>
460
+ </label>
461
+ </div>
462
+ {% endfor %}
463
+ </div>
464
+ {% endif %}
465
+ </div>
466
+ {% else %}
467
+ <div class="payment-methods">
468
+ <p class="payment-methods-empty">No payment methods available. Please contact support.</p>
469
+ </div>
470
+ {% endif %}
471
+
472
+ <!-- Payment gateway forms container -->
473
+ <div id="payment-gateway-forms" class="payment-gateway-forms"></div>
474
+ </div>
475
+ {% hook 'checkout_payment_methods_after' %}
476
+ </div>
477
+ {% hook 'checkout_payment_after' %}
478
+
479
+ <!-- Order Notes Section -->
480
+ {% hook 'checkout_order_notes_before' %}
481
+ <div class="checkout-section">
482
+ <h2 class="checkout-section-title">Order Notes (Optional)</h2>
483
+ <div class="checkout-section-content">
484
+ <div class="form-group">
485
+ <label for="order-notes" class="form-label">Special instructions for your order</label>
486
+ <textarea id="order-notes" name="orderNotes" class="form-textarea" rows="4" placeholder="Any special instructions..."></textarea>
487
+ </div>
488
+ </div>
489
+ </div>
490
+ {% hook 'checkout_order_notes_after' %}
491
+
492
+ <!-- Terms and Conditions -->
493
+ {% hook 'checkout_terms_before' %}
494
+ <div class="checkout-section">
495
+ <div class="checkout-section-content">
496
+ <div class="form-group">
497
+ <label class="checkbox-label">
498
+ <input type="checkbox" id="accept-terms" required>
499
+ <span>I agree to the <a href="/pages/terms" target="_blank">Terms and Conditions</a> and <a href="/pages/privacy" target="_blank">Privacy Policy</a></span>
500
+ </label>
501
+ </div>
502
+ </div>
503
+ </div>
504
+ {% hook 'checkout_terms_after' %}
505
+
506
+ <!-- Submit Button -->
507
+ {% hook 'checkout_submit_before' %}
508
+ <div class="checkout-actions">
509
+ <button type="submit" class="btn btn-primary btn-lg" id="checkout-submit"
510
+ {% if checkout.priceChangeMetadata.hasCriticalIssues %}disabled{% endif %}
511
+ data-original-text="Complete Order">
512
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="checkout-button-icon">
513
+ <path d="M9 12l2 2 4-4"></path>
514
+ <path d="M21 12c-1 0-3-1-3-3s2-3 3-3 3 1 3 3-2 3-3 3"></path>
515
+ <path d="M3 12c1 0 3-1 3-3s-2-3-3-3-3 1-3 3 2 3 3 3"></path>
516
+ </svg>
517
+ <span class="checkout-button-text">Complete Order</span>
518
+ <span class="btn-loading" style="display: none;">
519
+ <span class="btn-spinner"></span>
520
+ <span>Processing...</span>
521
+ </span>
522
+ </button>
523
+ <a href="/cart" class="btn btn-outline btn-lg">Return to Cart</a>
524
+ </div>
525
+
526
+ <!-- Price Change Acknowledgment -->
527
+ {% if checkout.priceChangeDetected or checkout.priceChangeMetadata.detected %}
528
+ <div class="price-change-acknowledgment" id="price-change-acknowledgment">
529
+ <label class="checkbox-label">
530
+ <input type="checkbox" id="acknowledge-price-changes" name="acknowledgePriceChanges" required>
531
+ <span>I acknowledge that prices have changed and I agree to pay the updated total.</span>
532
+ </label>
533
+ </div>
534
+ {% endif %}
535
+
536
+ {% hook 'checkout_submit_after' %}
537
+
538
+ </div>
539
+
540
+ <!-- Right Column: Order Summary -->
541
+ <div class="checkout-sidebar">
542
+ {% hook 'checkout_order_summary_before' %}
543
+
544
+ <!-- Coupon Section -->
545
+ {% hook 'checkout_coupons_before' %}
546
+ <div class="checkout-coupons-section">
547
+ <h2 class="checkout-section-title">Available Coupons</h2>
548
+ <div class="checkout-section-content">
549
+ <div id="coupons-container">
550
+ <p class="coupons-loading">Loading available coupons...</p>
551
+ </div>
552
+ <div id="coupons-error" class="coupons-error" style="display: none;"></div>
553
+ <div id="applied-coupon" class="applied-coupon" style="display: none;">
554
+ <div class="applied-coupon-info">
555
+ <span class="applied-coupon-code"></span>
556
+ <button type="button" class="btn-remove-coupon" id="remove-coupon-btn">Remove</button>
557
+ </div>
558
+ </div>
559
+ </div>
560
+ </div>
561
+ {% hook 'checkout_coupons_after' %}
562
+
563
+ <div class="order-summary">
564
+ <h2 class="order-summary-title">Order Summary</h2>
565
+
566
+ <div class="order-summary-items">
567
+ {% if cart.items %}
568
+ {% for item in cart.items %}
569
+ <div class="order-summary-item">
570
+ <div class="order-summary-item-image">
571
+ {% if item.image %}
572
+ <img src="{{ item.image }}" alt="{{ item.title }}" loading="lazy">
573
+ {% endif %}
574
+ </div>
575
+ <div class="order-summary-item-details">
576
+ <h4 class="order-summary-item-title">{{ item.title }}</h4>
577
+ {% if item.variantTitle and item.variantTitle != 'Default Title' %}
578
+ <p class="order-summary-item-variant">{{ item.variantTitle }}</p>
579
+ {% endif %}
580
+ <p class="order-summary-item-quantity">Qty: {{ item.quantity }}</p>
581
+ </div>
582
+ <div class="order-summary-item-price" data-item-price="{{ item.price }}" data-item-quantity="{{ item.quantity }}">
583
+ {{ item.price | times: item.quantity | money_with_settings: shop.settings }}
584
+ </div>
585
+ </div>
586
+ {% endfor %}
587
+ {% endif %}
588
+ </div>
589
+
590
+ <div class="order-summary-totals">
591
+ <div class="summary-line">
592
+ <span class="summary-label">Subtotal</span>
593
+ <span class="summary-value" data-summary-subtotal>
594
+ {% if checkout.pricing and checkout.pricing.subtotal != null %}
595
+ {{ checkout.pricing.subtotal | money_with_settings: shop.settings }}
596
+ {% elsif cart.subTotal and cart.subTotal > 0 %}
597
+ {{ cart.subTotal | money_with_settings: shop.settings }}
598
+ {% elsif cart.items and cart.items.size > 0 %}
599
+ {% assign calculated_subtotal = 0 %}
600
+ {% for item in cart.items %}
601
+ {% assign item_total = item.price | times: item.quantity %}
602
+ {% assign calculated_subtotal = calculated_subtotal | plus: item_total %}
603
+ {% endfor %}
604
+ {{ calculated_subtotal | money_with_settings: shop.settings }}
605
+ {% else %}
606
+ {{ 0 | money_with_settings: shop.settings }}
607
+ {% endif %}
608
+ </span>
609
+ </div>
610
+
611
+ <div class="summary-line summary-discount" data-summary-discount-line style="{% unless checkout.pricing and checkout.pricing.discount != null and checkout.pricing.discount > 0 %}display: none;{% endunless %}">
612
+ <span class="summary-label">Discount</span>
613
+ <span class="summary-value" data-summary-discount>
614
+ {% if checkout.pricing and checkout.pricing.discount != null and checkout.pricing.discount > 0 %}
615
+ -{{ checkout.pricing.discount | money_with_settings: shop.settings }}
616
+ {% elsif checkout.pricing and checkout.pricing.totalDiscounts != null and checkout.pricing.totalDiscounts > 0 %}
617
+ -{{ checkout.pricing.totalDiscounts | money_with_settings: shop.settings }}
618
+ {% else %}
619
+ {{ 0 | money_with_settings: shop.settings }}
620
+ {% endif %}
621
+ </span>
622
+ </div>
623
+
624
+ <div class="summary-line">
625
+ <span class="summary-label">Shipping</span>
626
+ <span class="summary-value" data-summary-shipping>
627
+ {% if checkout.pricing and checkout.pricing.shipping != null %}
628
+ {% if checkout.pricing.shipping == 0 %}
629
+ Free
630
+ {% else %}
631
+ {{ checkout.pricing.shipping | money_with_settings: shop.settings }}
632
+ {% endif %}
633
+ {% elsif checkout.shipping and checkout.shipping.price != null %}
634
+ {% if checkout.shipping.price == 0 %}
635
+ Free
636
+ {% else %}
637
+ {{ checkout.shipping.price | money_with_settings: shop.settings }}
638
+ {% endif %}
639
+ {% else %}
640
+ {% if cart.total > 5000 %}
641
+ Free
642
+ {% else %}
643
+ {{ 999 | money_with_settings: shop.settings }}
644
+ {% endif %}
645
+ {% endif %}
646
+ </span>
647
+ </div>
648
+
649
+ <div class="summary-line">
650
+ <span class="summary-label">Tax</span>
651
+ <span class="summary-value" data-summary-tax>
652
+ {% if checkout.pricing and checkout.pricing.tax != null %}
653
+ {{ checkout.pricing.tax | money_with_settings: shop.settings }}
654
+ {% else %}
655
+ {{ cart.total | times: 0.08 | money_with_settings: shop.settings }}
656
+ {% endif %}
657
+ </span>
658
+ </div>
659
+
660
+ <div class="summary-line summary-total">
661
+ <span class="summary-label">Total</span>
662
+ <span class="summary-value" data-summary-total>
663
+ {% if checkout.pricing and checkout.pricing.total != null %}
664
+ {{ checkout.pricing.total | money_with_settings: shop.settings }}
665
+ {% else %}
666
+ {% assign shipping = 0 %}
667
+ {% if cart.total <= 5000 %}
668
+ {% assign shipping = 999 %}
669
+ {% endif %}
670
+ {% assign tax = cart.total | times: 0.08 %}
671
+ {% assign total = cart.total | plus: shipping | plus: tax %}
672
+ {{ total | money_with_settings: shop.settings }}
673
+ {% endif %}
674
+ </span>
675
+ </div>
676
+ </div>
677
+ </div>
678
+ {% hook 'checkout_order_summary_after' %}
679
+ </div>
680
+ </div>
681
+ </form>
682
+ </div>
683
+ </section>
684
+ {% hook 'checkout_after' %}
685
+
686
+ <script>
687
+ // Expose checkout token from server-side context
688
+ const CHECKOUT_TOKEN = {% if checkout.token %}{{ checkout.token | json }}{% else %}null{% endif %};
689
+
690
+ // Debug: Log checkout pricing data
691
+ console.log('[CHECKOUT] Checkout pricing data:', {% if checkout.pricing %}{{ checkout.pricing | json }}{% else %}null{% endif %});
692
+ console.log('[CHECKOUT] Cart totals:', {
693
+ total: {{ cart.total | default: 0 }},
694
+ subTotal: {{ cart.subTotal | default: 0 }},
695
+ taxAmount: {{ cart.taxAmount | default: 0 }}
696
+ });
697
+
698
+ // ==================== CHECKOUT STATE MANAGEMENT ====================
699
+
700
+ // Flag to prevent API calls during checkout completion
701
+ window.checkoutInProgress = false;
702
+
703
+ // Status tracking for API calls
704
+ window.checkoutApiStatus = {
705
+ shippingAddress: 'idle',
706
+ billingAddress: 'idle',
707
+ shippingMethodsFetch: 'idle',
708
+ shippingMethodUpdate: 'idle',
709
+ orderNote: 'idle'
710
+ };
711
+
712
+ // Track if shipping method has been selected/updated
713
+ window.shippingMethodSelected = false;
714
+
715
+ // Check if shipping methods are required (section exists and has methods)
716
+ function isShippingMethodRequired() {
717
+ const section = document.getElementById('shipping-methods-section');
718
+ const container = document.getElementById('shipping-methods-container');
719
+
720
+ // If section doesn't exist or is hidden, shipping methods are not required
721
+ if (!section || section.style.display === 'none') {
722
+ return false;
723
+ }
724
+
725
+ // Check if container has shipping method radio buttons
726
+ if (container) {
727
+ const shippingMethodRadios = container.querySelectorAll('input[type="radio"][name^="shippingMethod"]');
728
+ return shippingMethodRadios.length > 0;
729
+ }
730
+
731
+ return false;
732
+ }
733
+
734
+ // Update checkout button state based on pending API calls and shipping method selection
735
+ function updateCheckoutButtonState() {
736
+ const submitBtn = document.getElementById('checkout-submit');
737
+ if (!submitBtn) return;
738
+
739
+ const buttonText = submitBtn.querySelector('.checkout-button-text');
740
+ const buttonIcon = submitBtn.querySelector('.checkout-button-icon');
741
+ const originalText = submitBtn.getAttribute('data-original-text') || 'Complete Order';
742
+
743
+ const hasPendingCalls = Object.values(window.checkoutApiStatus).some(status => status === 'pending');
744
+
745
+ // Check if shipping method is required and not selected
746
+ const shippingMethodRequired = isShippingMethodRequired();
747
+ const shippingMethodNotSelected = shippingMethodRequired && !window.shippingMethodSelected;
748
+
749
+ // Determine status message based on pending operations (priority order)
750
+ let statusMessage = null;
751
+
752
+ if (window.checkoutApiStatus.shippingMethodUpdate === 'pending') {
753
+ statusMessage = 'Calculating shipping fee...';
754
+ } else if (window.checkoutApiStatus.shippingMethodsFetch === 'pending') {
755
+ statusMessage = 'Loading shipping options...';
756
+ } else if (window.checkoutApiStatus.shippingAddress === 'pending') {
757
+ statusMessage = 'Updating shipping address...';
758
+ } else if (window.checkoutApiStatus.billingAddress === 'pending') {
759
+ statusMessage = 'Updating billing address...';
760
+ } else if (window.checkoutApiStatus.orderNote === 'pending') {
761
+ statusMessage = 'Saving order note...';
762
+ } else if (shippingMethodNotSelected) {
763
+ statusMessage = 'Please select a shipping method';
764
+ }
765
+
766
+ // Decide if the button should be disabled for this state
767
+ const shouldDisable = hasPendingCalls || shippingMethodNotSelected;
768
+ submitBtn.disabled = shouldDisable;
769
+
770
+ // If we are disabling the button but don't have a specific reason,
771
+ // show a generic fallback message so the user always sees some context.
772
+ if (!statusMessage && shouldDisable) {
773
+ statusMessage = 'Updating checkout...';
774
+ }
775
+
776
+ if (statusMessage && buttonText) {
777
+ buttonText.textContent = statusMessage;
778
+ // Add loading class for visual feedback
779
+ submitBtn.classList.add('checkout-button-loading');
780
+ // Optionally show spinner by rotating icon
781
+ if (buttonIcon) {
782
+ buttonIcon.style.animation = 'spin 1s linear infinite';
783
+ }
784
+ } else if (buttonText) {
785
+ // Restore original text
786
+ buttonText.textContent = originalText;
787
+ submitBtn.classList.remove('checkout-button-loading');
788
+ // Remove spinner animation
789
+ if (buttonIcon) {
790
+ buttonIcon.style.animation = '';
791
+ }
792
+ }
793
+ }
794
+
795
+ /**
796
+ * Get checkout token from server context, URL, or cookie
797
+ * This function is used by multiple checkout functions
798
+ * Exposed globally for plugins to use
799
+ */
800
+ window.getCheckoutToken = function getCheckoutToken() {
801
+ // First, try to use the token from server-side context (most reliable)
802
+ if (CHECKOUT_TOKEN) {
803
+ return CHECKOUT_TOKEN;
804
+ }
805
+
806
+ // Check URL parameter (hosted checkout: /checkout/{token})
807
+ const pathParts = window.location.pathname.split('/');
808
+ const tokenIndex = pathParts.indexOf('checkout');
809
+ if (tokenIndex !== -1 && tokenIndex < pathParts.length - 1) {
810
+ const tokenFromPath = pathParts[tokenIndex + 1];
811
+ if (tokenFromPath && tokenFromPath !== 'checkout') {
812
+ return tokenFromPath;
813
+ }
814
+ }
815
+
816
+ // Fallback to cookie
817
+ const cookies = document.cookie.split(';');
818
+ for (let cookie of cookies) {
819
+ const [name, value] = cookie.trim().split('=');
820
+ if (name === 'checkoutToken') {
821
+ return decodeURIComponent(value);
822
+ }
823
+ }
824
+ return null;
825
+ }
826
+
827
+ /**
828
+ * Button loading utility function
829
+ * Handles button loading states with optional loading text
830
+ * Supports both checkout-button-text and btn-text class naming conventions
831
+ */
832
+ function setButtonLoading(button, loading, loadingText = null) {
833
+ if (!button) return;
834
+
835
+ // Support both checkout-button-text and btn-text class names
836
+ const btnText = button.querySelector('.checkout-button-text') ||
837
+ button.querySelector('.btn-text');
838
+ const btnLoading = button.querySelector('.btn-loading');
839
+
840
+ if (loading) {
841
+ button.disabled = true;
842
+ button.classList.add('loading');
843
+ if (btnText) btnText.style.display = 'none';
844
+ if (btnLoading) {
845
+ btnLoading.style.display = 'flex';
846
+ if (loadingText && btnLoading.querySelector('span:last-child')) {
847
+ btnLoading.querySelector('span:last-child').textContent = loadingText;
848
+ }
849
+ } else if (loadingText) {
850
+ button.textContent = loadingText;
851
+ }
852
+ } else {
853
+ button.disabled = false;
854
+ button.classList.remove('loading');
855
+ if (btnText) btnText.style.display = 'inline';
856
+ if (btnLoading) btnLoading.style.display = 'none';
857
+ }
858
+ }
859
+
860
+ // States data from tenant settings - could be statesOrProvinces object or nested structure
861
+ const STATES_DATA = {% if states %}{{ states | json }}{% else %}{}{% endif %};
862
+ const COUNTRIES_DATA = {% if countries %}{{ countries | json }}{% else %}[]{% endif %};
863
+
864
+ // Debug: Log states data structure
865
+ console.log('[CHECKOUT] States data:', STATES_DATA);
866
+ console.log('[CHECKOUT] Countries data:', COUNTRIES_DATA);
867
+
868
+ // Helper to get states for a country (by ID or code)
869
+ function getStatesForCountry(countryIdentifier) {
870
+ if (!countryIdentifier) return null;
871
+
872
+ // Try to parse as number (country ID)
873
+ const countryId = parseInt(countryIdentifier, 10);
874
+ const isNumericId = !isNaN(countryId);
875
+
876
+ const codeUpper = String(countryIdentifier).toUpperCase();
877
+ const codeLower = String(countryIdentifier).toLowerCase();
878
+
879
+ console.log('[CHECKOUT] Looking for states for country:', countryIdentifier, isNumericId ? '(ID)' : '(code)');
880
+ console.log('[CHECKOUT] COUNTRIES_DATA:', COUNTRIES_DATA);
881
+
882
+ // First, check if countries array has statesOrProvinces
883
+ // The structure is: countries array with objects containing id, code2 and statesOrProvinces
884
+ if (Array.isArray(COUNTRIES_DATA) && COUNTRIES_DATA.length > 0) {
885
+ const country = COUNTRIES_DATA.find(c => {
886
+ // Match by ID first (if identifier is numeric)
887
+ if (isNumericId) {
888
+ const cId = c.id || c.countryId;
889
+ if (cId === countryId || String(cId) === String(countryId)) {
890
+ return true;
891
+ }
892
+ }
893
+
894
+ // Match by code2 (primary), code, countryCode, or name
895
+ const cCode2 = c.code2 || '';
896
+ const cCode = c.code || '';
897
+ const cCountryCode = c.countryCode || '';
898
+ const cName = c.name || '';
899
+
900
+ return cCode2.toUpperCase() === codeUpper ||
901
+ cCode2.toLowerCase() === codeLower ||
902
+ cCode.toUpperCase() === codeUpper ||
903
+ cCode.toLowerCase() === codeLower ||
904
+ cCountryCode.toUpperCase() === codeUpper ||
905
+ cCountryCode.toLowerCase() === codeLower ||
906
+ cName === countryIdentifier;
907
+ });
908
+
909
+ if (country) {
910
+ console.log('[CHECKOUT] Found country:', country);
911
+ // Check for statesOrProvinces property
912
+ if (country.statesOrProvinces && Array.isArray(country.statesOrProvinces)) {
913
+ console.log('[CHECKOUT] Found statesOrProvinces with', country.statesOrProvinces.length, 'states');
914
+ return country.statesOrProvinces;
915
+ }
916
+ if (country.states && Array.isArray(country.states)) {
917
+ console.log('[CHECKOUT] Found states with', country.states.length, 'states');
918
+ return country.states;
919
+ }
920
+ } else {
921
+ console.log('[CHECKOUT] Country not found in COUNTRIES_DATA for:', countryIdentifier);
922
+ }
923
+ }
924
+
925
+ // Fallback: Check if STATES_DATA is an object with country codes as keys (e.g., {IN: [...], US: [...]})
926
+ if (STATES_DATA && typeof STATES_DATA === 'object' && !Array.isArray(STATES_DATA)) {
927
+ // Try exact match
928
+ if (STATES_DATA[countryIdentifier]) {
929
+ return STATES_DATA[countryIdentifier];
930
+ }
931
+ if (STATES_DATA[codeUpper]) {
932
+ return STATES_DATA[codeUpper];
933
+ }
934
+ if (STATES_DATA[codeLower]) {
935
+ return STATES_DATA[codeLower];
936
+ }
937
+
938
+ // Try case-insensitive match
939
+ for (const key in STATES_DATA) {
940
+ if (key.toUpperCase() === codeUpper || key.toLowerCase() === codeLower) {
941
+ return STATES_DATA[key];
942
+ }
943
+ }
944
+ }
945
+
946
+ return null;
947
+ }
948
+
949
+ // Convert state dropdown to text input when no states are available
950
+ function convertStateToTextInput(stateSelect, selectedState = null) {
951
+ if (!stateSelect) return;
952
+
953
+ const formGroup = stateSelect.closest('.form-group');
954
+ if (!formGroup) return;
955
+
956
+ // Check if already converted
957
+ if (stateSelect.tagName === 'INPUT') return;
958
+
959
+ // Find or create text input (we have both select and input in the template)
960
+ let textInput = document.getElementById('shipping-state-text');
961
+ if (!textInput) {
962
+ // Create text input if it doesn't exist
963
+ textInput = document.createElement('input');
964
+ textInput.type = 'text';
965
+ textInput.id = stateSelect.id + '-text';
966
+ textInput.name = stateSelect.name;
967
+ textInput.className = 'form-input';
968
+ textInput.required = true;
969
+ textInput.placeholder = 'Enter state/province';
970
+ formGroup.appendChild(textInput);
971
+ }
972
+
973
+ // Hide select and show text input
974
+ stateSelect.style.display = 'none';
975
+ stateSelect.removeAttribute('required');
976
+ textInput.style.display = 'block';
977
+ textInput.required = true;
978
+
979
+ if (selectedState) {
980
+ textInput.value = selectedState;
981
+ }
982
+
983
+ console.log('[CHECKOUT] Converted state dropdown to text input for country without states');
984
+ }
985
+
986
+ // Populate states dropdown based on selected country
987
+ function populateStates(countrySelect, stateSelect, selectedState = null) {
988
+ if (!stateSelect || !countrySelect) {
989
+ console.warn('[CHECKOUT] Missing country or state select element');
990
+ return;
991
+ }
992
+
993
+ const countryIdentifier = countrySelect.value;
994
+ console.log('[CHECKOUT] Populating states for country:', countryIdentifier);
995
+
996
+ // Clear existing options
997
+ stateSelect.innerHTML = '<option value="">Select a state</option>';
998
+
999
+ if (!countryIdentifier) {
1000
+ console.log('[CHECKOUT] No country selected');
1001
+ return;
1002
+ }
1003
+
1004
+ // Try to find states for this country (by ID or code) using helper function
1005
+ let countryStates = getStatesForCountry(countryIdentifier);
1006
+
1007
+ if (countryStates) {
1008
+ console.log('[CHECKOUT] Found states for country:', countryIdentifier, countryStates);
1009
+ }
1010
+
1011
+ if (!countryStates) {
1012
+ console.log('[CHECKOUT] No states found for country:', countryIdentifier);
1013
+ // If no states found, convert dropdown to text input
1014
+ convertStateToTextInput(stateSelect, selectedState);
1015
+ return;
1016
+ }
1017
+
1018
+ console.log('[CHECKOUT] Found states for country:', countryStates);
1019
+
1020
+ // Handle different data structures
1021
+ let statesArray = [];
1022
+
1023
+ if (Array.isArray(countryStates)) {
1024
+ // Array format: [{code: 'CA', name: 'California'}, ...]
1025
+ statesArray = countryStates;
1026
+ } else if (typeof countryStates === 'object') {
1027
+ // Object format: {CA: 'California', NY: 'New York'} or {states: [...]}
1028
+ if (countryStates.states && Array.isArray(countryStates.states)) {
1029
+ statesArray = countryStates.states;
1030
+ } else {
1031
+ // Convert object to array
1032
+ statesArray = Object.entries(countryStates).map(([code, name]) => ({
1033
+ code: code,
1034
+ name: typeof name === 'string' ? name : (name.name || name.code || code)
1035
+ }));
1036
+ }
1037
+ }
1038
+
1039
+ // Populate the dropdown
1040
+ statesArray.forEach(state => {
1041
+ const option = document.createElement('option');
1042
+ // Handle state object structure: {id: 1, name: 'Andaman and Nicobar Islands', code: 'IN-AN'}
1043
+ const stateId = state.id || state.stateOrProvinceId || state.stateId;
1044
+ const stateCode = state.code || (typeof state === 'string' ? state : Object.keys(state)[0]);
1045
+ const stateName = state.name || state.label || (typeof state === 'string' ? state : state[stateCode]) || stateCode;
1046
+
1047
+ // Use stateId as the value (required by API)
1048
+ option.value = stateId ? String(stateId) : stateCode;
1049
+ option.textContent = stateName;
1050
+ if (stateId) {
1051
+ option.dataset.stateId = String(stateId);
1052
+ }
1053
+
1054
+ // Check if this should be selected
1055
+ if (selectedState) {
1056
+ const selectedStateStr = String(selectedState).toLowerCase();
1057
+ const stateIdStr = stateId ? String(stateId).toLowerCase() : '';
1058
+ const stateCodeStr = String(stateCode).toLowerCase();
1059
+ const stateNameStr = String(stateName).toLowerCase();
1060
+
1061
+ if (stateIdStr && stateIdStr === selectedStateStr) {
1062
+ option.selected = true;
1063
+ } else if (stateCodeStr === selectedStateStr ||
1064
+ stateNameStr === selectedStateStr ||
1065
+ stateCodeStr.includes(selectedStateStr) ||
1066
+ stateNameStr.includes(selectedStateStr)) {
1067
+ option.selected = true;
1068
+ }
1069
+ }
1070
+
1071
+ stateSelect.appendChild(option);
1072
+ });
1073
+
1074
+ console.log('[CHECKOUT] Populated', statesArray.length, 'states');
1075
+ }
1076
+
1077
+ // Initialize states dropdowns when DOM is ready
1078
+ function initializeStateDropdowns() {
1079
+ console.log('[CHECKOUT] initializeStateDropdowns() called');
1080
+ const shippingCountrySelect = document.getElementById('shipping-country');
1081
+ const shippingStateSelect = document.getElementById('shipping-state');
1082
+ const billingCountrySelect = document.getElementById('billing-country');
1083
+ const billingStateSelect = document.getElementById('billing-state');
1084
+
1085
+ console.log('[CHECKOUT] Country select:', shippingCountrySelect);
1086
+ console.log('[CHECKOUT] State select:', shippingStateSelect);
1087
+ console.log('[CHECKOUT] Selected country value:', shippingCountrySelect?.value);
1088
+
1089
+ if (!shippingCountrySelect || !shippingStateSelect) {
1090
+ console.warn('[CHECKOUT] Shipping country/state selects not found');
1091
+ return;
1092
+ }
1093
+
1094
+ // Populate shipping states on page load
1095
+ const initShippingStates = () => {
1096
+ const selectedCountry = shippingCountrySelect.value;
1097
+ // Try to get stateOrProvinceId first, fallback to province name/code
1098
+ const selectedState = {% if checkout.shippingAddress and checkout.shippingAddress.stateOrProvinceId %}{{ checkout.shippingAddress.stateOrProvinceId | json }}{% elsif checkout.shippingAddress and checkout.shippingAddress.province %}{{ checkout.shippingAddress.province | json }}{% else %}null{% endif %};
1099
+
1100
+ console.log('[CHECKOUT] Initializing shipping states. Country:', selectedCountry, 'State:', selectedState);
1101
+
1102
+ if (selectedCountry) {
1103
+ console.log('[CHECKOUT] Calling populateStates for country:', selectedCountry);
1104
+ try {
1105
+ populateStates(shippingCountrySelect, shippingStateSelect, selectedState);
1106
+ console.log('[CHECKOUT] populateStates completed');
1107
+ } catch (error) {
1108
+ console.error('[CHECKOUT] Error in populateStates:', error);
1109
+ }
1110
+
1111
+ // After populating states, check if address is complete and fetch shipping methods
1112
+ setTimeout(() => {
1113
+ if (typeof checkAndFetchShippingMethods === 'function') {
1114
+ checkAndFetchShippingMethods();
1115
+ }
1116
+ }, 200);
1117
+ }
1118
+ };
1119
+
1120
+ // Initialize immediately if country is already selected
1121
+ if (shippingCountrySelect.value) {
1122
+ initShippingStates();
1123
+ }
1124
+
1125
+ // Also initialize after a short delay to ensure DOM is ready
1126
+ setTimeout(initShippingStates, 100);
1127
+
1128
+ // Handle country change
1129
+ shippingCountrySelect.addEventListener('change', function() {
1130
+ console.log('[CHECKOUT] Shipping country changed to:', this.value);
1131
+ populateStates(shippingCountrySelect, shippingStateSelect);
1132
+ // Trigger address update to fetch shipping methods
1133
+ if (typeof checkAndFetchShippingMethods === 'function') {
1134
+ checkAndFetchShippingMethods();
1135
+ }
1136
+ });
1137
+
1138
+ // Handle state change
1139
+ shippingStateSelect.addEventListener('change', function() {
1140
+ console.log('[CHECKOUT] Shipping state changed to:', this.value);
1141
+ if (typeof checkAndFetchShippingMethods === 'function') {
1142
+ checkAndFetchShippingMethods();
1143
+ }
1144
+ });
1145
+
1146
+ // Populate billing states
1147
+ if (billingCountrySelect && billingStateSelect) {
1148
+ const initBillingStates = () => {
1149
+ const selectedCountry = billingCountrySelect.value;
1150
+ const selectedState = {% if checkout.billingAddress and checkout.billingAddress.province %}{{ checkout.billingAddress.province | json }}{% else %}null{% endif %};
1151
+
1152
+ console.log('[CHECKOUT] Initializing billing states. Country:', selectedCountry, 'State:', selectedState);
1153
+
1154
+ if (selectedCountry) {
1155
+ populateStates(billingCountrySelect, billingStateSelect, selectedState);
1156
+ }
1157
+ };
1158
+
1159
+ // Initialize immediately if country is already selected
1160
+ if (billingCountrySelect.value) {
1161
+ initBillingStates();
1162
+ }
1163
+
1164
+ // Also initialize after a short delay
1165
+ setTimeout(initBillingStates, 100);
1166
+
1167
+ // Handle country change
1168
+ billingCountrySelect.addEventListener('change', function() {
1169
+ console.log('[CHECKOUT] Billing country changed to:', this.value);
1170
+ populateStates(billingCountrySelect, billingStateSelect);
1171
+ });
1172
+ }
1173
+ }
1174
+
1175
+ // Initialize intl-tel-input for shipping phone
1176
+ function initializePhoneInput() {
1177
+ console.log('[CHECKOUT] initializePhoneInput() called');
1178
+ const shippingPhoneInput = document.getElementById('shipping-phone');
1179
+ console.log('[CHECKOUT] shippingPhoneInput element:', shippingPhoneInput);
1180
+ console.log('[CHECKOUT] intlTelInput available:', typeof intlTelInput !== 'undefined');
1181
+
1182
+ if (!shippingPhoneInput) {
1183
+ console.warn('[CHECKOUT] shipping-phone input element not found');
1184
+ return;
1185
+ }
1186
+
1187
+ if (typeof intlTelInput === 'undefined') {
1188
+ console.warn('[CHECKOUT] intlTelInput library not loaded yet');
1189
+ return;
1190
+ }
1191
+
1192
+ if (shippingPhoneInput && typeof intlTelInput !== 'undefined') {
1193
+ // Get existing phone number value
1194
+ const existingPhone = shippingPhoneInput.value || '';
1195
+
1196
+ // Check the country select value synchronously before initializing
1197
+ const countrySelect = document.getElementById('shipping-country');
1198
+ let initialCountry = 'auto';
1199
+ if (countrySelect && countrySelect.value) {
1200
+ const countryCode = countrySelect.options[countrySelect.selectedIndex]?.getAttribute('data-country-code2');
1201
+ if (countryCode) {
1202
+ initialCountry = countryCode.toLowerCase();
1203
+ }
1204
+ }
1205
+
1206
+ // Build configuration object for intl-tel-input
1207
+ const itiConfig = {
1208
+ utilsScript: 'https://cdn.jsdelivr.net/npm/intl-tel-input@23.0.0/build/js/utils.js',
1209
+ initialCountry: initialCountry,
1210
+ preferredCountries: ['us', 'gb', 'ca', 'au', 'in'],
1211
+ separateDialCode: true,
1212
+ nationalMode: false
1213
+ };
1214
+
1215
+ // Only include geoIpLookup when using auto-detection
1216
+ if (initialCountry === 'auto') {
1217
+ itiConfig.geoIpLookup = function(callback) {
1218
+ // Try to get country from shipping address country select (use outer scope variable)
1219
+ if (countrySelect && countrySelect.value) {
1220
+ const countryCode = countrySelect.options[countrySelect.selectedIndex]?.getAttribute('data-country-code2');
1221
+ if (countryCode) {
1222
+ callback(countryCode.toLowerCase());
1223
+ return;
1224
+ }
1225
+ }
1226
+ // Fallback to auto-detection via IP
1227
+ fetch('https://ipapi.co/json/')
1228
+ .then(res => res.json())
1229
+ .then(data => callback(data.country_code ? data.country_code.toLowerCase() : 'us'))
1230
+ .catch(() => callback('us'));
1231
+ };
1232
+ }
1233
+
1234
+ // Initialize intl-tel-input
1235
+ console.log('[CHECKOUT] Initializing intl-tel-input with config:', itiConfig);
1236
+ let iti;
1237
+ try {
1238
+ iti = intlTelInput(shippingPhoneInput, itiConfig);
1239
+ console.log('[CHECKOUT] intl-tel-input initialized successfully');
1240
+ } catch (error) {
1241
+ console.error('[CHECKOUT] Error initializing intl-tel-input:', error);
1242
+ return;
1243
+ }
1244
+
1245
+ // Store the instance for later use
1246
+ window.shippingPhoneIti = iti;
1247
+
1248
+ // Set existing phone number if available (after a small delay to ensure utils are loaded)
1249
+ if (existingPhone) {
1250
+ // Use setTimeout to ensure utils script has loaded
1251
+ setTimeout(function() {
1252
+ iti.setNumber(existingPhone);
1253
+ }, 100);
1254
+ }
1255
+
1256
+ // Update country when shipping country changes (countrySelect already declared above)
1257
+ if (countrySelect) {
1258
+ countrySelect.addEventListener('change', function() {
1259
+ const countryCode = this.options[this.selectedIndex]?.getAttribute('data-country-code2');
1260
+ if (countryCode) {
1261
+ iti.setCountry(countryCode.toLowerCase());
1262
+ }
1263
+ });
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ // Initialize when DOM is ready
1269
+ function initializeCheckout() {
1270
+ console.log('[CHECKOUT] Initializing checkout functions...');
1271
+ console.log('[CHECKOUT] intlTelInput available:', typeof intlTelInput !== 'undefined');
1272
+ console.log('[CHECKOUT] shipping-phone element:', document.getElementById('shipping-phone'));
1273
+ console.log('[CHECKOUT] shipping-country element:', document.getElementById('shipping-country'));
1274
+
1275
+ try {
1276
+ initializeStateDropdowns();
1277
+ console.log('[CHECKOUT] State dropdowns initialized');
1278
+ } catch (error) {
1279
+ console.error('[CHECKOUT] Error initializing state dropdowns:', error);
1280
+ }
1281
+
1282
+ try {
1283
+ initializePhoneInput();
1284
+ console.log('[CHECKOUT] Phone input initialized');
1285
+ } catch (error) {
1286
+ console.error('[CHECKOUT] Error initializing phone input:', error);
1287
+ }
1288
+
1289
+ // Initialize button state
1290
+ if (typeof updateCheckoutButtonState === 'function') {
1291
+ updateCheckoutButtonState();
1292
+ }
1293
+
1294
+ // Check if shipping address is already complete and fetch methods
1295
+ setTimeout(function() {
1296
+ if (typeof checkAndFetchShippingMethods === 'function') {
1297
+ checkAndFetchShippingMethods();
1298
+ }
1299
+ }, 500);
1300
+ }
1301
+
1302
+ // Wait for intl-tel-input to load if needed
1303
+ function waitForIntlTelInput(callback, maxAttempts = 20, attempt = 0) {
1304
+ if (typeof intlTelInput !== 'undefined') {
1305
+ callback();
1306
+ return;
1307
+ }
1308
+
1309
+ if (attempt >= maxAttempts) {
1310
+ console.warn('[CHECKOUT] intlTelInput not loaded after', maxAttempts * 100, 'ms, initializing without it');
1311
+ callback();
1312
+ return;
1313
+ }
1314
+
1315
+ setTimeout(function() {
1316
+ waitForIntlTelInput(callback, maxAttempts, attempt + 1);
1317
+ }, 100);
1318
+ }
1319
+
1320
+ if (document.readyState === 'loading') {
1321
+ document.addEventListener('DOMContentLoaded', function() {
1322
+ waitForIntlTelInput(initializeCheckout);
1323
+ });
1324
+ } else {
1325
+ // DOM is already ready
1326
+ waitForIntlTelInput(initializeCheckout);
1327
+ }
1328
+
1329
+ // Handle same as shipping checkbox
1330
+ document.getElementById('same-as-shipping')?.addEventListener('change', function(e) {
1331
+ const billingFields = document.getElementById('billing-address-fields');
1332
+ if (billingFields) {
1333
+ billingFields.style.display = e.target.checked ? 'none' : 'block';
1334
+ }
1335
+ });
1336
+
1337
+ // Handle payment method selection
1338
+ document.querySelectorAll('input[name="paymentMethod"]').forEach(radio => {
1339
+ radio.addEventListener('change', function() {
1340
+ const paymentType = this.getAttribute('data-payment-type') || '';
1341
+ const paymentId = this.getAttribute('data-payment-id') || this.value;
1342
+ const gatewayFormsContainer = document.getElementById('payment-gateway-forms');
1343
+
1344
+ // Hide all gateway forms
1345
+ if (gatewayFormsContainer) {
1346
+ gatewayFormsContainer.innerHTML = '';
1347
+ gatewayFormsContainer.style.display = 'none';
1348
+ }
1349
+
1350
+ // Show gateway form for PaymentGateway type
1351
+ if (paymentType === 'PaymentGateway') {
1352
+ // Load payment gateway plugin form
1353
+ loadPaymentGatewayForm(paymentId, gatewayFormsContainer);
1354
+ }
1355
+
1356
+ // Hide card form (legacy support)
1357
+ const cardForm = document.getElementById('card-payment-form');
1358
+ if (cardForm) {
1359
+ cardForm.style.display = 'none';
1360
+ }
1361
+ });
1362
+
1363
+ // Trigger change event on page load if first option is selected
1364
+ if (radio.checked) {
1365
+ radio.dispatchEvent(new Event('change'));
1366
+ }
1367
+ });
1368
+
1369
+ // Load payment gateway form
1370
+ async function loadPaymentGatewayForm(paymentMethodId, container) {
1371
+ if (!container || !paymentMethodId) return;
1372
+
1373
+ try {
1374
+ // Check if there's a plugin snippet for this gateway
1375
+ // This would typically be loaded via hook system, but for now we'll handle it client-side
1376
+ const gatewayElement = document.querySelector(`[data-gateway-id="${paymentMethodId}"]`);
1377
+ if (gatewayElement) {
1378
+ // Gateway forms are loaded via plugin hooks, so we just show the container
1379
+ container.style.display = 'block';
1380
+ }
1381
+ } catch (error) {
1382
+ console.error('Error loading payment gateway form:', error);
1383
+ }
1384
+ }
1385
+
1386
+ // Update shipping address when form fields change (debounced)
1387
+ let shippingAddressTimeout;
1388
+ const shippingFields = ['shipping-first-name', 'shipping-last-name', 'shipping-address', 'shipping-city', 'shipping-state', 'shipping-zip', 'shipping-country', 'shipping-phone'];
1389
+
1390
+ function attachShippingFieldListeners() {
1391
+ const markShippingAddressPending = () => {
1392
+ // As soon as the user edits shipping address fields, treat it as a
1393
+ // pending update so the checkout button stays disabled until we
1394
+ // either send the API call or decide we can't.
1395
+ window.checkoutApiStatus.shippingAddress = 'pending';
1396
+ updateCheckoutButtonState();
1397
+ };
1398
+
1399
+ shippingFields.forEach(fieldId => {
1400
+ const field = document.getElementById(fieldId);
1401
+ if (!field) return;
1402
+
1403
+ // Remove existing listeners by cloning (simple approach)
1404
+ const newField = field.cloneNode(true);
1405
+ field.parentNode.replaceChild(newField, field);
1406
+
1407
+ // Re-attach listeners
1408
+ newField.addEventListener('blur', function() {
1409
+ markShippingAddressPending();
1410
+ clearTimeout(shippingAddressTimeout);
1411
+ shippingAddressTimeout = setTimeout(updateShippingAddress, 500);
1412
+ });
1413
+
1414
+ // Also listen to change/input events for select dropdowns and input fields
1415
+ if (newField.tagName === 'SELECT' || newField.tagName === 'INPUT') {
1416
+ newField.addEventListener('change', function() {
1417
+ markShippingAddressPending();
1418
+ clearTimeout(shippingAddressTimeout);
1419
+ shippingAddressTimeout = setTimeout(updateShippingAddress, 500);
1420
+ });
1421
+ if (newField.tagName === 'INPUT') {
1422
+ newField.addEventListener('input', function() {
1423
+ markShippingAddressPending();
1424
+ clearTimeout(shippingAddressTimeout);
1425
+ shippingAddressTimeout = setTimeout(updateShippingAddress, 500);
1426
+ });
1427
+ }
1428
+ }
1429
+ });
1430
+ }
1431
+
1432
+ // Attach listeners initially
1433
+ attachShippingFieldListeners();
1434
+
1435
+ // Re-attach after state field might be converted to text input
1436
+ const originalConvertStateToTextInput = convertStateToTextInput;
1437
+ convertStateToTextInput = function(stateSelect, selectedState) {
1438
+ originalConvertStateToTextInput(stateSelect, selectedState);
1439
+ // Re-attach listeners after state field conversion
1440
+ setTimeout(attachShippingFieldListeners, 100);
1441
+ };
1442
+
1443
+ // Helper function to format money using shop settings
1444
+ function formatMoney(amount) {
1445
+ if (amount === null || amount === undefined || isNaN(amount)) {
1446
+ return '0.00';
1447
+ }
1448
+
1449
+ const num = parseFloat(amount);
1450
+ if (isNaN(num)) return String(amount);
1451
+
1452
+ // Get currency settings from page data
1453
+ const currencySymbol = document.body.dataset.shopCurrencySymbol || window.__SHOP_CURRENCY_SYMBOL__ || '₹';
1454
+ const currencyDecimalDigits = 2; // Default to 2 decimal places
1455
+
1456
+ // Check if amount is in cents (if > 1000, likely in cents, otherwise might be in actual currency)
1457
+ // For now, assume API returns actual currency amounts (not cents) based on double type in schema
1458
+ // But handle both cases: if amount seems like cents (> 1000 for typical prices), divide by 100
1459
+ let formattedAmount = num;
1460
+
1461
+ // If the amount is very large (> 1000), it might be in cents/paise
1462
+ // But since API schema shows double (decimal), we'll assume it's already in currency units
1463
+ // However, if prices from API seem to be in cents, we can detect and convert
1464
+
1465
+ // Format with proper decimal places
1466
+ formattedAmount = formattedAmount.toFixed(currencyDecimalDigits);
1467
+
1468
+ // Add thousand separators
1469
+ formattedAmount = formattedAmount.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
1470
+
1471
+ return currencySymbol + formattedAmount;
1472
+ }
1473
+
1474
+ // Check if shipping address is complete
1475
+ function isShippingAddressComplete() {
1476
+ const requiredFields = [
1477
+ 'shipping-first-name',
1478
+ 'shipping-last-name',
1479
+ 'shipping-address',
1480
+ 'shipping-city',
1481
+ 'shipping-zip',
1482
+ 'shipping-country'
1483
+ ];
1484
+
1485
+ // State is optional if no states are available for the country
1486
+ const stateField = document.getElementById('shipping-state');
1487
+ const stateRequired = stateField && stateField.tagName === 'SELECT' && stateField.options.length > 1;
1488
+
1489
+ const allRequired = requiredFields.every(fieldId => {
1490
+ const field = document.getElementById(fieldId);
1491
+ return field && field.value && field.value.trim() !== '';
1492
+ });
1493
+
1494
+ // If state is required (dropdown with options), check it too
1495
+ if (stateRequired) {
1496
+ return allRequired && stateField.value && stateField.value.trim() !== '';
1497
+ }
1498
+
1499
+ return allRequired;
1500
+ }
1501
+
1502
+ // Fetch shipping methods when address is complete
1503
+ async function fetchShippingMethods() {
1504
+ // Prevent fetching during checkout completion
1505
+ if (window.checkoutInProgress) return;
1506
+
1507
+ const checkoutToken = getCheckoutToken();
1508
+ if (!checkoutToken) {
1509
+ console.log('[CHECKOUT] No checkout token, skipping shipping methods fetch');
1510
+ return;
1511
+ }
1512
+
1513
+ // Track status
1514
+ window.checkoutApiStatus.shippingMethodsFetch = 'pending';
1515
+ updateCheckoutButtonState();
1516
+
1517
+ const isHostedCheckout = window.location.pathname.includes('/checkout/') &&
1518
+ window.location.pathname.split('/').length > 2;
1519
+ const endpoint = isHostedCheckout
1520
+ ? `/webstoreapi/checkout/${checkoutToken}/shipping-methods`
1521
+ : '/webstoreapi/checkout/shipping-methods';
1522
+
1523
+ const container = document.getElementById('shipping-methods-container');
1524
+ const section = document.getElementById('shipping-methods-section');
1525
+
1526
+ if (!container || !section) {
1527
+ console.warn('[CHECKOUT] Shipping methods container or section not found');
1528
+ return;
1529
+ }
1530
+
1531
+ container.innerHTML = '<p class="shipping-methods-loading">Loading shipping options...</p>';
1532
+ section.style.display = 'block';
1533
+
1534
+ try {
1535
+ console.log('[CHECKOUT] Fetching shipping methods from:', endpoint);
1536
+ const response = await fetch(endpoint);
1537
+
1538
+ const result = await response.json();
1539
+
1540
+ // Handle error response - check if address is required
1541
+ if (!response.ok) {
1542
+ if (result.requiresAddress || (result.error && result.error.includes('address'))) {
1543
+ console.log('[CHECKOUT] Shipping address required before fetching shipping methods');
1544
+ container.innerHTML = '<p class="shipping-methods-message">Please complete your shipping address to see available shipping options.</p>';
1545
+ section.style.display = 'block';
1546
+ return;
1547
+ }
1548
+ throw new Error(result.error || `HTTP ${response.status}: ${response.statusText}`);
1549
+ }
1550
+
1551
+ console.log('[CHECKOUT] Shipping methods response:', result);
1552
+
1553
+ // Handle both wrapped response {success: true, data: [...]} and direct array response
1554
+ let methods = [];
1555
+ if (result.success && result.data && Array.isArray(result.data)) {
1556
+ methods = result.data;
1557
+ } else if (Array.isArray(result)) {
1558
+ methods = result;
1559
+ }
1560
+
1561
+ if (methods.length > 0) {
1562
+ container.innerHTML = '';
1563
+
1564
+ // Get currently selected shipping method from checkout data
1565
+ const selectedShippingMethodHandle = {% if checkout.shipping and checkout.shipping.methodHandle %}{{ checkout.shipping.methodHandle | json }}{% elsif checkout.shipping and checkout.shipping.handle %}{{ checkout.shipping.handle | json }}{% else %}null{% endif %};
1566
+ console.log('[CHECKOUT] Currently selected shipping method:', selectedShippingMethodHandle);
1567
+
1568
+ // Store methods array for later use
1569
+ window.checkoutShippingMethods = methods;
1570
+
1571
+ // Extract all unique products from all shipping methods
1572
+ const productMap = new Map();
1573
+ methods.forEach((method) => {
1574
+ if (method.products && Array.isArray(method.products)) {
1575
+ method.products.forEach((product) => {
1576
+ const productId = product.productId || product.id;
1577
+ if (productId && !productMap.has(productId)) {
1578
+ productMap.set(productId, product);
1579
+ }
1580
+ });
1581
+ }
1582
+ });
1583
+
1584
+ const allProducts = Array.from(productMap.values());
1585
+
1586
+ // Helper function to get applicable method IDs for a product
1587
+ const getApplicableMethodIds = (productId) => {
1588
+ return methods
1589
+ .filter((method) => {
1590
+ if (!method.products || !Array.isArray(method.products)) return false;
1591
+ return method.products.some((p) => (p.productId || p.id) === productId);
1592
+ })
1593
+ .map((method) => String(method.id || method.index))
1594
+ .sort()
1595
+ .join(',');
1596
+ };
1597
+
1598
+ // Group products by their applicable shipping methods
1599
+ const productGroups = new Map();
1600
+ allProducts.forEach((product) => {
1601
+ const productId = product.productId || product.id;
1602
+ const methodSignature = getApplicableMethodIds(productId);
1603
+
1604
+ if (!productGroups.has(methodSignature)) {
1605
+ productGroups.set(methodSignature, {
1606
+ products: [],
1607
+ methodIds: methodSignature.split(',').filter(id => id)
1608
+ });
1609
+ }
1610
+ productGroups.get(methodSignature).products.push(product);
1611
+ });
1612
+
1613
+ // Create rows for each product group
1614
+ productGroups.forEach((group, methodSignature) => {
1615
+ const productRow = document.createElement('div');
1616
+ productRow.className = 'shipping-product-row';
1617
+
1618
+ // Left side: All products in this group
1619
+ const productColumn = document.createElement('div');
1620
+ productColumn.className = 'shipping-product-column';
1621
+
1622
+ group.products.forEach((product) => {
1623
+ const productItem = document.createElement('div');
1624
+ productItem.className = 'shipping-product-item';
1625
+
1626
+ // Product image
1627
+ const productImage = document.createElement('img');
1628
+ productImage.src = product.thumbnailUrl || '';
1629
+ productImage.alt = product.name || '';
1630
+ productImage.className = 'shipping-product-image';
1631
+ productImage.onerror = function() {
1632
+ this.style.display = 'none';
1633
+ };
1634
+
1635
+ // Product details
1636
+ const productDetails = document.createElement('div');
1637
+ productDetails.className = 'shipping-product-details';
1638
+
1639
+ const productName = document.createElement('div');
1640
+ productName.className = 'shipping-product-name';
1641
+ productName.textContent = product.name || '';
1642
+
1643
+ const productPrice = document.createElement('div');
1644
+ productPrice.className = 'shipping-product-price';
1645
+ const price = product.price !== undefined && product.price !== null ? parseFloat(product.price) : 0;
1646
+ const quantity = product.quantity !== undefined && product.quantity !== null ? parseFloat(product.quantity) : 1;
1647
+ productPrice.textContent = formatMoney(price) + 'x' + quantity;
1648
+
1649
+ productDetails.appendChild(productName);
1650
+ productDetails.appendChild(productPrice);
1651
+
1652
+ productItem.appendChild(productImage);
1653
+ productItem.appendChild(productDetails);
1654
+ productColumn.appendChild(productItem);
1655
+ });
1656
+
1657
+ // Right side: Shared shipping methods for this group
1658
+ const methodsColumn = document.createElement('div');
1659
+ methodsColumn.className = 'shipping-methods-column';
1660
+
1661
+ // Get the applicable methods for this group (all products in group share these)
1662
+ const applicableMethods = methods.filter((method) => {
1663
+ const methodId = String(method.id || method.index);
1664
+ return group.methodIds.includes(methodId);
1665
+ });
1666
+
1667
+ if (applicableMethods.length === 0) {
1668
+ // No shipping methods available for these products
1669
+ const noShippingMsg = document.createElement('div');
1670
+ noShippingMsg.className = 'shipping-unavailable-message';
1671
+ noShippingMsg.textContent = 'Sorry, This product cannot be shipped to your location';
1672
+ methodsColumn.appendChild(noShippingMsg);
1673
+ } else {
1674
+ // Create radio buttons for each applicable shipping method
1675
+ // Use a unique name per group to allow selection for all products in group
1676
+ const radioGroupName = `shippingMethod-group-${methodSignature}`;
1677
+
1678
+ applicableMethods.forEach((method, methodIndex) => {
1679
+ const methodId = method.id || method.index || methodIndex;
1680
+ const methodIdentifier = String(methodId);
1681
+
1682
+ // Check if this method is selected
1683
+ const isSelected = (!selectedShippingMethodHandle && methodIndex === 0) ||
1684
+ (selectedShippingMethodHandle && methodIdentifier === String(selectedShippingMethodHandle));
1685
+
1686
+ const methodDiv = document.createElement('div');
1687
+ methodDiv.className = 'shipping-method';
1688
+
1689
+ const label = document.createElement('label');
1690
+ label.className = 'shipping-method-label';
1691
+
1692
+ const radio = document.createElement('input');
1693
+ radio.type = 'radio';
1694
+ radio.name = radioGroupName;
1695
+ radio.value = methodIdentifier;
1696
+ radio.id = `shipping-method-${methodSignature}-${methodIndex}`;
1697
+ radio.dataset.methodId = methodIdentifier;
1698
+ radio.dataset.methodIndex = methodIndex;
1699
+ radio.checked = isSelected;
1700
+
1701
+ const content = document.createElement('div');
1702
+ content.className = 'shipping-method-content';
1703
+
1704
+ // Title and price row
1705
+ const titleRow = document.createElement('div');
1706
+ titleRow.className = 'shipping-method-title-row';
1707
+
1708
+ const name = document.createElement('span');
1709
+ name.className = 'shipping-method-name';
1710
+ name.textContent = method.title || method.name || 'Shipping';
1711
+
1712
+ const priceSpan = document.createElement('span');
1713
+ priceSpan.className = 'shipping-method-price';
1714
+ if (method.price !== undefined && method.price !== null && parseFloat(method.price) > 0) {
1715
+ priceSpan.textContent = formatMoney(method.price);
1716
+ } else {
1717
+ priceSpan.textContent = formatMoney(0);
1718
+ }
1719
+
1720
+ titleRow.appendChild(name);
1721
+ titleRow.appendChild(priceSpan);
1722
+ content.appendChild(titleRow);
1723
+
1724
+ // Time information row (if available)
1725
+ if (method.shippingTime || method.deliveryTime) {
1726
+ const timeRow = document.createElement('div');
1727
+ timeRow.className = 'shipping-method-time-row';
1728
+
1729
+ if (method.shippingTime) {
1730
+ const shippingTime = document.createElement('div');
1731
+ shippingTime.className = 'shipping-method-time';
1732
+ shippingTime.textContent = 'Ship by: ' + method.shippingTime;
1733
+ timeRow.appendChild(shippingTime);
1734
+ }
1735
+
1736
+ if (method.deliveryTime) {
1737
+ const deliveryTime = document.createElement('div');
1738
+ deliveryTime.className = 'shipping-method-time';
1739
+ deliveryTime.textContent = 'Delivery by: ' + method.deliveryTime;
1740
+ timeRow.appendChild(deliveryTime);
1741
+ }
1742
+
1743
+ content.appendChild(timeRow);
1744
+ }
1745
+
1746
+ label.appendChild(radio);
1747
+ label.appendChild(content);
1748
+ methodDiv.appendChild(label);
1749
+ methodsColumn.appendChild(methodDiv);
1750
+
1751
+ // Add change handler - store method reference in closure
1752
+ (function(methodObj) {
1753
+ radio.addEventListener('change', function() {
1754
+ if (this.checked) {
1755
+ console.log('[CHECKOUT] Shipping method changed for product group', methodSignature, 'to method:', methodObj);
1756
+ updateShippingMethod(methodObj);
1757
+ }
1758
+ });
1759
+ })(method);
1760
+ });
1761
+ }
1762
+
1763
+ productRow.appendChild(productColumn);
1764
+ productRow.appendChild(methodsColumn);
1765
+ container.appendChild(productRow);
1766
+ });
1767
+
1768
+ console.log('[CHECKOUT] Rendered', methods.length, 'shipping methods');
1769
+
1770
+ // Update shipping method on page load if a method is selected but not yet saved
1771
+ // Check if a shipping method is already selected from server
1772
+ if (selectedShippingMethodHandle) {
1773
+ // Shipping method already selected from server, mark as selected
1774
+ window.shippingMethodSelected = true;
1775
+ }
1776
+
1777
+ // Only update if no method was previously selected (to avoid unnecessary API calls)
1778
+ const selectedRadio = container.querySelector('input[type="radio"]:checked');
1779
+ if (selectedRadio && !selectedShippingMethodHandle) {
1780
+ // No method was previously selected, so update with the default selection
1781
+ const methodIndex = parseInt(selectedRadio.dataset.methodIndex, 10);
1782
+ if (methods[methodIndex]) {
1783
+ updateShippingMethod(methods[methodIndex]);
1784
+ }
1785
+ } else if (selectedRadio && selectedShippingMethodHandle) {
1786
+ // Method is already selected from server, mark as selected
1787
+ window.shippingMethodSelected = true;
1788
+ }
1789
+
1790
+ // Update button state after shipping methods are loaded
1791
+ updateCheckoutButtonState();
1792
+ } else {
1793
+ console.log('[CHECKOUT] No shipping methods available');
1794
+ container.innerHTML = '<p class="shipping-methods-empty">No shipping methods available for this address.</p>';
1795
+ }
1796
+
1797
+ // Mark as completed
1798
+ window.checkoutApiStatus.shippingMethodsFetch = 'completed';
1799
+ updateCheckoutButtonState();
1800
+ } catch (error) {
1801
+ console.error('[CHECKOUT] Error fetching shipping methods:', error);
1802
+ // Check if error is about missing address
1803
+ const errorMessage = error.message || '';
1804
+ if (errorMessage.includes('address') || errorMessage.includes('Address')) {
1805
+ container.innerHTML = '<p class="shipping-methods-message">Please complete your shipping address to see available shipping options.</p>';
1806
+ } else {
1807
+ container.innerHTML = '<p class="shipping-methods-error">Unable to load shipping options. Please try again.</p>';
1808
+ }
1809
+
1810
+ // Mark as completed even on error (to allow retry)
1811
+ window.checkoutApiStatus.shippingMethodsFetch = 'completed';
1812
+ updateCheckoutButtonState();
1813
+ }
1814
+ }
1815
+
1816
+ // Update shipping method
1817
+ async function updateShippingMethod(shippingMethod) {
1818
+ // Prevent updating during checkout completion
1819
+ if (window.checkoutInProgress) return;
1820
+
1821
+ const checkoutToken = getCheckoutToken();
1822
+ if (!checkoutToken) {
1823
+ console.error('[CHECKOUT] No checkout token available');
1824
+ return;
1825
+ }
1826
+
1827
+ if (!shippingMethod) {
1828
+ console.error('[CHECKOUT] Shipping method object is required');
1829
+ return;
1830
+ }
1831
+
1832
+ // Track status
1833
+ window.checkoutApiStatus.shippingMethodUpdate = 'pending';
1834
+ updateCheckoutButtonState();
1835
+
1836
+ const isHostedCheckout = window.location.pathname.includes('/checkout/') &&
1837
+ window.location.pathname.split('/').length > 2;
1838
+ const endpoint = isHostedCheckout
1839
+ ? `/webstoreapi/checkout/${checkoutToken}/shipping-method`
1840
+ : '/webstoreapi/checkout/shipping-method';
1841
+
1842
+ console.log('[CHECKOUT] Updating shipping method:', shippingMethod);
1843
+
1844
+ // Show loading state on selected radio button
1845
+ const methodId = shippingMethod.id || shippingMethod.index;
1846
+ const selectedRadio = document.querySelector(`input[name="shippingMethod"][value="${methodId}"]`);
1847
+ if (selectedRadio) {
1848
+ selectedRadio.disabled = true;
1849
+ }
1850
+
1851
+ try {
1852
+ // API expects shippingMethods array with the selected method object
1853
+ const requestBody = {
1854
+ shippingMethods: [shippingMethod]
1855
+ };
1856
+
1857
+ console.log('[CHECKOUT] Sending request to:', endpoint, 'with body:', requestBody);
1858
+
1859
+ const response = await fetch(endpoint, {
1860
+ method: 'PUT',
1861
+ headers: {
1862
+ 'Content-Type': 'application/json'
1863
+ },
1864
+ body: JSON.stringify(requestBody)
1865
+ });
1866
+
1867
+ console.log('[CHECKOUT] Response status:', response.status, response.statusText);
1868
+
1869
+ let result;
1870
+ try {
1871
+ result = await response.json();
1872
+ console.log('[CHECKOUT] Response data:', result);
1873
+ } catch (parseError) {
1874
+ console.error('[CHECKOUT] Failed to parse response as JSON:', parseError);
1875
+ const text = await response.text();
1876
+ console.error('[CHECKOUT] Response text:', text);
1877
+ throw new Error(`Invalid response from server: ${response.status} ${response.statusText}`);
1878
+ }
1879
+
1880
+ if (!response.ok) {
1881
+ const errorMessage = result.error || result.message || `HTTP ${response.status}: ${response.statusText}`;
1882
+ console.error('[CHECKOUT] API error:', errorMessage, result);
1883
+ throw new Error(errorMessage);
1884
+ }
1885
+
1886
+ if (!result.success) {
1887
+ const errorMessage = result.error || result.message || 'Failed to update shipping method';
1888
+ console.error('[CHECKOUT] Request failed:', errorMessage, result);
1889
+ throw new Error(errorMessage);
1890
+ }
1891
+
1892
+ console.log('[CHECKOUT] Shipping method updated successfully:', result);
1893
+
1894
+ // Re-enable radio button on success
1895
+ if (selectedRadio) {
1896
+ selectedRadio.disabled = false;
1897
+ }
1898
+
1899
+ // Update order summary with new pricing if available
1900
+ if (result.data && result.data.pricing) {
1901
+ // Map API pricing format to template format
1902
+ const mappedPricing = mapPricingFromApi(result.data.pricing);
1903
+ updateOrderSummary(mappedPricing);
1904
+ } else if (result.data) {
1905
+ // Check if pricing fields are directly in result.data
1906
+ const mappedPricing = mapPricingFromApi(result.data);
1907
+ updateOrderSummary(mappedPricing);
1908
+ } else {
1909
+ // Reload page to get updated checkout data with new totals
1910
+ console.log('[CHECKOUT] Reloading page to update order totals');
1911
+ window.location.reload();
1912
+ }
1913
+
1914
+ // Mark as completed and set shipping method selected flag
1915
+ window.checkoutApiStatus.shippingMethodUpdate = 'completed';
1916
+ window.shippingMethodSelected = true;
1917
+ updateCheckoutButtonState();
1918
+ } catch (error) {
1919
+ console.error('[CHECKOUT] Error updating shipping method:', error);
1920
+ console.error('[CHECKOUT] Error details:', {
1921
+ message: error.message,
1922
+ stack: error.stack,
1923
+ name: error.name
1924
+ });
1925
+
1926
+ // Re-enable radio button on error
1927
+ if (selectedRadio) {
1928
+ selectedRadio.disabled = false;
1929
+ }
1930
+
1931
+ // Show error message to user with more details
1932
+ const container = document.getElementById('shipping-methods-container');
1933
+ if (container) {
1934
+ // Remove existing error messages
1935
+ const existingErrors = container.querySelectorAll('.shipping-methods-error');
1936
+ existingErrors.forEach(el => el.remove());
1937
+
1938
+ const errorMsg = document.createElement('div');
1939
+ errorMsg.className = 'shipping-methods-error';
1940
+ errorMsg.textContent = error.message || 'Failed to update shipping method. Please try again.';
1941
+ errorMsg.style.display = 'block';
1942
+ errorMsg.style.marginTop = '10px';
1943
+ errorMsg.style.padding = '10px';
1944
+ errorMsg.style.backgroundColor = '#fee';
1945
+ errorMsg.style.color = '#c33';
1946
+ errorMsg.style.borderRadius = '4px';
1947
+
1948
+ container.appendChild(errorMsg);
1949
+
1950
+ // Remove error message after 8 seconds
1951
+ setTimeout(() => {
1952
+ errorMsg.remove();
1953
+ }, 8000);
1954
+ }
1955
+
1956
+ // Mark as completed even on error (to allow retry)
1957
+ window.checkoutApiStatus.shippingMethodUpdate = 'completed';
1958
+ updateCheckoutButtonState();
1959
+ }
1960
+ }
1961
+
1962
+ // Map API pricing format to template-friendly format
1963
+ function mapPricingFromApi(apiPricing) {
1964
+ if (!apiPricing) return null;
1965
+
1966
+ // API uses: subtotalPrice, totalPrice, totalTax, totalShipping, totalDiscounts
1967
+ // Template expects: subtotal, total, tax, shipping, discount
1968
+ return {
1969
+ subtotal: apiPricing.subtotalPrice || apiPricing.subtotal || 0,
1970
+ total: apiPricing.totalPrice || apiPricing.total || 0,
1971
+ tax: apiPricing.totalTax || apiPricing.tax || 0,
1972
+ shipping: apiPricing.totalShipping || apiPricing.shipping || 0,
1973
+ discount: apiPricing.totalDiscounts || apiPricing.discount || 0
1974
+ };
1975
+ }
1976
+
1977
+ // Calculate subtotal from cart items
1978
+ function calculateSubtotalFromItems() {
1979
+ const cartItems = document.querySelectorAll('.order-summary-item');
1980
+ let subtotal = 0;
1981
+
1982
+ cartItems.forEach(item => {
1983
+ const priceEl = item.querySelector('[data-item-price]');
1984
+ const qtyEl = item.querySelector('[data-item-quantity]');
1985
+
1986
+ if (priceEl && qtyEl) {
1987
+ const price = parseFloat(priceEl.getAttribute('data-item-price')) || 0;
1988
+ const quantity = parseInt(qtyEl.getAttribute('data-item-quantity')) || 0;
1989
+ subtotal += price * quantity;
1990
+ }
1991
+ });
1992
+
1993
+ return subtotal;
1994
+ }
1995
+
1996
+ // Update subtotal display
1997
+ function updateSubtotalDisplay(subtotal) {
1998
+ const subtotalEl = document.querySelector('[data-summary-subtotal]');
1999
+ if (subtotalEl && subtotal !== null && subtotal !== undefined) {
2000
+ subtotalEl.textContent = formatMoney(subtotal);
2001
+ console.log('[CHECKOUT] Updated subtotal display:', subtotal);
2002
+ }
2003
+ }
2004
+
2005
+ // Update order summary with new pricing
2006
+ function updateOrderSummary(pricing) {
2007
+ console.log('[CHECKOUT] Updating order summary with pricing:', pricing);
2008
+
2009
+ if (!pricing) {
2010
+ console.warn('[CHECKOUT] No pricing data provided, calculating from items');
2011
+ // If no pricing provided, calculate from cart items
2012
+ const calculatedSubtotal = calculateSubtotalFromItems();
2013
+ if (calculatedSubtotal > 0) {
2014
+ updateSubtotalDisplay(calculatedSubtotal);
2015
+ }
2016
+ return;
2017
+ }
2018
+
2019
+ // If pricing is in API format, map it first
2020
+ if (pricing.subtotalPrice !== undefined || pricing.totalPrice !== undefined) {
2021
+ pricing = mapPricingFromApi(pricing);
2022
+ console.log('[CHECKOUT] Mapped pricing from API format:', pricing);
2023
+ }
2024
+
2025
+ const totalsContainer = document.querySelector('.order-summary-totals');
2026
+ if (!totalsContainer) {
2027
+ console.warn('[CHECKOUT] Order summary totals container not found');
2028
+ return;
2029
+ }
2030
+
2031
+ // Update subtotal
2032
+ if (pricing.subtotal !== undefined && pricing.subtotal !== null && pricing.subtotal > 0) {
2033
+ updateSubtotalDisplay(pricing.subtotal);
2034
+ } else {
2035
+ // Fallback to calculating from items
2036
+ const calculatedSubtotal = calculateSubtotalFromItems();
2037
+ if (calculatedSubtotal > 0) {
2038
+ updateSubtotalDisplay(calculatedSubtotal);
2039
+ }
2040
+ }
2041
+
2042
+ // Handle discount - add or update discount line
2043
+ const discountEl = document.querySelector('[data-summary-discount]');
2044
+ const hasDiscount = pricing.discount !== undefined && pricing.discount !== null && pricing.discount > 0;
2045
+
2046
+ if (hasDiscount) {
2047
+ if (!discountEl) {
2048
+ // Insert discount line after subtotal
2049
+ const subtotalLine = totalsContainer.querySelector('.summary-line:first-child');
2050
+ if (subtotalLine) {
2051
+ const discountLine = document.createElement('div');
2052
+ discountLine.className = 'summary-line summary-discount';
2053
+ discountLine.innerHTML = `
2054
+ <span class="summary-label">Discount</span>
2055
+ <span class="summary-value" data-summary-discount>-${formatMoney(pricing.discount)}</span>
2056
+ `;
2057
+ subtotalLine.insertAdjacentElement('afterend', discountLine);
2058
+ }
2059
+ } else {
2060
+ // Update existing discount
2061
+ discountEl.textContent = '-' + formatMoney(pricing.discount);
2062
+ const discountLine = discountEl.closest('.summary-line');
2063
+ if (discountLine) {
2064
+ discountLine.style.display = '';
2065
+ }
2066
+ }
2067
+ } else {
2068
+ // Remove discount line if it exists
2069
+ if (discountEl) {
2070
+ const discountLine = discountEl.closest('.summary-line');
2071
+ if (discountLine) {
2072
+ discountLine.remove();
2073
+ }
2074
+ }
2075
+ }
2076
+
2077
+ // Update shipping
2078
+ if (pricing.shipping !== undefined && pricing.shipping !== null) {
2079
+ const shippingEl = document.querySelector('[data-summary-shipping]');
2080
+ if (shippingEl) {
2081
+ shippingEl.textContent = pricing.shipping === 0 ? 'Free' : formatMoney(pricing.shipping);
2082
+ }
2083
+ }
2084
+
2085
+ // Update tax
2086
+ if (pricing.tax !== undefined && pricing.tax !== null) {
2087
+ const taxEl = document.querySelector('[data-summary-tax]');
2088
+ if (taxEl) {
2089
+ taxEl.textContent = formatMoney(pricing.tax);
2090
+ }
2091
+ }
2092
+
2093
+ // Update total
2094
+ if (pricing.total !== undefined && pricing.total !== null) {
2095
+ const totalEl = document.querySelector('[data-summary-total]');
2096
+ if (totalEl) {
2097
+ totalEl.textContent = formatMoney(pricing.total);
2098
+ }
2099
+ }
2100
+
2101
+ console.log('[CHECKOUT] Order summary updated successfully');
2102
+ }
2103
+
2104
+ // Initialize subtotal on page load
2105
+ (function() {
2106
+ // Wait for DOM to be ready
2107
+ if (document.readyState === 'loading') {
2108
+ document.addEventListener('DOMContentLoaded', function() {
2109
+ setTimeout(initializeSubtotal, 100);
2110
+ });
2111
+ } else {
2112
+ setTimeout(initializeSubtotal, 100);
2113
+ }
2114
+
2115
+ function initializeSubtotal() {
2116
+ const subtotalEl = document.querySelector('[data-summary-subtotal]');
2117
+ if (!subtotalEl) return;
2118
+
2119
+ // Check if subtotal is already set (from server-side rendering)
2120
+ const currentSubtotal = subtotalEl.textContent.trim();
2121
+
2122
+ // If subtotal is 0 or empty, try to calculate from items
2123
+ if (!currentSubtotal || currentSubtotal === '$0.00' || currentSubtotal === '0.00' || currentSubtotal === '₹0.00') {
2124
+ const calculatedSubtotal = calculateSubtotalFromItems();
2125
+ if (calculatedSubtotal > 0) {
2126
+ updateSubtotalDisplay(calculatedSubtotal);
2127
+ }
2128
+ }
2129
+
2130
+ // Also update if checkout pricing is available
2131
+ {% if checkout.pricing %}
2132
+ const checkoutPricing = {% if checkout.pricing %}{{ checkout.pricing | json }}{% else %}null{% endif %};
2133
+ if (checkoutPricing && checkoutPricing.subtotal) {
2134
+ updateOrderSummary(checkoutPricing);
2135
+ }
2136
+ {% endif %}
2137
+ }
2138
+ })();
2139
+
2140
+ // Check and fetch shipping methods if address is complete
2141
+ function checkAndFetchShippingMethods() {
2142
+ // Prevent fetching during checkout completion
2143
+ if (window.checkoutInProgress) return;
2144
+
2145
+ if (isShippingAddressComplete()) {
2146
+ // Debounce the fetch
2147
+ clearTimeout(window.shippingMethodsTimeout);
2148
+ window.shippingMethodsTimeout = setTimeout(fetchShippingMethods, 500);
2149
+ }
2150
+ }
2151
+
2152
+ async function updateShippingAddress() {
2153
+ // Prevent updating during checkout completion
2154
+ if (window.checkoutInProgress) return;
2155
+
2156
+ const checkoutToken = getCheckoutToken();
2157
+ if (!checkoutToken) {
2158
+ // No valid checkout, clear pending state if we set it earlier
2159
+ if (window.checkoutApiStatus.shippingAddress === 'pending') {
2160
+ window.checkoutApiStatus.shippingAddress = 'idle';
2161
+ updateCheckoutButtonState();
2162
+ }
2163
+ return;
2164
+ }
2165
+
2166
+ // Track status
2167
+ window.checkoutApiStatus.shippingAddress = 'pending';
2168
+ updateCheckoutButtonState();
2169
+
2170
+ const formData = new FormData(document.getElementById('checkout-form'));
2171
+
2172
+ try {
2173
+ const isHostedCheckout = window.location.pathname.includes('/checkout/') &&
2174
+ window.location.pathname.split('/').length > 2;
2175
+ const endpoint = isHostedCheckout
2176
+ ? `/webstoreapi/checkout/${checkoutToken}/shipping-address`
2177
+ : '/webstoreapi/checkout/shipping-address';
2178
+
2179
+ // Get countryId from select dropdown
2180
+ const countrySelect = document.getElementById('shipping-country');
2181
+ const countryId = countrySelect ? countrySelect.value : null;
2182
+
2183
+ // Get stateOrProvinceId from either select dropdown or text input
2184
+ const stateSelect = document.getElementById('shipping-state');
2185
+ const stateTextInput = document.getElementById('shipping-state-text');
2186
+ let stateOrProvinceId = null;
2187
+
2188
+ if (stateSelect && stateSelect.style.display !== 'none' && stateSelect.value) {
2189
+ stateOrProvinceId = stateSelect.value;
2190
+ } else if (stateTextInput && stateTextInput.style.display !== 'none' && stateTextInput.value) {
2191
+ // For text input, we still need an ID - this should not happen if states are properly configured
2192
+ // But we'll send the text value and let backend handle validation
2193
+ stateOrProvinceId = stateTextInput.value;
2194
+ }
2195
+
2196
+ // Validate that we have both IDs
2197
+ if (!countryId || !stateOrProvinceId) {
2198
+ console.warn('[CHECKOUT] Missing countryId or stateOrProvinceId. Country:', countryId, 'State:', stateOrProvinceId);
2199
+ // Don't send request if IDs are missing. Since we previously
2200
+ // marked this as pending when the user started editing, reset
2201
+ // it back so the button state reflects that no call was sent.
2202
+ window.checkoutApiStatus.shippingAddress = 'idle';
2203
+ updateCheckoutButtonState();
2204
+ return;
2205
+ }
2206
+
2207
+ // Get phone number from intl-tel-input if available
2208
+ let shippingPhone = formData.get('shippingPhone');
2209
+ if (window.shippingPhoneIti) {
2210
+ const phoneNumber = window.shippingPhoneIti.getNumber();
2211
+ if (phoneNumber) {
2212
+ // Remove leading + sign
2213
+ shippingPhone = phoneNumber.replace(/^\+/, '');
2214
+ }
2215
+ }
2216
+
2217
+ const requestBody = {
2218
+ shippingFirstName: formData.get('shippingFirstName'),
2219
+ shippingLastName: formData.get('shippingLastName'),
2220
+ shippingAddress: formData.get('shippingAddress'),
2221
+ shippingCity: formData.get('shippingCity'),
2222
+ shippingZip: formData.get('shippingZip'),
2223
+ shippingPhone: shippingPhone,
2224
+ countryId: countryId,
2225
+ stateOrProvinceId: stateOrProvinceId
2226
+ };
2227
+
2228
+ console.log('[CHECKOUT] Updating shipping address with data:', requestBody);
2229
+
2230
+ const response = await fetch(endpoint, {
2231
+ method: 'PUT',
2232
+ headers: {
2233
+ 'Content-Type': 'application/json'
2234
+ },
2235
+ body: JSON.stringify(requestBody)
2236
+ });
2237
+
2238
+ if (!response.ok) {
2239
+ const result = await response.json();
2240
+ console.error('Failed to update shipping address:', result.error);
2241
+ // Mark as completed even on error (to allow retry)
2242
+ window.checkoutApiStatus.shippingAddress = 'completed';
2243
+ updateCheckoutButtonState();
2244
+ } else {
2245
+ // Mark as completed
2246
+ window.checkoutApiStatus.shippingAddress = 'completed';
2247
+ updateCheckoutButtonState();
2248
+
2249
+ // Only fetch shipping methods if checkout not in progress
2250
+ if (!window.checkoutInProgress) {
2251
+ checkAndFetchShippingMethods();
2252
+ }
2253
+ }
2254
+ } catch (error) {
2255
+ console.error('Error updating shipping address:', error);
2256
+ // Mark as completed even on error (to allow retry)
2257
+ window.checkoutApiStatus.shippingAddress = 'completed';
2258
+ updateCheckoutButtonState();
2259
+ }
2260
+ }
2261
+
2262
+ // Update billing address when form fields change (debounced)
2263
+ let billingAddressTimeout;
2264
+ const billingFields = ['billingFirstName', 'billingLastName', 'billingAddress', 'billingCity', 'billingState', 'billingZip', 'billingCountry'];
2265
+ billingFields.forEach(fieldId => {
2266
+ const field = document.getElementById(fieldId);
2267
+ if (field) {
2268
+ field.addEventListener('blur', function() {
2269
+ clearTimeout(billingAddressTimeout);
2270
+ billingAddressTimeout = setTimeout(updateBillingAddress, 500);
2271
+ });
2272
+ }
2273
+ });
2274
+
2275
+ async function updateBillingAddress() {
2276
+ // Prevent updating during checkout completion
2277
+ if (window.checkoutInProgress) return;
2278
+
2279
+ const checkoutToken = getCheckoutToken();
2280
+ if (!checkoutToken) return;
2281
+
2282
+ const sameAsShipping = document.getElementById('same-as-shipping')?.checked;
2283
+ if (sameAsShipping) return; // Don't update if same as shipping
2284
+
2285
+ // Track status
2286
+ window.checkoutApiStatus.billingAddress = 'pending';
2287
+ updateCheckoutButtonState();
2288
+
2289
+ const formData = new FormData(document.getElementById('checkout-form'));
2290
+
2291
+ try {
2292
+ // Get countryId from select dropdown
2293
+ const countrySelect = document.getElementById('billing-country');
2294
+ const countryId = countrySelect ? countrySelect.value : null;
2295
+
2296
+ // Get stateOrProvinceId from select dropdown
2297
+ const stateSelect = document.getElementById('billing-state');
2298
+ const stateOrProvinceId = stateSelect && stateSelect.value ? stateSelect.value : null;
2299
+
2300
+ // Validate that we have both IDs
2301
+ if (!countryId || !stateOrProvinceId) {
2302
+ console.warn('[CHECKOUT] Missing billing countryId or stateOrProvinceId. Country:', countryId, 'State:', stateOrProvinceId);
2303
+ // Don't send request if IDs are missing
2304
+ return;
2305
+ }
2306
+
2307
+ const response = await fetch('/webstoreapi/checkout/billing-address', {
2308
+ method: 'PUT',
2309
+ headers: {
2310
+ 'Content-Type': 'application/json'
2311
+ },
2312
+ body: JSON.stringify({
2313
+ billingFirstName: formData.get('billingFirstName'),
2314
+ billingLastName: formData.get('billingLastName'),
2315
+ billingAddress: formData.get('billingAddress'),
2316
+ billingCity: formData.get('billingCity'),
2317
+ billingZip: formData.get('billingZip'),
2318
+ countryId: countryId,
2319
+ stateOrProvinceId: stateOrProvinceId
2320
+ })
2321
+ });
2322
+
2323
+ if (!response.ok) {
2324
+ const result = await response.json();
2325
+ console.error('Failed to update billing address:', result.error);
2326
+ // Mark as completed even on error (to allow retry)
2327
+ window.checkoutApiStatus.billingAddress = 'completed';
2328
+ updateCheckoutButtonState();
2329
+ } else {
2330
+ // Mark as completed
2331
+ window.checkoutApiStatus.billingAddress = 'completed';
2332
+ updateCheckoutButtonState();
2333
+ }
2334
+ } catch (error) {
2335
+ console.error('Error updating billing address:', error);
2336
+ // Mark as completed even on error (to allow retry)
2337
+ window.checkoutApiStatus.billingAddress = 'completed';
2338
+ updateCheckoutButtonState();
2339
+ }
2340
+ }
2341
+
2342
+ // Handle same as shipping checkbox change
2343
+ document.getElementById('same-as-shipping')?.addEventListener('change', function(e) {
2344
+ if (e.target.checked) {
2345
+ // Clear billing address when same as shipping
2346
+ updateBillingAddress();
2347
+ } else {
2348
+ // Update billing address when unchecked
2349
+ updateBillingAddress();
2350
+ }
2351
+ });
2352
+
2353
+ // Update note when order notes field changes (debounced)
2354
+ let noteTimeout;
2355
+ const orderNotesField = document.getElementById('orderNotes');
2356
+ if (orderNotesField) {
2357
+ orderNotesField.addEventListener('blur', function() {
2358
+ clearTimeout(noteTimeout);
2359
+ noteTimeout = setTimeout(updateNote, 500);
2360
+ });
2361
+ }
2362
+
2363
+ async function updateNote() {
2364
+ // Prevent updating during checkout completion
2365
+ if (window.checkoutInProgress) return;
2366
+
2367
+ const checkoutToken = getCheckoutToken();
2368
+ if (!checkoutToken) return;
2369
+
2370
+ // Track status
2371
+ window.checkoutApiStatus.orderNote = 'pending';
2372
+ updateCheckoutButtonState();
2373
+
2374
+ const formData = new FormData(document.getElementById('checkout-form'));
2375
+ const note = formData.get('orderNotes') || '';
2376
+
2377
+ try {
2378
+ const response = await fetch('/webstoreapi/checkout/note', {
2379
+ method: 'PUT',
2380
+ headers: {
2381
+ 'Content-Type': 'application/json'
2382
+ },
2383
+ body: JSON.stringify({ note })
2384
+ });
2385
+
2386
+ if (!response.ok) {
2387
+ const result = await response.json();
2388
+ console.error('Failed to update note:', result.error);
2389
+ // Mark as completed even on error (to allow retry)
2390
+ window.checkoutApiStatus.orderNote = 'completed';
2391
+ updateCheckoutButtonState();
2392
+ } else {
2393
+ // Mark as completed
2394
+ window.checkoutApiStatus.orderNote = 'completed';
2395
+ updateCheckoutButtonState();
2396
+ }
2397
+ } catch (error) {
2398
+ console.error('Error updating note:', error);
2399
+ // Mark as completed even on error (to allow retry)
2400
+ window.checkoutApiStatus.orderNote = 'completed';
2401
+ updateCheckoutButtonState();
2402
+ }
2403
+ }
2404
+
2405
+ // Wait for Razorpay script to load
2406
+ async function waitForRazorpay(maxWaitTime = 10000) {
2407
+ const startTime = Date.now();
2408
+
2409
+ console.log('[CHECKOUT] Waiting for Razorpay to load...');
2410
+ console.log('[CHECKOUT] Current Razorpay status:', {
2411
+ typeof: typeof Razorpay,
2412
+ razorpayLoaded: window.razorpayLoaded,
2413
+ razorpayLoadError: window.razorpayLoadError,
2414
+ scriptExists: !!document.querySelector('script[src*="checkout.razorpay.com"]')
2415
+ });
2416
+
2417
+ // Check if there was a load error
2418
+ if (window.razorpayLoadError) {
2419
+ console.error('[CHECKOUT] Razorpay load error detected');
2420
+ return false;
2421
+ }
2422
+
2423
+ // If already loaded, return immediately
2424
+ if (typeof Razorpay !== 'undefined') {
2425
+ console.log('[CHECKOUT] Razorpay already available');
2426
+ return true;
2427
+ }
2428
+
2429
+ // Check if script tag exists, if not try to load it
2430
+ const scriptTag = document.querySelector('script[src*="checkout.razorpay.com"]');
2431
+ if (!scriptTag) {
2432
+ console.warn('[CHECKOUT] Razorpay script tag not found, attempting to load...');
2433
+ return new Promise((resolve) => {
2434
+ const razorpayScript = document.createElement('script');
2435
+ razorpayScript.src = 'https://checkout.razorpay.com/v1/checkout.js';
2436
+ razorpayScript.async = false;
2437
+ razorpayScript.onload = function() {
2438
+ console.log('[CHECKOUT] Razorpay script loaded dynamically');
2439
+ // Wait for Razorpay object to be available
2440
+ setTimeout(function() {
2441
+ if (typeof Razorpay !== 'undefined') {
2442
+ window.razorpayLoaded = true;
2443
+ resolve(true);
2444
+ } else {
2445
+ console.error('[CHECKOUT] Script loaded but Razorpay object not available');
2446
+ window.razorpayLoadError = true;
2447
+ resolve(false);
2448
+ }
2449
+ }, 300);
2450
+ };
2451
+ razorpayScript.onerror = function() {
2452
+ console.error('[CHECKOUT] Failed to load Razorpay script dynamically');
2453
+ window.razorpayLoadError = true;
2454
+ resolve(false);
2455
+ };
2456
+ (document.head || document.getElementsByTagName('head')[0]).appendChild(razorpayScript);
2457
+ });
2458
+ }
2459
+
2460
+ // Wait for Razorpay to become available (script exists but object not ready)
2461
+ return new Promise((resolve) => {
2462
+ // Listen for ready event if available
2463
+ const readyHandler = function() {
2464
+ console.log('[CHECKOUT] Razorpay ready event received');
2465
+ window.removeEventListener('razorpayReady', readyHandler);
2466
+ resolve(typeof Razorpay !== 'undefined');
2467
+ };
2468
+ window.addEventListener('razorpayReady', readyHandler);
2469
+
2470
+ // Also poll for Razorpay object
2471
+ let checkCount = 0;
2472
+ const checkInterval = setInterval(function() {
2473
+ checkCount++;
2474
+
2475
+ if (typeof Razorpay !== 'undefined') {
2476
+ console.log('[CHECKOUT] Razorpay object found after', checkCount * 100, 'ms');
2477
+ clearInterval(checkInterval);
2478
+ window.removeEventListener('razorpayReady', readyHandler);
2479
+ window.razorpayLoaded = true;
2480
+ resolve(true);
2481
+ } else if (window.razorpayLoadError) {
2482
+ console.error('[CHECKOUT] Razorpay load error detected during wait');
2483
+ clearInterval(checkInterval);
2484
+ window.removeEventListener('razorpayReady', readyHandler);
2485
+ resolve(false);
2486
+ } else if ((Date.now() - startTime) >= maxWaitTime) {
2487
+ console.error('[CHECKOUT] Timeout waiting for Razorpay after', maxWaitTime, 'ms');
2488
+ clearInterval(checkInterval);
2489
+ window.removeEventListener('razorpayReady', readyHandler);
2490
+ resolve(false);
2491
+ } else if (checkCount % 10 === 0) {
2492
+ console.log('[CHECKOUT] Still waiting for Razorpay...', checkCount * 100, 'ms');
2493
+ }
2494
+ }, 100);
2495
+ });
2496
+ }
2497
+
2498
+ // Helper function to handle Razorpay payment directly
2499
+ async function handleRazorpayPaymentDirect() {
2500
+ let checkoutToken = getCheckoutToken();
2501
+ console.log('[CHECKOUT] Starting Razorpay payment, checkout token:', checkoutToken ? checkoutToken.substring(0, 10) + '...' : 'not found');
2502
+
2503
+ if (!checkoutToken) {
2504
+ throw new Error('Checkout session not found. Please refresh the page and try again.');
2505
+ }
2506
+
2507
+ // Create Razorpay order
2508
+ const isHostedCheckout = window.location.pathname.includes('/checkout/') &&
2509
+ window.location.pathname.split('/').length > 2;
2510
+
2511
+ const endpoint = isHostedCheckout
2512
+ ? `/webstoreapi/payment/razorpay/create-order?token=${checkoutToken}`
2513
+ : '/webstoreapi/payment/razorpay/create-order';
2514
+
2515
+ console.log('[CHECKOUT] Calling Razorpay create-order endpoint:', endpoint);
2516
+
2517
+ const response = await fetch(endpoint, {
2518
+ method: 'POST',
2519
+ headers: {
2520
+ 'Content-Type': 'application/json'
2521
+ },
2522
+ body: JSON.stringify({
2523
+ checkoutToken: checkoutToken
2524
+ })
2525
+ });
2526
+
2527
+ if (!response.ok) {
2528
+ let errorMessage = 'Failed to create Razorpay order';
2529
+ let errorData = null;
2530
+
2531
+ try {
2532
+ errorData = await response.json();
2533
+ errorMessage = errorData.error || errorData.message || errorMessage;
2534
+ console.error('[CHECKOUT] Razorpay API error response:', errorData);
2535
+
2536
+ if (errorData.details) {
2537
+ console.error('[CHECKOUT] Error details:', errorData.details);
2538
+ }
2539
+
2540
+ // Provide user-friendly error messages for common scenarios
2541
+ if (errorMessage.includes('cart') || errorMessage.includes('Cart')) {
2542
+ errorMessage = 'Your cart is empty or no longer available. Please add items to your cart and try again.';
2543
+ } else if (errorMessage.includes('Checkout not found') || errorMessage.includes('checkout')) {
2544
+ errorMessage = 'Checkout session expired. Please refresh the page and try again.';
2545
+ } else if (errorMessage.includes('payment method not available')) {
2546
+ errorMessage = 'Razorpay payment method is not available at this time. Please try another payment method.';
2547
+ } else if (errorMessage.includes('minimum')) {
2548
+ errorMessage = errorMessage; // Keep the minimum amount error message as is
2549
+ }
2550
+ } catch (parseError) {
2551
+ console.error('[CHECKOUT] Failed to parse error response:', parseError);
2552
+ errorMessage = `Server error (${response.status}): ${response.statusText}`;
2553
+ }
2554
+
2555
+ throw new Error(errorMessage);
2556
+ }
2557
+
2558
+ const result = await response.json();
2559
+ console.log('[CHECKOUT] Razorpay order creation response:', result);
2560
+
2561
+ const orderData = result.data;
2562
+
2563
+ if (!orderData || !orderData.orderId) {
2564
+ console.error('[CHECKOUT] Invalid order data in response:', result);
2565
+ throw new Error('Failed to create Razorpay order. Please try again.');
2566
+ }
2567
+
2568
+ // Update checkout token if it was recreated by the backend
2569
+ if (orderData.checkoutToken && orderData.checkoutToken !== checkoutToken) {
2570
+ console.log('[CHECKOUT] Checkout token was recreated by backend, updating...');
2571
+ checkoutToken = orderData.checkoutToken;
2572
+ // Update the global CHECKOUT_TOKEN if it exists
2573
+ if (typeof CHECKOUT_TOKEN !== 'undefined') {
2574
+ CHECKOUT_TOKEN = checkoutToken;
2575
+ }
2576
+ }
2577
+
2578
+ // Open Razorpay checkout - ensure Razorpay is available
2579
+ if (typeof Razorpay === 'undefined') {
2580
+ console.error('[CHECKOUT] Razorpay not available in handleRazorpayPaymentDirect');
2581
+ // Wait a bit more and check again
2582
+ await new Promise(resolve => setTimeout(resolve, 500));
2583
+ if (typeof Razorpay === 'undefined') {
2584
+ throw new Error('Razorpay payment gateway is not available. Please refresh the page and try again.');
2585
+ }
2586
+ }
2587
+
2588
+ console.log('[CHECKOUT] Opening Razorpay checkout with order:', orderData.orderId);
2589
+
2590
+ const razorpay = new Razorpay({
2591
+ key: orderData.keyId,
2592
+ amount: orderData.amount,
2593
+ currency: orderData.currency,
2594
+ name: orderData.name || 'Store',
2595
+ description: orderData.description || 'Order Payment',
2596
+ order_id: orderData.orderId,
2597
+ prefill: orderData.prefill || {},
2598
+ theme: orderData.theme || { color: '#3399cc' },
2599
+ handler: async function(response) {
2600
+ console.log('[CHECKOUT] Razorpay payment completed, verifying payment...');
2601
+
2602
+ // Use the current checkout token (may have been updated if checkout was recreated)
2603
+ const currentCheckoutToken = checkoutToken || getCheckoutToken();
2604
+
2605
+ // Verify payment
2606
+ const verifyEndpoint = isHostedCheckout
2607
+ ? `/webstoreapi/payment/razorpay/verify?token=${currentCheckoutToken}`
2608
+ : '/webstoreapi/payment/razorpay/verify';
2609
+
2610
+ try {
2611
+ const verifyResponse = await fetch(verifyEndpoint, {
2612
+ method: 'POST',
2613
+ headers: {
2614
+ 'Content-Type': 'application/json'
2615
+ },
2616
+ body: JSON.stringify({
2617
+ checkoutToken: currentCheckoutToken,
2618
+ razorpay_order_id: response.razorpay_order_id,
2619
+ razorpay_payment_id: response.razorpay_payment_id,
2620
+ razorpay_signature: response.razorpay_signature
2621
+ })
2622
+ });
2623
+
2624
+ const verifyResult = await verifyResponse.json();
2625
+ console.log('[CHECKOUT] Payment verification response:', verifyResult);
2626
+
2627
+ if (verifyResult.success) {
2628
+ // Handle array of orders from CompleteCheckoutResponse
2629
+ const orders = Array.isArray(verifyResult.data) ? verifyResult.data : [verifyResult.data];
2630
+ const orderIds = orders.map(o => o.orderId || o.orderNumber).filter(Boolean).join(',');
2631
+ console.log('[CHECKOUT] Payment verified successfully, redirecting to order confirmation');
2632
+
2633
+ if (orderIds) {
2634
+ window.location.href = `/order-confirmation?orderIds=${orderIds}`;
2635
+ } else {
2636
+ window.location.href = '/order-confirmation';
2637
+ }
2638
+ } else {
2639
+ console.error('[CHECKOUT] Payment verification failed:', verifyResult);
2640
+ const errorMsg = verifyResult.error || 'Payment verification failed. Please contact support.';
2641
+ alert(errorMsg);
2642
+ }
2643
+ } catch (verifyError) {
2644
+ console.error('[CHECKOUT] Error during payment verification:', verifyError);
2645
+ alert('An error occurred while verifying your payment. Please contact support with your payment ID: ' + (response.razorpay_payment_id || 'N/A'));
2646
+ }
2647
+ },
2648
+ modal: {
2649
+ ondismiss: function() {
2650
+ console.log('[CHECKOUT] Razorpay popup closed by user');
2651
+ window.checkoutInProgress = false; // Reset flag when popup is closed
2652
+ const submitBtn = document.getElementById('checkout-submit');
2653
+ if (submitBtn) {
2654
+ setButtonLoading(submitBtn, false);
2655
+ }
2656
+ }
2657
+ }
2658
+ });
2659
+
2660
+ razorpay.open();
2661
+ }
2662
+
2663
+ // Handle form submission
2664
+ document.getElementById('checkout-form')?.addEventListener('submit', async function(e) {
2665
+ e.preventDefault();
2666
+
2667
+ // CRITICAL: Set flag immediately to prevent any API calls
2668
+ window.checkoutInProgress = true;
2669
+ clearTimeout(window.shippingMethodsTimeout);
2670
+ clearTimeout(shippingAddressTimeout);
2671
+ clearTimeout(billingAddressTimeout);
2672
+ clearTimeout(noteTimeout);
2673
+
2674
+ const submitBtn = document.getElementById('checkout-submit');
2675
+ const formData = new FormData(this);
2676
+ const paymentMethod = formData.get('paymentMethod');
2677
+
2678
+ // If Razorpay is selected, let the Razorpay plugin handle the payment
2679
+ if (paymentMethod === 'Razorpay') {
2680
+ console.log('[CHECKOUT] Razorpay payment method selected');
2681
+
2682
+ // Wait for Razorpay script to load (up to 10 seconds)
2683
+ setButtonLoading(submitBtn, true, 'Loading payment gateway...');
2684
+
2685
+ const razorpayLoaded = await waitForRazorpay(10000);
2686
+
2687
+ if (!razorpayLoaded) {
2688
+ console.error('[CHECKOUT] Razorpay failed to load after waiting');
2689
+ window.checkoutInProgress = false; // Reset flag on error
2690
+ alert('Razorpay payment gateway failed to load. Please refresh the page and try again. If the problem persists, please contact support.');
2691
+ setButtonLoading(submitBtn, false);
2692
+ return;
2693
+ }
2694
+
2695
+ console.log('[CHECKOUT] Razorpay loaded successfully, proceeding with payment');
2696
+
2697
+ // Proceed directly with Razorpay payment (do not re-call shipping/billing updates here)
2698
+ try {
2699
+ setButtonLoading(submitBtn, true, 'Initializing payment...');
2700
+ await handleRazorpayPaymentDirect();
2701
+ // Note: Don't reset checkoutInProgress here if popup opened successfully
2702
+ // It will be reset in the modal ondismiss handler or after successful payment
2703
+ console.log('[CHECKOUT] Razorpay payment initialized successfully, popup should be open');
2704
+ } catch (error) {
2705
+ console.error('[CHECKOUT] Razorpay payment error:', error);
2706
+ window.checkoutInProgress = false; // Reset flag on error
2707
+
2708
+ // Provide user-friendly error messages
2709
+ let userMessage = 'Unable to initialize Razorpay payment. ';
2710
+ if (error.message) {
2711
+ userMessage += error.message;
2712
+ } else {
2713
+ userMessage += 'Please try again or contact support.';
2714
+ }
2715
+
2716
+ alert(userMessage);
2717
+ if (submitBtn) {
2718
+ setButtonLoading(submitBtn, false);
2719
+ }
2720
+ }
2721
+ return;
2722
+ }
2723
+
2724
+ setButtonLoading(submitBtn, true, 'Processing...');
2725
+
2726
+ const checkoutToken = getCheckoutToken();
2727
+ if (!checkoutToken) {
2728
+ window.checkoutInProgress = false; // Reset flag on error
2729
+ alert('Checkout session expired. Please refresh the page and try again.');
2730
+ setButtonLoading(submitBtn, false);
2731
+ return;
2732
+ }
2733
+
2734
+ if (!paymentMethod) {
2735
+ window.checkoutInProgress = false; // Reset flag on error
2736
+ alert('Please select a payment method.');
2737
+ setButtonLoading(submitBtn, false);
2738
+ return;
2739
+ }
2740
+
2741
+ // Get the selected payment method's fee from the radio input
2742
+ const selectedPaymentInput = document.querySelector(`input[name="paymentMethod"][value="${paymentMethod}"]`);
2743
+ const paymentFee = selectedPaymentInput ? parseFloat(selectedPaymentInput.getAttribute('data-payment-fee') || '0') : 0;
2744
+
2745
+ // Directly call complete checkout API (do not re-call shipping/billing/shipping method updates here)
2746
+ try {
2747
+ // Use the payment method value (which is the id/name from API) as paymentMethodHandle
2748
+ const isHostedCheckout = window.location.pathname.includes('/checkout/') &&
2749
+ window.location.pathname.split('/').length > 2;
2750
+
2751
+ const endpoint = isHostedCheckout
2752
+ ? `/webstoreapi/checkout/${checkoutToken}/complete`
2753
+ : '/webstoreapi/checkout/complete';
2754
+
2755
+ const response = await fetch(endpoint, {
2756
+ method: 'POST',
2757
+ headers: {
2758
+ 'Content-Type': 'application/json'
2759
+ },
2760
+ body: JSON.stringify({
2761
+ paymentMethodHandle: paymentMethod,
2762
+ paymentFeeAmount: paymentFee
2763
+ })
2764
+ });
2765
+
2766
+ const result = await response.json();
2767
+
2768
+ if (result.success && result.data) {
2769
+ // Handle array of orders from CompleteCheckoutResponse
2770
+ const orders = Array.isArray(result.data) ? result.data : [result.data];
2771
+ const orderIds = orders.map(o => o.orderId || o.orderNumber).filter(Boolean).join(',');
2772
+ if (orderIds) {
2773
+ window.location.href = `/order-confirmation?orderIds=${orderIds}`;
2774
+ } else {
2775
+ window.location.href = '/order-confirmation';
2776
+ }
2777
+ } else {
2778
+ throw new Error(result.error || 'Checkout failed');
2779
+ }
2780
+ } catch (error) {
2781
+ console.error('Checkout error:', error);
2782
+ window.checkoutInProgress = false; // Reset flag on error
2783
+ alert('Checkout failed. Please try again.');
2784
+ setButtonLoading(submitBtn, false);
2785
+ }
2786
+ });
2787
+
2788
+ // ==================== COUPON FUNCTIONALITY ====================
2789
+
2790
+ /**
2791
+ * Fetch available discount codes for checkout
2792
+ */
2793
+ async function fetchDiscountCodes() {
2794
+ const checkoutToken = getCheckoutToken();
2795
+ if (!checkoutToken) {
2796
+ console.warn('[COUPONS] No checkout token available');
2797
+ return;
2798
+ }
2799
+
2800
+ const couponsContainer = document.getElementById('coupons-container');
2801
+ const couponsError = document.getElementById('coupons-error');
2802
+
2803
+ if (couponsContainer) {
2804
+ couponsContainer.innerHTML = '<p class="coupons-loading">Loading available coupons...</p>';
2805
+ }
2806
+
2807
+ if (couponsError) {
2808
+ couponsError.style.display = 'none';
2809
+ }
2810
+
2811
+ try {
2812
+ const response = await fetch(`/webstoreapi/checkout/${checkoutToken}/discount-codes`, {
2813
+ method: 'GET',
2814
+ headers: {
2815
+ 'Content-Type': 'application/json'
2816
+ }
2817
+ });
2818
+
2819
+ if (!response.ok) {
2820
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2821
+ }
2822
+
2823
+ const result = await response.json();
2824
+
2825
+ if (!result.success) {
2826
+ throw new Error(result.error || 'Failed to fetch discount codes');
2827
+ }
2828
+
2829
+ const discountCodes = result.data;
2830
+ displayDiscountCodes(discountCodes);
2831
+ } catch (error) {
2832
+ console.error('[COUPONS] Error fetching discount codes:', error);
2833
+ if (couponsContainer) {
2834
+ couponsContainer.innerHTML = '<p class="coupons-error">Unable to load coupons. Please try again later.</p>';
2835
+ }
2836
+ if (couponsError) {
2837
+ couponsError.textContent = error.message || 'Failed to load coupons';
2838
+ couponsError.style.display = 'block';
2839
+ }
2840
+ }
2841
+ }
2842
+
2843
+ /**
2844
+ * Display available discount codes
2845
+ */
2846
+ function displayDiscountCodes(discountCodes) {
2847
+ const couponsContainer = document.getElementById('coupons-container');
2848
+ if (!couponsContainer) return;
2849
+
2850
+ // Handle different response formats
2851
+ let codes = [];
2852
+ if (Array.isArray(discountCodes)) {
2853
+ codes = discountCodes;
2854
+ } else if (discountCodes && typeof discountCodes === 'object') {
2855
+ // If it's a single object, wrap it in an array
2856
+ if (discountCodes.code) {
2857
+ codes = [discountCodes];
2858
+ } else if (discountCodes.codes && Array.isArray(discountCodes.codes)) {
2859
+ codes = discountCodes.codes;
2860
+ }
2861
+ }
2862
+
2863
+ if (codes.length === 0) {
2864
+ couponsContainer.innerHTML = '<p class="coupons-empty">No coupons available at this time.</p>';
2865
+ return;
2866
+ }
2867
+
2868
+ const couponsList = document.createElement('div');
2869
+ couponsList.className = 'coupons-list';
2870
+
2871
+ codes.forEach((coupon, index) => {
2872
+ const couponItem = document.createElement('div');
2873
+ couponItem.className = 'coupon-item';
2874
+ couponItem.dataset.couponCode = coupon.code || coupon.couponCode || '';
2875
+
2876
+ const couponInfo = document.createElement('div');
2877
+ couponInfo.className = 'coupon-info';
2878
+
2879
+ const couponCode = document.createElement('div');
2880
+ couponCode.className = 'coupon-code';
2881
+ couponCode.textContent = coupon.code || coupon.couponCode || 'N/A';
2882
+
2883
+ const couponDescription = document.createElement('div');
2884
+ couponDescription.className = 'coupon-description';
2885
+ couponDescription.textContent = coupon.description || '';
2886
+
2887
+ if (coupon.minCartAmount) {
2888
+ const minAmount = document.createElement('div');
2889
+ minAmount.className = 'coupon-min-amount';
2890
+ minAmount.textContent = `Minimum order: ${formatMoney(coupon.minCartAmount)}`;
2891
+ couponInfo.appendChild(minAmount);
2892
+ }
2893
+
2894
+ couponInfo.appendChild(couponCode);
2895
+ if (coupon.description) {
2896
+ couponInfo.appendChild(couponDescription);
2897
+ }
2898
+
2899
+ const applyButton = document.createElement('button');
2900
+ applyButton.type = 'button';
2901
+ applyButton.className = 'btn btn-primary btn-sm apply-coupon-btn';
2902
+ applyButton.textContent = 'Apply';
2903
+ applyButton.dataset.couponCode = coupon.code || coupon.couponCode || '';
2904
+ applyButton.addEventListener('click', () => applyDiscountCode(coupon.code || coupon.couponCode));
2905
+
2906
+ couponItem.appendChild(couponInfo);
2907
+ couponItem.appendChild(applyButton);
2908
+ couponsList.appendChild(couponItem);
2909
+ });
2910
+
2911
+ couponsContainer.innerHTML = '';
2912
+ couponsContainer.appendChild(couponsList);
2913
+ }
2914
+
2915
+ /**
2916
+ * Apply discount code to checkout
2917
+ */
2918
+ async function applyDiscountCode(discountCode) {
2919
+ if (!discountCode) {
2920
+ alert('Please select a valid coupon code.');
2921
+ return;
2922
+ }
2923
+
2924
+ const checkoutToken = getCheckoutToken();
2925
+ if (!checkoutToken) {
2926
+ alert('Checkout session not found. Please refresh the page.');
2927
+ return;
2928
+ }
2929
+
2930
+ // Disable all apply buttons
2931
+ const applyButtons = document.querySelectorAll('.apply-coupon-btn');
2932
+ applyButtons.forEach(btn => {
2933
+ btn.disabled = true;
2934
+ btn.textContent = 'Applying...';
2935
+ });
2936
+
2937
+ try {
2938
+ const response = await fetch(`/webstoreapi/checkout/${checkoutToken}/apply-discount`, {
2939
+ method: 'POST',
2940
+ headers: {
2941
+ 'Content-Type': 'application/json'
2942
+ },
2943
+ body: JSON.stringify({
2944
+ discountCode: discountCode
2945
+ })
2946
+ });
2947
+
2948
+ if (!response.ok) {
2949
+ const errorData = await response.json().catch(() => ({}));
2950
+ throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
2951
+ }
2952
+
2953
+ const result = await response.json();
2954
+
2955
+ if (!result.success) {
2956
+ throw new Error(result.error || 'Failed to apply discount code');
2957
+ }
2958
+
2959
+ // Show applied coupon
2960
+ const appliedCouponDiv = document.getElementById('applied-coupon');
2961
+ const appliedCouponCode = appliedCouponDiv?.querySelector('.applied-coupon-code');
2962
+ if (appliedCouponDiv && appliedCouponCode) {
2963
+ appliedCouponCode.textContent = `Applied: ${discountCode}`;
2964
+ appliedCouponDiv.style.display = 'block';
2965
+ }
2966
+
2967
+ // Update pricing display
2968
+ updateCheckoutPricing(result.data);
2969
+
2970
+ // Show success message
2971
+ alert(`Coupon "${discountCode}" applied successfully!`);
2972
+
2973
+ // Reload page to show updated pricing (or update DOM directly)
2974
+ // For now, we'll reload to ensure consistency
2975
+ window.location.reload();
2976
+ } catch (error) {
2977
+ console.error('[COUPONS] Error applying discount code:', error);
2978
+ alert(error.message || 'Failed to apply coupon. Please try again.');
2979
+
2980
+ // Re-enable buttons
2981
+ applyButtons.forEach(btn => {
2982
+ btn.disabled = false;
2983
+ btn.textContent = 'Apply';
2984
+ });
2985
+ }
2986
+ }
2987
+
2988
+ /**
2989
+ * Update checkout pricing display
2990
+ */
2991
+ function updateCheckoutPricing(checkoutData) {
2992
+ if (!checkoutData || !checkoutData.pricing) return;
2993
+
2994
+ const pricing = checkoutData.pricing;
2995
+
2996
+ // Update subtotal
2997
+ const subtotalEl = document.querySelector('[data-summary-subtotal]');
2998
+ if (subtotalEl && pricing.subtotalPrice !== undefined) {
2999
+ subtotalEl.textContent = formatMoney(pricing.subtotalPrice);
3000
+ }
3001
+
3002
+ // Update discount
3003
+ const discountEl = document.querySelector('[data-summary-discount]');
3004
+ const discountLine = document.querySelector('[data-summary-discount-line]');
3005
+ if (pricing.totalDiscounts !== undefined && pricing.totalDiscounts > 0) {
3006
+ if (discountEl) {
3007
+ discountEl.textContent = `-${formatMoney(pricing.totalDiscounts)}`;
3008
+ }
3009
+ if (discountLine) {
3010
+ discountLine.style.display = 'flex';
3011
+ }
3012
+ } else {
3013
+ if (discountLine) {
3014
+ discountLine.style.display = 'none';
3015
+ }
3016
+ }
3017
+
3018
+ // Update shipping
3019
+ const shippingEl = document.querySelector('[data-summary-shipping]');
3020
+ if (shippingEl && pricing.totalShipping !== undefined) {
3021
+ shippingEl.textContent = pricing.totalShipping === 0 ? 'Free' : formatMoney(pricing.totalShipping);
3022
+ }
3023
+
3024
+ // Update tax
3025
+ const taxEl = document.querySelector('[data-summary-tax]');
3026
+ if (taxEl && pricing.totalTax !== undefined) {
3027
+ taxEl.textContent = formatMoney(pricing.totalTax);
3028
+ }
3029
+
3030
+ // Update total
3031
+ const totalEl = document.querySelector('[data-summary-total]');
3032
+ if (totalEl && pricing.totalPrice !== undefined) {
3033
+ totalEl.textContent = formatMoney(pricing.totalPrice);
3034
+ }
3035
+ }
3036
+
3037
+ /**
3038
+ * Format money value
3039
+ */
3040
+ function formatMoney(amount) {
3041
+ if (typeof amount !== 'number') {
3042
+ amount = parseFloat(amount) || 0;
3043
+ }
3044
+ // Use shop settings if available, otherwise default format
3045
+ const currencySymbol = {% if shop.settings.currencySymbol %}{{ shop.settings.currencySymbol | json }}{% else %}'₹'{% endif %};
3046
+ return `${currencySymbol}${amount.toFixed(2)}`;
3047
+ }
3048
+
3049
+ /**
3050
+ * Remove applied coupon
3051
+ */
3052
+ async function removeCoupon() {
3053
+ const appliedCouponDiv = document.getElementById('applied-coupon');
3054
+ const appliedCouponCode = appliedCouponDiv?.querySelector('.applied-coupon-code');
3055
+ if (!appliedCouponCode) return;
3056
+
3057
+ const couponCode = appliedCouponCode.textContent.replace('Applied: ', '').trim();
3058
+ if (!couponCode) return;
3059
+
3060
+ const checkoutToken = getCheckoutToken();
3061
+ if (!checkoutToken) return;
3062
+
3063
+ try {
3064
+ // Note: We would need a remove discount code endpoint
3065
+ // For now, we'll just reload the page after removing from UI
3066
+ appliedCouponDiv.style.display = 'none';
3067
+ window.location.reload();
3068
+ } catch (error) {
3069
+ console.error('[COUPONS] Error removing coupon:', error);
3070
+ alert('Failed to remove coupon. Please refresh the page.');
3071
+ }
3072
+ }
3073
+
3074
+ /**
3075
+ * Show currently applied coupon if any
3076
+ */
3077
+ function showAppliedCoupon() {
3078
+ // Check if there's a discount applied in the checkout pricing
3079
+ const discountValue = document.querySelector('[data-summary-discount]');
3080
+ const discountLine = document.querySelector('[data-summary-discount-line]');
3081
+
3082
+ if (discountValue && discountLine && discountLine.style.display !== 'none') {
3083
+ const discountText = discountValue.textContent.trim();
3084
+ // Try to extract coupon code from checkout data if available
3085
+ // For now, we'll just show that a discount is applied
3086
+ const appliedCouponDiv = document.getElementById('applied-coupon');
3087
+ const appliedCouponCode = appliedCouponDiv?.querySelector('.applied-coupon-code');
3088
+
3089
+ if (appliedCouponDiv && appliedCouponCode && discountText && discountText !== '₹0.00') {
3090
+ // If we have discount info, show it (we don't have the code, so show generic message)
3091
+ appliedCouponCode.textContent = 'Discount Applied';
3092
+ appliedCouponDiv.style.display = 'block';
3093
+ }
3094
+ }
3095
+ }
3096
+
3097
+ // Initialize coupon functionality on page load
3098
+ document.addEventListener('DOMContentLoaded', () => {
3099
+ // Fetch discount codes when page loads
3100
+ fetchDiscountCodes();
3101
+
3102
+ // Show applied coupon if any
3103
+ showAppliedCoupon();
3104
+
3105
+ // Set up remove coupon button
3106
+ const removeCouponBtn = document.getElementById('remove-coupon-btn');
3107
+ if (removeCouponBtn) {
3108
+ removeCouponBtn.addEventListener('click', removeCoupon);
3109
+ }
3110
+ });
3111
+ </script>
3112
+
3113
+ <!-- Price Change Handler -->
3114
+ <script src="{{ 'checkout-price-handler.js' | asset_url }}"></script>
3115
+
3116
+ <style>
3117
+ /* Checkout Page - Shopify Horizon Style */
3118
+ /* Base Typography - Shopify uses 10px base */
3119
+ /* Set base font-size on html when checkout page is present */
3120
+ html:has(.checkout-page) {
3121
+ font-size: 10px;
3122
+ }
3123
+
3124
+ .checkout-page {
3125
+ min-height: calc(100vh - 200px);
3126
+ padding: 0;
3127
+ background: var(--color-background, #ffffff);
3128
+ font-family: var(--font-body, -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol");
3129
+ color: var(--color-text, #000000);
3130
+ line-height: var(--line-height-base, 1.5);
3131
+ font-size: var(--text-base, 1.4rem);
3132
+ }
3133
+
3134
+ .checkout-container {
3135
+ max-width: var(--container-width, 1200px);
3136
+ margin: 0 auto;
3137
+ padding: 0;
3138
+ }
3139
+
3140
+ .checkout-header {
3141
+ margin-bottom: 0;
3142
+ padding: 1rem 1.5rem 0; /* Reduced top padding to optimize space */
3143
+ border-bottom: none;
3144
+ }
3145
+
3146
+ .checkout-title {
3147
+ font-size: var(--text-4xl, 2.8rem);
3148
+ font-weight: var(--font-weight-bold, 700);
3149
+ color: var(--color-text, #000000);
3150
+ margin: 0 -1px 0 0; /* Match Shopify's -1px margin */
3151
+ letter-spacing: var(--letter-spacing-heading, -0.01em);
3152
+ }
3153
+
3154
+ /* Price Change Banner */
3155
+ .price-change-banner {
3156
+ background: linear-gradient(135deg, var(--color-warning-light, #fff3cd) 0%, var(--color-warning, #ffe69c) 100%);
3157
+ border: 2px solid var(--color-warning, #ffc107);
3158
+ border-radius: var(--border-radius-large, 12px);
3159
+ padding: var(--spacing-component, 1.5rem);
3160
+ margin-bottom: var(--spacing-section, 2rem);
3161
+ box-shadow: 0 4px 12px rgba(255, 193, 7, 0.2);
3162
+ }
3163
+
3164
+ .price-change-banner.price-decrease {
3165
+ background: linear-gradient(135deg, var(--color-success-light, #d1f2eb) 0%, var(--color-success, #a8e6cf) 100%);
3166
+ border-color: var(--color-success, #28a745);
3167
+ }
3168
+
3169
+ .price-change-banner.price-critical {
3170
+ background: linear-gradient(135deg, var(--color-error-light, #f8d7da) 0%, var(--color-error, #f5c6cb) 100%);
3171
+ border-color: var(--color-error, #dc3545);
3172
+ }
3173
+
3174
+ .price-change-banner-content {
3175
+ display: flex;
3176
+ align-items: flex-start;
3177
+ gap: 1rem;
3178
+ }
3179
+
3180
+ .price-change-icon {
3181
+ flex-shrink: 0;
3182
+ color: #ff8c00;
3183
+ }
3184
+
3185
+ .price-change-banner.price-decrease .price-change-icon {
3186
+ color: #28a745;
3187
+ }
3188
+
3189
+ .price-change-banner.price-critical .price-change-icon {
3190
+ color: #dc3545;
3191
+ }
3192
+
3193
+ .price-change-message {
3194
+ flex: 1;
3195
+ }
3196
+
3197
+ .price-change-title {
3198
+ font-size: var(--text-xl, 1.25rem);
3199
+ font-weight: var(--font-weight-bold, 600);
3200
+ margin: 0 0 var(--spacing-element, 0.5rem) 0;
3201
+ color: var(--color-warning-dark, #856404);
3202
+ }
3203
+
3204
+ .price-change-banner.price-decrease .price-change-title {
3205
+ color: var(--color-success-dark, #155724);
3206
+ }
3207
+
3208
+ .price-change-banner.price-critical .price-change-title {
3209
+ color: var(--color-error-dark, #721c24);
3210
+ }
3211
+
3212
+ .price-change-description {
3213
+ margin: 0 0 0.75rem 0;
3214
+ color: #856404;
3215
+ line-height: 1.5;
3216
+ }
3217
+
3218
+ .price-change-banner.price-decrease .price-change-description {
3219
+ color: #155724;
3220
+ }
3221
+
3222
+ .price-change-banner.price-critical .price-change-description {
3223
+ color: #721c24;
3224
+ }
3225
+
3226
+ .price-change-warnings-list {
3227
+ margin: 0.75rem 0 0 0;
3228
+ padding-left: 1.5rem;
3229
+ color: #856404;
3230
+ }
3231
+
3232
+ .price-change-warnings-list li {
3233
+ margin-bottom: 0.25rem;
3234
+ }
3235
+
3236
+ .price-change-actions {
3237
+ display: flex;
3238
+ gap: 0.75rem;
3239
+ flex-shrink: 0;
3240
+ }
3241
+
3242
+ /* Price Change Details */
3243
+ .price-change-details {
3244
+ background: var(--color-surface, #f8f9fa);
3245
+ border: 1px solid var(--color-border, #dee2e6);
3246
+ border-radius: var(--border-radius-medium, 8px);
3247
+ padding: var(--spacing-component, 1.5rem);
3248
+ margin-bottom: var(--spacing-section, 2rem);
3249
+ }
3250
+
3251
+ .price-change-details-title {
3252
+ font-size: var(--text-lg, 1.125rem);
3253
+ font-weight: var(--font-weight-bold, 600);
3254
+ margin: 0 0 var(--spacing-component, 1rem) 0;
3255
+ color: var(--color-text, #1a1a1a);
3256
+ }
3257
+
3258
+ .price-change-items-list {
3259
+ display: flex;
3260
+ flex-direction: column;
3261
+ gap: 1rem;
3262
+ }
3263
+
3264
+ .price-change-item {
3265
+ display: flex;
3266
+ justify-content: space-between;
3267
+ align-items: center;
3268
+ padding: 1rem;
3269
+ background: white;
3270
+ border-radius: 6px;
3271
+ border: 1px solid #e9ecef;
3272
+ }
3273
+
3274
+ .price-change-item-info {
3275
+ flex: 1;
3276
+ }
3277
+
3278
+ .price-change-item-title {
3279
+ font-size: 1rem;
3280
+ font-weight: 500;
3281
+ margin: 0 0 0.25rem 0;
3282
+ color: #1a1a1a;
3283
+ }
3284
+
3285
+ .price-change-item-sku {
3286
+ font-size: 0.875rem;
3287
+ color: #6c757d;
3288
+ margin: 0;
3289
+ }
3290
+
3291
+ .price-change-item-prices {
3292
+ display: flex;
3293
+ align-items: center;
3294
+ gap: 0.5rem;
3295
+ }
3296
+
3297
+ .price-change-old-price {
3298
+ text-decoration: line-through;
3299
+ color: #6c757d;
3300
+ font-size: 0.9375rem;
3301
+ }
3302
+
3303
+ .price-change-arrow {
3304
+ color: #6c757d;
3305
+ font-weight: 600;
3306
+ }
3307
+
3308
+ .price-change-new-price {
3309
+ font-weight: 600;
3310
+ color: #1a1a1a;
3311
+ font-size: 1rem;
3312
+ }
3313
+
3314
+ .price-change-percentage {
3315
+ font-size: 0.875rem;
3316
+ font-weight: 500;
3317
+ padding: 0.25rem 0.5rem;
3318
+ border-radius: 4px;
3319
+ }
3320
+
3321
+ .price-change-percentage.price-increase {
3322
+ background: #fff3cd;
3323
+ color: #856404;
3324
+ }
3325
+
3326
+ .price-change-percentage.price-decrease {
3327
+ background: #d1f2eb;
3328
+ color: #155724;
3329
+ }
3330
+
3331
+ /* Price Change Acknowledgment */
3332
+ .price-change-acknowledgment {
3333
+ margin-top: 1rem;
3334
+ padding: 1rem;
3335
+ background: #f8f9fa;
3336
+ border-radius: 8px;
3337
+ border: 1px solid #dee2e6;
3338
+ }
3339
+
3340
+ .price-change-acknowledgment .checkbox-label {
3341
+ display: flex;
3342
+ align-items: flex-start;
3343
+ gap: 0.75rem;
3344
+ cursor: pointer;
3345
+ font-size: 0.9375rem;
3346
+ line-height: 1.5;
3347
+ }
3348
+
3349
+ .price-change-acknowledgment input[type="checkbox"] {
3350
+ margin-top: 0.25rem;
3351
+ flex-shrink: 0;
3352
+ }
3353
+
3354
+ @media (max-width: 768px) {
3355
+ .price-change-banner-content {
3356
+ flex-direction: column;
3357
+ }
3358
+
3359
+ .price-change-actions {
3360
+ width: 100%;
3361
+ flex-direction: column;
3362
+ }
3363
+
3364
+ .price-change-actions .btn {
3365
+ width: 100%;
3366
+ }
3367
+
3368
+ .price-change-item {
3369
+ flex-direction: column;
3370
+ align-items: flex-start;
3371
+ gap: 0.75rem;
3372
+ }
3373
+
3374
+ .price-change-item-prices {
3375
+ width: 100%;
3376
+ justify-content: space-between;
3377
+ }
3378
+ }
3379
+
3380
+ .checkout-layout {
3381
+ display: grid;
3382
+ grid-template-columns: 1fr;
3383
+ gap: 0;
3384
+ max-width: 1200px;
3385
+ margin: 0 auto;
3386
+ align-items: start; /* Align items to top */
3387
+ }
3388
+
3389
+ /* Desktop: Two-column layout - Shopify Horizon style */
3390
+ @media (min-width: 1000px) {
3391
+ .checkout-layout {
3392
+ grid-template-columns: 1fr 440px;
3393
+ gap: 0;
3394
+ align-items: start;
3395
+ }
3396
+
3397
+ .checkout-main {
3398
+ max-width: 100%;
3399
+ border-right: 1px solid #e5e7eb;
3400
+ padding-right: 3rem;
3401
+ }
3402
+
3403
+ .checkout-sidebar {
3404
+ position: sticky;
3405
+ top: 1rem; /* Reduced top offset to optimize space */
3406
+ align-self: start;
3407
+ padding-left: 3rem;
3408
+ background: #fafafa;
3409
+ max-height: calc(100vh - 2rem); /* Optimized height calculation */
3410
+ overflow-y: auto; /* Allow sidebar content to scroll */
3411
+ overflow-x: hidden;
3412
+ }
3413
+ }
3414
+
3415
+ .checkout-main {
3416
+ background: #ffffff;
3417
+ border-radius: 0;
3418
+ padding: 1.5rem 1.5rem; /* Reduced top padding to optimize space */
3419
+ box-shadow: none;
3420
+ }
3421
+
3422
+ .checkout-sidebar {
3423
+ background: #fafafa;
3424
+ border-radius: 0;
3425
+ padding: 1rem 1.5rem; /* Further reduced padding to optimize space */
3426
+ box-shadow: none;
3427
+ display: flex;
3428
+ flex-direction: column;
3429
+ min-height: 0; /* Allow flex shrinking */
3430
+ }
3431
+
3432
+ .checkout-section {
3433
+ margin-bottom: 2rem; /* Reduced spacing to optimize space */
3434
+ padding-bottom: 2rem; /* Reduced spacing to optimize space */
3435
+ border-bottom: 1px solid #e5e7eb;
3436
+ }
3437
+
3438
+ .checkout-section:last-of-type {
3439
+ margin-bottom: 0;
3440
+ border-bottom: none;
3441
+ padding-bottom: 0;
3442
+ }
3443
+
3444
+ .checkout-section-title {
3445
+ font-size: 2.1rem; /* 21px for H2 section titles */
3446
+ font-weight: 600;
3447
+ color: #000000;
3448
+ margin: 0 0 1.5rem;
3449
+ letter-spacing: 0;
3450
+ text-transform: none;
3451
+ line-height: 1.2;
3452
+ }
3453
+
3454
+ .checkout-section-title h2,
3455
+ h2.checkout-section-title {
3456
+ font-size: 2.1rem; /* 21px */
3457
+ font-weight: 600;
3458
+ color: #000000;
3459
+ margin: 0 0 1.5rem;
3460
+ }
3461
+
3462
+ .checkout-section-title h3,
3463
+ h3.checkout-section-title {
3464
+ font-size: 1.4rem; /* 14px */
3465
+ font-weight: 600;
3466
+ color: #000000;
3467
+ margin: 0 0 1rem;
3468
+ }
3469
+
3470
+ .checkout-section-content {
3471
+ margin-top: 0;
3472
+ flex: 1 1 auto;
3473
+ min-height: 0; /* Allow flex shrinking */
3474
+ overflow-y: auto; /* Allow scrolling of coupon content */
3475
+ overflow-x: hidden;
3476
+ }
3477
+
3478
+ /* Form Styles - Shopify style */
3479
+ .form-grid {
3480
+ display: grid;
3481
+ grid-template-columns: 1fr;
3482
+ gap: 1rem;
3483
+ }
3484
+
3485
+ @media (min-width: 640px) {
3486
+ .form-grid {
3487
+ grid-template-columns: repeat(2, 1fr);
3488
+ }
3489
+
3490
+ .form-group-full {
3491
+ grid-column: 1 / -1;
3492
+ }
3493
+ }
3494
+
3495
+ /* intl-tel-input styling */
3496
+ .iti {
3497
+ width: 100%;
3498
+ }
3499
+
3500
+ .iti__flag-container {
3501
+ z-index: 1;
3502
+ }
3503
+
3504
+ .iti__selected-flag {
3505
+ padding: 0 0.75rem 0 0.5rem;
3506
+ border-right: 1px solid var(--color-border, #e5e7eb);
3507
+ }
3508
+
3509
+ .iti__selected-flag:hover {
3510
+ background-color: var(--color-background, #f9fafb);
3511
+ }
3512
+
3513
+ .iti__country-list {
3514
+ z-index: 1000;
3515
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
3516
+ border: 1px solid var(--color-border, #e5e7eb);
3517
+ border-radius: var(--border-radius-medium, 8px);
3518
+ max-height: 200px;
3519
+ overflow-y: auto;
3520
+ }
3521
+
3522
+ .iti__country {
3523
+ padding: 0.5rem 0.75rem;
3524
+ }
3525
+
3526
+ .iti__country:hover,
3527
+ .iti__country.iti__highlight {
3528
+ background-color: var(--color-primary-light, #dbeafe);
3529
+ }
3530
+
3531
+ #shipping-phone {
3532
+ padding-left: 3.5rem;
3533
+ }
3534
+
3535
+ .form-group {
3536
+ display: flex;
3537
+ flex-direction: column;
3538
+ gap: 0.5rem;
3539
+ }
3540
+
3541
+ .form-label {
3542
+ font-size: 1.4rem; /* 14px */
3543
+ font-weight: 500;
3544
+ color: #000000;
3545
+ display: block;
3546
+ margin-bottom: 0.5rem;
3547
+ }
3548
+
3549
+ .form-input,
3550
+ .form-select,
3551
+ .form-textarea {
3552
+ width: 100%;
3553
+ padding: 13.5px 11px; /* Exact Shopify padding */
3554
+ font-size: 1.4rem; /* 14px */
3555
+ color: #000000;
3556
+ background: #ffffff;
3557
+ border: 1px solid #e5e7eb; /* Lighter border */
3558
+ border-radius: 8px; /* 8px border radius */
3559
+ transition: border-color 0.15s ease;
3560
+ font-family: inherit;
3561
+ -webkit-appearance: none;
3562
+ -moz-appearance: none;
3563
+ appearance: none;
3564
+ line-height: 1.5;
3565
+ }
3566
+
3567
+ .form-input::placeholder,
3568
+ .form-textarea::placeholder {
3569
+ color: #9ca3af;
3570
+ }
3571
+
3572
+ .form-select {
3573
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23000000' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
3574
+ background-repeat: no-repeat;
3575
+ background-position: right 1rem center;
3576
+ background-size: 12px;
3577
+ padding-right: 2.75rem;
3578
+ }
3579
+
3580
+ .form-input:focus,
3581
+ .form-select:focus,
3582
+ .form-textarea:focus {
3583
+ outline: none;
3584
+ border-color: #000000;
3585
+ box-shadow: none; /* Remove box-shadow for cleaner look */
3586
+ }
3587
+
3588
+ .form-textarea {
3589
+ resize: vertical;
3590
+ min-height: 100px;
3591
+ }
3592
+
3593
+ .checkbox-label {
3594
+ display: flex;
3595
+ align-items: flex-start;
3596
+ gap: 0.75rem;
3597
+ cursor: pointer;
3598
+ font-size: 1.4rem; /* 14px */
3599
+ color: #000000;
3600
+ line-height: 1.5;
3601
+ }
3602
+
3603
+ .checkbox-label input[type="checkbox"] {
3604
+ width: 18px;
3605
+ height: 18px;
3606
+ cursor: pointer;
3607
+ margin-top: 0.125rem;
3608
+ flex-shrink: 0;
3609
+ appearance: none;
3610
+ -webkit-appearance: none;
3611
+ -moz-appearance: none;
3612
+ border: 2px solid #d1d5db;
3613
+ border-radius: 4px; /* Slightly more rounded */
3614
+ background-color: #ffffff;
3615
+ position: relative;
3616
+ transition: all 0.2s ease;
3617
+ }
3618
+
3619
+ .checkbox-label input[type="checkbox"]:hover {
3620
+ border-color: #000000;
3621
+ }
3622
+
3623
+ .checkbox-label input[type="checkbox"]:checked {
3624
+ background-color: #000000;
3625
+ border-color: #000000;
3626
+ }
3627
+
3628
+ .checkbox-label input[type="checkbox"]:checked::after {
3629
+ content: '';
3630
+ position: absolute;
3631
+ left: 4px;
3632
+ top: 1px;
3633
+ width: 5px;
3634
+ height: 10px;
3635
+ border: solid #ffffff;
3636
+ border-width: 0 2px 2px 0;
3637
+ transform: rotate(45deg);
3638
+ display: block;
3639
+ }
3640
+
3641
+ .checkbox-label input[type="checkbox"]:focus {
3642
+ outline: 2px solid #000000;
3643
+ outline-offset: 2px;
3644
+ }
3645
+
3646
+ /* Payment Methods */
3647
+ .payment-methods {
3648
+ display: flex;
3649
+ flex-direction: column;
3650
+ gap: 1.5rem;
3651
+ margin-bottom: 1.5rem;
3652
+ }
3653
+
3654
+ .payment-method-group {
3655
+ display: flex;
3656
+ flex-direction: column;
3657
+ gap: 0.75rem;
3658
+ }
3659
+
3660
+ .payment-method-group-title {
3661
+ font-size: 1.4rem; /* 14px for H3 */
3662
+ font-weight: 600;
3663
+ color: #000000;
3664
+ margin: 0 0 1rem;
3665
+ }
3666
+
3667
+ .payment-method {
3668
+ border: 1px solid #e5e7eb;
3669
+ border-radius: 8px; /* 8px border radius */
3670
+ padding: 1rem;
3671
+ transition: all 0.15s ease;
3672
+ background: #ffffff;
3673
+ }
3674
+
3675
+ .payment-method:hover {
3676
+ border-color: #d1d5db;
3677
+ background: #fafafa;
3678
+ }
3679
+
3680
+ .payment-method:has(input[type="radio"]:checked) {
3681
+ border-color: #000000;
3682
+ background: #ffffff;
3683
+ }
3684
+
3685
+ .payment-method-label {
3686
+ display: flex;
3687
+ align-items: flex-start;
3688
+ gap: 0.75rem;
3689
+ cursor: pointer;
3690
+ margin: 0;
3691
+ }
3692
+
3693
+ .payment-method-label input[type="radio"] {
3694
+ width: 18px;
3695
+ height: 18px;
3696
+ cursor: pointer;
3697
+ margin-top: 0.125rem;
3698
+ flex-shrink: 0;
3699
+ appearance: none;
3700
+ -webkit-appearance: none;
3701
+ -moz-appearance: none;
3702
+ border: 2px solid #d1d5db;
3703
+ border-radius: 50%;
3704
+ background-color: #ffffff;
3705
+ position: relative;
3706
+ transition: all 0.2s ease;
3707
+ }
3708
+
3709
+ .payment-method-label input[type="radio"]:hover {
3710
+ border-color: #000000;
3711
+ }
3712
+
3713
+ .payment-method-label input[type="radio"]:checked {
3714
+ border-color: #000000;
3715
+ background-color: #ffffff;
3716
+ }
3717
+
3718
+ .payment-method-label input[type="radio"]:checked::after {
3719
+ background-color: #000000;
3720
+ }
3721
+
3722
+ .payment-method-label input[type="radio"]:checked::after {
3723
+ content: '';
3724
+ position: absolute;
3725
+ left: 50%;
3726
+ top: 50%;
3727
+ transform: translate(-50%, -50%);
3728
+ width: 8px;
3729
+ height: 8px;
3730
+ border-radius: 50%;
3731
+ background-color: #111827;
3732
+ display: block;
3733
+ }
3734
+
3735
+ .payment-method-label input[type="radio"]:focus {
3736
+ outline: 2px solid #000000;
3737
+ outline-offset: 2px;
3738
+ }
3739
+
3740
+ .payment-method-content {
3741
+ flex: 1;
3742
+ display: flex;
3743
+ flex-direction: column;
3744
+ gap: 0.25rem;
3745
+ }
3746
+
3747
+ .payment-method-name {
3748
+ font-size: 1.4rem; /* 14px */
3749
+ font-weight: 500;
3750
+ color: #000000;
3751
+ }
3752
+
3753
+ .payment-method-description {
3754
+ font-size: 1.4rem; /* 14px */
3755
+ color: #6b7280;
3756
+ font-weight: normal;
3757
+ }
3758
+
3759
+ .payment-method-balance {
3760
+ font-size: 0.8125rem;
3761
+ color: #059669;
3762
+ font-weight: 500;
3763
+ }
3764
+
3765
+ .payment-method-config {
3766
+ display: flex;
3767
+ flex-wrap: wrap;
3768
+ gap: 0.5rem;
3769
+ margin-top: 0.5rem;
3770
+ padding-top: 0.5rem;
3771
+ border-top: 1px solid #f3f4f6;
3772
+ }
3773
+
3774
+ .config-text,
3775
+ .config-fee,
3776
+ .config-min,
3777
+ .config-max {
3778
+ font-size: 1.2rem; /* 12px */
3779
+ color: #6b7280;
3780
+ padding: 0.25rem 0.5rem;
3781
+ background: #f9fafb;
3782
+ border-radius: 8px; /* 8px border radius */
3783
+ }
3784
+
3785
+ .config-fee {
3786
+ color: #dc2626;
3787
+ font-weight: 500;
3788
+ }
3789
+
3790
+ .payment-methods-empty {
3791
+ padding: 1rem;
3792
+ text-align: center;
3793
+ color: #6b7280;
3794
+ font-size: 1.4rem; /* 14px */
3795
+ }
3796
+
3797
+ /* Shipping Methods - Product-based Layout */
3798
+ .shipping-product-row {
3799
+ display: flex;
3800
+ gap: 2rem;
3801
+ align-items: flex-start;
3802
+ margin-bottom: 2rem;
3803
+ padding-bottom: 2rem;
3804
+ border-bottom: 1px solid #e5e7eb;
3805
+ }
3806
+
3807
+ .shipping-product-row:last-child {
3808
+ margin-bottom: 0;
3809
+ padding-bottom: 0;
3810
+ border-bottom: none;
3811
+ }
3812
+
3813
+ .shipping-product-column {
3814
+ flex: 0 0 300px;
3815
+ display: flex;
3816
+ flex-direction: column;
3817
+ gap: 1rem;
3818
+ }
3819
+
3820
+ .shipping-product-item {
3821
+ display: flex;
3822
+ gap: 1rem;
3823
+ align-items: center;
3824
+ }
3825
+
3826
+ .shipping-product-image {
3827
+ width: 60px;
3828
+ height: 60px;
3829
+ object-fit: cover;
3830
+ border-radius: 4px;
3831
+ flex-shrink: 0;
3832
+ background: #f3f4f6;
3833
+ }
3834
+
3835
+ .shipping-product-details {
3836
+ display: flex;
3837
+ flex-direction: column;
3838
+ gap: 0.25rem;
3839
+ flex: 1;
3840
+ }
3841
+
3842
+ .shipping-product-name {
3843
+ font-size: 1.4rem; /* 14px */
3844
+ font-weight: 500;
3845
+ color: #000000;
3846
+ }
3847
+
3848
+ .shipping-product-price {
3849
+ font-size: 1.4rem; /* 14px */
3850
+ color: #6b7280;
3851
+ }
3852
+
3853
+ .shipping-methods-column {
3854
+ flex: 1;
3855
+ display: flex;
3856
+ flex-direction: column;
3857
+ gap: 0.75rem;
3858
+ }
3859
+
3860
+ .shipping-unavailable-message {
3861
+ padding: 1rem;
3862
+ background-color: #fef3c7;
3863
+ border: 1px solid #fde68a;
3864
+ border-radius: 8px;
3865
+ color: #92400e;
3866
+ font-size: 1.4rem; /* 14px */
3867
+ text-align: center;
3868
+ }
3869
+
3870
+ .shipping-method {
3871
+ border: 1px solid #e5e7eb;
3872
+ border-radius: 8px; /* 8px border radius */
3873
+ padding: 1rem;
3874
+ transition: all 0.15s ease;
3875
+ background: #ffffff;
3876
+ }
3877
+
3878
+ .shipping-method:hover {
3879
+ border-color: #d1d5db;
3880
+ background: #fafafa;
3881
+ }
3882
+
3883
+ .shipping-method:has(input[type="radio"]:checked) {
3884
+ border-color: #000000;
3885
+ background: #ffffff;
3886
+ }
3887
+
3888
+ .shipping-method-label {
3889
+ display: flex;
3890
+ align-items: flex-start;
3891
+ gap: 0.75rem;
3892
+ cursor: pointer;
3893
+ margin: 0;
3894
+ }
3895
+
3896
+ .shipping-method-label input[type="radio"] {
3897
+ width: 18px;
3898
+ height: 18px;
3899
+ cursor: pointer;
3900
+ flex-shrink: 0;
3901
+ appearance: none;
3902
+ -webkit-appearance: none;
3903
+ -moz-appearance: none;
3904
+ border: 2px solid #d1d5db;
3905
+ border-radius: 50%;
3906
+ background-color: #ffffff;
3907
+ position: relative;
3908
+ transition: all 0.2s ease;
3909
+ margin-top: 2px;
3910
+ }
3911
+
3912
+ .shipping-method-label input[type="radio"]:hover {
3913
+ border-color: #000000;
3914
+ }
3915
+
3916
+ .shipping-method-label input[type="radio"]:checked {
3917
+ border-color: #000000;
3918
+ background-color: #ffffff;
3919
+ }
3920
+
3921
+ .shipping-method-label input[type="radio"]:checked::after {
3922
+ content: '';
3923
+ position: absolute;
3924
+ left: 50%;
3925
+ top: 50%;
3926
+ transform: translate(-50%, -50%);
3927
+ width: 8px;
3928
+ height: 8px;
3929
+ border-radius: 50%;
3930
+ background-color: #000000;
3931
+ display: block;
3932
+ }
3933
+
3934
+ .shipping-method-label input[type="radio"]:focus {
3935
+ outline: 2px solid #000000;
3936
+ outline-offset: 2px;
3937
+ }
3938
+
3939
+ .shipping-method-content {
3940
+ flex: 1;
3941
+ display: flex;
3942
+ flex-direction: column;
3943
+ gap: 0.5rem;
3944
+ }
3945
+
3946
+ .shipping-method-title-row {
3947
+ display: flex;
3948
+ justify-content: space-between;
3949
+ align-items: center;
3950
+ gap: 1rem;
3951
+ }
3952
+
3953
+ .shipping-method-name {
3954
+ font-size: 1.4rem; /* 14px */
3955
+ font-weight: 500;
3956
+ color: #000000;
3957
+ }
3958
+
3959
+ .shipping-method-price {
3960
+ font-size: 1.4rem; /* 14px */
3961
+ font-weight: 500;
3962
+ color: #000000;
3963
+ }
3964
+
3965
+ .shipping-method-time-row {
3966
+ display: flex;
3967
+ flex-direction: column;
3968
+ gap: 0.25rem;
3969
+ margin-top: 0.25rem;
3970
+ }
3971
+
3972
+ .shipping-method-time {
3973
+ font-size: 1.2rem; /* 12px */
3974
+ color: #6b7280;
3975
+ line-height: 1.5;
3976
+ }
3977
+
3978
+ /* Responsive: Stack columns on mobile */
3979
+ @media (max-width: 768px) {
3980
+ .shipping-product-row {
3981
+ flex-direction: column;
3982
+ gap: 1.5rem;
3983
+ }
3984
+
3985
+ .shipping-product-column {
3986
+ flex: 1;
3987
+ width: 100%;
3988
+ }
3989
+
3990
+ .shipping-methods-column {
3991
+ width: 100%;
3992
+ }
3993
+ }
3994
+
3995
+ .shipping-methods-loading,
3996
+ .shipping-methods-empty,
3997
+ .shipping-methods-error {
3998
+ padding: 1rem;
3999
+ text-align: center;
4000
+ color: #6b7280;
4001
+ font-size: 1.4rem; /* 14px */
4002
+ }
4003
+
4004
+ .shipping-methods-error {
4005
+ color: #dc2626;
4006
+ }
4007
+
4008
+ .payment-form {
4009
+ margin-top: 1.5rem;
4010
+ padding-top: 1.5rem;
4011
+ border-top: 1px solid #e5e7eb;
4012
+ }
4013
+
4014
+ .payment-gateway-forms {
4015
+ margin-top: 1.5rem;
4016
+ padding-top: 1.5rem;
4017
+ border-top: 1px solid #e5e7eb;
4018
+ }
4019
+
4020
+ /* Order Summary - Shopify Horizon Style */
4021
+ .order-summary {
4022
+ display: flex;
4023
+ flex-direction: column;
4024
+ flex: 1 1 auto; /* Take available space in flex container */
4025
+ min-height: 0; /* Allow flex shrinking */
4026
+ }
4027
+
4028
+ .order-summary-title {
4029
+ font-size: 2.1rem; /* 21px for H2 */
4030
+ font-weight: 600;
4031
+ color: #000000;
4032
+ margin: 0 0 0.75rem; /* Further reduced margin */
4033
+ letter-spacing: 0;
4034
+ flex-shrink: 0; /* Keep title fixed */
4035
+ }
4036
+
4037
+ .order-summary-items {
4038
+ display: flex;
4039
+ flex-direction: column;
4040
+ gap: 0.75rem; /* Reduced gap to fit more items */
4041
+ margin-bottom: 0;
4042
+ padding-bottom: 0.75rem;
4043
+ border-bottom: 1px solid #e5e7eb;
4044
+ flex: 1 1 auto; /* Take available space, allow shrinking */
4045
+ overflow-y: auto; /* Only items scroll */
4046
+ overflow-x: hidden;
4047
+ min-height: 0; /* Allow flex shrinking - critical for scrolling */
4048
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
4049
+ max-height: none; /* Remove fixed max-height, let flex handle it */
4050
+ }
4051
+
4052
+ .order-summary-item {
4053
+ display: flex;
4054
+ gap: 1rem;
4055
+ align-items: flex-start;
4056
+ }
4057
+
4058
+ .order-summary-item-image {
4059
+ flex-shrink: 0;
4060
+ width: 64px;
4061
+ height: 64px;
4062
+ border-radius: 6px;
4063
+ overflow: hidden;
4064
+ background: #f3f4f6;
4065
+ border: 1px solid #e5e7eb;
4066
+ }
4067
+
4068
+ .order-summary-item-image img {
4069
+ width: 100%;
4070
+ height: 100%;
4071
+ object-fit: cover;
4072
+ display: block;
4073
+ }
4074
+
4075
+ .order-summary-item-details {
4076
+ flex: 1;
4077
+ min-width: 0;
4078
+ }
4079
+
4080
+ .order-summary-item-title {
4081
+ font-size: 1.4rem; /* 14px */
4082
+ font-weight: 500;
4083
+ color: #000000;
4084
+ margin: 0 0 0.25rem;
4085
+ line-height: 1.4;
4086
+ }
4087
+
4088
+ .order-summary-item-variant {
4089
+ font-size: 1.4rem; /* 14px */
4090
+ color: #6b7280;
4091
+ margin: 0 0 0.25rem;
4092
+ }
4093
+
4094
+ .order-summary-item-quantity {
4095
+ font-size: 1.4rem; /* 14px */
4096
+ color: #6b7280;
4097
+ margin: 0;
4098
+ }
4099
+
4100
+ .order-summary-item-price {
4101
+ font-size: 1.4rem; /* 14px */
4102
+ font-weight: 600;
4103
+ color: #000000;
4104
+ flex-shrink: 0;
4105
+ margin-left: auto;
4106
+ }
4107
+
4108
+ .order-summary-totals {
4109
+ display: flex;
4110
+ flex-direction: column;
4111
+ gap: 0.5rem; /* Reduced gap */
4112
+ margin-top: auto; /* Push to bottom */
4113
+ padding-top: 0.75rem;
4114
+ padding-bottom: 0.5rem;
4115
+ flex-shrink: 0; /* Keep totals fixed, don't shrink */
4116
+ background: #fafafa; /* Match sidebar background */
4117
+ border-top: 1px solid #e5e7eb; /* Visual separator */
4118
+ }
4119
+
4120
+ .summary-line {
4121
+ display: flex;
4122
+ justify-content: space-between;
4123
+ align-items: center;
4124
+ padding: 0.5rem 0;
4125
+ font-size: 1.4rem; /* 14px */
4126
+ }
4127
+
4128
+ .summary-line.summary-discount {
4129
+ color: #059669; /* Green color for discount */
4130
+ }
4131
+
4132
+ .summary-line.summary-discount .summary-label {
4133
+ color: #059669;
4134
+ }
4135
+
4136
+ .summary-line.summary-discount .summary-value {
4137
+ color: #059669;
4138
+ font-weight: 500;
4139
+ }
4140
+
4141
+ .summary-label {
4142
+ color: #6b7280;
4143
+ font-size: 1.4rem; /* 14px */
4144
+ }
4145
+
4146
+ .summary-value {
4147
+ color: #000000;
4148
+ font-weight: 500;
4149
+ font-size: 1.4rem; /* 14px */
4150
+ }
4151
+
4152
+ .summary-total {
4153
+ padding-top: 1rem;
4154
+ border-top: 1px solid #e5e7eb;
4155
+ margin-top: 0.5rem;
4156
+ }
4157
+
4158
+ .summary-total .summary-label {
4159
+ font-size: 1.4rem; /* 14px */
4160
+ font-weight: 600;
4161
+ color: #000000;
4162
+ }
4163
+
4164
+ .summary-total .summary-value {
4165
+ font-size: 1.4rem; /* 14px */
4166
+ font-weight: 600;
4167
+ color: #000000;
4168
+ }
4169
+
4170
+ /* Checkout Actions */
4171
+ .checkout-actions {
4172
+ display: flex;
4173
+ flex-direction: column;
4174
+ gap: 0.75rem;
4175
+ margin-top: 2rem;
4176
+ padding-top: 2rem;
4177
+ border-top: 1px solid #e5e7eb;
4178
+ }
4179
+
4180
+ .checkout-actions .btn {
4181
+ width: 100%;
4182
+ padding: 1.4rem 1.5rem; /* Match Shopify button padding */
4183
+ font-size: 1.4rem; /* 14px */
4184
+ font-weight: 500;
4185
+ border-radius: 8px; /* 8px border radius */
4186
+ transition: all 0.15s ease;
4187
+ text-align: center;
4188
+ cursor: pointer;
4189
+ border: none;
4190
+ font-family: inherit;
4191
+ }
4192
+
4193
+ .checkout-actions .btn-primary {
4194
+ background: #000000;
4195
+ color: #ffffff;
4196
+ border: 1px solid #000000;
4197
+ }
4198
+
4199
+ .checkout-actions .btn-primary:hover:not(:disabled) {
4200
+ background: #1a1a1a;
4201
+ border-color: #1a1a1a;
4202
+ }
4203
+
4204
+ .checkout-actions .btn-primary:disabled {
4205
+ opacity: 0.6;
4206
+ cursor: not-allowed;
4207
+ }
4208
+
4209
+ /* Checkout Button Loading State */
4210
+ .checkout-button-loading {
4211
+ position: relative;
4212
+ }
4213
+
4214
+ .checkout-button-loading .checkout-button-icon {
4215
+ display: inline-block;
4216
+ }
4217
+
4218
+ /* Button loading states */
4219
+ .btn.loading {
4220
+ opacity: 0.7;
4221
+ cursor: not-allowed;
4222
+ pointer-events: none;
4223
+ transition: opacity 0.2s ease;
4224
+ }
4225
+
4226
+ .btn-loading {
4227
+ display: flex;
4228
+ align-items: center;
4229
+ gap: 0.5rem;
4230
+ }
4231
+
4232
+ .btn-spinner {
4233
+ width: 16px;
4234
+ height: 16px;
4235
+ border: 2px solid currentColor;
4236
+ border-top-color: transparent;
4237
+ border-radius: 50%;
4238
+ animation: spin 0.6s linear infinite;
4239
+ }
4240
+
4241
+ .checkout-button-text {
4242
+ transition: opacity 0.2s ease;
4243
+ }
4244
+
4245
+ @keyframes spin {
4246
+ from {
4247
+ transform: rotate(0deg);
4248
+ }
4249
+ to {
4250
+ transform: rotate(360deg);
4251
+ }
4252
+ }
4253
+
4254
+ .checkout-actions .btn-outline {
4255
+ background: transparent;
4256
+ color: #000000;
4257
+ border: 1px solid #d1d5db;
4258
+ }
4259
+
4260
+ .checkout-actions .btn-outline:hover {
4261
+ background: #fafafa;
4262
+ border-color: #9ca3af;
4263
+ }
4264
+
4265
+ /* Coupon Section Styles */
4266
+ .checkout-coupons-section {
4267
+ margin-bottom: 2rem;
4268
+ padding: 1.5rem;
4269
+ background: #fff;
4270
+ border: 1px solid #e5e7eb;
4271
+ border-radius: 0.8rem;
4272
+ flex-shrink: 0; /* Don't shrink, but allow scrolling within */
4273
+ max-height: 40vh; /* Limit coupon section height */
4274
+ display: flex;
4275
+ flex-direction: column;
4276
+ }
4277
+
4278
+ .checkout-coupons-section .checkout-section-title {
4279
+ font-size: 1.6rem;
4280
+ font-weight: 600;
4281
+ margin-bottom: 1.2rem;
4282
+ color: #111827;
4283
+ flex-shrink: 0; /* Keep title fixed */
4284
+ }
4285
+
4286
+ .coupons-loading,
4287
+ .coupons-empty {
4288
+ padding: 1rem;
4289
+ text-align: center;
4290
+ color: #6b7280;
4291
+ font-size: 1.4rem;
4292
+ }
4293
+
4294
+ .coupons-error {
4295
+ padding: 1rem;
4296
+ background: #fef2f2;
4297
+ border: 1px solid #fecaca;
4298
+ border-radius: 0.4rem;
4299
+ color: #991b1b;
4300
+ font-size: 1.4rem;
4301
+ margin-top: 1rem;
4302
+ }
4303
+
4304
+ .coupons-list {
4305
+ display: flex;
4306
+ flex-direction: column;
4307
+ gap: 1rem;
4308
+ min-height: 0; /* Allow flex shrinking */
4309
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
4310
+ }
4311
+
4312
+ .coupon-item {
4313
+ display: flex;
4314
+ justify-content: space-between;
4315
+ align-items: center;
4316
+ padding: 1.2rem;
4317
+ background: #f9fafb;
4318
+ border: 1px solid #e5e7eb;
4319
+ border-radius: 0.6rem;
4320
+ transition: all 0.2s ease;
4321
+ }
4322
+
4323
+ .coupon-item:hover {
4324
+ border-color: #d1d5db;
4325
+ background: #f3f4f6;
4326
+ }
4327
+
4328
+ .coupon-info {
4329
+ flex: 1;
4330
+ display: flex;
4331
+ flex-direction: column;
4332
+ gap: 0.4rem;
4333
+ }
4334
+
4335
+ .coupon-code {
4336
+ font-size: 1.5rem;
4337
+ font-weight: 600;
4338
+ color: #111827;
4339
+ }
4340
+
4341
+ .coupon-description {
4342
+ font-size: 1.3rem;
4343
+ color: #6b7280;
4344
+ }
4345
+
4346
+ .coupon-min-amount {
4347
+ font-size: 1.2rem;
4348
+ color: #9ca3af;
4349
+ margin-top: 0.4rem;
4350
+ }
4351
+
4352
+ .apply-coupon-btn {
4353
+ padding: 0.8rem 1.6rem;
4354
+ font-size: 1.4rem;
4355
+ font-weight: 500;
4356
+ white-space: nowrap;
4357
+ }
4358
+
4359
+ .apply-coupon-btn:disabled {
4360
+ opacity: 0.6;
4361
+ cursor: not-allowed;
4362
+ }
4363
+
4364
+ .applied-coupon {
4365
+ margin-top: 1rem;
4366
+ padding: 1.2rem;
4367
+ background: #ecfdf5;
4368
+ border: 1px solid #86efac;
4369
+ border-radius: 0.6rem;
4370
+ }
4371
+
4372
+ .applied-coupon-info {
4373
+ display: flex;
4374
+ justify-content: space-between;
4375
+ align-items: center;
4376
+ }
4377
+
4378
+ .applied-coupon-code {
4379
+ font-size: 1.4rem;
4380
+ font-weight: 600;
4381
+ color: #065f46;
4382
+ }
4383
+
4384
+ .btn-remove-coupon {
4385
+ padding: 0.6rem 1.2rem;
4386
+ font-size: 1.3rem;
4387
+ background: #dc2626;
4388
+ color: #fff;
4389
+ border: none;
4390
+ border-radius: 0.4rem;
4391
+ cursor: pointer;
4392
+ transition: background 0.2s ease;
4393
+ }
4394
+
4395
+ .btn-remove-coupon:hover {
4396
+ background: #b91c1c;
4397
+ }
4398
+
4399
+ /* Custom Scrollbar Styling for Better UX */
4400
+ .checkout-sidebar::-webkit-scrollbar,
4401
+ .order-summary-items::-webkit-scrollbar,
4402
+ .checkout-section-content::-webkit-scrollbar {
4403
+ width: 8px;
4404
+ }
4405
+
4406
+ .checkout-sidebar::-webkit-scrollbar-track,
4407
+ .order-summary-items::-webkit-scrollbar-track,
4408
+ .checkout-section-content::-webkit-scrollbar-track {
4409
+ background: #f1f1f1;
4410
+ border-radius: 4px;
4411
+ }
4412
+
4413
+ .checkout-sidebar::-webkit-scrollbar-thumb,
4414
+ .order-summary-items::-webkit-scrollbar-thumb,
4415
+ .checkout-section-content::-webkit-scrollbar-thumb {
4416
+ background: #888;
4417
+ border-radius: 4px;
4418
+ }
4419
+
4420
+ .checkout-sidebar::-webkit-scrollbar-thumb:hover,
4421
+ .order-summary-items::-webkit-scrollbar-thumb:hover,
4422
+ .checkout-section-content::-webkit-scrollbar-thumb:hover {
4423
+ background: #555;
4424
+ }
4425
+
4426
+ /* Firefox scrollbar styling */
4427
+ .checkout-sidebar,
4428
+ .order-summary-items,
4429
+ .checkout-section-content {
4430
+ scrollbar-width: thin;
4431
+ scrollbar-color: #888 #f1f1f1;
4432
+ }
4433
+
4434
+ /* Mobile Optimizations */
4435
+ @media (max-width: 1023px) {
4436
+ .checkout-page {
4437
+ padding: 1rem 0 2rem;
4438
+ }
4439
+
4440
+ .checkout-container {
4441
+ padding: 0 1rem;
4442
+ }
4443
+
4444
+ .checkout-title {
4445
+ font-size: 2.4rem; /* Slightly smaller on mobile */
4446
+ }
4447
+
4448
+ .checkout-sidebar {
4449
+ position: relative; /* Remove sticky on mobile */
4450
+ top: 0;
4451
+ max-height: none;
4452
+ padding: 1.5rem 1rem;
4453
+ overflow: visible; /* Allow natural flow on mobile - no scrollbar */
4454
+ }
4455
+
4456
+ .order-summary {
4457
+ max-height: none; /* Remove height restriction on mobile */
4458
+ }
4459
+
4460
+ .order-summary-items {
4461
+ max-height: none; /* Remove height restriction on mobile */
4462
+ flex: none; /* Don't use flex on mobile */
4463
+ overflow: visible; /* Allow natural flow - no scrollbar on mobile */
4464
+ }
4465
+
4466
+ .order-summary-totals {
4467
+ position: relative; /* Remove sticky on mobile */
4468
+ margin-top: 1rem;
4469
+ }
4470
+
4471
+ .checkout-coupons-section {
4472
+ max-height: none; /* Remove height restriction on mobile */
4473
+ }
4474
+
4475
+ .checkout-section-content {
4476
+ max-height: none; /* Remove height restriction on mobile */
4477
+ overflow: visible; /* Allow natural flow - no scrollbar on mobile */
4478
+ }
4479
+
4480
+ .checkout-section {
4481
+ margin-bottom: 1.5rem;
4482
+ padding-bottom: 1.5rem;
4483
+ }
4484
+ }
4485
+
4486
+ @media (max-width: 640px) {
4487
+ .form-grid {
4488
+ grid-template-columns: 1fr;
4489
+ }
4490
+
4491
+ .checkout-section {
4492
+ margin-bottom: 1.5rem;
4493
+ padding-bottom: 1.5rem;
4494
+ }
4495
+
4496
+ .checkout-section-title {
4497
+ font-size: 1.8rem; /* Slightly smaller on mobile */
4498
+ }
4499
+
4500
+ .checkout-section-title h2,
4501
+ h2.checkout-section-title {
4502
+ font-size: 1.8rem;
4503
+ }
4504
+
4505
+ .checkout-section-title h3,
4506
+ h3.checkout-section-title {
4507
+ font-size: 1.3rem;
4508
+ }
4509
+ }
4510
+ </style>
4511
+