Revert "Fix referral bonus display and layout spacing"

This commit is contained in:
Egor
2025-10-09 07:27:57 +03:00
committed by GitHub
parent b752278398
commit 46dca8c095
4 changed files with 2 additions and 1031 deletions

View File

@@ -13,9 +13,8 @@ from pathlib import Path
class Settings(BaseSettings):
BOT_TOKEN: str
BOT_USERNAME: Optional[str] = None
ADMIN_IDS: str = ""
SUPPORT_USERNAME: str = "@support"
SUPPORT_MENU_ENABLED: bool = True
@@ -528,14 +527,7 @@ class Settings(BaseSettings):
def get_trial_warning_hours(self) -> int:
return self.TRIAL_WARNING_HOURS
def get_bot_username(self) -> Optional[str]:
username = getattr(self, "BOT_USERNAME", None)
if not username:
return None
normalized = str(username).strip().lstrip("@")
return normalized or None
def is_notifications_enabled(self) -> bool:
return self.ENABLE_NOTIFICATIONS

View File

@@ -46,10 +46,6 @@ from app.utils.telegram_webapp import (
TelegramWebAppAuthError,
parse_webapp_init_data,
)
from app.utils.user_utils import (
get_detailed_referral_list,
get_user_referral_summary,
)
from ..dependencies import get_db_session
from ..schemas.miniapp import (
@@ -66,12 +62,6 @@ from ..schemas.miniapp import (
MiniAppPromoOffer,
MiniAppPromoOfferClaimRequest,
MiniAppPromoOfferClaimResponse,
MiniAppReferralInfo,
MiniAppReferralItem,
MiniAppReferralList,
MiniAppReferralRecentEarning,
MiniAppReferralStats,
MiniAppReferralTerms,
MiniAppRichTextDocument,
MiniAppSubscriptionRequest,
MiniAppSubscriptionResponse,
@@ -732,115 +722,6 @@ async def _load_subscription_links(
return payload
async def _build_referral_info(
db: AsyncSession,
user: User,
) -> Optional[MiniAppReferralInfo]:
referral_code = getattr(user, "referral_code", None)
referral_settings = settings.get_referral_settings() or {}
bot_username = settings.get_bot_username()
referral_link = None
if referral_code and bot_username:
referral_link = f"https://t.me/{bot_username}?start={referral_code}"
terms = MiniAppReferralTerms(
minimum_topup_kopeks=int(referral_settings.get("minimum_topup_kopeks") or 0),
minimum_topup_label=settings.format_price(int(referral_settings.get("minimum_topup_kopeks") or 0)),
first_topup_bonus_kopeks=int(referral_settings.get("first_topup_bonus_kopeks") or 0),
first_topup_bonus_label=settings.format_price(int(referral_settings.get("first_topup_bonus_kopeks") or 0)),
inviter_bonus_kopeks=int(referral_settings.get("inviter_bonus_kopeks") or 0),
inviter_bonus_label=settings.format_price(int(referral_settings.get("inviter_bonus_kopeks") or 0)),
commission_percent=float(referral_settings.get("commission_percent") or 0),
referred_user_reward_kopeks=int(referral_settings.get("referred_user_reward") or 0),
referred_user_reward_label=settings.format_price(int(referral_settings.get("referred_user_reward") or 0)),
)
summary = await get_user_referral_summary(db, user.id)
stats: Optional[MiniAppReferralStats] = None
recent_earnings: List[MiniAppReferralRecentEarning] = []
if summary:
total_earned_kopeks = int(summary.get("total_earned_kopeks") or 0)
month_earned_kopeks = int(summary.get("month_earned_kopeks") or 0)
stats = MiniAppReferralStats(
invited_count=int(summary.get("invited_count") or 0),
paid_referrals_count=int(summary.get("paid_referrals_count") or 0),
active_referrals_count=int(summary.get("active_referrals_count") or 0),
total_earned_kopeks=total_earned_kopeks,
total_earned_label=settings.format_price(total_earned_kopeks),
month_earned_kopeks=month_earned_kopeks,
month_earned_label=settings.format_price(month_earned_kopeks),
conversion_rate=float(summary.get("conversion_rate") or 0.0),
)
for earning in summary.get("recent_earnings", []) or []:
amount = int(earning.get("amount_kopeks") or 0)
recent_earnings.append(
MiniAppReferralRecentEarning(
amount_kopeks=amount,
amount_label=settings.format_price(amount),
reason=earning.get("reason"),
referral_name=earning.get("referral_name"),
created_at=earning.get("created_at"),
)
)
detailed = await get_detailed_referral_list(db, user.id, limit=50, offset=0)
referral_items: List[MiniAppReferralItem] = []
if detailed:
for item in detailed.get("referrals", []) or []:
total_earned = int(item.get("total_earned_kopeks") or 0)
balance = int(item.get("balance_kopeks") or 0)
referral_items.append(
MiniAppReferralItem(
id=int(item.get("id") or 0),
telegram_id=item.get("telegram_id"),
full_name=item.get("full_name"),
username=item.get("username"),
created_at=item.get("created_at"),
last_activity=item.get("last_activity"),
has_made_first_topup=bool(item.get("has_made_first_topup")),
balance_kopeks=balance,
balance_label=settings.format_price(balance),
total_earned_kopeks=total_earned,
total_earned_label=settings.format_price(total_earned),
topups_count=int(item.get("topups_count") or 0),
days_since_registration=item.get("days_since_registration"),
days_since_activity=item.get("days_since_activity"),
status=item.get("status"),
)
)
referral_list = MiniAppReferralList(
total_count=int(detailed.get("total_count") or 0) if detailed else 0,
has_next=bool(detailed.get("has_next")) if detailed else False,
has_prev=bool(detailed.get("has_prev")) if detailed else False,
current_page=int(detailed.get("current_page") or 1) if detailed else 1,
total_pages=int(detailed.get("total_pages") or 1) if detailed else 1,
items=referral_items,
)
if (
not referral_code
and not referral_link
and not referral_items
and not recent_earnings
and (not stats or (stats.invited_count == 0 and stats.total_earned_kopeks == 0))
):
return None
return MiniAppReferralInfo(
referral_code=referral_code,
referral_link=referral_link,
terms=terms,
stats=stats,
recent_earnings=recent_earnings,
referrals=referral_list,
)
@router.post("/subscription", response_model=MiniAppSubscriptionResponse)
async def get_subscription_details(
payload: MiniAppSubscriptionRequest,
@@ -1129,8 +1010,6 @@ async def get_subscription_details(
promo_offer_discount_source=promo_offer_source,
)
referral_info = await _build_referral_info(db, user)
return MiniAppSubscriptionResponse(
subscription_id=subscription.id,
remnawave_short_uuid=subscription.remnawave_short_uuid,
@@ -1171,7 +1050,6 @@ async def get_subscription_details(
branding=settings.get_miniapp_branding(),
faq=faq_payload,
legal_documents=legal_documents_payload,
referral=referral_info,
)

View File

@@ -175,73 +175,6 @@ class MiniAppLegalDocuments(BaseModel):
privacy_policy: Optional[MiniAppRichTextDocument] = None
class MiniAppReferralTerms(BaseModel):
minimum_topup_kopeks: int = 0
minimum_topup_label: Optional[str] = None
first_topup_bonus_kopeks: int = 0
first_topup_bonus_label: Optional[str] = None
inviter_bonus_kopeks: int = 0
inviter_bonus_label: Optional[str] = None
commission_percent: float = 0.0
referred_user_reward_kopeks: int = 0
referred_user_reward_label: Optional[str] = None
class MiniAppReferralStats(BaseModel):
invited_count: int = 0
paid_referrals_count: int = 0
active_referrals_count: int = 0
total_earned_kopeks: int = 0
total_earned_label: Optional[str] = None
month_earned_kopeks: int = 0
month_earned_label: Optional[str] = None
conversion_rate: float = 0.0
class MiniAppReferralRecentEarning(BaseModel):
amount_kopeks: int = 0
amount_label: Optional[str] = None
reason: Optional[str] = None
referral_name: Optional[str] = None
created_at: Optional[datetime] = None
class MiniAppReferralItem(BaseModel):
id: int
telegram_id: Optional[int] = None
full_name: Optional[str] = None
username: Optional[str] = None
created_at: Optional[datetime] = None
last_activity: Optional[datetime] = None
has_made_first_topup: bool = False
balance_kopeks: int = 0
balance_label: Optional[str] = None
total_earned_kopeks: int = 0
total_earned_label: Optional[str] = None
topups_count: int = 0
days_since_registration: Optional[int] = None
days_since_activity: Optional[int] = None
status: Optional[str] = None
class MiniAppReferralList(BaseModel):
total_count: int = 0
has_next: bool = False
has_prev: bool = False
current_page: int = 1
total_pages: int = 1
items: List[MiniAppReferralItem] = Field(default_factory=list)
class MiniAppReferralInfo(BaseModel):
referral_code: Optional[str] = None
referral_link: Optional[str] = None
terms: Optional[MiniAppReferralTerms] = None
stats: Optional[MiniAppReferralStats] = None
recent_earnings: List[MiniAppReferralRecentEarning] = Field(default_factory=list)
referrals: Optional[MiniAppReferralList] = None
class MiniAppSubscriptionResponse(BaseModel):
success: bool = True
subscription_id: int
@@ -275,5 +208,4 @@ class MiniAppSubscriptionResponse(BaseModel):
branding: Optional[MiniAppBranding] = None
faq: Optional[MiniAppFaq] = None
legal_documents: Optional[MiniAppLegalDocuments] = None
referral: Optional[MiniAppReferralInfo] = None

View File

@@ -1243,293 +1243,6 @@
font-size: 18px;
}
/* Referral Section */
.referral-card-summary {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
justify-content: flex-end;
}
.referral-card-summary-item {
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-end;
padding: 8px 12px;
border-radius: var(--radius-sm);
background: rgba(var(--primary-rgb), 0.08);
}
.referral-card-summary-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
}
.referral-card-summary-value {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
}
.referral-content {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 12px;
}
.referral-link-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
background: rgba(var(--primary-rgb), 0.04);
}
.referral-link-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.referral-link-label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
}
.referral-link-value {
font-size: 15px;
font-weight: 600;
color: var(--primary);
word-break: break-word;
}
.referral-copy-btn {
border: none;
border-radius: var(--radius-sm);
padding: 8px 12px;
background: var(--primary);
color: var(--tg-theme-button-text-color);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.referral-copy-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.referral-copy-btn:not(:disabled):active {
transform: scale(0.98);
}
.referral-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 12px;
}
.referral-stat-card {
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 12px;
background: rgba(var(--primary-rgb), 0.03);
}
.referral-stat-label {
font-size: 12px;
text-transform: uppercase;
color: var(--text-secondary);
letter-spacing: 0.04em;
margin-bottom: 6px;
display: block;
}
.referral-stat-value {
font-size: 18px;
font-weight: 700;
}
.referral-terms {
display: flex;
flex-direction: column;
gap: 8px;
}
.referral-section-title {
font-size: 13px;
font-weight: 700;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.referral-terms-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
margin: 0;
padding: 0;
}
.referral-term-item {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
background: rgba(var(--primary-rgb), 0.02);
}
.referral-term-label {
font-size: 13px;
color: var(--text-secondary);
}
.referral-term-value {
font-size: 14px;
font-weight: 600;
}
.referral-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 10px 14px;
background: transparent;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
}
.referral-toggle-btn:hover {
background: rgba(var(--primary-rgb), 0.06);
}
.referral-toggle-btn:active {
transform: scale(0.98);
}
.referral-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 12px;
margin: 0;
padding: 0;
}
.referral-item {
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 14px;
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(var(--primary-rgb), 0.02);
}
.referral-item-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.referral-item-name {
font-size: 15px;
font-weight: 600;
}
.referral-item-username {
font-size: 13px;
color: var(--text-secondary);
margin-top: 2px;
}
.referral-status {
font-size: 12px;
font-weight: 600;
padding: 4px 8px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.referral-status.active {
background: rgba(16, 185, 129, 0.12);
color: #047857;
}
.referral-status.inactive {
background: rgba(148, 163, 184, 0.12);
color: #475569;
}
.referral-status.new {
background: rgba(59, 130, 246, 0.12);
color: #1d4ed8;
}
.referral-item-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.referral-item-metric {
display: flex;
flex-direction: column;
gap: 4px;
}
.referral-item-label {
font-size: 12px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.referral-item-value {
font-size: 14px;
font-weight: 600;
}
.referral-item-dates {
display: flex;
flex-direction: column;
gap: 4px;
}
.referral-item-date {
font-size: 12px;
color: var(--text-secondary);
}
.referral-item-date strong {
color: var(--text-primary);
font-weight: 600;
}
/* Transaction History */
.history-list {
list-style: none;
@@ -2342,11 +2055,6 @@
grid-template-columns: repeat(2, 1fr);
}
.referral-card-summary {
margin-left: 0;
justify-content: space-between;
}
.user-header {
padding: 16px;
}
@@ -2451,10 +2159,6 @@
color: rgba(226, 232, 240, 0.75);
}
:root[data-theme="dark"] .referral-card-summary-item {
background: rgba(var(--primary-rgb), 0.2);
}
:root[data-theme="dark"] .promo-discount-badge.muted .promo-discount-value {
color: rgba(226, 232, 240, 0.75);
}
@@ -2749,52 +2453,6 @@
</div>
</div>
<!-- Referral Card (Expandable) -->
<div class="card expandable" id="referralCard">
<div class="card-header">
<div class="card-title">
<svg class="card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14c-4.418 0-8 2.239-8 5v1h16v-1c0-2.761-3.582-5-8-5z" />
</svg>
<span data-i18n="card.referral.title">Referral Program</span>
</div>
<div class="referral-card-summary hidden" id="referralCardSummary">
<div class="referral-card-summary-item">
<span class="referral-card-summary-label" data-i18n="referral.stats.invited">Invited</span>
<span class="referral-card-summary-value" id="referralSummaryInvited">0</span>
</div>
<div class="referral-card-summary-item">
<span class="referral-card-summary-label" data-i18n="referral.stats.total">Total earned</span>
<span class="referral-card-summary-value" id="referralSummaryEarned"></span>
</div>
</div>
<svg class="expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
<div class="card-content">
<div class="referral-content hidden" id="referralContent">
<div class="referral-link-section">
<div class="referral-link-header">
<span class="referral-link-label" data-i18n="referral.link.label">Your referral link</span>
<button class="referral-copy-btn" type="button" id="referralCopyBtn" data-i18n="referral.link.copy">Copy link</button>
</div>
<div class="referral-link-value" id="referralLinkValue"></div>
</div>
<div class="referral-stats" id="referralStats"></div>
<div class="referral-terms">
<div class="referral-section-title" data-i18n="referral.terms.title">Program terms</div>
<ul class="referral-terms-list" id="referralTermsList"></ul>
</div>
<button class="referral-toggle-btn" type="button" id="referralToggleBtn" data-i18n="referral.toggle.open">My referrals</button>
<ul class="referral-list hidden" id="referralList"></ul>
<div class="empty-state hidden" id="referralListEmpty" data-i18n="referral.list.empty">You have no referrals yet</div>
</div>
<div class="empty-state hidden" id="referralEmpty" data-i18n="referral.empty">Referral information is unavailable</div>
</div>
</div>
<!-- History Card (Expandable) -->
<div class="card expandable" id="historyCard">
<div class="card-header">
@@ -2919,8 +2577,6 @@
let hasAnimatedCards = false;
let promoOfferTimers = [];
let promoOfferTimerHandle = null;
let referralListExpanded = false;
let referralCopyResetHandle = null;
if (typeof tg.expand === 'function') {
tg.expand();
@@ -3095,7 +2751,6 @@
'promo_code.error.generic': 'Unable to activate the promo code.',
'promo_code.error.unauthorized': 'Authorization failed. Please reopen the mini app from Telegram.',
'promo_code.error.network': 'Network error. Please try again later.',
'card.referral.title': 'Referral Program',
'card.history.title': 'Transaction History',
'card.servers.title': 'Connected Servers',
'card.devices.title': 'Connected Devices',
@@ -3132,36 +2787,6 @@
'history.type.subscription_payment': 'Subscription payment',
'history.type.refund': 'Refund',
'history.type.referral_reward': 'Referral reward',
'referral.link.label': 'Your referral link',
'referral.link.copy': 'Copy link',
'referral.link.copied': 'Link copied',
'referral.empty': 'Referral information is unavailable',
'referral.stats.invited': 'Invited',
'referral.stats.active': 'Active referrals',
'referral.stats.paid': 'Paying referrals',
'referral.stats.total': 'Total earned',
'referral.stats.month': 'Earned this month',
'referral.stats.conversion': 'Conversion',
'referral.terms.title': 'Program terms',
'referral.terms.minimum_topup': 'Minimum top-up for rewards',
'referral.terms.first_topup': 'Referral first top-up bonus',
'referral.terms.inviter_bonus': 'Your first top-up bonus',
'referral.terms.referred_bonus': 'Bonus for invited friend',
'referral.terms.commission': 'Commission from each top-up',
'referral.toggle.open': 'My referrals',
'referral.toggle.close': 'Hide referrals',
'referral.list.empty': 'You have no referrals yet',
'referral.meta.earned': 'Earned',
'referral.meta.topups': 'Top-ups',
'referral.meta.joined': 'Joined',
'referral.meta.last_activity': 'Last activity',
'referral.copy.success': 'Referral link copied to clipboard.',
'referral.copy.failure': 'Unable to copy the referral link automatically. Please copy it manually: {value}',
'referral.copy.unavailable': 'Copying is unavailable. Please copy the link manually.',
'referral.status.active': 'Active',
'referral.status.inactive': 'Inactive',
'referral.status.paying': 'Paying',
'referral.referrals.unknown': 'Referral',
'servers.empty': 'No servers connected yet',
'devices.empty': 'No devices connected yet',
'promo_levels.total_spent': 'Total spent',
@@ -3267,7 +2892,6 @@
'promo_code.error.generic': 'Не удалось активировать промокод.',
'promo_code.error.unauthorized': 'Ошибка авторизации. Откройте мини-приложение из Telegram.',
'promo_code.error.network': 'Ошибка сети. Попробуйте ещё раз.',
'card.referral.title': 'Реферальная программа',
'card.history.title': 'История операций',
'card.servers.title': 'Подключённые серверы',
'card.devices.title': 'Подключенные устройства',
@@ -3304,36 +2928,6 @@
'history.type.subscription_payment': 'Оплата подписки',
'history.type.refund': 'Возврат',
'history.type.referral_reward': 'Реферальное вознаграждение',
'referral.link.label': 'Ваша реферальная ссылка',
'referral.link.copy': 'Скопировать ссылку',
'referral.link.copied': 'Ссылка скопирована',
'referral.empty': 'Данные по партнёрской программе недоступны',
'referral.stats.invited': 'Приглашено',
'referral.stats.active': 'Активных',
'referral.stats.paid': 'Платящих',
'referral.stats.total': 'Всего заработано',
'referral.stats.month': 'Заработано за месяц',
'referral.stats.conversion': 'Конверсия',
'referral.terms.title': 'Условия программы',
'referral.terms.minimum_topup': 'Минимальное пополнение для бонусов',
'referral.terms.first_topup': 'Бонус за первое пополнение друга',
'referral.terms.inviter_bonus': 'Ваш бонус за его первое пополнение',
'referral.terms.referred_bonus': 'Бонус приглашённому другу',
'referral.terms.commission': 'Комиссия с каждого пополнения',
'referral.toggle.open': 'Мои рефералы',
'referral.toggle.close': 'Скрыть список',
'referral.list.empty': 'У вас пока нет рефералов',
'referral.meta.earned': 'Заработано',
'referral.meta.topups': 'Пополнения',
'referral.meta.joined': 'Регистрация',
'referral.meta.last_activity': 'Последняя активность',
'referral.copy.success': 'Реферальная ссылка скопирована в буфер.',
'referral.copy.failure': 'Не удалось скопировать ссылку автоматически. Скопируйте вручную: {value}',
'referral.copy.unavailable': 'Копирование недоступно. Скопируйте ссылку вручную.',
'referral.status.active': 'Активен',
'referral.status.inactive': 'Неактивен',
'referral.status.paying': 'Оплачивает',
'referral.referrals.unknown': 'Реферал',
'servers.empty': 'Подключённых серверов пока нет',
'devices.empty': 'Подключённых устройств пока нет',
'promo_levels.total_spent': 'Всего потрачено',
@@ -3787,7 +3381,6 @@
userData = payload;
userData.subscriptionUrl = userData.subscription_url || null;
userData.subscriptionCryptoLink = userData.subscription_crypto_link || null;
userData.referral = userData.referral || null;
const normalizedPurchaseUrl = normalizeUrl(
userData.subscription_purchase_url
@@ -3998,7 +3591,6 @@
renderPromoOffers();
renderPromoSection();
renderBalanceSection();
renderReferralSection();
renderTransactionHistory();
renderServersList();
renderDevicesList();
@@ -5101,36 +4693,6 @@
return formatCurrency(normalized / 100, currencyCode);
}
function resolveReferralTermValue(labelValue, fallbackKopeks) {
const formattedLabel = typeof labelValue === 'string'
? labelValue.trim()
: labelValue;
if (formattedLabel) {
return formattedLabel;
}
if (fallbackKopeks === undefined || fallbackKopeks === null) {
return null;
}
return formatPriceFromKopeks(fallbackKopeks);
}
function formatReferralCommission(value) {
if (value === undefined || value === null) {
return null;
}
const numeric = Number.parseFloat(value);
if (!Number.isFinite(numeric)) {
return null;
}
const formatted = numeric.toFixed(1).replace(/\.0$/, '');
return `${formatted}%`;
}
function formatDate(value) {
if (!value) {
return '—';
@@ -5198,338 +4760,6 @@
amountElement.textContent = formatCurrency(balanceRubles, currency);
}
function updateReferralToggleState() {
const list = document.getElementById('referralList');
const empty = document.getElementById('referralListEmpty');
const toggle = document.getElementById('referralToggleBtn');
const hasItems = Boolean(list && list.childElementCount);
const shouldShowList = hasItems && referralListExpanded;
const shouldShowEmpty = !hasItems && referralListExpanded;
if (list) {
list.classList.toggle('hidden', !shouldShowList);
}
if (empty) {
empty.classList.toggle('hidden', !shouldShowEmpty);
}
if (toggle) {
const labelKey = referralListExpanded ? 'referral.toggle.close' : 'referral.toggle.open';
const label = t(labelKey);
toggle.textContent = label === labelKey ? (referralListExpanded ? 'Hide referrals' : 'My referrals') : label;
toggle.disabled = false;
}
}
function renderReferralSection() {
const card = document.getElementById('referralCard');
const content = document.getElementById('referralContent');
const emptyState = document.getElementById('referralEmpty');
const data = userData?.referral;
const summaryContainer = document.getElementById('referralCardSummary');
const summaryInvited = document.getElementById('referralSummaryInvited');
const summaryEarned = document.getElementById('referralSummaryEarned');
if (!card || !content || !emptyState) {
return;
}
referralListExpanded = false;
if (referralCopyResetHandle) {
clearTimeout(referralCopyResetHandle);
referralCopyResetHandle = null;
}
const copyBtn = document.getElementById('referralCopyBtn');
if (copyBtn) {
copyBtn.disabled = !navigator.clipboard;
copyBtn.dataset.copyValue = '';
copyBtn.textContent = t('referral.link.copy');
}
if (summaryContainer && summaryInvited && summaryEarned) {
summaryContainer.classList.add('hidden');
summaryInvited.textContent = '0';
summaryEarned.textContent = '—';
}
if (!data) {
content.classList.add('hidden');
emptyState.classList.remove('hidden');
emptyState.textContent = t('referral.empty');
updateReferralToggleState();
card.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
content.classList.remove('hidden');
card.classList.remove('hidden');
const linkValue = document.getElementById('referralLinkValue');
const link = data.referral_link || '';
const code = data.referral_code || '';
let copyTarget = '';
if (link) {
copyTarget = link;
} else if (code) {
copyTarget = code;
}
if (linkValue) {
linkValue.textContent = copyTarget || '—';
}
if (copyBtn) {
copyBtn.disabled = !navigator.clipboard || !copyTarget;
copyBtn.dataset.copyValue = copyTarget;
copyBtn.textContent = t('referral.link.copy');
}
const statsData = data.stats || null;
const statsContainer = document.getElementById('referralStats');
if (statsContainer) {
statsContainer.innerHTML = '';
const stats = statsData || {};
const entries = [
{ key: 'invited_count', label: 'referral.stats.invited', formatter: value => String(value ?? 0) },
{ key: 'active_referrals_count', label: 'referral.stats.active', formatter: value => String(value ?? 0) },
{ key: 'paid_referrals_count', label: 'referral.stats.paid', formatter: value => String(value ?? 0) },
{
key: 'total_earned_label',
label: 'referral.stats.total',
formatter: (_, full) => full.total_earned_label
|| formatPriceFromKopeks(full.total_earned_kopeks || 0),
},
{
key: 'month_earned_label',
label: 'referral.stats.month',
formatter: (_, full) => full.month_earned_label
|| formatPriceFromKopeks(full.month_earned_kopeks || 0),
},
{
key: 'conversion_rate',
label: 'referral.stats.conversion',
formatter: value => `${Number.parseFloat(value ?? 0).toFixed(1)}%`,
},
];
entries.forEach(entry => {
const { key, label, formatter } = entry;
const rawValue = key === 'total_earned_label' || key === 'month_earned_label'
? stats
: stats[key];
const hasValue = key === 'total_earned_label' || key === 'month_earned_label'
? Boolean((stats[key] ?? stats[key.replace('_label', '_kopeks')]) !== undefined)
: rawValue !== undefined && rawValue !== null;
if (!hasValue) {
if (key === 'conversion_rate') {
// Show conversion even if zero
} else if (key === 'total_earned_label' || key === 'month_earned_label') {
// always display totals
} else {
return;
}
}
const cardElement = document.createElement('div');
cardElement.className = 'referral-stat-card';
const labelElement = document.createElement('span');
labelElement.className = 'referral-stat-label';
labelElement.textContent = t(label);
const valueElement = document.createElement('span');
valueElement.className = 'referral-stat-value';
const formatted = formatter(rawValue, stats);
valueElement.textContent = formatted;
cardElement.appendChild(labelElement);
cardElement.appendChild(valueElement);
statsContainer.appendChild(cardElement);
});
}
if (summaryContainer && summaryInvited && summaryEarned) {
if (statsData) {
summaryInvited.textContent = String(statsData.invited_count ?? 0);
const totalLabel = statsData.total_earned_label
|| formatPriceFromKopeks(statsData.total_earned_kopeks || 0);
summaryEarned.textContent = totalLabel;
summaryContainer.classList.remove('hidden');
} else {
summaryContainer.classList.add('hidden');
}
}
const termsList = document.getElementById('referralTermsList');
if (termsList) {
termsList.innerHTML = '';
const terms = data.terms || {};
const entries = [
{
label: 'referral.terms.minimum_topup',
value: resolveReferralTermValue(
terms.minimum_topup_label,
terms.minimum_topup_kopeks,
),
},
{
label: 'referral.terms.first_topup',
value: resolveReferralTermValue(
terms.first_topup_bonus_label,
terms.first_topup_bonus_kopeks,
),
},
{
label: 'referral.terms.inviter_bonus',
value: resolveReferralTermValue(
terms.inviter_bonus_label,
terms.inviter_bonus_kopeks,
),
},
{
label: 'referral.terms.referred_bonus',
value: resolveReferralTermValue(
terms.referred_user_reward_label,
terms.referred_user_reward_kopeks,
),
},
{
label: 'referral.terms.commission',
value: formatReferralCommission(terms.commission_percent),
},
];
entries
.filter(entry => entry.value !== undefined
&& entry.value !== null
&& String(entry.value).trim() !== '')
.forEach(entry => {
const item = document.createElement('li');
item.className = 'referral-term-item';
const label = document.createElement('span');
label.className = 'referral-term-label';
const labelKey = t(entry.label);
label.textContent = labelKey === entry.label ? entry.label : labelKey;
const value = document.createElement('span');
value.className = 'referral-term-value';
value.textContent = entry.value;
item.appendChild(label);
item.appendChild(value);
termsList.appendChild(item);
});
}
const list = document.getElementById('referralList');
const emptyListState = document.getElementById('referralListEmpty');
if (list && emptyListState) {
list.innerHTML = '';
const referrals = Array.isArray(data?.referrals?.items) ? data.referrals.items : [];
referrals.forEach(referral => {
const item = document.createElement('li');
item.className = 'referral-item';
const header = document.createElement('div');
header.className = 'referral-item-header';
const displayName = referral.full_name
|| referral.username
|| (referral.telegram_id ? `ID ${referral.telegram_id}` : t('referral.referrals.unknown'));
const titleWrapper = document.createElement('div');
titleWrapper.className = 'referral-item-name';
titleWrapper.textContent = displayName;
const statusBadge = document.createElement('span');
const status = (referral.status || '').toLowerCase();
let statusKey = `referral.status.${status}`;
if (!status || !t(statusKey) || t(statusKey) === statusKey) {
if (referral.has_made_first_topup) {
statusKey = 'referral.status.paying';
} else if (status === 'active') {
statusKey = 'referral.status.active';
} else {
statusKey = 'referral.status.inactive';
}
}
const resolved = t(statusKey);
statusBadge.textContent = resolved === statusKey ? statusKey.split('.').pop() : resolved;
statusBadge.className = 'referral-status';
const statusText = statusBadge.textContent?.toLowerCase() || '';
if (statusText.includes('inactive') || statusText.includes('неактив')) {
statusBadge.classList.remove('active');
statusBadge.classList.add('inactive');
} else if (statusText.includes('active') || statusText.includes('актив')) {
statusBadge.classList.add('active');
} else if (statusKey === 'referral.status.paying') {
statusBadge.classList.add('new');
}
header.appendChild(titleWrapper);
header.appendChild(statusBadge);
item.appendChild(header);
if (referral.username && referral.username !== displayName) {
const username = document.createElement('div');
username.className = 'referral-item-username';
username.textContent = `@${referral.username.replace(/^@/, '')}`;
item.appendChild(username);
}
const metrics = document.createElement('div');
metrics.className = 'referral-item-grid';
const earnedMetric = document.createElement('div');
earnedMetric.className = 'referral-item-metric';
const earnedLabel = document.createElement('span');
earnedLabel.className = 'referral-item-label';
earnedLabel.textContent = t('referral.meta.earned');
const earnedValue = document.createElement('span');
earnedValue.className = 'referral-item-value';
earnedValue.textContent = referral.total_earned_label
|| formatPriceFromKopeks(referral.total_earned_kopeks || 0);
earnedMetric.appendChild(earnedLabel);
earnedMetric.appendChild(earnedValue);
const topupsMetric = document.createElement('div');
topupsMetric.className = 'referral-item-metric';
const topupsLabel = document.createElement('span');
topupsLabel.className = 'referral-item-label';
topupsLabel.textContent = t('referral.meta.topups');
const topupsValue = document.createElement('span');
topupsValue.className = 'referral-item-value';
topupsValue.textContent = String(referral.topups_count ?? 0);
topupsMetric.appendChild(topupsLabel);
topupsMetric.appendChild(topupsValue);
metrics.appendChild(earnedMetric);
metrics.appendChild(topupsMetric);
const dates = document.createElement('div');
dates.className = 'referral-item-dates';
const joined = document.createElement('span');
joined.className = 'referral-item-date';
const joinedLabel = t('referral.meta.joined');
joined.innerHTML = `<strong>${joinedLabel === 'referral.meta.joined' ? 'Joined' : joinedLabel}:</strong> ${escapeHtml(formatDate(referral.created_at))}`;
const lastActivity = document.createElement('span');
lastActivity.className = 'referral-item-date';
const lastActivityLabel = t('referral.meta.last_activity');
const lastActivityValue = formatDate(referral.last_activity) || '—';
lastActivity.innerHTML = `<strong>${lastActivityLabel === 'referral.meta.last_activity' ? 'Last activity' : lastActivityLabel}:</strong> ${escapeHtml(lastActivityValue)}`;
dates.appendChild(joined);
dates.appendChild(lastActivity);
item.appendChild(metrics);
item.appendChild(dates);
list.appendChild(item);
});
updateReferralToggleState();
}
}
function renderTransactionHistory() {
const list = document.getElementById('historyList');
const emptyState = document.getElementById('historyEmpty');
@@ -6527,67 +5757,6 @@
}
});
document.getElementById('referralToggleBtn')?.addEventListener('click', () => {
referralListExpanded = !referralListExpanded;
updateReferralToggleState();
});
document.getElementById('referralCopyBtn')?.addEventListener('click', async () => {
const button = document.getElementById('referralCopyBtn');
const value = button?.dataset.copyValue || '';
if (!button || !value) {
showPopup(
t('referral.copy.unavailable') || 'Copying is unavailable. Please copy the link manually.',
t('notifications.copy.title.failure') || 'Copy failed'
);
return;
}
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(value);
} else {
const textArea = document.createElement('textarea');
textArea.value = value;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
showPopup(
t('referral.copy.success') || 'Referral link copied to clipboard.',
t('notifications.copy.title.success') || 'Copied'
);
const copiedLabel = t('referral.link.copied');
button.textContent = copiedLabel === 'referral.link.copied' ? 'Link copied' : copiedLabel;
clearTimeout(referralCopyResetHandle);
referralCopyResetHandle = setTimeout(() => {
const defaultLabel = t('referral.link.copy');
button.textContent = defaultLabel === 'referral.link.copy' ? 'Copy link' : defaultLabel;
}, 2000);
} catch (error) {
console.warn('Clipboard copy failed:', error);
let failureMessage = t('referral.copy.failure');
if (!failureMessage || failureMessage === 'referral.copy.failure') {
const fallback = t('notifications.copy.failure') || 'Unable to copy automatically.';
failureMessage = `${fallback} ${value}`;
} else {
failureMessage = failureMessage.replace('{value}', value);
}
showPopup(
failureMessage,
t('notifications.copy.title.failure') || 'Copy failed'
);
}
});
document.getElementById('purchaseBtn')?.addEventListener('click', () => {
const link = getEffectivePurchaseUrl();
if (!link) {