Merge pull request 'feat(ui): настоящий share friend-code + ссылка по текущему боту (service_language)' (#66) from feat/friend-invite-share into development
CI / changes (push) Successful in 2s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 13s
CI / ui (push) Successful in 47s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m0s

This commit was merged in pull request #66.
This commit is contained in:
2026-06-15 15:42:19 +00:00
31 changed files with 439 additions and 50 deletions
+31 -2
View File
@@ -19,6 +19,7 @@ import {
telegramHaptic,
telegramLaunch,
telegramOnEvent,
telegramRequestFullscreen,
telegramSetChrome,
} from './telegram';
import { parseStartParam } from './deeplink';
@@ -62,6 +63,10 @@ export const app = $state<{
/** Whether an operator feedback reply awaits the player, for the lobby ⚙️ badge (combined
* with friend requests) and the Settings → Info badge. */
feedbackReplyUnread: boolean;
/** Whether to show the "outdated invite link" notice: set when a Telegram deep-link friend
* code is already used/expired, so the visitor lands in the lobby with a gentle pointer to
* the bot instead of a scary error on the Friends screen. */
staleInvite: boolean;
/** Monotonic counter bumped when the app returns to the foreground without the live stream
* having dropped. An open game watches it to refetch once, recovering an in-game event shed
* from a full hub buffer while suspended (the stream-drop case is covered by streamAlive). */
@@ -83,6 +88,7 @@ export const app = $state<{
notifications: 0,
chatUnread: {},
feedbackReplyUnread: false,
staleInvite: false,
resync: 0,
});
@@ -134,6 +140,11 @@ export function showToast(text: string, kind: Toast['kind'] = 'info'): void {
toastTimer = setTimeout(() => (app.toast = null), 4000);
}
/** dismissStaleInvite hides the outdated-invite-link notice (the user acknowledged it). */
export function dismissStaleInvite(): void {
app.staleInvite = false;
}
/** clearChatUnread resets a game's unread chat-message count (called when its chat is opened). */
export function clearChatUnread(gameId: string): void {
if (app.chatUnread[gameId]) app.chatUnread = { ...app.chatUnread, [gameId]: 0 };
@@ -457,6 +468,10 @@ export async function bootstrap(): Promise<void> {
telegramOnEvent('safeAreaChanged', syncTelegramSafeArea);
telegramOnEvent('fullscreenChanged', syncTelegramSafeArea);
telegramDisableVerticalSwipes();
// On mobile, go immersive fullscreen like Telegram's own Mini Apps; the fullscreenChanged
// listener above then re-syncs the safe-area insets. Desktop keeps the bot's full-size
// window. No-op on clients predating Bot API 8.0.
telegramRequestFullscreen();
try {
await adoptSession(await gateway.authTelegram(launch.initData));
// A blocked account skips deep-link routing — the blocked screen overlays every route.
@@ -491,13 +506,27 @@ async function routeStartParam(param: string): Promise<void> {
navigate(`/game/${link.id}`);
return;
case 'friendCode':
navigate('/friends');
try {
const friend = await gateway.friendCodeRedeem(link.code);
navigate('/friends');
showToast(t('friends.added', { name: friend.displayName }));
void refreshNotifications();
} catch (err) {
handleError(err);
const code = err instanceof GatewayError ? err.code : '';
if (code === 'self_relation') {
// Tapping your own invite link redeems your own code: a friendly note, not the
// scary "can't do that to yourself" error.
navigate('/friends');
showToast(t('friends.selfInvite'));
} else if (code === 'friend_code_invalid') {
// A previously shared link whose single-use, 12h code is already spent or expired:
// land in the lobby and gently point at the bot, rather than a red error on Friends.
navigate('/');
app.staleInvite = true;
} else {
navigate('/friends');
handleError(err);
}
}
return;
default: