sfrjrt
This commit is contained in:
parent
f789fe6405
commit
3844e74936
5
.env
5
.env
@ -15,4 +15,7 @@ SESSION_SECRET=irgendein_langer_geheimer_zufallstext_123!
|
||||
|
||||
MAIL_HOST=smtp.ionos.de
|
||||
MAIL_USER=register@dynastyofknights.com
|
||||
MAIL_PASS=111168-j-62217DwmbwPK
|
||||
MAIL_PASS=111168-j-62217DwmbwPK
|
||||
|
||||
STRIPE_SECRET_KEY=sk_live_:J1d5&Sa0!_~>lLHuyxa$VC:AuOh
|
||||
STRIPE_WEBHOOK_SECRET=whsec_oaZPQsV2Hj<D"=B%6oW$0fZ`x+3
|
||||
21
app.js
21
app.js
@ -28,6 +28,7 @@ const { registerArenaHandlers } = require("./sockets/arena");
|
||||
const { registerChatHandlers } = require("./sockets/chat");
|
||||
const boosterRoutes = require("./routes/booster.route");
|
||||
const pointsRoutes = require("./routes/points.route");
|
||||
const { router: shopRoutes } = require("./routes/shop.route");
|
||||
|
||||
const compression = require("compression");
|
||||
|
||||
@ -57,18 +58,26 @@ app.use(
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://js.stripe.com"],
|
||||
scriptSrcAttr: ["'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||
imgSrc: ["'self'", "data:", "blob:"],
|
||||
connectSrc: ["'self'", "ws:", "wss:"],
|
||||
frameAncestors: ["'self'"], // Erlaubt iframe von eigener Domain
|
||||
imgSrc: ["'self'", "data:", "blob:", "https://*.stripe.com"],
|
||||
connectSrc: ["'self'", "ws:", "wss:", "https://api.stripe.com"],
|
||||
frameSrc: ["https://js.stripe.com", "https://hooks.stripe.com"],
|
||||
frameAncestors: ["'self'"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/* ========================
|
||||
Stripe Webhook (raw body –
|
||||
muss VOR express.json stehen!)
|
||||
======================== */
|
||||
const { router: shopRouteWebhook } = require("./routes/shop.route");
|
||||
app.use("/api", shopRouteWebhook);
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5000,
|
||||
@ -319,7 +328,7 @@ app.get("/api/hud", requireLogin, async (req, res) => {
|
||||
[userId],
|
||||
);
|
||||
const [[currency]] = await db.query(
|
||||
"SELECT silver, gold, gems, wood, stone FROM account_currency WHERE account_id = ?",
|
||||
"SELECT silver, gold, gems, wood, stone, iron FROM account_currency WHERE account_id = ?",
|
||||
[userId],
|
||||
);
|
||||
res.json({
|
||||
@ -329,6 +338,7 @@ app.get("/api/hud", requireLogin, async (req, res) => {
|
||||
gems: currency?.gems || 0,
|
||||
wood: currency?.wood || 0,
|
||||
stone: currency?.stone || 0,
|
||||
iron: currency?.iron || 0,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -385,6 +395,7 @@ app.use("/arena", arenaRoutes);
|
||||
app.use("/api", boosterRoutes);
|
||||
app.use("/api", require("./routes/daily.route"));
|
||||
app.use("/api/points", pointsRoutes);
|
||||
app.use("/api", shopRoutes);
|
||||
|
||||
/* ========================
|
||||
404 Handler
|
||||
|
||||
@ -300,3 +300,202 @@
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Shop Overlay & Popup
|
||||
========================= */
|
||||
|
||||
#shop-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 3999;
|
||||
display: none;
|
||||
}
|
||||
#shop-overlay.active { display: block; }
|
||||
|
||||
#shop-popup {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.92);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
width: min(1100px, 95vw);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
|
||||
background: url("/images/parchment.png") center / cover no-repeat;
|
||||
border: 4px solid #6b4b2a;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 60px rgba(0,0,0,0.95);
|
||||
|
||||
z-index: 4000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||
}
|
||||
#shop-popup.active {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#shop-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(#6b4b2a, #3c2414);
|
||||
border-bottom: 2px solid #8b6a3c;
|
||||
border-radius: 8px 8px 0 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#shop-title {
|
||||
font-family: "Tangerine", serif;
|
||||
font-size: 46px;
|
||||
color: #f0d9a6;
|
||||
text-shadow: 0 2px 6px black;
|
||||
}
|
||||
|
||||
#shop-close {
|
||||
font-size: 22px;
|
||||
color: #f0d9a6;
|
||||
cursor: pointer;
|
||||
width: 34px; height: 34px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
#shop-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
||||
|
||||
#shop-subtitle {
|
||||
text-align: center;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 14px;
|
||||
color: #5c3b20;
|
||||
padding: 14px 24px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#shop-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
.shop-loading {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
font-family: "Cinzel", serif;
|
||||
color: #8b6a3c;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* Shop Karte */
|
||||
.shop-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 20px 14px 14px;
|
||||
background: linear-gradient(145deg, rgba(58,40,16,0.85), rgba(26,15,4,0.85));
|
||||
border: 2px solid #8b6a3c;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
|
||||
}
|
||||
.shop-card:hover {
|
||||
border-color: #f0d060;
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.7), 0 0 14px rgba(200,160,60,0.3);
|
||||
}
|
||||
|
||||
.shop-badge {
|
||||
position: absolute;
|
||||
top: -10px; right: -10px;
|
||||
background: linear-gradient(#c8200a, #8b1006);
|
||||
border: 2px solid #ff6040;
|
||||
border-radius: 20px;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
padding: 3px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shop-gem-icon {
|
||||
font-size: 42px;
|
||||
filter: drop-shadow(0 2px 6px rgba(0,100,255,0.5));
|
||||
}
|
||||
|
||||
.shop-package-name {
|
||||
font-family: "Tangerine", serif;
|
||||
font-size: 28px;
|
||||
color: #f0d9a6;
|
||||
text-shadow: 0 1px 4px black;
|
||||
}
|
||||
|
||||
.shop-gems-amount {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #f0d060;
|
||||
}
|
||||
|
||||
.shop-gems-label {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: #a08060;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.shop-breakdown {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: #c8a86a;
|
||||
}
|
||||
|
||||
.shop-price-per {
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 10px;
|
||||
color: #8b6a3c;
|
||||
}
|
||||
|
||||
.shop-buy-btn {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: linear-gradient(#6b4b2a, #3c2414);
|
||||
border: 2px solid #f0d060;
|
||||
border-radius: 8px;
|
||||
color: #f0d9a6;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.shop-buy-btn:hover {
|
||||
background: linear-gradient(#8b6b3a, #5c3a1a);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 12px rgba(240,208,96,0.4);
|
||||
}
|
||||
.shop-buy-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
#shop-footer {
|
||||
text-align: center;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: #8b6a3c;
|
||||
padding: 0 24px 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
115
public/js/shop.js
Normal file
115
public/js/shop.js
Normal file
@ -0,0 +1,115 @@
|
||||
/* ============================================================
|
||||
public/js/shop.js
|
||||
Shop-Popup – Gems kaufen
|
||||
============================================================ */
|
||||
|
||||
const shopOverlay = document.getElementById("shop-overlay");
|
||||
const shopPopup = document.getElementById("shop-popup");
|
||||
const shopClose = document.getElementById("shop-close");
|
||||
const shopGrid = document.getElementById("shop-grid");
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
Shop öffnen
|
||||
════════════════════════════════════════════ */
|
||||
export async function openShop() {
|
||||
shopOverlay.classList.add("active");
|
||||
shopPopup.classList.add("active");
|
||||
await loadPackages();
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
Shop schließen
|
||||
════════════════════════════════════════════ */
|
||||
function closeShop() {
|
||||
shopOverlay.classList.remove("active");
|
||||
shopPopup.classList.remove("active");
|
||||
}
|
||||
|
||||
shopClose?.addEventListener("click", closeShop);
|
||||
shopOverlay?.addEventListener("click", closeShop);
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") closeShop();
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
Pakete laden
|
||||
════════════════════════════════════════════ */
|
||||
async function loadPackages() {
|
||||
shopGrid.innerHTML = `<div class="shop-loading">Lade Angebote...</div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/shop/packages");
|
||||
const pkgs = await res.json();
|
||||
renderPackages(pkgs);
|
||||
} catch {
|
||||
shopGrid.innerHTML = `<div class="shop-loading">Fehler beim Laden.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
Pakete rendern
|
||||
════════════════════════════════════════════ */
|
||||
function renderPackages(pkgs) {
|
||||
shopGrid.innerHTML = pkgs.map(pkg => {
|
||||
const total = pkg.gems + pkg.bonus;
|
||||
const priceEur = (pkg.price / 100).toFixed(2).replace(".", ",");
|
||||
const hasBonus = pkg.bonus > 0;
|
||||
const perGem = (pkg.price / total).toFixed(1);
|
||||
|
||||
return `
|
||||
<div class="shop-card" data-id="${pkg.id}">
|
||||
${hasBonus ? `<div class="shop-badge">+${pkg.bonus} Bonus!</div>` : ""}
|
||||
<div class="shop-gem-icon">💠</div>
|
||||
<div class="shop-package-name">${pkg.name}</div>
|
||||
<div class="shop-gems-amount">${total.toLocaleString("de-DE")}</div>
|
||||
<div class="shop-gems-label">Gems</div>
|
||||
${hasBonus ? `<div class="shop-breakdown">${pkg.gems} + <span style="color:#f0d060">${pkg.bonus} Bonus</span></div>` : ""}
|
||||
<div class="shop-price-per">~ ${perGem}¢ pro Gem</div>
|
||||
<button class="shop-buy-btn" data-id="${pkg.id}">
|
||||
${priceEur} €
|
||||
</button>
|
||||
</div>`;
|
||||
}).join("");
|
||||
|
||||
shopGrid.querySelectorAll(".shop-buy-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => checkout(btn.dataset.id, btn));
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
Checkout starten
|
||||
════════════════════════════════════════════ */
|
||||
async function checkout(packageId, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Weiterleiten...";
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/shop/checkout", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ packageId }),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || !data.url) {
|
||||
alert(data.error || "Fehler beim Starten der Zahlung.");
|
||||
btn.disabled = false;
|
||||
btn.textContent = getPrice(packageId);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = data.url;
|
||||
} catch {
|
||||
alert("Verbindungsfehler.");
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Fehler";
|
||||
}
|
||||
}
|
||||
|
||||
function getPrice(id) {
|
||||
const prices = {
|
||||
starter: "1,99 €", abenteurer: "7,99 €",
|
||||
ritter: "14,99 €", fuerst: "34,99 €", koenig: "59,99 €"
|
||||
};
|
||||
return prices[id] || "Kaufen";
|
||||
}
|
||||
142
routes/shop.route.js
Normal file
142
routes/shop.route.js
Normal file
@ -0,0 +1,142 @@
|
||||
/* ============================================================
|
||||
routes/shop.route.js
|
||||
Stripe Shop – Gems kaufen
|
||||
|
||||
.env benötigt:
|
||||
STRIPE_SECRET_KEY=sk_live_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
APP_URL=https://spiel.dynastyofknights.com
|
||||
============================================================ */
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const db = require("../database/database");
|
||||
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
|
||||
|
||||
function requireLogin(req, res, next) {
|
||||
if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" });
|
||||
next();
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
Shop-Pakete (Preis in Cent!)
|
||||
════════════════════════════════════════════ */
|
||||
const PACKAGES = [
|
||||
{ id: "starter", name: "Starter", gems: 100, price: 199, bonus: 0 },
|
||||
{ id: "abenteurer", name: "Abenteurer", gems: 500, price: 799, bonus: 50 },
|
||||
{ id: "ritter", name: "Ritter", gems: 1000, price: 1499, bonus: 150 },
|
||||
{ id: "fuerst", name: "Fürst", gems: 2500, price: 3499, bonus: 500 },
|
||||
{ id: "koenig", name: "König", gems: 5000, price: 5999, bonus: 1500 },
|
||||
];
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
GET /api/shop/packages
|
||||
Pakete für Frontend laden
|
||||
════════════════════════════════════════════ */
|
||||
router.get("/shop/packages", requireLogin, (req, res) => {
|
||||
res.json(PACKAGES);
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
POST /api/shop/checkout
|
||||
Stripe Checkout Session erstellen
|
||||
Body: { packageId: "ritter" }
|
||||
════════════════════════════════════════════ */
|
||||
router.post("/shop/checkout", requireLogin, async (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
const { packageId } = req.body;
|
||||
|
||||
const pkg = PACKAGES.find(p => p.id === packageId);
|
||||
if (!pkg) return res.status(400).json({ error: "Paket nicht gefunden." });
|
||||
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
payment_method_types: ["card", "paypal"],
|
||||
line_items: [{
|
||||
price_data: {
|
||||
currency: "eur",
|
||||
product_data: {
|
||||
name: `${pkg.name} Paket – ${pkg.gems + pkg.bonus} 💠 Gems`,
|
||||
description: pkg.bonus > 0
|
||||
? `${pkg.gems} Gems + ${pkg.bonus} Bonus-Gems`
|
||||
: `${pkg.gems} Gems`,
|
||||
images: [`${process.env.APP_URL}/images/items/blauer-cristal.png`],
|
||||
},
|
||||
unit_amount: pkg.price,
|
||||
},
|
||||
quantity: 1,
|
||||
}],
|
||||
mode: "payment",
|
||||
success_url: `${process.env.APP_URL}/launcher?payment=success`,
|
||||
cancel_url: `${process.env.APP_URL}/launcher?payment=cancel`,
|
||||
metadata: {
|
||||
userId: String(userId),
|
||||
packageId: pkg.id,
|
||||
gems: String(pkg.gems + pkg.bonus),
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ url: session.url });
|
||||
} catch (err) {
|
||||
console.error("Stripe Fehler:", err);
|
||||
res.status(500).json({ error: "Zahlung konnte nicht gestartet werden." });
|
||||
}
|
||||
});
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
POST /api/shop/webhook
|
||||
Stripe Webhook – Zahlung bestätigt
|
||||
WICHTIG: Muss VOR express.json() registriert
|
||||
werden → raw body nötig!
|
||||
════════════════════════════════════════════ */
|
||||
router.post(
|
||||
"/shop/webhook",
|
||||
express.raw({ type: "application/json" }),
|
||||
async (req, res) => {
|
||||
const sig = req.headers["stripe-signature"];
|
||||
|
||||
let event;
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
process.env.STRIPE_WEBHOOK_SECRET
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Webhook Signatur-Fehler:", err.message);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
|
||||
if (event.type === "checkout.session.completed") {
|
||||
const session = event.data.object;
|
||||
const userId = parseInt(session.metadata.userId);
|
||||
const gems = parseInt(session.metadata.gems);
|
||||
const packageId = session.metadata.packageId;
|
||||
|
||||
try {
|
||||
/* Gems gutschreiben */
|
||||
await db.query(
|
||||
"UPDATE account_currency SET gems = gems + ? WHERE account_id = ?",
|
||||
[gems, userId]
|
||||
);
|
||||
|
||||
/* Kauf protokollieren */
|
||||
await db.query(
|
||||
`INSERT INTO shop_purchases
|
||||
(user_id, package_id, gems, stripe_session_id, created_at)
|
||||
VALUES (?, ?, ?, ?, NOW())`,
|
||||
[userId, packageId, gems, session.id]
|
||||
);
|
||||
|
||||
console.log(`✅ Shop: User ${userId} erhält ${gems} Gems (${packageId})`);
|
||||
} catch (err) {
|
||||
console.error("Gems gutschreiben Fehler:", err);
|
||||
return res.status(500).end();
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ received: true });
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = { router, PACKAGES };
|
||||
@ -667,7 +667,7 @@
|
||||
<span class="hud-res-icon">🪙</span>
|
||||
<span class="hud-res-value" id="hud-gold">0</span>
|
||||
</div>
|
||||
<button id="hud-gold-btn">Gold</button>
|
||||
<button id="hud-gold-btn">Shop</button>
|
||||
</div>
|
||||
|
||||
<div class="hud-res-row">
|
||||
@ -892,6 +892,22 @@
|
||||
<div class="qm-popup-body" id="qm-body-boosterjagd"></div>
|
||||
</div>
|
||||
|
||||
<!-- ================================
|
||||
Shop Popup
|
||||
================================ -->
|
||||
<div id="shop-overlay"></div>
|
||||
<div id="shop-popup">
|
||||
<div id="shop-header">
|
||||
<span id="shop-title">💠 Gems kaufen</span>
|
||||
<span id="shop-close">✕</span>
|
||||
</div>
|
||||
<div id="shop-subtitle">Unterstütze das Spiel und erhalte wertvolle Gems!</div>
|
||||
<div id="shop-grid"></div>
|
||||
<div id="shop-footer">
|
||||
Sichere Zahlung via Stripe · Kreditkarte & PayPal akzeptiert
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="game-notification-overlay"></div>
|
||||
<div id="game-notification">
|
||||
<div class="notification-header">
|
||||
@ -1022,7 +1038,23 @@
|
||||
<script type="module" src="/js/quickmenu.js?v=2"></script>
|
||||
<script type="module">
|
||||
import { loadHud } from "/js/hud.js?v=2";
|
||||
import { openShop } from "/js/shop.js";
|
||||
loadHud();
|
||||
|
||||
// Shop-Button
|
||||
document.getElementById("hud-gold-btn")
|
||||
?.addEventListener("click", openShop);
|
||||
|
||||
// Stripe Rückkehr-Benachrichtigung
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get("payment") === "success") {
|
||||
window.showNotification("Zahlung erfolgreich! Deine Gems wurden gutgeschrieben. 💠", "Shop", "💠");
|
||||
history.replaceState({}, "", "/launcher");
|
||||
loadHud(); // HUD sofort aktualisieren
|
||||
} else if (params.get("payment") === "cancel") {
|
||||
window.showNotification("Zahlung abgebrochen.", "Shop", "💠");
|
||||
history.replaceState({}, "", "/launcher");
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user