feat(variants): default a registered account to Erudit + Russian Scrabble #288
@@ -285,7 +285,7 @@ func (s *Store) AttachIdentity(ctx context.Context, accountID uuid.UUID, kind, e
|
|||||||
// for an already-durable account.
|
// for an already-durable account.
|
||||||
//
|
//
|
||||||
// The promotion also widens the guest's narrow variant set (GuestVariantPreferences,
|
// 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
|
// hint really does gain Russian Scrabble. A guest who changed the set themselves is left
|
||||||
// alone: only the untouched guest default is replaced.
|
// alone: only the untouched guest default is replaced.
|
||||||
func (s *Store) ClearGuest(ctx context.Context, accountID uuid.UUID) error {
|
func (s *Store) ClearGuest(ctx context.Context, accountID uuid.UUID) error {
|
||||||
|
|||||||
@@ -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
|
**registered** account defaults to the two Russian-alphabet games — **Erudit + Russian
|
||||||
Scrabble** (a DB column default); a **guest** is deliberately narrower and starts on
|
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
|
**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 —
|
(`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
|
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
|
backfilled** when the default moves. At least one variant must stay selected. The picker is
|
||||||
|
|||||||
+2
-1
@@ -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
|
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**
|
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
|
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
|
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
|
**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
|
(auto-match, an AI game, or a friend invitation you create) — a variant you have not enabled is
|
||||||
|
|||||||
@@ -462,8 +462,8 @@ Telegram. Внутри VK Mini App те же настройки — и язык
|
|||||||
которые ты разрешаешь себя подбирать; **новый зарегистрированный аккаунт стартует с Эрудитом и
|
которые ты разрешаешь себя подбирать; **новый зарегистрированный аккаунт стартует с Эрудитом и
|
||||||
русским Scrabble**, а вход по **диплинку промо-бота** дополнительно включает **английский
|
русским Scrabble**, а вход по **диплинку промо-бота** дополнительно включает **английский
|
||||||
Scrabble**, если язык Telegram не русский. **Гость** стартует **только с Эрудитом**, и «Новая игра»
|
Scrabble**, если язык Telegram не русский. **Гость** стартует **только с Эрудитом**, и «Новая игра»
|
||||||
предлагает ему зарегистрироваться ради остальных; регистрация расширяет аккаунт до набора
|
предлагает ему **войти** ради остальных (та же формулировка и ссылка, что и у гостевой заглушки в
|
||||||
зарегистрированного. Смена набора по умолчанию никогда не переписывает уже существующие аккаунты.
|
статистике, обе ведут в Settings → Profile); вход расширяет аккаунт до набора зарегистрированного. Смена набора по умолчанию никогда не переписывает уже существующие аккаунты.
|
||||||
Нужно оставить выбранным **хотя бы один**. Именно этот список предлагает **Новая игра**, когда ты
|
Нужно оставить выбранным **хотя бы один**. Именно этот список предлагает **Новая игра**, когда ты
|
||||||
запускаешь партию (авто-подбор, игра с ИИ или приглашение друга, которое ты создаёшь), — не
|
запускаешь партию (авто-подбор, игра с ИИ или приглашение друга, которое ты создаёшь), — не
|
||||||
включённый вариант не предлагается, и сервер его отклоняет. На партии, в которые тебя
|
включённый вариант не предлагается, и сервер его отклоняет. На партии, в которые тебя
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export const en = {
|
|||||||
'common.you': 'You',
|
'common.you': 'You',
|
||||||
'common.guest': 'Guest',
|
'common.guest': 'Guest',
|
||||||
'common.save': 'Save',
|
'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.title': 'Sign in',
|
||||||
'login.guest': 'Play as guest',
|
'login.guest': 'Play as guest',
|
||||||
@@ -83,10 +87,8 @@ export const en = {
|
|||||||
'new.moveLimit': 'Move time: {n} h 00 min',
|
'new.moveLimit': 'Move time: {n} h 00 min',
|
||||||
'new.searchHint':
|
'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.',
|
'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
|
// The tail of the guest's invitation to sign in (common.signInLink opens it), shown under the
|
||||||
// two keys because the first word is a link to the profile screen; the tail carries its own
|
// single variant a guest is offered.
|
||||||
// leading space, since Svelte trims literal whitespace at the edges of markup.
|
|
||||||
'new.guestVariantHintLink': 'Register',
|
|
||||||
'new.guestVariantHint': ' to play the Russian or English Scrabble.',
|
'new.guestVariantHint': ' to play the Russian or English Scrabble.',
|
||||||
|
|
||||||
// First-run coachmark hints (components/Coachmark.svelte). The leading emoji in each comment
|
// 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.winRate': 'Win rate',
|
||||||
'stats.maxGame': 'Best game',
|
'stats.maxGame': 'Best game',
|
||||||
'stats.maxWord': 'Best move',
|
'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.exportGcg': 'Export game',
|
||||||
'game.exportChoice': 'Export the finished game as:',
|
'game.exportChoice': 'Export the finished game as:',
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'common.you': 'Вы',
|
'common.you': 'Вы',
|
||||||
'common.guest': 'Гость',
|
'common.guest': 'Гость',
|
||||||
'common.save': 'Сохранить',
|
'common.save': 'Сохранить',
|
||||||
|
'common.signInLink': 'Войдите',
|
||||||
|
|
||||||
'login.title': 'Вход',
|
'login.title': 'Вход',
|
||||||
'login.guest': 'Играть как гость',
|
'login.guest': 'Играть как гость',
|
||||||
@@ -84,7 +85,6 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'new.moveLimit': 'Время на ход: {n} ч. 00 мин.',
|
'new.moveLimit': 'Время на ход: {n} ч. 00 мин.',
|
||||||
'new.searchHint':
|
'new.searchHint':
|
||||||
'Иногда поиск соперника может занять некоторое время. Если не захотите ждать после начала игры, можете вернуться в приложение через несколько минут.',
|
'Иногда поиск соперника может занять некоторое время. Если не захотите ждать после начала игры, можете вернуться в приложение через несколько минут.',
|
||||||
'new.guestVariantHintLink': 'Зарегистрируйтесь',
|
|
||||||
'new.guestVariantHint': ', чтобы играть в русский или английский вариант Скрэббл.',
|
'new.guestVariantHint': ', чтобы играть в русский или английский вариант Скрэббл.',
|
||||||
|
|
||||||
// Подсказки первого запуска (components/Coachmark.svelte).
|
// Подсказки первого запуска (components/Coachmark.svelte).
|
||||||
@@ -417,7 +417,7 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'stats.winRate': 'Доля побед',
|
'stats.winRate': 'Доля побед',
|
||||||
'stats.maxGame': 'Лучшая игра',
|
'stats.maxGame': 'Лучшая игра',
|
||||||
'stats.maxWord': 'Лучший ход',
|
'stats.maxWord': 'Лучший ход',
|
||||||
'stats.guestHint': 'Войдите, чтобы вести статистику.',
|
'stats.guestHint': ', чтобы вести статистику.',
|
||||||
|
|
||||||
'game.exportGcg': 'Экспорт партии',
|
'game.exportGcg': 'Экспорт партии',
|
||||||
'game.exportChoice': 'Экспортировать завершённую партию как:',
|
'game.exportChoice': 'Экспортировать завершённую партию как:',
|
||||||
|
|||||||
@@ -90,11 +90,11 @@ export function availableVariants(preferences: Variant[] | undefined): VariantOp
|
|||||||
return ALL_VARIANTS.filter((v) => effective.includes(v.id));
|
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
|
// 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
|
// (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
|
// guest is created with Erudit only, while signing in widens the account to both Russian-alphabet
|
||||||
// games. Offline it is suppressed — registration needs the network, so the link would be dead.
|
// 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 {
|
export function showRegisterHint(isGuest: boolean, offered: number, offline: boolean): boolean {
|
||||||
return isGuest && !offline && offered === 1;
|
return isGuest && !offline && offered === 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,11 +396,12 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<!-- A guest is offered Erudit alone; registering widens the account to both Russian games
|
<!-- A guest is offered Erudit alone; signing in widens the account to both Russian games
|
||||||
(and Settings then offers English), so point them at the profile screen. -->
|
(and Settings then offers English), so point them at the profile screen — the same
|
||||||
|
wording and link the Stats guest stub uses. -->
|
||||||
{#if showRegisterHint(guest, variants.length, offlineMode.active)}
|
{#if showRegisterHint(guest, variants.length, offlineMode.active)}
|
||||||
<p class="guesthint">
|
<p class="guesthint">
|
||||||
<button class="hintlink" onclick={() => navigate('/profile')}>{t('new.guestVariantHintLink')}</button>{t('new.guestVariantHint')}
|
<button class="hintlink" onclick={() => navigate('/profile')}>{t('common.signInLink')}</button>{t('new.guestVariantHint')}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if variants.some((v) => supportsMultipleWordsToggle(v.id))}
|
{#if variants.some((v) => supportsMultipleWordsToggle(v.id))}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import WordTiles from '../components/WordTiles.svelte';
|
import WordTiles from '../components/WordTiles.svelte';
|
||||||
import { app, handleError } from '../lib/app.svelte';
|
import { app, handleError } from '../lib/app.svelte';
|
||||||
import { gateway } from '../lib/gateway';
|
import { gateway } from '../lib/gateway';
|
||||||
|
import { navigate } from '../lib/router.svelte';
|
||||||
import { t, i18n, type MessageKey } from '../lib/i18n/index.svelte';
|
import { t, i18n, type MessageKey } from '../lib/i18n/index.svelte';
|
||||||
import { gamesPlayed, hintSharePercent, winRate } from '../lib/stats';
|
import { gamesPlayed, hintSharePercent, winRate } from '../lib/stats';
|
||||||
import { ALL_VARIANTS, variantNameKey } from '../lib/variants';
|
import { ALL_VARIANTS, variantNameKey } from '../lib/variants';
|
||||||
@@ -56,7 +57,11 @@
|
|||||||
<Screen title={t('stats.title')} back="/">
|
<Screen title={t('stats.title')} back="/">
|
||||||
<div class="page">
|
<div class="page">
|
||||||
{#if app.profile?.isGuest}
|
{#if app.profile?.isGuest}
|
||||||
<p class="muted">{t('stats.guestHint')}</p>
|
<!-- The opening word links to the profile screen, where a guest signs in (the same wording
|
||||||
|
and link New Game shows a guest under its lone variant). -->
|
||||||
|
<p class="muted">
|
||||||
|
<button class="hintlink" onclick={() => navigate('/profile')}>{t('common.signInLink')}</button>{t('stats.guestHint')}
|
||||||
|
</p>
|
||||||
{:else if stats}
|
{:else if stats}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#each cards as c (c.key)}
|
{#each cards as c (c.key)}
|
||||||
@@ -89,6 +94,16 @@
|
|||||||
.muted {
|
.muted {
|
||||||
color: var(--text-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 {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
|||||||
Reference in New Issue
Block a user