@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.
- package/README.md +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- 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
|
+
|