fix: separate HWID device renewal flows
Keep one-off device top-ups scoped to the active subscription term and move device renewal into subscription checkout. Carry HWID renewal metadata through provider callbacks and webhooks, including YooKassa saved-card flows. Add admin extension controls, docs, demo data, and regression coverage.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
import { Tooltip } from "$components/ui/primitives.js";
|
||||
|
||||
import Button from "$components/ui/button.svelte";
|
||||
import Checkbox from "$components/ui/checkbox.svelte";
|
||||
import Dialog from "$components/ui/dialog.svelte";
|
||||
import EmailCodeScreen from "./auth/EmailCodeScreen.svelte";
|
||||
import Input from "$components/ui/input.svelte";
|
||||
@@ -52,6 +53,7 @@
|
||||
export let selectedTariff = null;
|
||||
export let selectedTariffKey = "";
|
||||
export let selectedTariffPlans = [];
|
||||
export let renewHwidDevices = true;
|
||||
export let setPasswordBusy = false;
|
||||
export let setPasswordCode = "";
|
||||
export let setPasswordConfirm = "";
|
||||
@@ -72,6 +74,81 @@
|
||||
function priceLabel(plan) {
|
||||
return priceLabelFn(plan, selectedMethod);
|
||||
}
|
||||
function methodUsesStars() {
|
||||
return String(selectedMethod || "")
|
||||
.toLowerCase()
|
||||
.includes("stars");
|
||||
}
|
||||
function hwidRenewalFor(plan) {
|
||||
return plan?.hwid_renewal?.available ? plan.hwid_renewal : null;
|
||||
}
|
||||
function isSubscriptionPlan(plan) {
|
||||
const saleMode = String(plan?.sale_mode || "subscription").toLowerCase();
|
||||
return saleMode === "subscription";
|
||||
}
|
||||
function hwidRenewalAvailableForMethod(plan) {
|
||||
const renewal = hwidRenewalFor(plan);
|
||||
if (!subscription?.active || !isSubscriptionPlan(plan) || !renewal) return false;
|
||||
if (methodUsesStars()) return Number(renewal.stars_price || 0) > 0;
|
||||
return Number(renewal.price || 0) > 0;
|
||||
}
|
||||
function planWithSelectedHwidRenewal(plan) {
|
||||
if (!plan || !renewHwidDevices || !hwidRenewalAvailableForMethod(plan)) return plan;
|
||||
const renewal = hwidRenewalFor(plan);
|
||||
const withRenewal = {
|
||||
...plan,
|
||||
price: Number(plan.price || 0) + Number(renewal.price || 0),
|
||||
};
|
||||
if (Number(plan.stars_price || 0) > 0 && Number(renewal.stars_price || 0) > 0) {
|
||||
withRenewal.stars_price = Number(plan.stars_price || 0) + Number(renewal.stars_price || 0);
|
||||
}
|
||||
return withRenewal;
|
||||
}
|
||||
function paymentPriceLabel(plan) {
|
||||
return priceLabelFn(planWithSelectedHwidRenewal(plan), selectedMethod);
|
||||
}
|
||||
function hwidRenewalPriceLabel(plan = selectedPlan) {
|
||||
const renewal = hwidRenewalFor(plan);
|
||||
if (!renewal) return "";
|
||||
return priceLabelFn(
|
||||
{
|
||||
price: renewal.price || 0,
|
||||
stars_price: renewal.stars_price,
|
||||
currency: renewal.currency || plan?.currency,
|
||||
},
|
||||
selectedMethod
|
||||
);
|
||||
}
|
||||
function showHwidRenewalBlock() {
|
||||
return hwidRenewalAvailableForMethod(selectedPlan);
|
||||
}
|
||||
function showHwidRenewalUnavailableNote() {
|
||||
return Boolean(
|
||||
subscription?.active &&
|
||||
Number(subscription?.extra_hwid_devices || 0) > 0 &&
|
||||
isSubscriptionPlan(selectedPlan) &&
|
||||
!showHwidRenewalBlock()
|
||||
);
|
||||
}
|
||||
function hwidRenewalCount(plan = selectedPlan) {
|
||||
return Number(hwidRenewalFor(plan)?.device_count || subscription?.extra_hwid_devices || 0);
|
||||
}
|
||||
function hwidRenewalHint(plan = selectedPlan) {
|
||||
const renewal = hwidRenewalFor(plan);
|
||||
if (renewal?.valid_from_text && renewal?.valid_until_text) {
|
||||
return t("wa_hwid_devices_renewal_checkbox_hint", {
|
||||
from: renewal.valid_from_text,
|
||||
to: renewal.valid_until_text,
|
||||
});
|
||||
}
|
||||
return t("wa_hwid_devices_renewal_checkbox_hint_short");
|
||||
}
|
||||
function showHwidDesyncNotice() {
|
||||
return Boolean(
|
||||
subscription?.device_topup_renewal_available &&
|
||||
subscription?.extra_hwid_devices_valid_until_text
|
||||
);
|
||||
}
|
||||
function planKey(plan) {
|
||||
return planKeyFn(plan);
|
||||
}
|
||||
@@ -197,10 +274,34 @@
|
||||
<p>{subscriptionPurchaseDescription}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if subscription?.active && Number(subscription?.extra_hwid_devices || 0) > 0}
|
||||
{#if showHwidRenewalBlock()}
|
||||
<label class="hwid-renewal-option">
|
||||
<Checkbox
|
||||
checked={renewHwidDevices}
|
||||
ariaLabel={t("wa_hwid_devices_renewal_checkbox_aria")}
|
||||
onCheckedChange={(checked) => (renewHwidDevices = checked)}
|
||||
/>
|
||||
<span>
|
||||
<strong>
|
||||
{t("wa_hwid_devices_renewal_checkbox", {
|
||||
count: hwidRenewalCount(),
|
||||
price: hwidRenewalPriceLabel(),
|
||||
})}
|
||||
</strong>
|
||||
<small>{hwidRenewalHint()}</small>
|
||||
{#if showHwidDesyncNotice()}
|
||||
<small class="hwid-renewal-warning">
|
||||
{t("wa_hwid_devices_desync_notice", {
|
||||
date: subscription.extra_hwid_devices_valid_until_text,
|
||||
})}
|
||||
</small>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
{:else if showHwidRenewalUnavailableNote()}
|
||||
<div class="subscription-purchase-description">
|
||||
<p>
|
||||
{t("wa_hwid_devices_renewal_notice", {
|
||||
{t("wa_hwid_devices_renewal_unavailable", {
|
||||
count: Number(subscription.extra_hwid_devices || 0),
|
||||
date: subscription.extra_hwid_devices_valid_until_text || "",
|
||||
})}
|
||||
@@ -243,7 +344,7 @@
|
||||
disabled={!selectedPlan || !methods.length || payBusy}
|
||||
>
|
||||
{t("wa_pay")}
|
||||
{selectedPlan ? priceLabel(selectedPlan) : ""}
|
||||
{selectedPlan ? paymentPriceLabel(selectedPlan) : ""}
|
||||
<LockKeyhole size={17} />
|
||||
</Button>
|
||||
{:else}
|
||||
@@ -261,10 +362,34 @@
|
||||
<p>{subscriptionPurchaseDescription}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if subscription?.active && Number(subscription?.extra_hwid_devices || 0) > 0}
|
||||
{#if showHwidRenewalBlock()}
|
||||
<label class="hwid-renewal-option">
|
||||
<Checkbox
|
||||
checked={renewHwidDevices}
|
||||
ariaLabel={t("wa_hwid_devices_renewal_checkbox_aria")}
|
||||
onCheckedChange={(checked) => (renewHwidDevices = checked)}
|
||||
/>
|
||||
<span>
|
||||
<strong>
|
||||
{t("wa_hwid_devices_renewal_checkbox", {
|
||||
count: hwidRenewalCount(),
|
||||
price: hwidRenewalPriceLabel(),
|
||||
})}
|
||||
</strong>
|
||||
<small>{hwidRenewalHint()}</small>
|
||||
{#if showHwidDesyncNotice()}
|
||||
<small class="hwid-renewal-warning">
|
||||
{t("wa_hwid_devices_desync_notice", {
|
||||
date: subscription.extra_hwid_devices_valid_until_text,
|
||||
})}
|
||||
</small>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
{:else if showHwidRenewalUnavailableNote()}
|
||||
<div class="subscription-purchase-description">
|
||||
<p>
|
||||
{t("wa_hwid_devices_renewal_notice", {
|
||||
{t("wa_hwid_devices_renewal_unavailable", {
|
||||
count: Number(subscription.extra_hwid_devices || 0),
|
||||
date: subscription.extra_hwid_devices_valid_until_text || "",
|
||||
})}
|
||||
@@ -310,7 +435,7 @@
|
||||
disabled={!selectedPlan || !methods.length || payBusy}
|
||||
>
|
||||
{t("wa_pay")}
|
||||
{selectedPlan ? priceLabel(selectedPlan) : ""}
|
||||
{selectedPlan ? paymentPriceLabel(selectedPlan) : ""}
|
||||
<LockKeyhole size={17} />
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -127,6 +127,19 @@
|
||||
: "";
|
||||
}
|
||||
|
||||
function deviceTopupPlanTitle(plan) {
|
||||
return t("wa_hwid_devices_package", {
|
||||
count: Number(plan?.device_count || plan?.months || 0),
|
||||
});
|
||||
}
|
||||
|
||||
function deviceTopupPlanHint(plan) {
|
||||
if (plan?.valid_until_text) {
|
||||
return t("wa_hwid_devices_active_until", { date: plan.valid_until_text });
|
||||
}
|
||||
return plan?.subtitle || deviceTopupOptions?.tariff_name || "";
|
||||
}
|
||||
|
||||
function tariffChangeModalDescription() {
|
||||
if (!changeOptions) return "";
|
||||
return changeOptions?.current
|
||||
@@ -369,16 +382,7 @@
|
||||
{#if !deviceTopupOptions}
|
||||
<DialogOptionsSkeleton label={t("wa_tariff_options_loading")} rows={3} />
|
||||
{:else if deviceTopupOptions?.plans?.length}
|
||||
{#if deviceTopupOptions?.renewal_available}
|
||||
<div class="topup-carryover-note">
|
||||
<p>
|
||||
{t("wa_hwid_devices_renewal_offer", {
|
||||
count: Number(deviceTopupOptions.renewal_recommended_count || 0),
|
||||
date: deviceTopupOptions.extra_hwid_devices_valid_until_text || "",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{:else if Number(deviceTopupOptions?.extra_hwid_devices || 0) > 0 && deviceTopupOptions?.extra_hwid_devices_valid_until_text}
|
||||
{#if Number(deviceTopupOptions?.extra_hwid_devices || 0) > 0 && deviceTopupOptions?.extra_hwid_devices_valid_until_text}
|
||||
<div class="topup-carryover-note">
|
||||
<p>
|
||||
{t("wa_hwid_devices_valid_until", {
|
||||
@@ -397,12 +401,8 @@
|
||||
onclick={() => (selectedDeviceTopupPlan = plan)}
|
||||
>
|
||||
<span class="option-row-main">
|
||||
<strong
|
||||
>{t("wa_hwid_devices_package", {
|
||||
count: Number(plan.device_count || plan.months || 0),
|
||||
})}</strong
|
||||
>
|
||||
<small>{plan.subtitle || deviceTopupOptions.tariff_name}</small>
|
||||
<strong>{deviceTopupPlanTitle(plan)}</strong>
|
||||
<small>{deviceTopupPlanHint(plan)}</small>
|
||||
</span>
|
||||
<span class="option-row-meta">
|
||||
<em>{priceLabel(plan)}</em>
|
||||
|
||||
Reference in New Issue
Block a user