Add Square deposit payment integration

- Square Web Payments SDK card element in booking form
- Delayed-capture hold ($100) on booking submit — not charged until confirmed
- Live payment status field: Verifying card → Authorizing → Confirmed w/ hold ID
- Admin: Capture / Void / Refund actions for each booking
- square_payment_id returned in API response for frontend confirmation display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 18:33:16 +00:00
parent 8f5362aa95
commit cca3129f6e
4 changed files with 324 additions and 35 deletions
+97 -21
View File
@@ -811,7 +811,17 @@
</select>
<input type="date" name="date" required />
<textarea name="message" placeholder="Anything else we should know? (optional)"></textarea>
<button type="submit">Send Booking Request</button>
<!-- Square deposit card -->
<div style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:1rem;margin-top:0.25rem">
<p style="font-size:0.78rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:rgba(249,115,22,0.8);margin-bottom:0.5rem">Refundable Deposit — $100</p>
<p style="font-size:0.8rem;color:rgba(255,255,255,0.5);margin-bottom:0.85rem;line-height:1.5">A $100 hold will be placed on your card — <strong style="color:rgba(255,255,255,0.75)">not charged</strong> until your booking is confirmed. Released in full if declined or at return.</p>
<div id="card-container" style="min-height:44px"></div>
<p id="card-errors" style="color:#f87171;font-size:0.78rem;margin-top:0.4rem;display:none"></p>
<div id="deposit-status" style="display:none;margin-top:0.6rem;font-size:0.82rem;border-radius:6px;padding:0.5rem 0.75rem;line-height:1.5"></div>
</div>
<button type="submit" id="submitBtn">Submit Booking Request</button>
<div class="form-msg" id="formMsg"></div>
</form>
</div>
@@ -834,6 +844,7 @@
<p style="margin-top:0.5rem;">Polaris Slingshot&reg; is a registered trademark of Polaris Inc. We are an independent rental operator.</p>
</footer>
<script src="https://web.squarecdn.com/v1/square.js"></script>
<script>
// ── Mobile nav ───────────────────────────────────────────────────────────────
const navToggle = document.getElementById('navToggle');
@@ -973,52 +984,117 @@
});
}
// ── Square Web Payments ───────────────────────────────────────────────────────
let squareCard = null;
async function initSquare() {
if (!window.Square) return;
const cardEl = document.getElementById('card-container');
if (cardEl) cardEl.style.display = '';
try {
const payments = Square.payments('sq0idp-YSM7BU9IVyOWSzpeP-0nzQ', 'L8GZYHYKE95CE');
squareCard = await payments.card({
style: {
'.input-container': { borderColor: 'rgba(255,255,255,0.12)', borderRadius: '6px' },
'.input-container.is-focus': { borderColor: '#f97316' },
'.input-container.is-error': { borderColor: '#ef4444' },
'input': { color: '#ffffff', fontSize: '15px' },
'.message-text': { color: '#f87171' },
}
});
await squareCard.attach('#card-container');
} catch (e) {
console.warn('Square init error:', e);
}
}
initSquare();
// ── Booking Form ─────────────────────────────────────────────────────────────
document.getElementById('bookingForm').addEventListener('submit', async function(e) {
e.preventDefault();
const form = this;
const msg = document.getElementById('formMsg');
const btn = form.querySelector('button[type="submit"]');
btn.textContent = 'Sending...';
const form = this;
const msg = document.getElementById('formMsg');
const btn = document.getElementById('submitBtn');
const cardErrors = document.getElementById('card-errors');
const depositStatus = document.getElementById('deposit-status');
btn.textContent = 'Processing…';
btn.disabled = true;
msg.className = 'form-msg';
msg.style.display = 'none';
if (cardErrors) { cardErrors.style.display = 'none'; cardErrors.textContent = ''; }
const data = {
name: form.querySelector('[name="name"]').value,
email: form.querySelector('[name="email"]').value,
phone: form.querySelector('[name="phone"]').value,
package: form.querySelector('[name="package"]').value,
date: form.querySelector('[name="date"]').value,
message: form.querySelector('[name="message"]').value,
};
function setDepStatus(text, type) {
if (!depositStatus) return;
if (!text) { depositStatus.style.display = 'none'; depositStatus.textContent = ''; return; }
depositStatus.textContent = text;
depositStatus.style.display = 'block';
const map = {
processing: { background: 'rgba(249,115,22,0.1)', color: 'rgba(249,115,22,0.95)', border: '1px solid rgba(249,115,22,0.25)' },
success: { background: 'rgba(22,163,74,0.15)', color: '#86efac', border: '1px solid rgba(22,163,74,0.35)' },
error: { background: 'rgba(239,68,68,0.1)', color: '#fca5a5', border: '1px solid rgba(239,68,68,0.25)' },
};
Object.assign(depositStatus.style, map[type] || {});
}
try {
const res = await fetch('/contact.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
let squareToken = null;
if (squareCard) {
setDepStatus('Verifying card…', 'processing');
const result = await squareCard.tokenize();
if (result.status !== 'OK') {
const errMsg = result.errors ? result.errors.map(x => x.message).join(', ') : 'Card error — please check your details.';
if (cardErrors) { cardErrors.textContent = errMsg; cardErrors.style.display = 'block'; }
setDepStatus('', '');
btn.textContent = 'Submit Booking Request';
btn.disabled = false;
return;
}
squareToken = result.token;
setDepStatus('Card verified — authorizing $100 deposit hold…', 'processing');
}
const data = {
name: form.querySelector('[name="name"]').value,
email: form.querySelector('[name="email"]').value,
phone: form.querySelector('[name="phone"]').value,
package: form.querySelector('[name="package"]').value,
date: form.querySelector('[name="date"]').value,
message: form.querySelector('[name="message"]').value,
square_token: squareToken,
};
const res = await fetch('/contact.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
const json = await res.json();
if (json.success) {
if (json.deposit_held) {
const holdSuffix = json.square_payment_id ? ' · Confirmation: …' + json.square_payment_id.slice(-10).toUpperCase() : '';
setDepStatus('✓ $100 deposit hold authorized' + holdSuffix, 'success');
const cardEl = document.getElementById('card-container');
if (cardEl) cardEl.style.display = 'none';
} else {
setDepStatus('', '');
}
msg.className = 'form-msg success';
msg.innerHTML = json.message || 'Thanks! We received your request and will be in touch soon.';
msg.innerHTML = 'Booking request received! Your reference is <strong>' + json.ref + '</strong>. We\'ll be in touch shortly.';
msg.style.display = 'block';
form.reset();
if (squareCard) { squareCard.destroy(); squareCard = null; }
selectedDate = null;
if (dateInput) dateInput.min = new Date().toISOString().split('T')[0];
btn.textContent = 'Request Sent!';
btn.style.background = '#16a34a';
// Refresh calendar to show newly booked date
loadCalendar(calMonth, calYear);
} else {
throw new Error(json.error || 'Something went wrong.');
}
} catch (err) {
setDepStatus('', '');
msg.className = 'form-msg error';
msg.textContent = err.message || 'Something went wrong. Please try again or call us directly.';
msg.style.display = 'block';
btn.textContent = 'Send Booking Request';
btn.textContent = 'Submit Booking Request';
btn.disabled = false;
}
});