PageSourceSearch

https://smartsignpro.ai/assets/controllers/checkout_form_controlle…cab52ee8e0b42dc992b1467f2f70c0d7a1d00ea9.js

js smartsignpro.ai collected 2026-09-24 14:07:48 UTC 14,888 bytes, 416 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = [
5    "fulfillmentType", "deliveryFields",
6    "tipPercent", "customTip", "customTipRadio",
7    "tipAmount", "serviceFee", "serviceFeeRow", "total",
8    "deliveryRow", "deliveryAmount", "deliveryLabel",
9    "discountCode", "applyButton", "discountMessage", "discountRow", "discountAmount",
10    "specialsRow", "specialsAmount",
11    "taxRow", "taxAmount", "subtotalAmount",
12    "freeItemRow", "freeItemName", "freeItemPrice",
13    "emailField",
14    "deliveryPhone", "deliveryLine1", "deliveryLine2", "deliveryCity", "deliveryState", "deliveryPostal",
15    "quoteButton", "quoteMessage", "shippingMethod"
16  ]
17
18  static values = {
19    subtotalCents: Number,
20    discountCents: Number,
21    specialsDiscountCents: Number,
22    taxCents: Number,
23    deliveryFeeCents: Number,
24    serviceFeeIncluded: Boolean,
25    serviceFeeTiers: Object,
26    signedIn: Boolean,
27    previewDiscountUrl: String,
28    emailCheckUrl: String,
29    uberDirect: Boolean,
30    deliveryQuoteUrl: String,
31    quoteRequired: Boolean,
32    shippingMode: Boolean,
33    shippingMethods: Array,
34    shippingFreeOverCents: Number,
35    freeShipping: Boolean
36  }
37
38  connect() {
39    this.emailExists = false
40    this.signinPrompted = false
41    this.quoteOk = !this.quoteRequiredValue
42    this.baseSubtotalCents = this.subtotalCentsValue
43    this.toggleDelivery()
44    this.refreshShippingFee()
45    this.recalculate()
46  }
47
48  onFulfillmentChange() {
49    this.toggleDelivery()
50    if (this.uberDirectValue && this.isDelivery()) {
51      this.quoteOk = false
52      this.setQuoteMessage("")
53    } else {
54      this.quoteOk = !this.quoteRequiredValue || !this.isDelivery()
55    }
56    this.refreshShippingFee()
57    this.recalculate()
58  }
59
60  onShippingMethodChange() {
61    this.refreshShippingFee()
62    this.recalculate()
63  }
64
65  toggleDelivery() {
66    if (!this.hasDeliveryFieldsTarget) return
67    const show = this.isDelivery() || this.shippingModeValue
68    this.deliveryFieldsTarget.classList.toggle("hidden", !show)
69  }
70
71  isDelivery() {
72    return this.hasFulfillmentTypeTarget && this.fulfillmentTypeTarget.value === "delivery"
73  }
74
75  isShipping() {
76    return this.shippingModeValue || (this.hasFulfillmentTypeTarget && this.fulfillmentTypeTarget.value === "shipping")
77  }
78
79  onTipChange(event) {
80    const radio = event.target
81    if (radio.value === "percent" && this.hasTipPercentTarget) {
82      this.tipPercentTarget.value = radio.dataset.tipPercent || "0"
83    }
84    this.recalculate()
85  }
86
87  onCustomTipInput() {
88    if (this.hasCustomTipRadioTarget && !this.customTipRadioTarget.checked) {
89      this.customTipRadioTarget.checked = true
90    }
91    this.recalculate()
92  }
93
94  selectedTipType() {
95    const checked = this.element.querySelector('input[name="checkout[tip_type]"]:checked')
96    return checked ? checked.value : "none"
97  }
98
99  tipCents(taxableSubtotal) {
100    if (this.shippingModeValue) return 0
101
102    switch (this.selectedTipType()) {
103      case "percent": {
104        const pct = this.hasTipPercentTarget ? parseFloat(this.tipPercentTarget.value) || 0 : 0
105        return Math.round((taxableSubtotal * pct) / 100)
106      }
107      case "custom": {
108        const dollars = this.hasCustomTipTarget ? parseFloat(this.customTipTarget.value) || 0 : 0
109        return Math.round(dollars * 100)
110      }
111      default:
112        return 0
113    }
114  }
115
116  serviceFeeCents(serviceFeeBase) {
117    const tiers = this.serviceFeeTiersValue || {}
118    const threshold = Number(tiers.threshold_cents) || 2000
119    const lowPercent = Number(tiers.low_percent) || 5
120    const lowFixed = Number(tiers.low_fixed_cents) || 50
121    const highPercent = Number(tiers.high_percent) || 6.5
122
123    if (serviceFeeBase < threshold) {
124      return Math.round((serviceFeeBase * lowPercent) / 100) + lowFixed
125    }
126    return Math.round((serviceFeeBase * highPercent) / 100)
127  }
128
129  selectedShippingMethodName() {
130    if (this.hasShippingMethodTarget) return this.shippingMethodTarget.value
131    const methods = this.shippingMethodsValue || []
132    return methods[0]?.name || ""
133  }
134
135  refreshShippingFee() {
136    if (!this.isShipping()) return
137
138    if (this.freeShippingValue) {
139      this.deliveryFeeCentsValue = 0
140      return
141    }
142
143    const freeOver = Number(this.shippingFreeOverCentsValue) || 0
144    if (freeOver > 0 && this.subtotalCentsValue >= freeOver) {
145      this.deliveryFeeCentsValue = 0
146      return
147    }
148
149    const methods = this.shippingMethodsValue || []
150    const name = this.selectedShippingMethodName()
151    const method = methods.find((m) => m.name === name) || methods[0]
152    if (!method) {
153      this.deliveryFeeCentsValue = 0
154      return
155    }
156
157    const tiers = Array.isArray(method.tiers) ? method.tiers : []
158    const matching = tiers.filter((t) => this.subtotalCentsValue >= Number(t.min_subtotal_cents || 0))
159    const tier = matching.length
160      ? matching.reduce((best, t) => (Number(t.min_subtotal_cents) >= Number(best.min_subtotal_cents) ? t : best))
161      : tiers[0]
162    this.deliveryFeeCentsValue = Number(tier?.fee_cents) || 0
163  }
164
165  recalculate() {
166    const combinedDiscount = this.discountCentsValue + (this.specialsDiscountCentsValue || 0)
167    const taxableSubtotal = Math.max(this.subtotalCentsValue - combinedDiscount, 0)
168    const tip = this.tipCents(taxableSubtotal)
169    const charged = this.isDelivery() || this.isShipping()
170    const delivery = charged ? this.deliveryFeeCentsValue : 0
171
172    // Service fee excludes delivery/shipping (matches server CalculateTotalsService).
173    const serviceFeeBase = taxableSubtotal + this.taxCentsValue + tip
174    const preFee = serviceFeeBase + delivery
175    const serviceFee = this.serviceFeeCents(serviceFeeBase)
176    const total = this.serviceFeeIncludedValue ? preFee : preFee + serviceFee
177    const showServiceFee = !this.serviceFeeIncludedValue
178
179    if (this.hasSpecialsRowTarget) {
180      this.specialsRowTarget.classList.toggle("hidden", (this.specialsDiscountCentsValue || 0) <= 0)
181      if (this.hasSpecialsAmountTarget) {
182        this.specialsAmountTarget.textContent = `\u2212${this.format(this.specialsDiscountCentsValue || 0)}`
183      }
184    }
185    if (this.hasDiscountRowTarget) {
186      this.discountRowTarget.classList.toggle("hidden", this.discountCentsValue <= 0)
187      if (this.hasDiscountAmountTarget) this.discountAmountTarget.textContent = `\u2212${this.format(this.discountCentsValue)}`
188    }
189    if (this.hasSubtotalAmountTarget) {
190      this.subtotalAmountTarget.textContent = this.format(this.subtotalCentsValue)
191    }
192    if (this.hasTaxRowTarget) {
193      this.taxRowTarget.classList.toggle("hidden", this.taxCentsValue <= 0)
194      if (this.hasTaxAmountTarget) this.taxAmountTarget.textContent = this.format(this.taxCentsValue)
195    }
196    if (this.hasDeliveryRowTarget) {
197      this.deliveryRowTarget.classList.toggle("hidden", !charged && delivery <= 0)
198      if (this.hasDeliveryLabelTarget) {
199        this.deliveryLabelTarget.textContent = this.isShipping() ? "Shipping" : "Delivery"
200      }
201      if (this.hasDeliveryAmountTarget) this.deliveryAmountTarget.textContent = this.format(delivery)
202    }
203    if (this.hasTipAmountTarget) this.tipAmountTarget.textContent = this.format(tip)
204    if (this.hasServiceFeeRowTarget) {
205      this.serviceFeeRowTarget.classList.toggle("hidden", !showServiceFee)
206    }
207    if (this.hasServiceFeeTarget) this.serviceFeeTarget.textContent = this.format(serviceFee)
208    if (this.hasTotalTarget) this.totalTarget.textContent = this.format(total)
209  }
210
211  async requestDeliveryQuote(event) {
212    if (event) event.preventDefault()
213    if (this.shippingModeValue) return
214    if (!this.uberDirectValue || !this.isDelivery() || !this.deliveryQuoteUrlValue) return
215
216    const line1 = this.hasDeliveryLine1Target ? this.deliveryLine1Target.value.trim() : ""
217    const city = this.hasDeliveryCityTarget ? this.deliveryCityTarget.value.trim() : ""
218    const state = this.hasDeliveryStateTarget ? this.deliveryStateTarget.value.trim() : ""
219    const postal = this.hasDeliveryPostalTarget ? this.deliveryPostalTarget.value.trim() : ""
220    if (!line1 || !city || !state || !postal) {
221      this.quoteOk = false
222      this.setQuoteMessage("Enter a complete address to see delivery price.", false)
223      return
224    }
225
226    if (this.hasQuoteButtonTarget) {
227      this.quoteButtonTarget.disabled = true
228      this.quoteButtonTarget.classList.add("loading")
229    }
230    this.setQuoteMessage("Checking delivery…")
231
232    try {
233      const response = await fetch(this.deliveryQuoteUrlValue, {
234        method: "POST",
235        headers: {
236          "Content-Type": "application/json",
237          "Accept": "application/json",
238          "X-CSRF-Token": this.csrfToken()
239        },
240        body: JSON.stringify({
241          line1,
242          line2: this.hasDeliveryLine2Target ? this.deliveryLine2Target.value.trim() : "",
243          city,
244          state,
245          postal_code: postal,
246          phone: this.hasDeliveryPhoneTarget ? this.deliveryPhoneTarget.value.trim() : ""
247        })
248      })
249      const data = await response.json()
250      if (!response.ok || data.error) {
251        this.quoteOk = false
252        this.deliveryFeeCentsValue = 0
253        this.setQuoteMessage(data.error || "Delivery is not available to this address.", false)
254        this.recalculate()
255        return
256      }
257
258      this.quoteOk = true
259      this.deliveryFeeCentsValue = Number(data.delivery_fee_cents) || 0
260      const eta = data.duration_minutes ? ` · ~${data.duration_minutes} min` : ""
261      this.setQuoteMessage(`Delivery ${data.delivery_fee_formatted || this.format(this.deliveryFeeCentsValue)}${eta}`, true)
262      this.recalculate()
263    } catch (_error) {
264      this.quoteOk = false
265      this.setQuoteMessage("Couldn't check delivery. Try again.", false)
266    } finally {
267      if (this.hasQuoteButtonTarget) {
268        this.quoteButtonTarget.disabled = false
269        this.quoteButtonTarget.classList.remove("loading")
270      }
271    }
272  }
273
274  setQuoteMessage(message, ok = null) {
275    if (!this.hasQuoteMessageTarget) return
276    this.quoteMessageTarget.textContent = message || ""
277    this.quoteMessageTarget.classList.toggle("text-success", ok === true)
278    this.quoteMessageTarget.classList.toggle("text-error", ok === false)
279  }
280
281  async applyDiscount(event) {
282    if (event) event.preventDefault()
283    if (!this.hasDiscountCodeTarget || !this.previewDiscountUrlValue) return
284
285    const code = this.discountCodeTarget.value.trim()
286    this.setApplyLoading(true)
287
288    try {
289      const response = await fetch(this.previewDiscountUrlValue, {
290        method: "POST",
291        headers: {
292          "Content-Type": "application/json",
293          "Accept": "application/json",
294          "X-CSRF-Token": this.csrfToken()
295        },
296        body: JSON.stringify({
297          checkout: {
298            discount_code: code,
299            fulfillment_type: this.hasFulfillmentTypeTarget ? this.fulfillmentTypeTarget.value : "",
300            shipping_method_name: this.selectedShippingMethodName()
301          }
302        })
303      })
304      const data = await response.json()
305      const freeItem = data.free_item
306      this.taxCentsValue = Number(data.tax_cents) || 0
307      this.freeShippingValue = Boolean(data.free_shipping)
308      if (typeof data.delivery_fee_cents === "number") {
309        this.deliveryFeeCentsValue = data.delivery_fee_cents
310      }
311      if (freeItem && freeItem.name) {
312        this.discountCentsValue = 0
313        this.subtotalCentsValue = this.baseSubtotalCents
314      } else {
315        this.discountCentsValue = Number(data.discount_cents) || 0
316        this.subtotalCentsValue = this.baseSubtotalCents
317      }
318      this.refreshShippingFee()
319      this.showFreeItem(freeItem)
320      this.showDiscountMessage(data.valid, data.message)
321      this.recalculate()
322    } catch (_error) {
323      this.showDiscountMessage(false, "Couldn't apply the code. Please try again.")
324    } finally {
325      this.setApplyLoading(false)
326    }
327  }
328
329  setApplyLoading(loading) {
330    if (!this.hasApplyButtonTarget) return
331    this.applyButtonTarget.disabled = loading
332    this.applyButtonTarget.classList.toggle("loading", loading)
333  }
334
335  showFreeItem(item) {
336    if (!this.hasFreeItemRowTarget) return
337
338    const show = Boolean(item && item.name)
339    this.freeItemRowTarget.classList.toggle("hidden", !show)
340    if (this.hasFreeItemNameTarget) {
341      this.freeItemNameTarget.textContent = show ? item.name : ""
342    }
343    if (this.hasFreeItemPriceTarget) {
344      const cents = Number(item && item.price_cents) || 0
345      this.freeItemPriceTarget.textContent = cents > 0 ? this.format(cents) : ""
346      this.freeItemPriceTarget.classList.toggle("hidden", !show || cents <= 0)
347    }
348  }
349
350  showDiscountMessage(valid, message) {
351    if (!this.hasDiscountMessageTarget) return
352    if (!message) {
353      this.discountMessageTarget.classList.add("hidden")
354      return
355    }
356    this.discountMessageTarget.textContent = message
357    this.discountMessageTarget.classList.remove("hidden")
358    this.discountMessageTarget.classList.toggle("text-success", valid)
359    this.discountMessageTarget.classList.toggle("text-error", !valid)
360  }
361
362  async checkEmail() {
363    if (this.signedInValue || !this.hasEmailFieldTarget || !this.emailCheckUrlValue) return
364
365    const email = this.emailFieldTarget.value.trim()
366    if (!email || !email.includes("@")) {
367      this.emailExists = false
368      return
369    }
370
371    try {
372      const url = `${this.emailCheckUrlValue}?email=${encodeURIComponent(email)}`
373      const response = await fetch(url, { headers: { Accept: "application/json" } })
374      const data = await response.json()
375      this.emailExists = Boolean(data.exists)
376      if (this.emailExists && !this.signinPrompted) this.openSigninDialog(email)
377    } catch (_error) {
378      this.emailExists = false
379    }
380  }
381
382  onSubmit(event) {
383    if (this.quoteRequiredValue && this.isDelivery() && !this.quoteOk) {
384      event.preventDefault()
385      event.stopPropagation()
386      this.setQuoteMessage("Check delivery price before paying.", false)
387      this.requestDeliveryQuote()
388      return
389    }
390
391    if (this.signedInValue || !this.emailExists || this.signinPrompted) return
392
393    event.preventDefault()
394    event.stopPropagation()
395    this.openSigninDialog(this.hasEmailFieldTarget ? this.emailFieldTarget.value.trim() : "")
396  }
397
398  openSigninDialog(email) {
399    const dialog = document.getElementById("checkout-signin-dialog")
400    if (!dialog || typeof dialog.showModal !== "function") return
401
402    this.signinPrompted = true
403    const emailInput = dialog.querySelector("#checkout-signin-email")
404    if (emailInput) emailInput.value = email
405    dialog.showModal()
406  }
407
408  csrfToken() {
409    const meta = document.querySelector('meta[name="csrf-token"]')
410    return meta ? meta.content : ""
411  }
412
413  format(cents) {
414    return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(cents / 100)
415  }
416};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.