diff --git a/backend/internal/account/link.go b/backend/internal/account/link.go index f711aad..7de9f55 100644 --- a/backend/internal/account/link.go +++ b/backend/internal/account/link.go @@ -285,7 +285,7 @@ func (s *Store) AttachIdentity(ctx context.Context, accountID uuid.UUID, kind, e // for an already-durable account. // // The promotion also widens the guest's narrow variant set (GuestVariantPreferences, -// Эрудит alone) to the registered default, so a player who registers from the New Game +// Эрудит alone) to the registered default, so a player who signs in from the New Game // hint really does gain Russian Scrabble. A guest who changed the set themselves is left // alone: only the untouched guest default is replaced. func (s *Store) ClearGuest(ctx context.Context, accountID uuid.UUID) error { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 72be347..282de0a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -249,7 +249,7 @@ arrive from a platform rather than completing a mandatory registration). **registered** account defaults to the two Russian-alphabet games — **Erudit + Russian Scrabble** (a DB column default); a **guest** is deliberately narrower and starts on **Erudit only** (`account.ProvisionGuest` writes the set explicitly), with New Game inviting - the guest to register for the rest. Registering promotes the set to the registered default + the guest to sign in for the rest. Signing in promotes the set to the registered default (`account.ClearGuest`), but only while the guest still carries the untouched guest set — a player who chose their own set is never overruled, and **existing accounts are not backfilled** when the default moves. At least one variant must stay selected. The picker is diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index 896a171..87a83f1 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -455,7 +455,8 @@ variants — Erudite, Russian Scrabble and English Scrabble, shown **Erudite-fir yourself to be matched into; a **newly registered account starts with Erudite + Russian Scrabble**, and arriving through the **promo bot's deep link** also enables **English Scrabble** when your Telegram language is not Russian. A **guest** starts with **Erudite only** and New Game -invites them to register for the rest; registering widens the account to the registered default. +invites them to **sign in** for the rest (the same wording and link as the Stats guest stub, both +routing to Settings → Profile); signing in widens the account to the registered default. Changing the defaults never rewrites accounts that already exist. You must keep **at least one** selected. This list is exactly what **New Game** offers when you start a game (auto-match, an AI game, or a friend invitation you create) — a variant you have not enabled is diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index 15aae53..1113e0a 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -462,8 +462,8 @@ Telegram. Внутри VK Mini App те же настройки — и язык которые ты разрешаешь себя подбирать; **новый зарегистрированный аккаунт стартует с Эрудитом и русским Scrabble**, а вход по **диплинку промо-бота** дополнительно включает **английский Scrabble**, если язык Telegram не русский. **Гость** стартует **только с Эрудитом**, и «Новая игра» -предлагает ему зарегистрироваться ради остальных; регистрация расширяет аккаунт до набора -зарегистрированного. Смена набора по умолчанию никогда не переписывает уже существующие аккаунты. +предлагает ему **войти** ради остальных (та же формулировка и ссылка, что и у гостевой заглушки в +статистике, обе ведут в Settings → Profile); вход расширяет аккаунт до набора зарегистрированного. Смена набора по умолчанию никогда не переписывает уже существующие аккаунты. Нужно оставить выбранным **хотя бы один**. Именно этот список предлагает **Новая игра**, когда ты запускаешь партию (авто-подбор, игра с ИИ или приглашение друга, которое ты создаёшь), — не включённый вариант не предлагается, и сервер его отклоняет. На партии, в которые тебя diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index 7f5125c..2b37159 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -41,6 +41,10 @@ export const en = { 'common.you': 'You', 'common.guest': 'Guest', 'common.save': 'Save', + // The opening word of a guest's invitation to sign in, rendered as a link to the profile + // screen (New Game, Stats). Shared so the two screens cannot drift apart; each screen keeps + // its own tail, which carries its leading space — Svelte trims literal edge whitespace. + 'common.signInLink': 'Sign in', 'login.title': 'Sign in', 'login.guest': 'Play as guest', @@ -83,10 +87,8 @@ export const en = { 'new.moveLimit': 'Move time: {n} h 00 min', 'new.searchHint': 'Finding an opponent can sometimes take a while. If you do not want to wait, close the app after starting the game and come back in a couple of minutes.', - // The guest's invitation to register, shown under the single variant a guest is offered. It is - // two keys because the first word is a link to the profile screen; the tail carries its own - // leading space, since Svelte trims literal whitespace at the edges of markup. - 'new.guestVariantHintLink': 'Register', + // The tail of the guest's invitation to sign in (common.signInLink opens it), shown under the + // single variant a guest is offered. 'new.guestVariantHint': ' to play the Russian or English Scrabble.', // First-run coachmark hints (components/Coachmark.svelte). The leading emoji in each comment @@ -420,7 +422,8 @@ export const en = { 'stats.winRate': 'Win rate', 'stats.maxGame': 'Best game', 'stats.maxWord': 'Best move', - 'stats.guestHint': 'Sign in to track your statistics.', + // The tail of the guest's invitation to sign in (common.signInLink opens it). + 'stats.guestHint': ' to track your statistics.', 'game.exportGcg': 'Export game', 'game.exportChoice': 'Export the finished game as:', diff --git a/ui/src/lib/i18n/ru.ts b/ui/src/lib/i18n/ru.ts index d528c4d..c698eee 100644 --- a/ui/src/lib/i18n/ru.ts +++ b/ui/src/lib/i18n/ru.ts @@ -42,6 +42,7 @@ export const ru: Record = { 'common.you': 'Вы', 'common.guest': 'Гость', 'common.save': 'Сохранить', + 'common.signInLink': 'Войдите', 'login.title': 'Вход', 'login.guest': 'Играть как гость', @@ -84,7 +85,6 @@ export const ru: Record = { 'new.moveLimit': 'Время на ход: {n} ч. 00 мин.', 'new.searchHint': 'Иногда поиск соперника может занять некоторое время. Если не захотите ждать после начала игры, можете вернуться в приложение через несколько минут.', - 'new.guestVariantHintLink': 'Зарегистрируйтесь', 'new.guestVariantHint': ', чтобы играть в русский или английский вариант Скрэббл.', // Подсказки первого запуска (components/Coachmark.svelte). @@ -417,7 +417,7 @@ export const ru: Record = { 'stats.winRate': 'Доля побед', 'stats.maxGame': 'Лучшая игра', 'stats.maxWord': 'Лучший ход', - 'stats.guestHint': 'Войдите, чтобы вести статистику.', + 'stats.guestHint': ', чтобы вести статистику.', 'game.exportGcg': 'Экспорт партии', 'game.exportChoice': 'Экспортировать завершённую партию как:', diff --git a/ui/src/lib/variants.ts b/ui/src/lib/variants.ts index 475d37f..84246e0 100644 --- a/ui/src/lib/variants.ts +++ b/ui/src/lib/variants.ts @@ -90,11 +90,11 @@ export function availableVariants(preferences: Variant[] | undefined): VariantOp return ALL_VARIANTS.filter((v) => effective.includes(v.id)); } -// showRegisterHint reports whether New Game invites the player to register in order to unlock the +// showRegisterHint reports whether New Game invites the player to sign in in order to unlock the // other games, given whether they are a guest (isGuest), how many variants they are offered // (offered) and whether the app is in offline mode (offline). It is the guest's case alone: a -// guest is created with Erudit only, while registering widens the account to both Russian-alphabet -// games. Offline it is suppressed — registration needs the network, so the link would be dead. +// guest is created with Erudit only, while signing in widens the account to both Russian-alphabet +// games. Offline it is suppressed — signing in needs the network, so the link would be dead. export function showRegisterHint(isGuest: boolean, offered: number, offline: boolean): boolean { return isGuest && !offline && offered === 1; } diff --git a/ui/src/screens/NewGame.svelte b/ui/src/screens/NewGame.svelte index e5c856f..624307a 100644 --- a/ui/src/screens/NewGame.svelte +++ b/ui/src/screens/NewGame.svelte @@ -396,11 +396,12 @@ {/each} - + {#if showRegisterHint(guest, variants.length, offlineMode.active)}

- {t('new.guestVariantHint')} + {t('new.guestVariantHint')}

{/if} {#if variants.some((v) => supportsMultipleWordsToggle(v.id))} diff --git a/ui/src/screens/Stats.svelte b/ui/src/screens/Stats.svelte index 56f9672..e8bb123 100644 --- a/ui/src/screens/Stats.svelte +++ b/ui/src/screens/Stats.svelte @@ -4,6 +4,7 @@ import WordTiles from '../components/WordTiles.svelte'; import { app, handleError } from '../lib/app.svelte'; import { gateway } from '../lib/gateway'; + import { navigate } from '../lib/router.svelte'; import { t, i18n, type MessageKey } from '../lib/i18n/index.svelte'; import { gamesPlayed, hintSharePercent, winRate } from '../lib/stats'; import { ALL_VARIANTS, variantNameKey } from '../lib/variants'; @@ -56,7 +57,11 @@
{#if app.profile?.isGuest} -

{t('stats.guestHint')}

+ +

+ {t('stats.guestHint')} +

{:else if stats}
{#each cards as c (c.key)} @@ -89,6 +94,16 @@ .muted { color: var(--text-muted); } + /* The inline link-styled button opening the guest stub (mirrors New Game's .hintlink). */ + .hintlink { + background: none; + border: none; + padding: 0; + color: var(--accent); + font: inherit; + text-decoration: underline; + cursor: pointer; + } .grid { display: grid; grid-template-columns: repeat(2, 1fr);