Standard-Set für deutsche Online-Shops recherchiert und umgesetzt: PayPal,
Klarna, Kreditkarte (Visa & Mastercard, neu) und Banküberweisung (Vorkasse).
- PayPal: echte PayPal Smart Buttons über das offizielle JS-SDK (Sandbox-
Client-ID, kostenlos & ohne Konto testbar). Klick öffnet den echten
PayPal-Login-Popup und schließt eine (Test-)Zahlung wirklich ab. Für den
Live-Betrieb genügt später der Tausch gegen VanVans echte (kostenlose)
PayPal-Business-Client-ID.
- Banküberweisung (Vorkasse): braucht gar keine externe Anbindung — zeigt
jetzt VanVans echte Kontodaten an, die sie im neuen Adminbereich-Feld
"🏦 Bankverbindung" selbst einträgt. Damit ist diese Zahlungsart schon
zu 100% fertig, ganz ohne Drittanbieter.
- Klarna & Kreditkarte: echte Live-Anbindung braucht ein Händlerkonto bzw.
einen Zahlungsdienstleister (Phase 2, Cloudflare Worker). Bis dahin ein
sauber gestalteter, funktionierender Verbindungs-/Eingabe-Ablauf, der
1:1 austauschbar ist, sobald die echten Zugänge da sind.
- Auswahl einer Zahlungsart klappt jetzt direkt das passende Panel auf
statt nur eine Radiobox zu markieren.
Verifiziert per Puppeteer-Funktionstest gegen den echten Build: PayPal-
SDK lädt und rendert einen echten Button (iframe), Kreditkartenfelder,
Klarna-Demo-Verbindung und echte Bankdaten-Anzeige funktionieren
fehlerfrei, Adminbereich lädt weiterhin ohne Konfigurationsfehler.
282 lines
14 KiB
Plaintext
282 lines
14 KiB
Plaintext
---
|
||
import Layout from "../../../layouts/Layout.astro";
|
||
import type { Locale } from "../../../i18n/config";
|
||
import { useTranslations } from "../../../i18n/ui";
|
||
|
||
const lang: Locale = "en";
|
||
const t = useTranslations(lang);
|
||
---
|
||
<Layout title="Checkout" description="Complete your order at Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
|
||
<section class="section-tight">
|
||
<div class="container">
|
||
<span class="eyebrow">{t.checkout.eyebrow}</span>
|
||
<h1>{t.checkout.title}</h1>
|
||
<p class="todo-note">{t.checkout.todoNote}</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-tight">
|
||
<div class="container checkout-layout">
|
||
<form class="frm card" id="checkout-form">
|
||
<div class="checkout-split">
|
||
<div class="checkout-split-col">
|
||
<h3>{t.checkout.step2}</h3>
|
||
<div class="payment-options">
|
||
<label class="pay-option" style="--brand-color:#009cde; --brand-bg:#003087;">
|
||
<span class="pay-icon" style="background:linear-gradient(160deg, #0070ba, #003087); color:#fff;">P</span>
|
||
<span>PayPal</span>
|
||
<input type="radio" name="pay" value="paypal" checked />
|
||
</label>
|
||
<label class="pay-option" style="--brand-color:#ffb3c7; --brand-bg:#17120f;">
|
||
<span class="pay-icon" style="background:linear-gradient(160deg, #ffc9d8, #ffb3c7); color:#0a0a0a;">K</span>
|
||
<span>Klarna</span>
|
||
<input type="radio" name="pay" value="klarna" />
|
||
</label>
|
||
<label class="pay-option" style="--brand-color:#ff9838; --brand-bg:#241f3d;">
|
||
<span class="pay-icon pay-icon-cc" style="background:linear-gradient(160deg, #2a2a3d, #14141f);">
|
||
<span class="cc-dot cc-dot-a"></span><span class="cc-dot cc-dot-b"></span>
|
||
</span>
|
||
<span>{t.checkout.creditCard}<span class="pay-sub">{t.checkout.creditCardSub}</span></span>
|
||
<input type="radio" name="pay" value="kreditkarte" />
|
||
</label>
|
||
<label class="pay-option" style="--brand-color:#7fa8c9; --brand-bg:#3b5a76;">
|
||
<span class="pay-icon pay-icon-emoji" style="background:linear-gradient(160deg, #4d7599, #2c4258);">🏦</span>
|
||
<span>{t.checkout.bankTransfer}</span>
|
||
<input type="radio" name="pay" value="ueberweisung" />
|
||
</label>
|
||
</div>
|
||
|
||
<div class="pay-connect">
|
||
<div class="pay-panel" data-panel="paypal">
|
||
<p>{t.checkout.payPalHint}</p>
|
||
<div id="paypal-button-container"></div>
|
||
</div>
|
||
<div class="pay-panel" data-panel="klarna" hidden>
|
||
<p>{t.checkout.klarnaHint}</p>
|
||
<button type="button" class="btn btn-outline pay-connect-btn" id="klarna-connect">{t.checkout.klarnaConnect}</button>
|
||
<p class="pay-connect-status" id="klarna-status"></p>
|
||
</div>
|
||
<div class="pay-panel" data-panel="kreditkarte" hidden>
|
||
<p>{t.checkout.cardHint}</p>
|
||
<div class="card-fields">
|
||
<div>
|
||
<label for="cc-number">{t.checkout.cardNumber}</label>
|
||
<input type="text" id="cc-number" inputmode="numeric" placeholder="1234 5678 9012 3456" maxlength="19" />
|
||
</div>
|
||
<div class="grid grid-2">
|
||
<div><label for="cc-expiry">{t.checkout.cardExpiry}</label><input type="text" id="cc-expiry" placeholder="MM/YY" maxlength="5" /></div>
|
||
<div><label for="cc-cvc">{t.checkout.cardCvc}</label><input type="text" id="cc-cvc" inputmode="numeric" placeholder="123" maxlength="4" /></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="pay-panel" data-panel="ueberweisung" hidden>
|
||
<p>{t.checkout.bankHintReal}</p>
|
||
<div class="bank-details" id="bank-details"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="checkout-split-divider" aria-hidden="true"><span>🧵</span></div>
|
||
<div class="checkout-split-col">
|
||
<h3>{t.checkout.step1}</h3>
|
||
<div><label for="email">{t.checkout.email}</label><input id="email" type="email" required /></div>
|
||
<div class="grid grid-2">
|
||
<div><label for="vorname">{t.checkout.firstName}</label><input id="vorname" type="text" required /></div>
|
||
<div><label for="nachname">{t.checkout.lastName}</label><input id="nachname" type="text" required /></div>
|
||
</div>
|
||
<div><label for="strasse">{t.checkout.street}</label><input id="strasse" type="text" placeholder="1 Example Street" required /></div>
|
||
<div class="grid grid-2">
|
||
<div><label for="plz">{t.checkout.zip}</label><input id="plz" type="text" inputmode="numeric" maxlength="8" placeholder="SW1A 1AA" required /></div>
|
||
<div><label for="ort">{t.checkout.city}</label><input id="ort" type="text" placeholder="London" required /></div>
|
||
</div>
|
||
<div>
|
||
<label for="land">{t.checkout.country}</label>
|
||
<select id="land">
|
||
<option value="de">Germany</option>
|
||
<option value="at">Austria</option>
|
||
<option value="ch">Switzerland</option>
|
||
<option value="lu">Luxembourg</option>
|
||
</select>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<hr class="divider" />
|
||
<h3>{t.checkout.step3}</h3>
|
||
<div id="checkout-summary" class="checkout-summary"></div>
|
||
<p class="small">{t.common.vatNote}</p>
|
||
|
||
<div class="checkbox-row">
|
||
<input id="agb" type="checkbox" required />
|
||
<label for="agb">{t.checkout.agbPrefix} <a href="/agb/">{t.checkout.agbLink}</a> {t.checkout.agbSuffix}</label>
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<input id="widerruf" type="checkbox" required />
|
||
<label for="widerruf">{t.checkout.revocationPrefix} <a href="/widerruf/">{t.checkout.revocationLink}</a> {t.checkout.revocationAnd} <a href="/datenschutz/">{t.checkout.privacyLink}</a> {t.checkout.revocationSuffix}</label>
|
||
</div>
|
||
|
||
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
|
||
</form>
|
||
|
||
<aside class="card checkout-aside">
|
||
<div class="coupon-row">
|
||
<label for="coupon-input">{t.cart.couponLabel}</label>
|
||
<div class="coupon-input-group">
|
||
<input type="text" id="coupon-input" placeholder={t.cart.couponPlaceholder} />
|
||
<button type="button" id="coupon-apply" class="btn btn-outline btn-sm">{t.cart.couponApply}</button>
|
||
</div>
|
||
<p class="small" id="coupon-status"></p>
|
||
</div>
|
||
<hr class="divider stitched" />
|
||
<h3>🚚 {t.checkout.shippingHintTitle}</h3>
|
||
<p class="small">{t.checkout.shippingHint}</p>
|
||
<a class="small" href="/en/versand-zahlung/">{t.checkout.shippingLink}</a>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
</Layout>
|
||
|
||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/checkout/danke/", checkoutLang: lang }}>
|
||
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
|
||
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel };
|
||
</script>
|
||
<script type="module">
|
||
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon } from "../../../scripts/cart";
|
||
import { formatPrice } from "../../../i18n/format";
|
||
import { products } from "../../../data/products";
|
||
import { berechneVersandkosten } from "../../../data/versand";
|
||
import { effektiverZeilenpreis } from "../../../data/rabatt";
|
||
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
|
||
|
||
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel } = window.__checkoutVars;
|
||
|
||
const summary = document.getElementById("checkout-summary");
|
||
const landSelect = document.getElementById("land");
|
||
const cart = getCart();
|
||
const subtotal = cartTotal();
|
||
let currentTotal = 0;
|
||
|
||
function renderSummary() {
|
||
const land = landSelect.value;
|
||
const shipping = cart.length === 0 ? 0 : berechneVersandkosten(land, cart.map((i) => ({ slug: i.slug, preis: i.preis, menge: i.menge })), products);
|
||
const coupon = appliedCoupon();
|
||
const discount = couponDiscount(subtotal);
|
||
const total = Math.max(0, subtotal - discount) + shipping;
|
||
currentTotal = total;
|
||
|
||
summary.innerHTML = `
|
||
${cart.map((i) => {
|
||
const produkt = products.find((p) => p.slug === i.slug);
|
||
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
|
||
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
|
||
}).join("")}
|
||
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
|
||
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
|
||
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
|
||
`;
|
||
|
||
const couponStatus = document.getElementById("coupon-status");
|
||
const couponInput = document.getElementById("coupon-input");
|
||
if (coupon && discount > 0) {
|
||
couponStatus.innerHTML = `✅ ${couponAppliedTemplate.replace("__C__", coupon.code)} <a href="#" id="coupon-clear">✕ ${couponRemoveLabel}</a>`;
|
||
couponInput.value = coupon.code;
|
||
const clearLink = document.getElementById("coupon-clear");
|
||
if (clearLink) clearLink.addEventListener("click", (e) => { e.preventDefault(); clearCoupon(); renderSummary(); });
|
||
} else {
|
||
couponStatus.textContent = couponInput.dataset.invalid === "1" ? couponInvalid : "";
|
||
}
|
||
}
|
||
|
||
renderSummary();
|
||
landSelect.addEventListener("change", renderSummary);
|
||
|
||
// Zahlungsart-Auswahl: pro Methode ein Panel, nur die gewählte ist sichtbar. Macht die
|
||
// Auswahl sofort "aktionabel" statt ein reines Formularfeld zu sein.
|
||
const payPanels = document.querySelectorAll(".pay-panel");
|
||
function showPayPanel(method) {
|
||
payPanels.forEach((p) => { p.hidden = p.dataset.panel !== method; });
|
||
}
|
||
document.querySelectorAll('input[name="pay"]').forEach((radio) => {
|
||
radio.addEventListener("change", () => showPayPanel(radio.value));
|
||
});
|
||
const initialPay = document.querySelector('input[name="pay"]:checked');
|
||
if (initialPay) showPayPanel(initialPay.value);
|
||
|
||
// PayPal: echte PayPal Smart Buttons über das offizielle JS-SDK (Sandbox-Client-ID "sb" —
|
||
// kostenlos, ohne eigenes Konto testbar). Klick öffnet den echten PayPal-Login-Popup. Für den
|
||
// Live-Betrieb muss hier nur "sb" durch VanVans echte (kostenlose) PayPal-Business-Client-ID
|
||
// ersetzt werden, siehe developer.paypal.com.
|
||
let paypalLoaded = false;
|
||
function loadPayPalButtons() {
|
||
if (paypalLoaded || !document.getElementById("paypal-button-container")) return;
|
||
paypalLoaded = true;
|
||
const script = document.createElement("script");
|
||
script.src = "https://www.paypal.com/sdk/js?client-id=sb¤cy=EUR&intent=capture";
|
||
script.onload = () => {
|
||
if (!window.paypal) return;
|
||
window.paypal.Buttons({
|
||
style: { layout: "vertical", color: "blue", shape: "pill", label: "paypal" },
|
||
createOrder: (data, actions) => actions.order.create({
|
||
purchase_units: [{ amount: { value: Math.max(0.01, currentTotal).toFixed(2), currency_code: "EUR" } }],
|
||
}),
|
||
onApprove: (data, actions) => actions.order.capture().then(() => { location.href = thankYouPath; }),
|
||
}).render("#paypal-button-container");
|
||
};
|
||
document.head.appendChild(script);
|
||
}
|
||
loadPayPalButtons();
|
||
|
||
// Klarna: echtes "Verbinden" braucht eine serverseitige Session mit VanVans Klarna-Händlerkonto
|
||
// (Phase 2, Cloudflare Worker). Bis dahin ein sauber gestalteter Demo-Verbindungsablauf, damit
|
||
// die Oberfläche schon fertig aussieht und 1:1 austauschbar ist, sobald die Anbindung steht.
|
||
const klarnaBtn = document.getElementById("klarna-connect");
|
||
const klarnaStatus = document.getElementById("klarna-status");
|
||
if (klarnaBtn) {
|
||
klarnaBtn.addEventListener("click", () => {
|
||
klarnaBtn.disabled = true;
|
||
klarnaBtn.textContent = klarnaConnectingLabel;
|
||
setTimeout(() => {
|
||
klarnaBtn.textContent = klarnaConnectLabel;
|
||
klarnaBtn.disabled = false;
|
||
klarnaStatus.textContent = "✅ " + klarnaConnectedLabel;
|
||
}, 1100);
|
||
});
|
||
}
|
||
|
||
// Banküberweisung (Vorkasse): braucht KEINE externe Anbindung — zeigt einfach VanVans echte,
|
||
// im Adminbereich hinterlegte Kontodaten an. Das ist die einzige der vier Zahlungsarten, die
|
||
// schon zu 100% "fertig" funktioniert.
|
||
const bankDetailsEl = document.getElementById("bank-details");
|
||
if (bankDetailsEl) {
|
||
if (bankverbindungVollstaendig(bankverbindung)) {
|
||
bankDetailsEl.innerHTML = `
|
||
<div class="bank-row"><span>${bankHolderLabel}</span><span>${bankverbindung.kontoinhaber}</span></div>
|
||
<div class="bank-row"><span>${bankIbanLabel}</span><span>${bankverbindung.iban}</span></div>
|
||
${bankverbindung.bic ? `<div class="bank-row"><span>${bankBicLabel}</span><span>${bankverbindung.bic}</span></div>` : ""}
|
||
${bankverbindung.bank ? `<div class="bank-row"><span>${bankBankNameLabel}</span><span>${bankverbindung.bank}</span></div>` : ""}
|
||
`;
|
||
} else {
|
||
bankDetailsEl.innerHTML = `<p class="small">${bankPlaceholderLabel}</p>`;
|
||
}
|
||
}
|
||
|
||
document.getElementById("coupon-apply").addEventListener("click", () => {
|
||
const input = document.getElementById("coupon-input");
|
||
const code = input.value.trim();
|
||
if (!code) { clearCoupon(); renderSummary(); return; }
|
||
setCouponCode(code);
|
||
input.dataset.invalid = appliedCoupon() ? "0" : "1";
|
||
renderSummary();
|
||
});
|
||
|
||
document.getElementById("checkout-form").addEventListener("submit", (e) => {
|
||
e.preventDefault();
|
||
if (cart.length === 0) {
|
||
alert(emptyCartAlert);
|
||
return;
|
||
}
|
||
location.href = thankYouPath;
|
||
});
|
||
</script>
|