Files
remnawave-minishop/bot/app/web/frontend/src/webapp/PaymentDialogs.svelte
T

357 lines
14 KiB
Svelte

<script>
import { ArrowLeft, CheckCircle2, CircleX, LockKeyhole, RefreshCw, TriangleAlert } from "lucide-svelte";
import { Tooltip } from "$components/ui/primitives.js";
import Button from "$components/ui/button.svelte";
import Card from "$components/ui/card.svelte";
import Dialog from "$components/ui/dialog.svelte";
import Input from "$components/ui/input.svelte";
import {
planKey as planKeyFn,
planDisplayTitle as planDisplayTitleFn,
planSubtitle as planSubtitleFn,
planUnitHint as planUnitHintFn,
tariffLimitLabel as tariffLimitLabelFn,
priceLabel as priceLabelFn,
} from "../lib/webapp/tariffs.js";
import { Bitcoin, CreditCard } from "lucide-svelte";
export let createPayment = () => {};
export let deviceConfirmOpen = false;
export let deviceDisconnectBusy = false;
export let deviceToDisconnect = null;
export let disconnectDevice = () => {};
export let linkEmailBusy = false;
export let linkEmailCode = "";
export let linkEmailFieldError = "";
export let linkEmailIsError = false;
export let linkEmailOpen = false;
export let linkEmailPending = "";
export let linkEmailResendCooldown = 0;
export let linkEmailStatus = "";
export let linkEmailValue = "";
export let hasMultipleTariffs = false;
export let methods = [];
export let payBusy = false;
export let paymentModalOpen = false;
export let paymentStep = "tariff";
export let plans = [];
export let selectedMethod = "";
export let selectedPlan = null;
export let selectedTariff = null;
export let selectedTariffKey = "";
export let selectedTariffPlans = [];
export let singleTariffMode = false;
export let subscription = {};
export let tariffCatalog = [];
export let tariffMode = false;
export let trafficMode = false;
function methodMeta(method) {
const id = String(method?.id || "").toLowerCase();
if (id.includes("platega_sbp")) return { title: t("wa_method_platega_sbp_card"), icon: CreditCard };
if (id.includes("platega_crypto")) return { title: t("wa_method_platega_crypto"), icon: Bitcoin };
if (id.includes("yookassa") || id.includes("card")) return { title: t("pay_with_yookassa_button"), icon: null };
if (id.includes("severpay")) return { title: t("pay_with_severpay_button"), icon: null };
if (id.includes("freekassa")) return { title: t("pay_with_sbp_button"), icon: null };
if (id.includes("cryptopay") || id.includes("crypto")) return { title: t("pay_with_cryptopay_button"), icon: null };
if (id.includes("stars")) return { title: t("pay_with_stars_button"), icon: null };
if (id.includes("sbp")) return { title: t("pay_with_sbp_button"), icon: null };
return { title: t("wa_method_other_title"), icon: null };
}
function priceLabel(plan) { return priceLabelFn(plan, selectedMethod); }
function planKey(plan) { return planKeyFn(plan); }
function planDisplayTitle(plan) { return planDisplayTitleFn(plan, { trafficMode, t }); }
function planSubtitle(plan) { return planSubtitleFn(plan, { t, termUnitLabel }); }
function planUnitHint(plan) { return planUnitHintFn(plan, { trafficMode, selectedMethod, t }); }
function tariffLimitLabel(tariff) { return tariffLimitLabelFn(tariff, { t }); }
function paymentTitle() {
if (singleTariffMode) {
return selectedTariff?.billing_model === "traffic" ? t("wa_traffic_packages_title") : t("wa_subscription_title");
}
if (tariffMode) return t("wa_tariffs_title");
return trafficMode ? t("wa_traffic_packages_title") : t("wa_subscription_title");
}
function paymentDescription() {
if (tariffMode) {
if (singleTariffMode) {
return selectedTariff?.billing_model === "traffic" ? t("wa_traffic_packages_choose") : t("wa_subscription_choose_period");
}
return paymentStep === "checkout" && selectedTariff
? t("wa_tariff_choose_period_payment", { tariff: selectedTariff.title })
: t("wa_tariffs_choose");
}
return trafficMode ? t("wa_traffic_packages_choose") : t("wa_subscription_choose_period");
}
export let closeDeviceDisconnectDialog = () => {};
export let closeLinkEmailDialog = () => {};
export let closePaymentModal = () => {};
export let backToTariffList = () => {};
export let continueWithSelectedTariff = () => {};
export let requestLinkEmailCode = () => {};
export let selectTariff = () => {};
export let t = (key) => key;
export let termUnitLabel = () => "";
export let verifyLinkEmailCode = () => {};
</script>
<Dialog
open={paymentModalOpen}
title={paymentTitle()}
description={paymentDescription()}
closeLabel={t("wa_close")}
onclose={closePaymentModal}
class="payment-dialog-card"
>
<div class="payment-dialog-body">
{#if tariffMode && !singleTariffMode && paymentStep === "tariff"}
{#if tariffCatalog.length}
<div class="option-list tariff-list">
{#each tariffCatalog as tariff}
<button
class:active={selectedTariffKey === tariff.key}
class="option-row tariff-row"
type="button"
onclick={() => selectTariff(tariff)}
>
<span class="option-row-main">
<strong>{tariff.title}</strong>
<small>{tariff.description || t("wa_tariff_no_description")}</small>
</span>
<span class="option-row-meta">
<em>{tariffLimitLabel(tariff)}</em>
{#if selectedTariffKey === tariff.key}
<CheckCircle2 size={18} />
{:else}
<ArrowRight size={17} />
{/if}
</span>
</button>
{/each}
</div>
<Button class="wide bottom-action payment-submit-button" onclick={continueWithSelectedTariff} disabled={!selectedTariffKey}>
{t("wa_next")}
<ArrowRight size={17} />
</Button>
{:else}
<Card class="empty-card">{t("wa_no_tariff_change_options")}</Card>
{/if}
{:else}
{#if tariffMode}
{#if !singleTariffMode && !(subscription?.active && subscription?.tariff_key && tariffCatalog.some((t) => t.key === subscription.tariff_key))}
<button class="back-inline" type="button" onclick={backToTariffList}>
<ArrowLeft size={16} />
{t("wa_back_to_tariffs")}
</button>
{/if}
{#if hasMultipleTariffs && selectedTariff}
<p class="tariff-step-caption">{t("wa_selected_tariff", { tariff: selectedTariff.title })}</p>
{/if}
{/if}
{#if selectedTariffPlans.length}
<div class="period-grid period-grid-two-columns">
{#each selectedTariffPlans as plan}
<button
class:active={planKey(selectedPlan) === planKey(plan)}
class="period-card"
type="button"
onclick={() => (selectedPlan = plan)}
>
<strong>{planSubtitle(plan) || planDisplayTitle(plan)}</strong>
<span>{priceLabel(plan)}</span>
{#if planUnitHint(plan)}
<small>{planUnitHint(plan)}</small>
{/if}
{#if planKey(selectedPlan) === planKey(plan)}
<CheckCircle2 size={18} />
{/if}
</button>
{/each}
</div>
<div class="payment-divider" aria-hidden="true"></div>
<div class="method-grid">
{#if methods.length}
{#each methods as method}
{@const meta = methodMeta(method)}
<button
class:active={selectedMethod === method.id}
class="method-card"
type="button"
onclick={() => (selectedMethod = method.id)}
>
<span class="method-card-main">
{#if meta.icon}
<svelte:component this={meta.icon} size={19} />
{/if}
<strong>{meta.title}</strong>
</span>
</button>
{/each}
{:else}
<Card class="empty-card">{t("wa_payment_methods_not_configured")}</Card>
{/if}
</div>
<Button class="wide bottom-action payment-submit-button" onclick={createPayment} disabled={!selectedPlan || !methods.length || payBusy}>
{t("wa_pay")} {selectedPlan ? priceLabel(selectedPlan) : ""}
<LockKeyhole size={17} />
</Button>
{:else}
<Card class="empty-card">{t("wa_no_tariff_change_options")}</Card>
{/if}
{/if}
{#if !tariffMode}
<div class="period-grid period-grid-two-columns">
{#each plans as plan}
<button
class:active={planKey(selectedPlan) === planKey(plan)}
class="period-card"
type="button"
onclick={() => (selectedPlan = plan)}
>
<strong>{planDisplayTitle(plan)}</strong>
{#if planSubtitle(plan)}
<em>{planSubtitle(plan)}</em>
{/if}
<span>{priceLabel(plan)}</span>
{#if planUnitHint(plan)}
<small>{planUnitHint(plan)}</small>
{/if}
{#if planKey(selectedPlan) === planKey(plan)}
<CheckCircle2 size={18} />
{/if}
</button>
{/each}
</div>
<div class="payment-divider" aria-hidden="true"></div>
<div class="method-grid">
{#if methods.length}
{#each methods as method}
{@const meta = methodMeta(method)}
<button
class:active={selectedMethod === method.id}
class="method-card"
type="button"
onclick={() => (selectedMethod = method.id)}
>
<span class="method-card-main">
{#if meta.icon}
<svelte:component this={meta.icon} size={19} />
{/if}
<strong>{meta.title}</strong>
</span>
</button>
{/each}
{:else}
<Card class="empty-card">{t("wa_payment_methods_not_configured")}</Card>
{/if}
</div>
<Button class="wide bottom-action payment-submit-button" onclick={createPayment} disabled={!selectedPlan || !methods.length || payBusy}>
{t("wa_pay")} {selectedPlan ? priceLabel(selectedPlan) : ""}
<LockKeyhole size={17} />
</Button>
{/if}
</div>
</Dialog>
<Dialog
open={deviceConfirmOpen}
title={t("wa_devices_disconnect_title")}
description={t("wa_devices_disconnect_desc", {
device: deviceToDisconnect?.display_name || t("wa_device_fallback_name", { index: deviceToDisconnect?.index || "" }),
})}
closeLabel={t("wa_close")}
onclose={closeDeviceDisconnectDialog}
class="payment-dialog-card"
>
<div class="payment-dialog-body">
<Button variant="outline" class="wide device-danger-button" onclick={disconnectDevice} disabled={deviceDisconnectBusy}>
<CircleX size={17} />
{t("wa_devices_disconnect_confirm")}
</Button>
<Button variant="secondary" class="wide" onclick={closeDeviceDisconnectDialog} disabled={deviceDisconnectBusy}>
{t("wa_cancel")}
</Button>
</div>
</Dialog>
<Dialog
open={linkEmailOpen}
title={t("wa_link_email_modal_title")}
description={linkEmailPending ? t("wa_email_sent_to", { email: linkEmailPending }) : t("wa_link_email_modal_desc")}
closeLabel={t("wa_close")}
onclose={closeLinkEmailDialog}
class={`payment-dialog-card${linkEmailPending ? " link-email-dialog-card" : ""}`}
>
<div class="payment-dialog-body">
{#if !linkEmailPending}
<div class="field-error-wrap">
<Tooltip.Root open={Boolean(linkEmailFieldError)}>
<Input
bind:value={linkEmailValue}
type="email"
placeholder={t("wa_email_placeholder")}
autocomplete="email"
class={linkEmailFieldError ? "input-error" : ""}
on:input={() => (linkEmailFieldError = "")}
/>
{#if linkEmailFieldError}
<Tooltip.Trigger class="field-error-trigger" aria-label={linkEmailFieldError}>
<span class="field-error-icon" aria-hidden="true"><TriangleAlert size={18} /></span>
</Tooltip.Trigger>
{/if}
{#if linkEmailFieldError}
<Tooltip.Portal>
<Tooltip.Content class="field-error-tooltip">{linkEmailFieldError}</Tooltip.Content>
</Tooltip.Portal>
{/if}
</Tooltip.Root>
</div>
<Button class="wide bottom-action payment-submit-button" onclick={requestLinkEmailCode} disabled={linkEmailBusy}>
{t("wa_send_code_email")}
</Button>
{:else}
<div class="link-email-code-layout">
<div class="otp-wrap link-email-code-center">
<label class="otp-input-wrap">
<input
bind:value={linkEmailCode}
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
aria-label={t("wa_email_code_aria")}
/>
<span class="otp-slots" aria-hidden="true">
{#each Array.from({ length: 6 }) as _, index}
<span class:filled={linkEmailCode[index]}>{linkEmailCode[index] || ""}</span>
{/each}
</span>
</label>
<Button class="wide bottom-action payment-submit-button" onclick={verifyLinkEmailCode} disabled={linkEmailBusy}>
{t("wa_confirm")}
</Button>
</div>
<button
class="link-button link-email-resend"
type="button"
onclick={requestLinkEmailCode}
disabled={linkEmailBusy || linkEmailResendCooldown > 0}
>
<RefreshCw size={15} />
{linkEmailResendCooldown > 0
? t("wa_auth_resend_wait", { seconds: linkEmailResendCooldown })
: t("wa_resend_code")}
</button>
</div>
{/if}
{#if linkEmailStatus}
<p class:error={linkEmailIsError} class="status-line">{linkEmailStatus}</p>
{/if}
</div>
</Dialog>