diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 3e267ba6..1b82544a 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -6,6 +6,9 @@ The English translation covers the latest versions only; the full history (in Ru ## Unreleased +- [phone] Invite: a balance credit shows the amount +- [tech] Referral variants: balance percent, tiers, friend discount and retention. Off by default. Cancelling a journal row takes back days and balance. Migration 013 + ## 0.30.1-beta.6 - [phone] The embedded 2160 Player is updated to 0.2.26. The seek bar is full width with equal side gutters, elapsed time on the left and remaining on the right (both show --:-- until duration is known), and buffering shows a spinner in the centre button diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cdb5e53..e86b53b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ## Не выпущено +- [phone] Приглашение: если рефереру начислен баланс, экран показывает сумму +- [tech] Рефералка: процент на баланс, уровни, скидка другу и удержание. По умолчанию выключены. Отмена журнала снимает дни и баланс. Миграция 013 + ## 0.30.1-beta.6 - [phone] Встроенный плеер 2160 обновлён до 0.2.26. Полоса перемотки на всю ширину с одинаковыми боковыми отступами, прошедшее слева и остаток справа (пока длина неизвестна, обе стороны --:--), при буферизации в центре спиннер diff --git a/mobile/src/lib/referralLoad.ts b/mobile/src/lib/referralLoad.ts index 73d9799c..900111a1 100644 --- a/mobile/src/lib/referralLoad.ts +++ b/mobile/src/lib/referralLoad.ts @@ -8,9 +8,10 @@ export interface PhoneReferral { code: string; friends: number; noticeDays: number; + noticeCredit: number; } -const OFF: PhoneReferral = { enabled: false, code: '', friends: 0, noticeDays: 0 }; +const OFF: PhoneReferral = { enabled: false, code: '', friends: 0, noticeDays: 0, noticeCredit: 0 }; export function loadPhoneReferral(): Promise { if (!billingSession()) return Promise.resolve(OFF); @@ -18,7 +19,7 @@ export function loadPhoneReferral(): Promise { (acc) => { const row = acc && acc.referral; if (!row || !row.enabled) return OFF; - return { enabled: true, code: row.code || '', friends: row.friends || 0, noticeDays: row.noticeDays || 0 }; + return { enabled: true, code: row.code || '', friends: row.friends || 0, noticeDays: row.noticeDays || 0, noticeCredit: row.noticeCredit || 0 }; }, () => OFF, ); diff --git a/mobile/src/screens/Invite.tsx b/mobile/src/screens/Invite.tsx index 2cf2e834..4fb31258 100644 --- a/mobile/src/screens/Invite.tsx +++ b/mobile/src/screens/Invite.tsx @@ -13,6 +13,7 @@ export interface InviteCard { code: string; friends: number; noticeDays?: number; + noticeCredit?: number; } /** The program is off until an admin turns it on. A loader reads the signed-in card from /v1/me. */ @@ -87,6 +88,7 @@ export function Invite({

{t('invite.code', { code: view.code })}

{t('invite.friends', { n: view.friends })}

{card.noticeDays ?

{t('invite.noticeApp', { n: card.noticeDays })}

: null} + {card.noticeCredit ?

{t('invite.noticeCredit', { n: card.noticeCredit })}

: null} {qr ? {t('invite.qr')} : null} - - void save({ referrerDays: Number(e.target.value) })} /> - - void save({ friendDays: Number(e.target.value) })} /> - - void save({ balancePercent: Number(e.target.value) })} /> - - - -

Начисление через {local.delayDays ?? 7} дн. после оплаты. Годовой лимит {local.yearlyCap ?? 12} мес. Только при активной подписке реферера и оплате друга.

+ + void save({ referrerDays: clamp(e.target.value, 14) })} /> +

Базовые дни за первого оплатившего друга. Потолок одного начисления - 14 дней.

+ + void save({ friendDays: clamp(e.target.value, 14) })} /> + + void save({ delayDays: clamp(e.target.value, 365) })} /> +

Дни, баланс и удержание начисляются только после этой паузы. 0 - сразу после оплаты.

+ + void save({ yearlyCap: clamp(e.target.value, 120) })} /> +

Сколько месяцев бонусных дней реферер может получить за 365 дней. 0 - без лимита. Месяц считается как 30 дней.

+ + + + void save({ balancePercent: clamp(e.target.value, 100) })} /> + + void save({ balanceMax: clamp(e.target.value, 1000000) })} /> + + void save({ balanceMonthlyCap: clamp(e.target.value, 1000000) })} /> +

Рубли падают на внутренний баланс подписки и списываются при следующей оплате. Пока переключатель выключен, процент не начисляется. Уровень может поднять процент, если он выше.

+ + +

Порог - сколько друзей уже оплатили. Дни уровня заменяют базовые, если они больше, но не больше 14. Процент уровня берется, если он выше процента на баланс. Пустой список ничего не меняет.

+ {local.tiers ? ( +
+ {tiers.map((tier, index) => ( +
+ { + const next = tiers.slice(); + next[index] = { ...tier, friends: clamp(e.target.value, 10000) }; + saveTiers(next); + }} /> + { + const next = tiers.slice(); + next[index] = { ...tier, days: clamp(e.target.value, 14) }; + saveTiers(next); + }} /> + { + const next = tiers.slice(); + next[index] = { ...tier, percent: clamp(e.target.value, 100) }; + saveTiers(next); + }} /> + +
+ ))} + +
+ ) : null} + + + + void save({ friendDiscountPercent: clamp(e.target.value, 100) })} /> + + void save({ friendDiscountRub: clamp(e.target.value, 1000000) })} /> + + void save({ friendDiscountDays: clamp(e.target.value, 3650) })} /> +

Только первая оплата подписки, и только пока у реферера есть доступ. Процент и рубли складываются. Цена в боте и в приложении берется уже со скидкой и замораживается в заказе.

+ + + + void save({ retentionMonths: Math.max(1, clamp(e.target.value, 36)) })} /> + + void save({ retentionDays: clamp(e.target.value, 14) })} /> +

Месяц - следующая оплата через 27-45 дней. Большой разрыв начинает счет заново. 0 дней - бонус не выдается, даже если переключатель включен.

+ +

Бонус только при активной подписке реферера и оплате друга. Нельзя пригласить себя. Один Telegram или одно устройство - одно приглашение.

Журнал

{rows.length === 0 ?

Начислений нет

: rows.map((row) => ( -
- {row.friendId || row.id} · {row.status} · {row.days} дн. +
+ + {row.friendId || row.id} · {variantLabel(row.variant)} · {row.status} · {row.days} дн. + {row.credit ? ' · ' + row.credit + ' ₽' : ''} + {row.shortfall ? ' · ' + row.shortfall : ''} + {row.status !== 'cancelled' ? (