feat: surface trial activation in mini app

This commit is contained in:
3252a8
2026-05-24 18:48:01 +03:00
parent 235ee25d9f
commit 6f4074e8d6
13 changed files with 513 additions and 10 deletions
+1
View File
@@ -7,6 +7,7 @@ def setup_subscription_webapp_routes(app: web.Application) -> None:
app.router.add_get("/login/password", index_route) app.router.add_get("/login/password", index_route)
app.router.add_get("/home", index_route) app.router.add_get("/home", index_route)
app.router.add_get("/install", index_route) app.router.add_get("/install", index_route)
app.router.add_get("/trial", index_route)
app.router.add_get("/open-app", app_deeplink_route) app.router.add_get("/open-app", app_deeplink_route)
app.router.add_get(r"/s/{share_token:[a-f0-9]{32}}", index_route) app.router.add_get(r"/s/{share_token:[a-f0-9]{32}}", index_route)
app.router.add_get("/invite", index_route) app.router.add_get("/invite", index_route)
+19 -5
View File
@@ -4,6 +4,7 @@ from aiogram.types import InlineKeyboardMarkup, WebAppInfo
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
from bot.utils.install_links import bot_install_guide_url from bot.utils.install_links import bot_install_guide_url
from bot.utils.mini_app_url import subscription_mini_app_trial_url
from config.settings import Settings from config.settings import Settings
BOT_MENU_CONTEXT = "bot" BOT_MENU_CONTEXT = "bot"
@@ -82,12 +83,29 @@ def payment_options_back_callback(sale_mode: str = "subscription") -> str:
return subscription_options_callback(context) return subscription_options_callback(context)
def _trial_activation_button(lang: str, i18n_instance, settings: Settings) -> InlineKeyboardButton:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
if settings.SUBSCRIPTION_MINI_APP_URL:
trial_url = subscription_mini_app_trial_url(settings) or settings.SUBSCRIPTION_MINI_APP_URL
return InlineKeyboardButton(
text=_(key="menu_activate_trial_button"),
web_app=WebAppInfo(url=trial_url),
)
return InlineKeyboardButton(
text=_(key="menu_activate_trial_button"),
callback_data="main_action:request_trial",
)
def get_main_menu_inline_keyboard( def get_main_menu_inline_keyboard(
lang: str, i18n_instance, settings: Settings, show_trial_button: bool = False lang: str, i18n_instance, settings: Settings, show_trial_button: bool = False
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if show_trial_button and settings.TRIAL_ENABLED:
builder.row(_trial_activation_button(lang, i18n_instance, settings))
if settings.SUBSCRIPTION_MINI_APP_URL: if settings.SUBSCRIPTION_MINI_APP_URL:
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
@@ -130,11 +148,7 @@ def get_bot_interface_inline_keyboard(
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if show_trial_button and settings.TRIAL_ENABLED: if show_trial_button and settings.TRIAL_ENABLED:
builder.row( builder.row(_trial_activation_button(lang, i18n_instance, settings))
InlineKeyboardButton(
text=_(key="menu_activate_trial_button"), callback_data="main_action:request_trial"
)
)
if settings.SUBSCRIPTION_MINI_APP_URL: if settings.SUBSCRIPTION_MINI_APP_URL:
builder.row( builder.row(
+5
View File
@@ -48,6 +48,11 @@ def subscription_mini_app_install_url(settings: Settings) -> Optional[str]:
return subscription_mini_app_path_url(settings, "/install") return subscription_mini_app_path_url(settings, "/install")
def subscription_mini_app_trial_url(settings: Settings) -> Optional[str]:
"""Return the trial activation URL inside the Mini App."""
return subscription_mini_app_path_url(settings, "/trial")
def subscription_public_install_url(settings: Settings, share_token: str) -> Optional[str]: def subscription_public_install_url(settings: Settings, share_token: str) -> Optional[str]:
"""Return the public install guide URL for a normalized share token.""" """Return the public install guide URL for a normalized share token."""
token = normalize_install_share_token(share_token) token = normalize_install_share_token(share_token)
+50 -4
View File
@@ -22,6 +22,7 @@
import SettingsScreen from "./webapp/screens/SettingsScreen.svelte"; import SettingsScreen from "./webapp/screens/SettingsScreen.svelte";
import SupportScreen from "./webapp/screens/SupportScreen.svelte"; import SupportScreen from "./webapp/screens/SupportScreen.svelte";
import SupportTicketScreen from "./webapp/screens/SupportTicketScreen.svelte"; import SupportTicketScreen from "./webapp/screens/SupportTicketScreen.svelte";
import TrialActivationScreen from "./webapp/screens/TrialActivationScreen.svelte";
import { import {
LANGUAGE_FLAGS, LANGUAGE_FLAGS,
@@ -118,6 +119,8 @@
let publicInstallSubscription = null; let publicInstallSubscription = null;
let publicInstallToken = ""; let publicInstallToken = "";
let trialBusy = false; let trialBusy = false;
let trialActivationResult = null;
let trialActivationError = "";
let promoCode = ""; let promoCode = "";
let promoBusy = false; let promoBusy = false;
let promoStatus = ""; let promoStatus = "";
@@ -564,7 +567,7 @@
: section === "install" && !canUseInstallGuides() : section === "install" && !canUseInstallGuides()
? "home" ? "home"
: section; : section;
activeTab = nextSection === "install" ? "home" : nextSection; activeTab = nextSection === "install" || nextSection === "trial" ? "home" : nextSection;
screen = nextSection; screen = nextSection;
if (nextSection === "devices") devicesStore.loadDevices(devicesEnabled); if (nextSection === "devices") devicesStore.loadDevices(devicesEnabled);
if (nextSection === "support") { if (nextSection === "support") {
@@ -949,7 +952,12 @@
} }
const initialSupportTicketId = const initialSupportTicketId =
section === "support" ? supportTicketIdFromPath(window.location.pathname) : null; section === "support" ? supportTicketIdFromPath(window.location.pathname) : null;
activeTab = section === "admin" ? "settings" : section === "install" ? "home" : section; activeTab =
section === "admin"
? "settings"
: section === "install" || section === "trial"
? "home"
: section;
screen = section; screen = section;
mode = "app"; mode = "app";
if (payload.settings?.support_tickets_enabled !== false) { if (payload.settings?.support_tickets_enabled !== false) {
@@ -1151,6 +1159,19 @@
openConnectLink(); openConnectLink();
} }
function openTrialInstallOrConnect() {
if (canUseInstallGuides()) {
goInstall();
return;
}
const url = trialActivationResult?.connect_url || trialActivationResult?.config_link;
if (url) {
openExternalLink(url);
return;
}
openConnectLink();
}
async function copyText(value, success = t("wa_copied")) { async function copyText(value, success = t("wa_copied")) {
if (!value) { if (!value) {
showToast(t("wa_unavailable")); showToast(t("wa_unavailable"));
@@ -1199,19 +1220,30 @@
} }
} }
async function activateTrial() { async function activateTrial(options = {}) {
if (trialBusy) return; if (trialBusy) return;
const stayOnTrial = Boolean(options?.stayOnTrial);
trialBusy = true; trialBusy = true;
trialActivationResult = null;
trialActivationError = "";
try { try {
const response = await api("/trial/activate", { const response = await api("/trial/activate", {
method: "POST", method: "POST",
body: JSON.stringify({}), body: JSON.stringify({}),
}); });
if (!response.ok) throw response; if (!response.ok) throw response;
trialActivationResult = response;
showToast(t("wa_trial_activated")); showToast(t("wa_trial_activated"));
await loadData(); await loadData();
if (stayOnTrial) {
activeTab = "home";
screen = "trial";
syncSectionPath("trial", true);
}
} catch (error) { } catch (error) {
showToast(error?.message || t("wa_trial_activation_failed")); const message = error?.message || t("wa_trial_activation_failed");
trialActivationError = message;
showToast(message);
} finally { } finally {
trialBusy = false; trialBusy = false;
} }
@@ -1569,6 +1601,20 @@
{copyText} {copyText}
{t} {t}
/> />
{:else if screen === "trial"}
<TrialActivationScreen
{appSettings}
{brand}
{brandTitle}
{subscription}
{trialBusy}
trialResult={trialActivationResult}
trialError={trialActivationError}
{activateTrial}
openInstallOrConnect={openTrialInstallOrConnect}
{goHome}
{t}
/>
{:else if screen === "invite"} {:else if screen === "invite"}
<InviteScreen <InviteScreen
{referral} {referral}
+1
View File
@@ -21,6 +21,7 @@ export const WEBAPP_LANGUAGE_ORDER = ["ru", "en"];
export const APP_SECTION_PATHS = { export const APP_SECTION_PATHS = {
home: "/home", home: "/home",
install: "/install", install: "/install",
trial: "/trial",
invite: "/invite", invite: "/invite",
devices: "/devices", devices: "/devices",
support: "/support", support: "/support",
+92 -1
View File
@@ -390,6 +390,15 @@ export async function mockApi(path, options = {}, context = {}) {
}, },
}; };
} }
if (path === "/admin/panel/internal-squads") {
return {
ok: true,
squads: [
{ uuid: "db786ee8-816b-4760-80aa-1fc7a3669ff2", name: "Base RU" },
{ uuid: "2f2f6e0a-1f2d-4e80-a33b-0ebf3a409012", name: "Trial warmup" },
],
};
}
if (path === "/admin/themes") { if (path === "/admin/themes") {
if (String(options.method || "GET").toUpperCase() === "PUT") { if (String(options.method || "GET").toUpperCase() === "PUT") {
try { try {
@@ -524,6 +533,75 @@ export async function mockApi(path, options = {}, context = {}) {
}, },
], ],
}, },
{
id: "pricing",
order: 11,
fields: [
{
key: "TRIAL_ENABLED",
type: "bool",
section: "pricing",
subsection: "trial",
label: "Триал включён",
value: true,
},
{
key: "TRIAL_DURATION_DAYS",
type: "int",
section: "pricing",
subsection: "trial",
label: "Длительность триала (дней)",
value: 3,
},
{
key: "TRIAL_TRAFFIC_LIMIT_GB",
type: "float",
section: "pricing",
subsection: "trial",
label: "Лимит трафика триала (ГБ)",
value: 5,
},
{
key: "TRIAL_TRAFFIC_STRATEGY",
type: "string",
section: "pricing",
subsection: "trial",
label: "Стратегия сброса трафика триала",
value: "NO_RESET",
},
{
key: "TRIAL_SQUAD_UUIDS",
type: "string",
section: "pricing",
subsection: "trial",
label: "Internal Squads для триала",
value: "2f2f6e0a-1f2d-4e80-a33b-0ebf3a409012",
},
...[
["MONTH_1_ENABLED", "bool", true],
["RUB_PRICE_1_MONTH", "float", 150],
["STARS_PRICE_1_MONTH", "int", 0],
["MONTH_3_ENABLED", "bool", true],
["RUB_PRICE_3_MONTHS", "float", 400],
["STARS_PRICE_3_MONTHS", "int", 0],
["MONTH_6_ENABLED", "bool", false],
["RUB_PRICE_6_MONTHS", "float", 750],
["STARS_PRICE_6_MONTHS", "int", 0],
["MONTH_12_ENABLED", "bool", false],
["RUB_PRICE_12_MONTHS", "float", 1200],
["STARS_PRICE_12_MONTHS", "int", 0],
["TRAFFIC_PACKAGES", "string", "10:99,50:399"],
["STARS_TRAFFIC_PACKAGES", "string", ""],
].map(([key, type, value]) => ({
key,
type,
section: "pricing",
subsection: "legacy_tariffs",
label: key,
value,
})),
],
},
], ],
}; };
if (cleanPath === "/admin/support/stats") { if (cleanPath === "/admin/support/stats") {
@@ -713,13 +791,26 @@ export async function mockApi(path, options = {}, context = {}) {
remaining_text: "5 д. 0 ч.", remaining_text: "5 д. 0 ч.",
end_date_text: "05.05.2026 12:00", end_date_text: "05.05.2026 12:00",
days_left: 5, days_left: 5,
config_link: "https://sub.example.com/sub/trial-preview-token",
connect_url: "https://sub.example.com/connect/trial-preview-token",
panel_short_uuid: "trial-preview-token",
install_share_token: "8f559061460e8fede78ef18dce887236",
install_share_url: "https://app.example.com/s/8f559061460e8fede78ef18dce887236",
traffic_limit: "10 GB", traffic_limit: "10 GB",
traffic_limit_bytes: 10737418240, traffic_limit_bytes: 10737418240,
traffic_used: "0 B", traffic_used: "0 B",
traffic_used_bytes: 0, traffic_used_bytes: 0,
}; };
DEV_MOCK.data.settings.trial_available = false; DEV_MOCK.data.settings.trial_available = false;
return { ok: true, activated: true, end_date_text: "05.05.2026 12:00" }; return {
ok: true,
activated: true,
days: 5,
end_date_text: "05.05.2026 12:00",
traffic_gb: 10,
config_link: "https://sub.example.com/sub/trial-preview-token",
connect_url: "https://sub.example.com/connect/trial-preview-token",
};
} }
if (path === "/auth/logout") return { ok: true }; if (path === "/auth/logout") return { ok: true };
if (path === "/account/language" && String(options.method || "").toUpperCase() === "POST") { if (path === "/account/language" && String(options.method || "").toUpperCase() === "POST") {
+1
View File
@@ -7,6 +7,7 @@ export function normalizeSection(value) {
if ( if (
section === "invite" || section === "invite" ||
section === "install" || section === "install" ||
section === "trial" ||
section === "devices" || section === "devices" ||
section === "support" || section === "support" ||
section === "settings" || section === "settings" ||
@@ -0,0 +1,278 @@
<script>
import { onMount } from "svelte";
import {
ArrowLeft,
CheckCircle2,
CircleX,
Download,
Gift,
RefreshCw,
} from "$components/ui/icons.js";
import BrandMark from "$lib/webapp/BrandMark.svelte";
import Button from "$components/ui/button.svelte";
import Card from "$components/ui/card.svelte";
import { formatTrafficGb } from "../../lib/webapp/formatters.js";
export let appSettings = {};
export let brand = {};
export let brandTitle = "";
export let subscription = {};
export let trialBusy = false;
export let trialResult = null;
export let trialError = "";
export let activateTrial = () => {};
export let openInstallOrConnect = () => {};
export let goHome = () => {};
export let t = (key, _params = {}, fallback = "") => fallback || key;
let requested = false;
$: trialEnabled = Boolean(appSettings?.trial_enabled);
$: trialAvailable = Boolean(appSettings?.trial_available);
$: canRequestTrial = Boolean(trialEnabled && trialAvailable && !subscription?.active);
$: isTrialStatus =
Boolean(trialResult?.activated) ||
String(subscription?.status || "")
.toUpperCase()
.includes("TRIAL");
$: hasActiveAccess = Boolean(subscription?.active || trialResult?.activated);
$: successTitle = isTrialStatus
? t("wa_trial_activated")
: t("wa_home_subscription_active", {}, "Subscription active");
$: endDateText = trialResult?.end_date_text || subscription?.end_date_text || "";
$: daysLeft = Number(
trialResult?.days || subscription?.days_left || appSettings?.trial_duration_days || 0
);
$: trafficLabel = trialTrafficLabel();
function trialTrafficLabel() {
const resultTraffic = Number(trialResult?.traffic_gb || 0);
const settingsTraffic = Number(appSettings?.trial_traffic_limit_gb || 0);
const limit = resultTraffic || settingsTraffic;
return limit > 0 ? formatTrafficGb(limit) : t("wa_unlimited_traffic");
}
onMount(() => {
if (!requested && canRequestTrial) {
requested = true;
activateTrial({ stayOnTrial: true });
}
});
</script>
<main class="trial-activation-screen">
<div class="login-brand trial-activation-brand">
<BrandMark {brand} size="lg" />
<h1>{brandTitle}</h1>
</div>
<Card class="trial-activation-card">
<div
class={`trial-activation-icon ${
trialBusy ? "trial-activation-icon-loading" : hasActiveAccess ? "is-success" : "is-muted"
}`}
aria-hidden="true"
>
{#if trialBusy}
<RefreshCw size={27} />
{:else if hasActiveAccess}
<CheckCircle2 size={30} />
{:else if trialError || !canRequestTrial}
<CircleX size={30} />
{:else}
<Gift size={30} />
{/if}
</div>
<div class="trial-activation-copy" aria-busy={trialBusy}>
{#if trialBusy}
<h2>{t("wa_trial_activation_loading", {}, "Activating trial...")}</h2>
<p>{t("wa_trial_activation_wait", {}, "Preparing access and connection details.")}</p>
{:else if hasActiveAccess}
<h2>{successTitle}</h2>
<p>
{t(
"wa_trial_active_hint",
{},
"Access is ready. Install the app and import the profile."
)}
</p>
<dl class="trial-activation-facts">
{#if endDateText}
<div>
<dt>{t("wa_trial_active_until_label", {}, "Active until")}</dt>
<dd>{endDateText}</dd>
</div>
{/if}
{#if daysLeft > 0}
<div>
<dt>{t("wa_trial_days_left_label", {}, "Time left")}</dt>
<dd>{t("wa_trial_days_left", { days: daysLeft }, "{days} days")}</dd>
</div>
{/if}
<div>
<dt>{t("wa_trial_traffic_label", {}, "Traffic")}</dt>
<dd>{trafficLabel}</dd>
</div>
</dl>
{:else if trialError}
<h2>{t("wa_trial_activation_failed")}</h2>
<p>{trialError}</p>
{:else}
<h2>{t("wa_trial_unavailable_title", {}, "Trial is unavailable")}</h2>
<p>
{t(
"wa_trial_unavailable_hint",
{},
"Trial may already be used, or this account already has active access."
)}
</p>
{/if}
</div>
</Card>
<div class="trial-activation-actions">
{#if hasActiveAccess}
<Button class="wide" onclick={openInstallOrConnect}>
<Download size={18} />
{t("wa_install_and_configure")}
</Button>
{:else if trialError && canRequestTrial}
<Button
class="wide"
onclick={() => activateTrial({ stayOnTrial: true })}
disabled={trialBusy}
>
<RefreshCw size={18} />
{t("wa_trial_retry", {}, "Try again")}
</Button>
{/if}
<Button class="wide" variant="secondary" onclick={goHome}>
<ArrowLeft size={18} />
{t("wa_nav_home", {}, "Home")}
</Button>
</div>
</main>
<style>
.trial-activation-screen {
display: grid;
min-height: calc(100dvh - 34px);
align-content: center;
gap: 18px;
padding-bottom: 86px;
animation: section-enter 0.22s ease-out both;
}
.trial-activation-brand {
gap: 8px;
}
.trial-activation-brand h1 {
font-size: 25px;
}
:global(.trial-activation-card) {
display: grid;
justify-items: center;
gap: 14px;
padding: 20px 16px 18px;
text-align: center;
}
.trial-activation-icon {
display: grid;
width: 58px;
height: 58px;
place-items: center;
border: 1px solid var(--surface-subtle-border);
border-radius: 50%;
color: var(--muted);
background: color-mix(in srgb, var(--panel) 70%, transparent);
}
.trial-activation-icon.is-success {
border-color: color-mix(in srgb, var(--accent) 52%, var(--border));
color: var(--accent);
background: color-mix(in srgb, var(--accent) 13%, var(--panel));
}
:global(.trial-activation-icon-loading svg) {
animation: trial-spin 0.8s linear infinite;
}
.trial-activation-copy {
display: grid;
gap: 8px;
width: 100%;
}
.trial-activation-copy h2,
.trial-activation-copy p {
margin: 0;
}
.trial-activation-copy h2 {
color: var(--text);
font-size: 19px;
font-weight: 900;
line-height: 1.15;
}
.trial-activation-copy p {
color: var(--muted);
font-size: 13px;
line-height: 1.42;
}
.trial-activation-facts {
display: grid;
gap: 8px;
width: 100%;
margin: 8px 0 0;
}
.trial-activation-facts div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
padding: 9px 10px;
border: 1px solid var(--surface-subtle-border);
border-radius: 12px;
background: var(--surface-subtle);
text-align: left;
}
.trial-activation-facts dt,
.trial-activation-facts dd {
min-width: 0;
margin: 0;
font-size: 12px;
line-height: 1.2;
}
.trial-activation-facts dt {
color: var(--muted);
}
.trial-activation-facts dd {
overflow-wrap: anywhere;
color: var(--text);
font-weight: 850;
text-align: right;
}
.trial-activation-actions {
display: grid;
gap: 10px;
}
@keyframes trial-spin {
to {
transform: rotate(360deg);
}
}
</style>
+10
View File
@@ -749,6 +749,16 @@
"wa_trial_details": "{days} days, {traffic}", "wa_trial_details": "{days} days, {traffic}",
"wa_trial_activated": "Trial activated", "wa_trial_activated": "Trial activated",
"wa_trial_activation_failed": "Failed to activate trial", "wa_trial_activation_failed": "Failed to activate trial",
"wa_trial_activation_loading": "Activating trial...",
"wa_trial_activation_wait": "Preparing access and connection details.",
"wa_trial_active_hint": "Access is ready. Install the app and import the profile.",
"wa_trial_active_until_label": "Active until",
"wa_trial_days_left_label": "Time left",
"wa_trial_days_left": "{days} days",
"wa_trial_traffic_label": "Traffic",
"wa_trial_unavailable_title": "Trial is unavailable",
"wa_trial_unavailable_hint": "Trial may already be used, or this account already has active access.",
"wa_trial_retry": "Try again",
"wa_install_and_configure": "Install and configure", "wa_install_and_configure": "Install and configure",
"wa_payment_methods_not_configured": "Payment methods are not configured yet", "wa_payment_methods_not_configured": "Payment methods are not configured yet",
"wa_pay": "Pay", "wa_pay": "Pay",
+10
View File
@@ -749,6 +749,16 @@
"wa_trial_details": "{days} дн., {traffic}", "wa_trial_details": "{days} дн., {traffic}",
"wa_trial_activated": "Пробный период активирован", "wa_trial_activated": "Пробный период активирован",
"wa_trial_activation_failed": "Не удалось активировать пробный период", "wa_trial_activation_failed": "Не удалось активировать пробный период",
"wa_trial_activation_loading": "Активируем пробный период...",
"wa_trial_activation_wait": "Готовим доступ и данные для подключения.",
"wa_trial_active_hint": "Доступ готов. Установите приложение и импортируйте профиль.",
"wa_trial_active_until_label": "Действует до",
"wa_trial_days_left_label": "Осталось",
"wa_trial_days_left": "{days} дн.",
"wa_trial_traffic_label": "Трафик",
"wa_trial_unavailable_title": "Пробный период недоступен",
"wa_trial_unavailable_hint": "Он уже использован или для аккаунта уже есть активный доступ.",
"wa_trial_retry": "Попробовать еще раз",
"wa_install_and_configure": "Установить и настроить", "wa_install_and_configure": "Установить и настроить",
"wa_payment_methods_not_configured": "Способы оплаты пока не настроены", "wa_payment_methods_not_configured": "Способы оплаты пока не настроены",
"wa_pay": "Оплатить", "wa_pay": "Оплатить",
+5
View File
@@ -5,6 +5,7 @@ from bot.utils.mini_app_url import (
subscription_mini_app_install_url, subscription_mini_app_install_url,
subscription_mini_app_path_url, subscription_mini_app_path_url,
subscription_mini_app_topup_url, subscription_mini_app_topup_url,
subscription_mini_app_trial_url,
subscription_public_install_url, subscription_public_install_url,
) )
from config.settings import Settings from config.settings import Settings
@@ -62,6 +63,10 @@ class MiniAppUrlTests(unittest.TestCase):
subscription_mini_app_install_url(s), subscription_mini_app_install_url(s),
"https://app.example.com/webapp/install", "https://app.example.com/webapp/install",
) )
self.assertEqual(
subscription_mini_app_trial_url(s),
"https://app.example.com/webapp/trial",
)
def test_subscription_public_install_url_uses_origin(self): def test_subscription_public_install_url_uses_origin(self):
s = Settings( s = Settings(
+40
View File
@@ -67,6 +67,46 @@ class UserBotMenuTests(unittest.TestCase):
self.assertIn("main_action:bot_interface", callbacks) self.assertIn("main_action:bot_interface", callbacks)
self.assertIn("main_action:info", callbacks) self.assertIn("main_action:info", callbacks)
def test_main_menu_shows_trial_button_as_mini_app_deeplink_when_available(self):
markup = get_main_menu_inline_keyboard(
"en",
self.i18n,
self.settings,
show_trial_button=True,
)
trial_button = markup.inline_keyboard[0][0]
self.assertEqual(trial_button.text, self.i18n.gettext("en", "menu_activate_trial_button"))
self.assertIsNone(trial_button.callback_data)
self.assertEqual(trial_button.web_app.url, "https://app.example.com/trial")
def test_main_menu_trial_button_falls_back_to_bot_callback_without_mini_app(self):
self.settings.SUBSCRIPTION_MINI_APP_URL = ""
markup = get_main_menu_inline_keyboard(
"en",
self.i18n,
self.settings,
show_trial_button=True,
)
trial_button = markup.inline_keyboard[0][0]
self.assertEqual(trial_button.callback_data, "main_action:request_trial")
self.assertIsNone(trial_button.web_app)
def test_bot_interface_trial_button_uses_mini_app_deeplink_when_available(self):
markup = get_bot_interface_inline_keyboard(
"en",
self.i18n,
self.settings,
show_trial_button=True,
)
trial_button = markup.inline_keyboard[0][0]
self.assertEqual(trial_button.web_app.url, "https://app.example.com/trial")
def test_bot_interface_buttons_return_to_bot_interface(self): def test_bot_interface_buttons_return_to_bot_interface(self):
markup = get_bot_interface_inline_keyboard("en", self.i18n, self.settings) markup = get_bot_interface_inline_keyboard("en", self.i18n, self.settings)
+1
View File
@@ -75,6 +75,7 @@ class WebAppRouteContractTests(unittest.TestCase):
("GET", "/login/password"): "index_route", ("GET", "/login/password"): "index_route",
("GET", "/home"): "index_route", ("GET", "/home"): "index_route",
("GET", "/install"): "index_route", ("GET", "/install"): "index_route",
("GET", "/trial"): "index_route",
("GET", "/open-app"): "app_deeplink_route", ("GET", "/open-app"): "app_deeplink_route",
("GET", "/s/{share_token}"): "index_route", ("GET", "/s/{share_token}"): "index_route",
("GET", "/invite"): "index_route", ("GET", "/invite"): "index_route",