feat: embed propose-question widget inline in main game page
- Move /pregunta flow into a compact ProposeQuestion component rendered inside <main> above the footer - Tier cards, username input, Redsys payment, polling, and question submission all work inline without leaving the game page - Payment redirects now go to /?credito_ok=ORDER and /?ko=1 so the game stays in view during the full payment cycle - Badge shows live questions remaining; updates on each submission - Removed /pregunta link from footer (functionality is now inline) - .propose-* CSS: compact tier grid, textarea+button row, spinner Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
180
frontend.css
180
frontend.css
@@ -769,6 +769,186 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Propose Question widget ─────────────────────────────────── */
|
||||||
|
|
||||||
|
.propose {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 14px 0 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__title {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4caf7d;
|
||||||
|
background: rgba(76, 175, 125, 0.12);
|
||||||
|
border: 1px solid rgba(76, 175, 125, 0.25);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__badge--empty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__tiers {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__tier {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 8px 6px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--sans);
|
||||||
|
color: var(--text);
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__tier:hover { border-color: #444; }
|
||||||
|
|
||||||
|
.propose__tier--selected {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: rgba(217, 119, 87, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__tier__price {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__tier__label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__row--mt { margin-top: 2px; }
|
||||||
|
|
||||||
|
.propose__input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__input:focus { outline: none; border-color: #444; }
|
||||||
|
.propose__input::placeholder { color: var(--text-muted); }
|
||||||
|
|
||||||
|
.propose__textarea {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
resize: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__textarea:focus { outline: none; border-color: #444; }
|
||||||
|
.propose__textarea::placeholder { color: var(--text-muted); }
|
||||||
|
|
||||||
|
.propose__btn {
|
||||||
|
padding: 7px 14px;
|
||||||
|
background: var(--accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--sans);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__btn:hover:not(:disabled) { opacity: 0.85; }
|
||||||
|
.propose__btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.propose__hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__msg {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__msg--ok { color: #4caf7d; }
|
||||||
|
.propose__msg--error { color: #ff6b6b; }
|
||||||
|
|
||||||
|
.propose__spinner {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: propose-spin 0.8s linear infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes propose-spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
.propose__link-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propose__link-btn:hover { color: var(--text-dim); }
|
||||||
|
|
||||||
/* ── Desktop (1024px+) ───────────────────────────────────────── */
|
/* ── Desktop (1024px+) ───────────────────────────────────────── */
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
|
|||||||
299
frontend.tsx
299
frontend.tsx
@@ -57,6 +57,65 @@ type ViewerCountMessage = {
|
|||||||
};
|
};
|
||||||
type ServerMessage = StateMessage | ViewerCountMessage;
|
type ServerMessage = StateMessage | ViewerCountMessage;
|
||||||
|
|
||||||
|
// ── Credit / Propose ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type CreditInfo = {
|
||||||
|
token: string;
|
||||||
|
username: string;
|
||||||
|
expiresAt: number;
|
||||||
|
tier: string;
|
||||||
|
questionsLeft: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CREDIT_STORAGE_KEY = "argumentes_credito";
|
||||||
|
|
||||||
|
const PROPOSE_TIERS = [
|
||||||
|
{ id: "basico", label: "10 preguntas", price: "0,99€" },
|
||||||
|
{ id: "pro", label: "200 preguntas", price: "9,99€" },
|
||||||
|
{ id: "ilimitado", label: "Ilimitadas", price: "19,99€" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type ProposeTierId = (typeof PROPOSE_TIERS)[number]["id"];
|
||||||
|
|
||||||
|
function loadCredit(): CreditInfo | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CREDIT_STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const c = JSON.parse(raw) as CreditInfo;
|
||||||
|
if (!c.token || !c.expiresAt || c.expiresAt < Date.now()) {
|
||||||
|
localStorage.removeItem(CREDIT_STORAGE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRedsysForm(data: {
|
||||||
|
tpvUrl: string;
|
||||||
|
merchantParams: string;
|
||||||
|
signature: string;
|
||||||
|
signatureVersion: string;
|
||||||
|
}) {
|
||||||
|
const form = document.createElement("form");
|
||||||
|
form.method = "POST";
|
||||||
|
form.action = data.tpvUrl;
|
||||||
|
for (const [name, value] of Object.entries({
|
||||||
|
Ds_SignatureVersion: data.signatureVersion,
|
||||||
|
Ds_MerchantParameters: data.merchantParams,
|
||||||
|
Ds_Signature: data.signature,
|
||||||
|
})) {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "hidden";
|
||||||
|
input.name = name;
|
||||||
|
input.value = value;
|
||||||
|
form.appendChild(input);
|
||||||
|
}
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Model colors & logos ─────────────────────────────────────────────────────
|
// ── Model colors & logos ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const MODEL_COLORS: Record<string, string> = {
|
const MODEL_COLORS: Record<string, string> = {
|
||||||
@@ -533,6 +592,241 @@ function Standings({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Propose Question (inline widget) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
function ProposeQuestion() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const creditOkOrder = params.get("credito_ok");
|
||||||
|
const isKo = params.get("ko") === "1";
|
||||||
|
|
||||||
|
const [credit, setCredit] = useState<CreditInfo | null>(null);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const [verifying, setVerifying] = useState(false);
|
||||||
|
const [verifyError, setVerifyError] = useState(false);
|
||||||
|
const [selectedTier, setSelectedTier] = useState<ProposeTierId | null>(null);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [buying, setBuying] = useState(false);
|
||||||
|
const [buyError, setBuyError] = useState<string | null>(null);
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
|
const [sent, setSent] = useState(false);
|
||||||
|
const [koDismissed, setKoDismissed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCredit(loadCredit());
|
||||||
|
setLoaded(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!creditOkOrder || !loaded || credit) return;
|
||||||
|
setVerifying(true);
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
async function poll() {
|
||||||
|
if (attempts >= 15) { setVerifying(false); setVerifyError(true); return; }
|
||||||
|
attempts++;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/credito/estado?order=${encodeURIComponent(creditOkOrder!)}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json() as {
|
||||||
|
found: boolean; status?: string; token?: string;
|
||||||
|
username?: string; expiresAt?: number; tier?: string;
|
||||||
|
questionsLeft?: number | null;
|
||||||
|
};
|
||||||
|
if (data.found && data.status === "active" && data.token && data.expiresAt) {
|
||||||
|
const newCredit: CreditInfo = {
|
||||||
|
token: data.token, username: data.username ?? "",
|
||||||
|
expiresAt: data.expiresAt, tier: data.tier ?? "",
|
||||||
|
questionsLeft: data.questionsLeft ?? null,
|
||||||
|
};
|
||||||
|
localStorage.setItem(CREDIT_STORAGE_KEY, JSON.stringify(newCredit));
|
||||||
|
setCredit(newCredit);
|
||||||
|
setVerifying(false);
|
||||||
|
history.replaceState(null, "", "/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* retry */ }
|
||||||
|
setTimeout(poll, 2000);
|
||||||
|
}
|
||||||
|
poll();
|
||||||
|
}, [creditOkOrder, loaded, credit]);
|
||||||
|
|
||||||
|
async function handleBuyCredit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!selectedTier) return;
|
||||||
|
setBuyError(null);
|
||||||
|
setBuying(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/credito/iniciar", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ tier: selectedTier, username: username.trim() }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(await res.text());
|
||||||
|
await submitRedsysForm(await res.json());
|
||||||
|
} catch (err) {
|
||||||
|
setBuyError(err instanceof Error ? err.message : "Error al procesar el pago");
|
||||||
|
setBuying(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmitQuestion(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!credit) return;
|
||||||
|
setSubmitError(null);
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/pregunta/enviar", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ text: text.trim(), token: credit.token }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 401) {
|
||||||
|
localStorage.removeItem(CREDIT_STORAGE_KEY);
|
||||||
|
setCredit(null);
|
||||||
|
throw new Error("Acceso expirado o sin preguntas disponibles.");
|
||||||
|
}
|
||||||
|
throw new Error(await res.text() || `Error ${res.status}`);
|
||||||
|
}
|
||||||
|
const data = await res.json() as { ok: boolean; questionsLeft: number | null };
|
||||||
|
const updated: CreditInfo = { ...credit, questionsLeft: data.questionsLeft };
|
||||||
|
localStorage.setItem(CREDIT_STORAGE_KEY, JSON.stringify(updated));
|
||||||
|
setCredit(updated);
|
||||||
|
setText("");
|
||||||
|
setSent(true);
|
||||||
|
setTimeout(() => setSent(false), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
setSubmitError(err instanceof Error ? err.message : "Error al enviar");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded) return null;
|
||||||
|
|
||||||
|
const tierInfo = selectedTier ? PROPOSE_TIERS.find(t => t.id === selectedTier) : null;
|
||||||
|
const exhausted = credit !== null && credit.questionsLeft !== null && credit.questionsLeft <= 0;
|
||||||
|
|
||||||
|
// Verifying payment
|
||||||
|
if (verifying || (creditOkOrder && !credit)) {
|
||||||
|
return (
|
||||||
|
<div className="propose">
|
||||||
|
<div className="propose__head">
|
||||||
|
<span className="propose__title">Verificando pago</span>
|
||||||
|
{!verifyError && <div className="propose__spinner" />}
|
||||||
|
</div>
|
||||||
|
{verifyError && (
|
||||||
|
<p className="propose__msg propose__msg--error">
|
||||||
|
No se pudo confirmar. Recarga si el pago se completó.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active credit — question form
|
||||||
|
if (credit) {
|
||||||
|
const badge = credit.questionsLeft === null
|
||||||
|
? "Ilimitadas"
|
||||||
|
: `${credit.questionsLeft} restante${credit.questionsLeft !== 1 ? "s" : ""}`;
|
||||||
|
return (
|
||||||
|
<div className="propose">
|
||||||
|
<div className="propose__head">
|
||||||
|
<span className="propose__title">Propón una pregunta · {credit.username}</span>
|
||||||
|
<span className={`propose__badge ${exhausted ? "propose__badge--empty" : ""}`}>{badge}</span>
|
||||||
|
</div>
|
||||||
|
{sent && <p className="propose__msg propose__msg--ok">✓ ¡Enviada! Se usará en el próximo sorteo.</p>}
|
||||||
|
{!exhausted ? (
|
||||||
|
<form onSubmit={handleSubmitQuestion}>
|
||||||
|
<div className="propose__row">
|
||||||
|
<textarea
|
||||||
|
className="propose__textarea"
|
||||||
|
value={text}
|
||||||
|
onChange={e => setText(e.target.value)}
|
||||||
|
placeholder='"La peor cosa que puedes encontrar en ___"'
|
||||||
|
rows={2}
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
|
<button className="propose__btn" type="submit" disabled={submitting || text.trim().length < 10}>
|
||||||
|
{submitting ? "…" : "Enviar"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="propose__hint">
|
||||||
|
{text.length}/200 · mín. 10 ·{" "}
|
||||||
|
<button type="button" className="propose__link-btn" onClick={() => {
|
||||||
|
localStorage.removeItem(CREDIT_STORAGE_KEY);
|
||||||
|
setCredit(null);
|
||||||
|
}}>
|
||||||
|
cerrar sesión
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{submitError && <p className="propose__msg propose__msg--error">{submitError}</p>}
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="propose__row">
|
||||||
|
<p className="propose__msg propose__msg--error" style={{ flex: 1 }}>Has agotado tus preguntas.</p>
|
||||||
|
<button className="propose__btn" onClick={() => {
|
||||||
|
localStorage.removeItem(CREDIT_STORAGE_KEY);
|
||||||
|
setCredit(null);
|
||||||
|
}}>Nuevo plan</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier selection
|
||||||
|
return (
|
||||||
|
<div className="propose">
|
||||||
|
<div className="propose__head">
|
||||||
|
<span className="propose__title">Propón preguntas al juego</span>
|
||||||
|
</div>
|
||||||
|
{isKo && !koDismissed && (
|
||||||
|
<p className="propose__msg propose__msg--error">
|
||||||
|
El pago no se completó.{" "}
|
||||||
|
<button type="button" className="propose__link-btn" onClick={() => setKoDismissed(true)}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="propose__tiers">
|
||||||
|
{PROPOSE_TIERS.map(tier => (
|
||||||
|
<button
|
||||||
|
key={tier.id}
|
||||||
|
type="button"
|
||||||
|
className={`propose__tier ${selectedTier === tier.id ? "propose__tier--selected" : ""}`}
|
||||||
|
onClick={() => setSelectedTier(tier.id)}
|
||||||
|
>
|
||||||
|
<span className="propose__tier__price">{tier.price}</span>
|
||||||
|
<span className="propose__tier__label">{tier.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{selectedTier && (
|
||||||
|
<form onSubmit={handleBuyCredit} className="propose__row propose__row--mt">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="propose__input"
|
||||||
|
value={username}
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
placeholder="Tu nombre en el marcador"
|
||||||
|
maxLength={30}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button type="submit" className="propose__btn" disabled={buying || !username.trim()}>
|
||||||
|
{buying ? "…" : `Pagar ${tierInfo?.price}`}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
{buyError && <p className="propose__msg propose__msg--error">{buyError}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Connecting ───────────────────────────────────────────────────────────────
|
// ── Connecting ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function ConnectingScreen() {
|
function ConnectingScreen() {
|
||||||
@@ -707,6 +1001,8 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ProposeQuestion />
|
||||||
|
|
||||||
<footer className="site-footer">
|
<footer className="site-footer">
|
||||||
<p>IAs compiten respondiendo preguntas absurdas — los jueces votan, tú también puedes.</p>
|
<p>IAs compiten respondiendo preguntas absurdas — los jueces votan, tú también puedes.</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -714,8 +1010,7 @@ function App() {
|
|||||||
<a href="https://cloudhost.es" target="_blank" rel="noopener noreferrer">
|
<a href="https://cloudhost.es" target="_blank" rel="noopener noreferrer">
|
||||||
Cloud Host
|
Cloud Host
|
||||||
</a>
|
</a>
|
||||||
{" "}— La web simplificada, la nube gestionada ·{" "}
|
{" "}— La web simplificada, la nube gestionada
|
||||||
<a href="/pregunta">propón preguntas</a>
|
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -635,8 +635,8 @@ const server = Bun.serve<WsData>({
|
|||||||
isTest,
|
isTest,
|
||||||
orderId,
|
orderId,
|
||||||
amount: tierInfo.amount,
|
amount: tierInfo.amount,
|
||||||
urlOk: `${baseUrl}/pregunta?credito_ok=${orderId}`,
|
urlOk: `${baseUrl}/?credito_ok=${orderId}`,
|
||||||
urlKo: `${baseUrl}/pregunta?ko=1`,
|
urlKo: `${baseUrl}/?ko=1`,
|
||||||
merchantUrl: `${baseUrl}/api/redsys/notificacion`,
|
merchantUrl: `${baseUrl}/api/redsys/notificacion`,
|
||||||
productDescription: `Acceso argument.es — ${tierInfo.label}`,
|
productDescription: `Acceso argument.es — ${tierInfo.label}`,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user