mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 22:58:32 +02:00
Removes operator-private material from the shipped tree, corrects documentation
that had drifted from the code, deletes dead code, and applies a consistent
prose style.
Private material moved out of the public tree (docs/private/, git-ignored):
- the wallet keyfile recovery runbook and its helper scripts, which together
published the mainnet wallet file path, the service account, backup snapshot
filenames, and the wallet RPC listener state. The helper duplicated the same
constants, so relocating the document alone would not have removed them.
- the stagenet parallel-candidate runbook and its fixtures.
- the dated production-state record, the internal remediation plan, and the
TODO list. None are reference documentation for an outside reader.
Redacted or genericised:
- operator home-directory paths in shipped scripts and units. The asset build
script now takes KENNEY_ASSETS_DIR instead of a hard-coded path.
- private LAN addresses in tests, env examples and deploy docs, replaced with
RFC 5737 documentation addresses.
- the default alert sender and the notification hub example.
- the Monero wallet unit now matches the Wownero one: a dedicated service
account and a config file, instead of the operator's login account with the
wallet password in argv where any local user can read it from /proc.
Ignore rules now cover every .env variant rather than the bare name, plus local
agent state. The deployment docs instruct operators to create .env.mainnet and
.env.stagenet, which the exact-name rule would have allowed to be committed.
Documentation corrected against the code: per-chain dungeon level counts, the
premium unlock rule, Tavern chat scope, backup retention, the chain-profile
fallback, the environment template location, and several sections describing
shipped work as pending.
Dead code removed: configPersistence, rpccalls, a duplicate vendored ROT.js
bundle, the unused tile-picker overlay, the unreachable legacy game-size path
and its vestigial parameter, spectator stubs that shadowed the real
SpectatorManager, and commented-out debugging. nip98 now imports the shared
nostr loader rather than duplicating it.
Prose style: em-dashes removed throughout in favour of colons, semicolons and
commas; comments restated as present-tense descriptions of the code rather than
narration of past changes.
127 suites / 1015 tests pass.
501 lines
24 KiB
JavaScript
501 lines
24 KiB
JavaScript
/**
|
||
* Payment UI Manager
|
||
* Handles the game mode selection and payment modal interactions
|
||
*/
|
||
const PaymentUI = {
|
||
config: null,
|
||
userCredits: 0,
|
||
hasPayoutAddress: false,
|
||
selectedPackageId: null,
|
||
|
||
init: function() {
|
||
console.log("Initializing PaymentUI...");
|
||
|
||
// Bind close button
|
||
$('#close-payment').on('click', function() {
|
||
$('#payment-ui').hide();
|
||
});
|
||
|
||
// Bind mode options (use delegation for dynamic content)
|
||
$('#payment-ui').on('click', '.mode-option', function() {
|
||
// The "Buy Selected Package" button ALSO carries .mode-option (for styling) but has its
|
||
// own handler below; without this guard it double-fired here with NO data attrs, defaulting
|
||
// to single_game, so buying a 10-credit pack ALSO created a spurious 1-credit invoice.
|
||
if (this.id === 'buy-credits-btn') return;
|
||
const mode = $(this).data('mode');
|
||
const action = $(this).data('action');
|
||
const packageId = $(this).data('package-id');
|
||
if (!mode && !action) return; // stray click with no intent
|
||
PaymentUI.handleModeSelection(mode, action, packageId);
|
||
});
|
||
|
||
// Bind package radio buttons
|
||
$('#payment-ui').on('change', 'input[name="credit-package"]', function() {
|
||
PaymentUI.selectedPackageId = $(this).val();
|
||
// Enable buy button
|
||
$('#buy-credits-btn').prop('disabled', false);
|
||
});
|
||
|
||
// Bind buy credits button
|
||
$('#payment-ui').on('click', '#buy-credits-btn', function() {
|
||
if (PaymentUI.selectedPackageId) {
|
||
PaymentUI.handleModeSelection('PAID_CREDITS', 'buy_credits', PaymentUI.selectedPackageId);
|
||
}
|
||
});
|
||
|
||
// Bind check payment button
|
||
$('#check-payment').on('click', function() {
|
||
const paymentId = $(this).data('payment-id');
|
||
if (paymentId && window.socket) {
|
||
window.socket.emit('check_payment_status', { paymentId });
|
||
$(this).text('Checking...');
|
||
setTimeout(() => $('#check-payment').text('🔄 Check Payment'), 1000);
|
||
}
|
||
});
|
||
|
||
// Bind copy button
|
||
$('#copy-address').on('click', function() {
|
||
const addr = $('#payment-address').val();
|
||
if(addr) {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(addr).then(() => {
|
||
$('#copy-address').text('Copied!');
|
||
setTimeout(() => $('#copy-address').html('📋 Copy Address'), 2000);
|
||
});
|
||
} else {
|
||
const $temp = $("<input>");
|
||
$("body").append($temp);
|
||
$temp.val(addr).select();
|
||
document.execCommand("copy");
|
||
$temp.remove();
|
||
$('#copy-address').text('Copied!');
|
||
setTimeout(() => $('#copy-address').html('📋 Copy Address'), 2000);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Bind Shop Button
|
||
$('#shopButton').on('click', function() {
|
||
PaymentUI.show();
|
||
});
|
||
|
||
// Listen for credits updates
|
||
if (window.socket) {
|
||
window.socket.on('credits_update', function(data) {
|
||
PaymentUI.updateCredits(data.balance);
|
||
});
|
||
}
|
||
},
|
||
|
||
updateConfig: function(config) {
|
||
console.log("PaymentUI config updated:", config);
|
||
this.config = config;
|
||
this.render();
|
||
},
|
||
|
||
updateCredits: function(balance) {
|
||
this.userCredits = balance || 0;
|
||
this.render();
|
||
$('#user-credits-display').text(this.userCredits);
|
||
},
|
||
|
||
formatPrice: function(atomicAmount) {
|
||
const currency = this.config?.cryptoType || 'WOW';
|
||
const decimals = currency === 'XMR' ? 12 : 11;
|
||
const divisor = Math.pow(10, decimals);
|
||
return (atomicAmount / divisor).toFixed(decimals === 12 ? 4 : 2);
|
||
},
|
||
|
||
escapeHtml: function(value) {
|
||
return String(value == null ? '' : value).replace(/[&<>"']/g, function(ch) {
|
||
return ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
})[ch];
|
||
});
|
||
},
|
||
|
||
grantLabel: function(packId) {
|
||
const pack = window.RK && window.RK.PACKS ? window.RK.PACKS[packId] : null;
|
||
const labels = {
|
||
'generated-skins': 'Premium skins',
|
||
'iso-dungeon': 'Isometric dungeon',
|
||
'kenney-3d-characters': '3D characters',
|
||
'roguelike-interior': 'Roguelike interior'
|
||
};
|
||
return (pack && pack.label) || labels[packId] || packId;
|
||
},
|
||
|
||
grantSummaryHtml: function(grants, options = {}) {
|
||
if (!grants) return '';
|
||
const parts = [];
|
||
if (options.includeCredits && Number(grants.credits || 0) > 0) {
|
||
parts.push(`${Number(grants.credits)} credits`);
|
||
}
|
||
(grants.packs || []).forEach(packId => parts.push(this.grantLabel(packId)));
|
||
if (grants.premiumLevel) {
|
||
parts.push(`${grants.premiumLevel} tier`);
|
||
}
|
||
if (parts.length === 0) return '';
|
||
return `<div style="font-size:0.78em;color:#fbbf24;margin-top:4px;margin-left:${options.indent || 0}px;">Includes: ${this.escapeHtml(parts.join(' • '))}</div>`;
|
||
},
|
||
|
||
payoutSummaryHtml: function(mode, currency) {
|
||
const outcomes = this.config?.payoutOutcomes?.[mode];
|
||
if (!outcomes?.escape || !outcomes?.escapeWithTreasure) {
|
||
return '<span style="color:#f87171;">Exact payout terms are unavailable. Reload before entering paid play.</span>';
|
||
}
|
||
const describe = (label, outcome) => {
|
||
if (!outcome.payable) {
|
||
const why = outcome.suppressedReason === 'below_minimum' ? ' (below configured minimum)' : '';
|
||
return `${label}: no payout${why}`;
|
||
}
|
||
const amount = `${this.escapeHtml(outcome.amountFormatted)} ${this.escapeHtml(currency)}`;
|
||
if (outcome.capApplied) return `${label}: ${amount} (configured cap applied)`;
|
||
return `${label}: ${this.escapeHtml(outcome.multiplier)}× = ${amount}`;
|
||
};
|
||
return `<span style="color:#4ade80;">💰 ${describe('Escape', outcomes.escape)} • ${describe('Treasure', outcomes.escapeWithTreasure)}</span>`;
|
||
},
|
||
|
||
render: function() {
|
||
if (!this.config) return;
|
||
|
||
// Display label (sXMR on stagenet): distinct from cryptoType, which formatPrice
|
||
// still uses for decimals.
|
||
const currency = this.config.currencyLabel || this.config.cryptoType || 'WOW';
|
||
const hasCredits = this.userCredits >= (this.config.creditsPerGame || 1);
|
||
const creditPackages = this.config.creditPackages || [];
|
||
const cosmeticProducts = this.config.cosmeticProducts || [];
|
||
|
||
const $container = $('.game-modes');
|
||
$container.empty();
|
||
|
||
// Reset selected package
|
||
this.selectedPackageId = null;
|
||
|
||
// Explain the two leaderboards up front when both paid and free are available.
|
||
if (this.config.paymentsEnabled && this.config.freePlayEnabled) {
|
||
const paidEntryKinds = [];
|
||
if (this.config.creditsModeEnabled) paidEntryKinds.push('credits');
|
||
if (this.config.directModeEnabled) paidEntryKinds.push('single entry');
|
||
const paidEntryCopy = paidEntryKinds.join(' or ') || 'a paid entry';
|
||
$container.append(
|
||
'<div style="margin-bottom:14px;padding:10px;background:rgba(245,158,11,0.08);border:1px solid #92400e;border-radius:6px;font-size:0.85em;line-height:1.5;">' +
|
||
'🏅 <strong style="color:#fbbf24;">Pay</strong> (' + this.escapeHtml(paidEntryCopy) + ') to put your high scores in the <strong style="color:#fbbf24;">Hall of Champions</strong>.<br>' +
|
||
'🪙 <strong style="color:#8ab4f8;">Play free</strong>: high scores go to the <strong style="color:#8ab4f8;">Pleb leaderboard</strong> instead.' +
|
||
'</div>'
|
||
);
|
||
}
|
||
|
||
// === SECTION 1: Use existing credits (if available) ===
|
||
if (hasCredits && this.config.creditsModeEnabled) {
|
||
const creditsPayoutsEnabled = this.config.creditsPayoutsEnabled;
|
||
|
||
let payoutNote = '';
|
||
if (creditsPayoutsEnabled) {
|
||
payoutNote = `${this.payoutSummaryHtml('credits', currency)}<br><span style="color:#fbbf24;">🏅 High scores enter the Hall of Champions</span>`;
|
||
} else {
|
||
payoutNote = '<span style="color:#fbbf24;">🏅 High scores enter the Hall of Champions</span>';
|
||
}
|
||
|
||
$container.append(`
|
||
<div class="payment-section" style="margin-bottom:15px;padding:12px;background:#0d3320;border:2px solid #4ade80;border-radius:6px;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||
<strong style="color:#4ade80;font-size:1.1em;">🎮 You have ${this.userCredits} credits</strong>
|
||
</div>
|
||
<div style="font-size:0.85em;margin-bottom:10px;">
|
||
${payoutNote}
|
||
</div>
|
||
<button class="mode-option recommended" data-mode="PAID_CREDITS" data-action="use_credit"
|
||
style="width:100%;padding:10px;background:#166534;border:none;color:#fff;cursor:pointer;border-radius:4px;">
|
||
<strong>▶ PLAY NOW</strong> (use 1 credit)
|
||
</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// === SECTION 2: Direct payment (pay per game) ===
|
||
if (this.config.directModeEnabled) {
|
||
const price = this.config.singleGamePriceFormatted || this.formatPrice(this.config.singleGamePrice);
|
||
// Payout-honest: only show multipliers when payouts are actually on (default off, not on).
|
||
const directPayoutsEnabled = this.config.directPayoutsEnabled != null
|
||
? !!this.config.directPayoutsEnabled
|
||
: !!this.config.payoutsEnabled;
|
||
|
||
let payoutInfo = '';
|
||
if (directPayoutsEnabled) {
|
||
payoutInfo = `${this.payoutSummaryHtml('direct', currency)}<br><span style="color:#fbbf24;">🏅 High scores enter the Hall of Champions</span>`;
|
||
} else {
|
||
payoutInfo = '<span style="color:#fbbf24;">🏅 High scores enter the Hall of Champions</span>';
|
||
}
|
||
|
||
$container.append(`
|
||
<div class="payment-section" style="margin-bottom:15px;padding:12px;background:#1a1a2e;border:1px solid #444;border-radius:6px;">
|
||
<div style="margin-bottom:8px;">
|
||
<strong style="color:#f0f0f0;">⚡ Single-game entry</strong>
|
||
<span style="float:right;color:#fbbf24;font-weight:bold;">${price} ${currency}</span>
|
||
</div>
|
||
<div style="font-size:0.85em;color:#aaa;margin-bottom:6px;">
|
||
${payoutInfo}
|
||
</div>
|
||
<div style="font-size:0.78em;color:#888;margin-bottom:10px;">Adds one purchase-progress point for unlock thresholds; no spendable credit is added.</div>
|
||
<button class="mode-option" data-mode="PAID_SINGLE" data-action="pay_direct"
|
||
style="width:100%;padding:8px;background:#3730a3;border:none;color:#fff;cursor:pointer;border-radius:4px;">
|
||
💰 Pay ${price} ${currency} to Play
|
||
</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// === SECTION 3: Buy credits (bulk discount) ===
|
||
if (this.config.creditsModeEnabled && creditPackages.length > 0) {
|
||
const singlePrice = this.config.singleGamePrice;
|
||
const creditsPayoutsEnabled = this.config.creditsPayoutsEnabled;
|
||
|
||
// Build payout info for credits mode
|
||
let creditsPayoutInfo = '';
|
||
if (creditsPayoutsEnabled) {
|
||
creditsPayoutInfo = `<div style="font-size:0.85em;margin-bottom:10px;">${this.payoutSummaryHtml('credits', currency)}</div>`;
|
||
} else {
|
||
creditsPayoutInfo = `<div style="font-size:0.85em;color:#fbbf24;margin-bottom:10px;">🏅 High scores enter the Hall of Champions • bulk discount</div>`;
|
||
}
|
||
|
||
let packagesHtml = '';
|
||
creditPackages.forEach((pkg, index) => {
|
||
const totalCredits = pkg.credits + (pkg.bonus || 0);
|
||
const pricePerCredit = pkg.price / totalCredits;
|
||
const priceFormatted = pkg.priceFormatted || this.formatPrice(pkg.price);
|
||
const perGamePrice = this.formatPrice(pricePerCredit);
|
||
|
||
// Calculate discount vs direct play
|
||
let discountBadge = '';
|
||
if (singlePrice > 0) {
|
||
const directCostForSameGames = singlePrice * totalCredits;
|
||
const savings = ((directCostForSameGames - pkg.price) / directCostForSameGames * 100).toFixed(0);
|
||
if (savings > 0) {
|
||
discountBadge = `<span style="background:#166534;color:#4ade80;padding:2px 6px;border-radius:3px;font-size:0.75em;margin-left:8px;">Save ${savings}%</span>`;
|
||
}
|
||
}
|
||
|
||
const bonusText = pkg.bonus > 0
|
||
? `<span style="color:#4ade80;"> +${pkg.bonus} bonus</span>`
|
||
: '';
|
||
const grantSummary = this.grantSummaryHtml(pkg.grants, { indent: 22 });
|
||
|
||
const checked = index === 0 ? 'checked' : '';
|
||
if (index === 0) this.selectedPackageId = pkg.id;
|
||
|
||
packagesHtml += `
|
||
<label style="display:block;padding:10px;margin:5px 0;background:#252540;border:1px solid #444;border-radius:4px;cursor:pointer;"
|
||
class="package-option" data-package-id="${pkg.id}">
|
||
<input type="radio" name="credit-package" value="${this.escapeHtml(pkg.id)}" ${checked} style="margin-right:10px;">
|
||
<strong>${pkg.credits} credits${bonusText}</strong>${discountBadge}
|
||
<span style="float:right;color:#fbbf24;font-weight:bold;">${priceFormatted} ${currency}</span>
|
||
<div style="font-size:0.8em;color:#888;margin-top:4px;margin-left:22px;">
|
||
${perGamePrice} ${currency}/game for ${totalCredits} games
|
||
</div>
|
||
${grantSummary}
|
||
</label>
|
||
`;
|
||
});
|
||
|
||
$container.append(`
|
||
<div class="payment-section" style="margin-bottom:15px;padding:12px;background:#1a1a2e;border:1px solid #444;border-radius:6px;">
|
||
<div style="margin-bottom:10px;">
|
||
<strong style="color:#f0f0f0;">🎫 Buy Credits</strong>
|
||
<span style="font-size:0.85em;color:#888;margin-left:10px;">Bulk discount • tied to this browser session</span>
|
||
</div>
|
||
${creditsPayoutInfo}
|
||
<div class="packages-list">
|
||
${packagesHtml}
|
||
</div>
|
||
<button id="buy-credits-btn" class="mode-option"
|
||
style="width:100%;padding:10px;margin-top:10px;background:#7c3aed;border:none;color:#fff;cursor:pointer;border-radius:4px;font-weight:bold;">
|
||
🛒 Buy Selected Package
|
||
</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// === SECTION 4: Standalone cosmetic/render products ===
|
||
if (this.config.paymentsEnabled && cosmeticProducts.length > 0) {
|
||
let productsHtml = '';
|
||
cosmeticProducts.forEach(product => {
|
||
const priceFormatted = product.priceFormatted || this.formatPrice(product.price);
|
||
const label = this.escapeHtml(product.label || product.id);
|
||
const grantSummary = this.grantSummaryHtml(product.grants, { includeCredits: true });
|
||
productsHtml += `
|
||
<div style="padding:10px;margin:5px 0;background:#252540;border:1px solid #444;border-radius:4px;">
|
||
<div style="display:flex;gap:10px;justify-content:space-between;align-items:center;">
|
||
<strong style="color:#f0f0f0;">${label}</strong>
|
||
<span style="color:#fbbf24;font-weight:bold;">${priceFormatted} ${currency}</span>
|
||
</div>
|
||
${grantSummary}
|
||
<button class="mode-option" data-mode="SHOP" data-action="buy_product" data-package-id="${this.escapeHtml(product.id)}"
|
||
style="width:100%;padding:8px;margin-top:8px;background:#334155;border:none;color:#fff;cursor:pointer;border-radius:4px;">
|
||
Unlock Pack
|
||
</button>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
$container.append(`
|
||
<div class="payment-section" style="margin-bottom:15px;padding:12px;background:#1a1a2e;border:1px solid #444;border-radius:6px;">
|
||
<div style="margin-bottom:10px;">
|
||
<strong style="color:#f0f0f0;">Premium Packs</strong>
|
||
<span style="font-size:0.85em;color:#888;margin-left:10px;">Cosmetics and render modes</span>
|
||
</div>
|
||
${productsHtml}
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// === Credits balance display (if has some but not enough) ===
|
||
if (this.config.creditsModeEnabled && this.userCredits > 0 && !hasCredits) {
|
||
$container.prepend(`
|
||
<div style="text-align:center;margin-bottom:10px;padding:8px;background:#1a1a2e;border:1px solid #f59e0b;border-radius:4px;">
|
||
<span style="color:#f59e0b;">⚠️ You have ${this.userCredits} credits (need ${this.config.creditsPerGame || 1} to play)</span>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// === FREE play === shown when the instance is free-only OR offers free as a choice
|
||
// alongside paid (freePlayEnabled). Free games go to the Pleb leaderboard.
|
||
if (this.config.freePlayEnabled) {
|
||
const note = this.config.paymentsEnabled
|
||
? '🪙 Free: high scores go to the Pleb leaderboard'
|
||
: 'No payments required';
|
||
$container.append(`
|
||
<button class="mode-option" data-mode="FREE" data-action="free"
|
||
style="width:100%;padding:12px;background:#166534;border:none;color:#fff;cursor:pointer;border-radius:4px;">
|
||
<strong>🆓 Play Free</strong><br>
|
||
<span style="font-size:0.8em;color:#aaa;">${note}</span>
|
||
</button>
|
||
`);
|
||
}
|
||
},
|
||
|
||
handleModeSelection: function(mode, action, packageId, acknowledgementReady, legalAcknowledgement) {
|
||
console.log("Mode selected:", mode, "Action:", action, "Package:", packageId);
|
||
|
||
if (mode === 'FREE') {
|
||
$('#payment-ui').hide();
|
||
if (window.socket && typeof SocketHandlers !== 'undefined' && SocketHandlers.emitFairGameStart) {
|
||
// play_free records the game as FREE (Pleb leaderboard, no payout) even on
|
||
// an instance that also sells credits/entry.
|
||
SocketHandlers.emitFairGameStart('play_free', { free: true });
|
||
}
|
||
return;
|
||
}
|
||
|
||
// A checkbox in the browser is not a legal/identity verification, but requiring a fresh
|
||
// per-session statement prevents the UI from silently opening a value-bearing action.
|
||
if (!acknowledgementReady) {
|
||
if (typeof CommerceConsent === 'undefined' || !CommerceConsent.require) {
|
||
$('#payment-status').text('Paid-play disclosures are unavailable. No payment was requested.');
|
||
return;
|
||
}
|
||
CommerceConsent.require(function(acknowledgement) {
|
||
PaymentUI.handleModeSelection(mode, action, packageId, true, acknowledgement);
|
||
});
|
||
return;
|
||
}
|
||
|
||
// If using existing credits, just start the game
|
||
if (action === 'use_credit' && this.userCredits >= 1) {
|
||
$('#payment-ui').hide();
|
||
if (window.socket && typeof SocketHandlers !== 'undefined' && SocketHandlers.emitFairGameStart) {
|
||
SocketHandlers.emitFairGameStart('auto_start', {
|
||
legalAcknowledgement: legalAcknowledgement
|
||
}, true);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Request payment with type and optional packageId
|
||
let paymentType = 'single_game';
|
||
if (action === 'buy_credits') {
|
||
paymentType = 'credits_package';
|
||
} else if (action === 'buy_product') {
|
||
paymentType = 'cosmetic_pack';
|
||
}
|
||
|
||
const requestData = { type: paymentType };
|
||
if (legalAcknowledgement) requestData.legalAcknowledgement = legalAcknowledgement;
|
||
if (packageId) {
|
||
if (paymentType === 'cosmetic_pack') {
|
||
requestData.productId = packageId;
|
||
} else {
|
||
requestData.packageId = packageId;
|
||
}
|
||
}
|
||
|
||
if (window.socket) {
|
||
if (paymentType === 'single_game' && typeof SocketHandlers !== 'undefined' && SocketHandlers.fairnessAttempt) {
|
||
var fairRequest = SocketHandlers.fairnessAttempt(requestData);
|
||
if (!fairRequest) return;
|
||
window.socket.emit('request_payment', fairRequest);
|
||
} else {
|
||
window.socket.emit('request_payment', requestData);
|
||
}
|
||
|
||
// Show loading state
|
||
$('.game-modes').hide();
|
||
$('#payment-details').show();
|
||
$('#payment-status').text('Requesting payment address...');
|
||
$('#payment-amount').text('...');
|
||
$('#payment-address').val('Loading...');
|
||
}
|
||
},
|
||
|
||
showPaymentRequest: function(data) {
|
||
$('#payment-ui').show();
|
||
$('#payment-details').show();
|
||
$('.game-modes').hide();
|
||
|
||
const isCreditsPurchase = data.paymentType === 'credits_package';
|
||
const isProductPurchase = data.paymentType === 'cosmetic_pack';
|
||
let headerText = '💳 Payment Required';
|
||
if (isCreditsPurchase && data.package) {
|
||
const bonus = data.package.bonus > 0 ? ` +${data.package.bonus} bonus` : '';
|
||
headerText = `🎫 Buy ${data.package.credits}${bonus} Credits`;
|
||
} else if (isProductPurchase && data.package) {
|
||
headerText = `Buy ${data.package.label || data.package.id || 'Pack'}`;
|
||
}
|
||
$('.payment-header strong').text(headerText);
|
||
|
||
const displayAmount = data.humanAmount || data.amountFormatted || data.amount;
|
||
$('#payment-amount').text(displayAmount + ' ' + (data.currency || ''));
|
||
$('#payment-address').val(data.address);
|
||
$('#check-payment').data('payment-id', data.paymentId || data.id);
|
||
|
||
if (data.reused) {
|
||
$('#payment-status').html('<span style="color:#ff0">♻️ Using existing pending payment</span>');
|
||
} else {
|
||
const statusText = isCreditsPurchase
|
||
? 'Send payment to receive credits...'
|
||
: (isProductPurchase ? 'Send payment to unlock this pack...' : 'Waiting for payment...');
|
||
$('#payment-status').text(statusText);
|
||
}
|
||
},
|
||
|
||
show: function() {
|
||
this.render(); // Ensure options are up-to-date with current config
|
||
$('#payment-ui').show();
|
||
$('.game-modes').show();
|
||
$('#payment-details').hide();
|
||
$('.payment-header strong').text('🎮 Choose How to Play');
|
||
}
|
||
};
|
||
|
||
// Expose globally
|
||
window.PaymentUI = PaymentUI;
|
||
|
||
// Initialize when DOM is ready
|
||
$(document).ready(function() {
|
||
PaymentUI.init();
|
||
});
|