Compare commits
2 Commits
711fe6e594
...
03eb8044ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 03eb8044ff | |||
| 6679260d0a |
@@ -267,6 +267,8 @@ jobs:
|
|||||||
TELEGRAM_TEST_ENV: "true"
|
TELEGRAM_TEST_ENV: "true"
|
||||||
VITE_TELEGRAM_BOT_ID: ${{ vars.TEST_VITE_TELEGRAM_BOT_ID }}
|
VITE_TELEGRAM_BOT_ID: ${{ vars.TEST_VITE_TELEGRAM_BOT_ID }}
|
||||||
VITE_TELEGRAM_LINK: ${{ vars.TEST_VITE_TELEGRAM_LINK }}
|
VITE_TELEGRAM_LINK: ${{ vars.TEST_VITE_TELEGRAM_LINK }}
|
||||||
|
VITE_TELEGRAM_LINK_EN: ${{ vars.TEST_VITE_TELEGRAM_LINK_EN }}
|
||||||
|
VITE_TELEGRAM_LINK_RU: ${{ vars.TEST_VITE_TELEGRAM_LINK_RU }}
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN: ${{ vars.TEST_VITE_TELEGRAM_GAME_CHANNEL_NAME_EN }}
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN: ${{ vars.TEST_VITE_TELEGRAM_GAME_CHANNEL_NAME_EN }}
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU: ${{ vars.TEST_VITE_TELEGRAM_GAME_CHANNEL_NAME_RU }}
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU: ${{ vars.TEST_VITE_TELEGRAM_GAME_CHANNEL_NAME_RU }}
|
||||||
VITE_GATEWAY_URL: ${{ vars.TEST_VITE_GATEWAY_URL }}
|
VITE_GATEWAY_URL: ${{ vars.TEST_VITE_GATEWAY_URL }}
|
||||||
|
|||||||
@@ -1560,12 +1560,15 @@ cannot submit; three-way admin filter.
|
|||||||
unchanged). The DAWG/solver build-time agreement (the original caveat, shared with TODO-2) was
|
unchanged). The DAWG/solver build-time agreement (the original caveat, shared with TODO-2) was
|
||||||
discharged in Stage 14: the dict repo builds against the published solver + pinned
|
discharged in Stage 14: the dict repo builds against the published solver + pinned
|
||||||
`dafsa`/`alphabet`, byte-identical to the fixtures.
|
`dafsa`/`alphabet`, byte-identical to the fixtures.
|
||||||
- **TODO-5 — QR friend codes (owner's idea, Stage 8).** *Partially done in Stage 9:*
|
- **TODO-5 — QR friend codes (owner's idea, Stage 8).** *Partially done in Stage 9, extended later:*
|
||||||
the deep-link scheme now exists (`f<code>`, shared Go ↔ TS), the bot redeems it on
|
the deep-link scheme exists (`f<code>`, shared Go ↔ TS), the bot redeems it on launch, and the UI
|
||||||
launch, and the UI shows a **share-to-Telegram** link for an issued code when
|
offers a real **Share** control for an issued code — Telegram's native share-to-chat picker inside
|
||||||
`VITE_TELEGRAM_LINK` is configured. **Still open:** render the link as a **QR** so a
|
the Mini App (`openTelegramLink` + `t.me/share/url`), the system share sheet (`navigator.share`) on
|
||||||
friend can add you by scanning rather than tapping/typing. The code semantics
|
the web, else copy-the-link. The shared link points at the **same bot the player signed in through**:
|
||||||
(12 h TTL, single use, one active per issuer) stay as-is; only the delivery changes.
|
it carries the session's `service_language` (now on the `Session` wire) and picks
|
||||||
|
`VITE_TELEGRAM_LINK_EN`/`_RU`, falling back to `VITE_TELEGRAM_LINK`. **Still open:** render the link
|
||||||
|
as a **QR** so a friend can add you by scanning. The code semantics (12 h TTL, single use, one active
|
||||||
|
per issuer) stay as-is; only the delivery changes.
|
||||||
- **TODO-6 — smart default for the friend-game "game type" (owner's idea, Stage 8).**
|
- **TODO-6 — smart default for the friend-game "game type" (owner's idea, Stage 8).**
|
||||||
The play-with-friends form has no preselected variant today (an empty, required
|
The play-with-friends form has no preselected variant today (an empty, required
|
||||||
pick). Default it from the player's history (the variant they play most, from
|
pick). Default it from the player's history (the variant they play most, from
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type sessionResponse struct {
|
|||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
IsGuest bool `json:"is_guest"`
|
IsGuest bool `json:"is_guest"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
|
ServiceLanguage string `json:"service_language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// okResponse is a simple success acknowledgement.
|
// okResponse is a simple success acknowledgement.
|
||||||
@@ -163,6 +164,7 @@ func sessionResponseFor(token string, acc account.Account) sessionResponse {
|
|||||||
UserID: acc.ID.String(),
|
UserID: acc.ID.String(),
|
||||||
IsGuest: acc.IsGuest,
|
IsGuest: acc.IsGuest,
|
||||||
DisplayName: acc.DisplayName,
|
DisplayName: acc.DisplayName,
|
||||||
|
ServiceLanguage: acc.ServiceLanguage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func (s *Server) handleTelegramAuth(c *gin.Context) {
|
|||||||
s.abortErr(c, err)
|
s.abortErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
acc.ServiceLanguage = req.ServiceLanguage // reflect this login's bot in the session response
|
||||||
s.mintSession(c, acc)
|
s.mintSession(c, acc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,9 @@ GM_BASICAUTH_HASH= # required; `caddy hash-password` bcrypt
|
|||||||
|
|
||||||
# --- UI build args (baked into the gateway image) ---------------------------
|
# --- UI build args (baked into the gateway image) ---------------------------
|
||||||
VITE_TELEGRAM_BOT_ID=
|
VITE_TELEGRAM_BOT_ID=
|
||||||
VITE_TELEGRAM_LINK=
|
VITE_TELEGRAM_LINK= # fallback friend-invite Mini App link (per-bot below)
|
||||||
|
VITE_TELEGRAM_LINK_EN= # friend-invite Mini App link, English bot (full URL, e.g. https://t.me/<bot>/<app>)
|
||||||
|
VITE_TELEGRAM_LINK_RU= # friend-invite Mini App link, Russian bot (full URL)
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN= # landing "Play in Telegram" link, English bot
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN= # landing "Play in Telegram" link, English bot
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU= # landing "Play in Telegram" link, Russian bot
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU= # landing "Play in Telegram" link, Russian bot
|
||||||
VITE_GATEWAY_URL=
|
VITE_GATEWAY_URL=
|
||||||
|
|||||||
+3
-1
@@ -84,7 +84,9 @@ connector **fails at boot** if both are empty.
|
|||||||
| `TELEGRAM_API_BASE_URL` | variable | _(empty)_ | Override the Bot API host (a mock/self-hosted server); empty = `https://api.telegram.org`. |
|
| `TELEGRAM_API_BASE_URL` | variable | _(empty)_ | Override the Bot API host (a mock/self-hosted server); empty = `https://api.telegram.org`. |
|
||||||
| `GATEWAY_DEFAULT_SUPPORTED_LANGUAGES` | variable | `en,ru` | Variant-gating set for non-Telegram logins (web/email/guest). |
|
| `GATEWAY_DEFAULT_SUPPORTED_LANGUAGES` | variable | `en,ru` | Variant-gating set for non-Telegram logins (web/email/guest). |
|
||||||
| `VITE_TELEGRAM_BOT_ID` | variable | _(empty)_ | UI build-arg: numeric bot id for the web Login Widget. |
|
| `VITE_TELEGRAM_BOT_ID` | variable | _(empty)_ | UI build-arg: numeric bot id for the web Login Widget. |
|
||||||
| `VITE_TELEGRAM_LINK` | variable | _(empty)_ | UI build-arg: deep-link base for share-to-Telegram (e.g. `https://t.me/<bot>/<app>`). |
|
| `VITE_TELEGRAM_LINK` | variable | _(empty)_ | UI build-arg: **fallback** friend-invite Mini App link (e.g. `https://t.me/<bot>/<app>`), used when the per-bot link below is unset. |
|
||||||
|
| `VITE_TELEGRAM_LINK_EN` | variable | _(empty)_ | UI build-arg: friend-invite Mini App link for the **English** bot (full URL, `https://t.me/<bot>/<app>` — `<app>` is the Mini App short name from BotFather). The friend-code share uses the link matching the bot the player signed in through. |
|
||||||
|
| `VITE_TELEGRAM_LINK_RU` | variable | _(empty)_ | UI build-arg: same for the **Russian** bot. |
|
||||||
| `VITE_TELEGRAM_GAME_CHANNEL_NAME_EN` | variable | _(empty)_ | UI build-arg: the landing "Play in Telegram" link for the **English** bot (e.g. `https://t.me/Scrabble_Game`). |
|
| `VITE_TELEGRAM_GAME_CHANNEL_NAME_EN` | variable | _(empty)_ | UI build-arg: the landing "Play in Telegram" link for the **English** bot (e.g. `https://t.me/Scrabble_Game`). |
|
||||||
| `VITE_TELEGRAM_GAME_CHANNEL_NAME_RU` | variable | _(empty)_ | UI build-arg: the landing "Play in Telegram" link for the **Russian** bot (e.g. `https://t.me/Erudit_Game`). |
|
| `VITE_TELEGRAM_GAME_CHANNEL_NAME_RU` | variable | _(empty)_ | UI build-arg: the landing "Play in Telegram" link for the **Russian** bot (e.g. `https://t.me/Erudit_Game`). |
|
||||||
| `VITE_GATEWAY_URL` | variable | _(empty)_ | UI build-arg: gateway origin; empty = same-origin (the usual single-origin deploy). |
|
| `VITE_GATEWAY_URL` | variable | _(empty)_ | UI build-arg: gateway origin; empty = same-origin (the usual single-origin deploy). |
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ services:
|
|||||||
args:
|
args:
|
||||||
VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-}
|
VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-}
|
||||||
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
|
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
|
||||||
|
VITE_TELEGRAM_LINK_EN: ${VITE_TELEGRAM_LINK_EN:-}
|
||||||
|
VITE_TELEGRAM_LINK_RU: ${VITE_TELEGRAM_LINK_RU:-}
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN: ${VITE_TELEGRAM_GAME_CHANNEL_NAME_EN:-}
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN: ${VITE_TELEGRAM_GAME_CHANNEL_NAME_EN:-}
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU: ${VITE_TELEGRAM_GAME_CHANNEL_NAME_RU:-}
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU: ${VITE_TELEGRAM_GAME_CHANNEL_NAME_RU:-}
|
||||||
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
|
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
|
||||||
@@ -169,6 +171,8 @@ services:
|
|||||||
args:
|
args:
|
||||||
VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-}
|
VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-}
|
||||||
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
|
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
|
||||||
|
VITE_TELEGRAM_LINK_EN: ${VITE_TELEGRAM_LINK_EN:-}
|
||||||
|
VITE_TELEGRAM_LINK_RU: ${VITE_TELEGRAM_LINK_RU:-}
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN: ${VITE_TELEGRAM_GAME_CHANNEL_NAME_EN:-}
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN: ${VITE_TELEGRAM_GAME_CHANNEL_NAME_EN:-}
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU: ${VITE_TELEGRAM_GAME_CHANNEL_NAME_RU:-}
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU: ${VITE_TELEGRAM_GAME_CHANNEL_NAME_RU:-}
|
||||||
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
|
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
|
||||||
|
|||||||
@@ -23,12 +23,16 @@ RUN corepack enable && corepack prepare pnpm@11.0.9 --activate
|
|||||||
# VITE_APP_VERSION carries `git describe` for the About screen (defaults to "dev").
|
# VITE_APP_VERSION carries `git describe` for the About screen (defaults to "dev").
|
||||||
ARG VITE_TELEGRAM_BOT_ID=
|
ARG VITE_TELEGRAM_BOT_ID=
|
||||||
ARG VITE_TELEGRAM_LINK=
|
ARG VITE_TELEGRAM_LINK=
|
||||||
|
ARG VITE_TELEGRAM_LINK_EN=
|
||||||
|
ARG VITE_TELEGRAM_LINK_RU=
|
||||||
ARG VITE_TELEGRAM_GAME_CHANNEL_NAME_EN=
|
ARG VITE_TELEGRAM_GAME_CHANNEL_NAME_EN=
|
||||||
ARG VITE_TELEGRAM_GAME_CHANNEL_NAME_RU=
|
ARG VITE_TELEGRAM_GAME_CHANNEL_NAME_RU=
|
||||||
ARG VITE_GATEWAY_URL=
|
ARG VITE_GATEWAY_URL=
|
||||||
ARG VITE_APP_VERSION=
|
ARG VITE_APP_VERSION=
|
||||||
ENV VITE_TELEGRAM_BOT_ID=$VITE_TELEGRAM_BOT_ID \
|
ENV VITE_TELEGRAM_BOT_ID=$VITE_TELEGRAM_BOT_ID \
|
||||||
VITE_TELEGRAM_LINK=$VITE_TELEGRAM_LINK \
|
VITE_TELEGRAM_LINK=$VITE_TELEGRAM_LINK \
|
||||||
|
VITE_TELEGRAM_LINK_EN=$VITE_TELEGRAM_LINK_EN \
|
||||||
|
VITE_TELEGRAM_LINK_RU=$VITE_TELEGRAM_LINK_RU \
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN=$VITE_TELEGRAM_GAME_CHANNEL_NAME_EN \
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN=$VITE_TELEGRAM_GAME_CHANNEL_NAME_EN \
|
||||||
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU=$VITE_TELEGRAM_GAME_CHANNEL_NAME_RU \
|
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU=$VITE_TELEGRAM_GAME_CHANNEL_NAME_RU \
|
||||||
VITE_GATEWAY_URL=$VITE_GATEWAY_URL \
|
VITE_GATEWAY_URL=$VITE_GATEWAY_URL \
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type SessionResp struct {
|
|||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
IsGuest bool `json:"is_guest"`
|
IsGuest bool `json:"is_guest"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
|
ServiceLanguage string `json:"service_language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfileResp is an account's own profile.
|
// ProfileResp is an account's own profile.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func encodeSession(s backendclient.SessionResp, supportedLangs []string) []byte
|
|||||||
token := b.CreateString(s.Token)
|
token := b.CreateString(s.Token)
|
||||||
uid := b.CreateString(s.UserID)
|
uid := b.CreateString(s.UserID)
|
||||||
name := b.CreateString(s.DisplayName)
|
name := b.CreateString(s.DisplayName)
|
||||||
|
svcLang := b.CreateString(s.ServiceLanguage)
|
||||||
langs := buildSupportedLanguagesVector(b, supportedLangs)
|
langs := buildSupportedLanguagesVector(b, supportedLangs)
|
||||||
fb.SessionStart(b)
|
fb.SessionStart(b)
|
||||||
fb.SessionAddToken(b, token)
|
fb.SessionAddToken(b, token)
|
||||||
@@ -43,6 +44,7 @@ func encodeSession(s backendclient.SessionResp, supportedLangs []string) []byte
|
|||||||
fb.SessionAddIsGuest(b, s.IsGuest)
|
fb.SessionAddIsGuest(b, s.IsGuest)
|
||||||
fb.SessionAddDisplayName(b, name)
|
fb.SessionAddDisplayName(b, name)
|
||||||
fb.SessionAddSupportedLanguages(b, langs)
|
fb.SessionAddSupportedLanguages(b, langs)
|
||||||
|
fb.SessionAddServiceLanguage(b, svcLang)
|
||||||
b.Finish(fb.SessionEnd(b))
|
b.Finish(fb.SessionEnd(b))
|
||||||
return b.FinishedBytes()
|
return b.FinishedBytes()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestTelegramAuthForwardsSeedFields(t *testing.T) {
|
|||||||
t.Errorf("unexpected path %q", r.URL.Path)
|
t.Errorf("unexpected path %q", r.URL.Path)
|
||||||
}
|
}
|
||||||
_ = json.NewDecoder(r.Body).Decode(&gotBody)
|
_ = json.NewDecoder(r.Body).Decode(&gotBody)
|
||||||
_, _ = w.Write([]byte(`{"token":"tok-tg","user_id":"u-tg","is_guest":false,"display_name":"Иван"}`))
|
_, _ = w.Write([]byte(`{"token":"tok-tg","user_id":"u-tg","is_guest":false,"display_name":"Иван","service_language":"ru"}`))
|
||||||
})
|
})
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -67,6 +67,11 @@ func TestTelegramAuthForwardsSeedFields(t *testing.T) {
|
|||||||
if got := sessionLanguages(sess); len(got) != 1 || got[0] != "ru" {
|
if got := sessionLanguages(sess); len(got) != 1 || got[0] != "ru" {
|
||||||
t.Errorf("session supported_languages = %v, want [ru]", got)
|
t.Errorf("session supported_languages = %v, want [ru]", got)
|
||||||
}
|
}
|
||||||
|
// The bot's service language rides the Session so the UI can build a share link to
|
||||||
|
// the same bot.
|
||||||
|
if got := string(sess.ServiceLanguage()); got != "ru" {
|
||||||
|
t.Errorf("session service_language = %q, want ru", got)
|
||||||
|
}
|
||||||
// The validated launch fields are forwarded so the backend can seed a new account;
|
// The validated launch fields are forwarded so the backend can seed a new account;
|
||||||
// service_language is recorded to route the account's later out-of-app push.
|
// service_language is recorded to route the account's later out-of-app push.
|
||||||
if gotBody["external_id"] != "42" || gotBody["language_code"] != "ru" || gotBody["first_name"] != "Иван" || gotBody["service_language"] != "ru" {
|
if gotBody["external_id"] != "42" || gotBody["language_code"] != "ru" || gotBody["first_name"] != "Иван" || gotBody["service_language"] != "ru" {
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ table Session {
|
|||||||
is_guest:bool;
|
is_guest:bool;
|
||||||
display_name:string;
|
display_name:string;
|
||||||
supported_languages:[string];
|
supported_languages:[string];
|
||||||
|
// service_language is the language tag (en/ru) of the Telegram bot the session was
|
||||||
|
// minted through; empty for a non-Telegram login. The UI uses it to build a share
|
||||||
|
// link that points at the same bot.
|
||||||
|
service_language:string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ack is a simple success acknowledgement (e.g. an email-code request).
|
// Ack is a simple success acknowledgement (e.g. an email-code request).
|
||||||
|
|||||||
@@ -94,8 +94,16 @@ func (rcv *Session) SupportedLanguagesLength() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rcv *Session) ServiceLanguage() []byte {
|
||||||
|
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
|
||||||
|
if o != 0 {
|
||||||
|
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SessionStart(builder *flatbuffers.Builder) {
|
func SessionStart(builder *flatbuffers.Builder) {
|
||||||
builder.StartObject(5)
|
builder.StartObject(6)
|
||||||
}
|
}
|
||||||
func SessionAddToken(builder *flatbuffers.Builder, token flatbuffers.UOffsetT) {
|
func SessionAddToken(builder *flatbuffers.Builder, token flatbuffers.UOffsetT) {
|
||||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(token), 0)
|
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(token), 0)
|
||||||
@@ -115,6 +123,9 @@ func SessionAddSupportedLanguages(builder *flatbuffers.Builder, supportedLanguag
|
|||||||
func SessionStartSupportedLanguagesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
|
func SessionStartSupportedLanguagesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
|
||||||
return builder.StartVector(4, numElems, 4)
|
return builder.StartVector(4, numElems, 4)
|
||||||
}
|
}
|
||||||
|
func SessionAddServiceLanguage(builder *flatbuffers.Builder, serviceLanguage flatbuffers.UOffsetT) {
|
||||||
|
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(serviceLanguage), 0)
|
||||||
|
}
|
||||||
func SessionEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
func SessionEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||||
return builder.EndObject()
|
return builder.EndObject()
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -28,8 +28,11 @@ pnpm codegen # regenerate src/gen from edge.proto + scrabble.fbs (dev-time)
|
|||||||
`GATEWAY_URL` overrides the dev proxy target; `VITE_GATEWAY_URL` sets the runtime
|
`GATEWAY_URL` overrides the dev proxy target; `VITE_GATEWAY_URL` sets the runtime
|
||||||
gateway origin for a packaged (non-proxied) build. `VITE_TELEGRAM_BOT_ID`
|
gateway origin for a packaged (non-proxied) build. `VITE_TELEGRAM_BOT_ID`
|
||||||
enables the "Link Telegram" web sign-in (the Login Widget) — inert until the site
|
enables the "Link Telegram" web sign-in (the Login Widget) — inert until the site
|
||||||
domain is registered with BotFather (`/setdomain`); `VITE_TELEGRAM_LINK` is the
|
domain is registered with BotFather (`/setdomain`); `VITE_TELEGRAM_LINK_EN` /
|
||||||
share-to-Telegram deep-link base. `VITE_TELEGRAM_GAME_CHANNEL_NAME_EN` / `VITE_TELEGRAM_GAME_CHANNEL_NAME_RU`
|
`VITE_TELEGRAM_LINK_RU` are the per-bot friend-invite Mini App links (full URL
|
||||||
|
`https://t.me/<bot>/<app>`; the share picks the one matching the bot the player
|
||||||
|
signed in through, falling back to `VITE_TELEGRAM_LINK`).
|
||||||
|
`VITE_TELEGRAM_GAME_CHANNEL_NAME_EN` / `VITE_TELEGRAM_GAME_CHANNEL_NAME_RU`
|
||||||
are the per-language "Play in Telegram" links shown on the landing page.
|
are the per-language "Play in Telegram" links shown on the landing page.
|
||||||
|
|
||||||
The build has **two entries**: the game SPA (`index.html`, served at `/app/` and
|
The build has **two entries**: the game SPA (`index.html`, served at `/app/` and
|
||||||
|
|||||||
@@ -58,8 +58,15 @@ supportedLanguagesLength():number {
|
|||||||
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
|
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serviceLanguage():string|null
|
||||||
|
serviceLanguage(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||||
|
serviceLanguage(optionalEncoding?:any):string|Uint8Array|null {
|
||||||
|
const offset = this.bb!.__offset(this.bb_pos, 14);
|
||||||
|
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||||
|
}
|
||||||
|
|
||||||
static startSession(builder:flatbuffers.Builder) {
|
static startSession(builder:flatbuffers.Builder) {
|
||||||
builder.startObject(5);
|
builder.startObject(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addToken(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset) {
|
static addToken(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset) {
|
||||||
@@ -94,18 +101,23 @@ static startSupportedLanguagesVector(builder:flatbuffers.Builder, numElems:numbe
|
|||||||
builder.startVector(4, numElems, 4);
|
builder.startVector(4, numElems, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static addServiceLanguage(builder:flatbuffers.Builder, serviceLanguageOffset:flatbuffers.Offset) {
|
||||||
|
builder.addFieldOffset(5, serviceLanguageOffset, 0);
|
||||||
|
}
|
||||||
|
|
||||||
static endSession(builder:flatbuffers.Builder):flatbuffers.Offset {
|
static endSession(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||||
const offset = builder.endObject();
|
const offset = builder.endObject();
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
static createSession(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset, userIdOffset:flatbuffers.Offset, isGuest:boolean, displayNameOffset:flatbuffers.Offset, supportedLanguagesOffset:flatbuffers.Offset):flatbuffers.Offset {
|
static createSession(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset, userIdOffset:flatbuffers.Offset, isGuest:boolean, displayNameOffset:flatbuffers.Offset, supportedLanguagesOffset:flatbuffers.Offset, serviceLanguageOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||||
Session.startSession(builder);
|
Session.startSession(builder);
|
||||||
Session.addToken(builder, tokenOffset);
|
Session.addToken(builder, tokenOffset);
|
||||||
Session.addUserId(builder, userIdOffset);
|
Session.addUserId(builder, userIdOffset);
|
||||||
Session.addIsGuest(builder, isGuest);
|
Session.addIsGuest(builder, isGuest);
|
||||||
Session.addDisplayName(builder, displayNameOffset);
|
Session.addDisplayName(builder, displayNameOffset);
|
||||||
Session.addSupportedLanguages(builder, supportedLanguagesOffset);
|
Session.addSupportedLanguages(builder, supportedLanguagesOffset);
|
||||||
|
Session.addServiceLanguage(builder, serviceLanguageOffset);
|
||||||
return Session.endSession(builder);
|
return Session.endSession(builder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ describe('codec', () => {
|
|||||||
isGuest: true,
|
isGuest: true,
|
||||||
displayName: 'Me',
|
displayName: 'Me',
|
||||||
supportedLanguages: ['en', 'ru'],
|
supportedLanguages: ['en', 'ru'],
|
||||||
|
serviceLanguage: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,7 +312,7 @@ describe('codec', () => {
|
|||||||
b.finish(fb.LinkResult.endLinkResult(b));
|
b.finish(fb.LinkResult.endLinkResult(b));
|
||||||
const r = decodeLinkResult(b.asUint8Array());
|
const r = decodeLinkResult(b.asUint8Array());
|
||||||
expect(r.status).toBe('merged');
|
expect(r.status).toBe('merged');
|
||||||
expect(r.session).toEqual({ token: 'tok-9', userId: 'a-1', isGuest: false, displayName: 'Kaya', supportedLanguages: [] });
|
expect(r.session).toEqual({ token: 'tok-9', userId: 'a-1', isGuest: false, displayName: 'Kaya', supportedLanguages: [], serviceLanguage: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('decodes an Invitation with inviter and invitees', () => {
|
it('decodes an Invitation with inviter and invitees', () => {
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ function sessionFromTable(t: fb.Session): Session {
|
|||||||
isGuest: t.isGuest(),
|
isGuest: t.isGuest(),
|
||||||
displayName: s(t.displayName()),
|
displayName: s(t.displayName()),
|
||||||
supportedLanguages,
|
supportedLanguages,
|
||||||
|
serviceLanguage: s(t.serviceLanguage()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,4 +35,17 @@ describe('shareLink', () => {
|
|||||||
vi.stubEnv('VITE_TELEGRAM_LINK', 'https://t.me/bot/app');
|
vi.stubEnv('VITE_TELEGRAM_LINK', 'https://t.me/bot/app');
|
||||||
expect(shareLink('f123456')).toBe('https://t.me/bot/app?startapp=f123456');
|
expect(shareLink('f123456')).toBe('https://t.me/bot/app?startapp=f123456');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('picks the per-bot base by language', () => {
|
||||||
|
vi.stubEnv('VITE_TELEGRAM_LINK_EN', 'https://t.me/enbot/app');
|
||||||
|
vi.stubEnv('VITE_TELEGRAM_LINK_RU', 'https://t.me/rubot/app');
|
||||||
|
expect(shareLink('f1', 'en')).toBe('https://t.me/enbot/app?startapp=f1');
|
||||||
|
expect(shareLink('f1', 'ru')).toBe('https://t.me/rubot/app?startapp=f1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the language-agnostic base when the per-bot one is unset', () => {
|
||||||
|
vi.stubEnv('VITE_TELEGRAM_LINK', 'https://t.me/fallback/app');
|
||||||
|
vi.stubEnv('VITE_TELEGRAM_LINK_RU', '');
|
||||||
|
expect(shareLink('f1', 'ru')).toBe('https://t.me/fallback/app?startapp=f1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+21
-5
@@ -36,13 +36,29 @@ export const invitationParam = (id: string): string => 'i' + id;
|
|||||||
/** friendCodeParam builds the start parameter that redeems a friend code. */
|
/** friendCodeParam builds the start parameter that redeems a friend code. */
|
||||||
export const friendCodeParam = (code: string): string => 'f' + code;
|
export const friendCodeParam = (code: string): string => 'f' + code;
|
||||||
|
|
||||||
|
function envVar(name: string): string | undefined {
|
||||||
|
return (import.meta.env as Record<string, string | undefined>)[name];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* shareLink wraps a deep-link start parameter in a t.me Mini App link, using the
|
* telegramBase returns the Mini App link base (e.g. https://t.me/<bot>/<app>) for a
|
||||||
* VITE_TELEGRAM_LINK base (e.g. https://t.me/<bot>/<app>). It returns null when the
|
* bot language: VITE_TELEGRAM_LINK_EN / _RU when lang is en/ru, else the
|
||||||
* base is not configured, so callers can hide the share affordance.
|
* language-agnostic VITE_TELEGRAM_LINK. Returns null when none is configured, so a
|
||||||
|
* shared link points at the same bot the player signed in through.
|
||||||
*/
|
*/
|
||||||
export function shareLink(param: string): string | null {
|
function telegramBase(lang: string): string | null {
|
||||||
const base = import.meta.env.VITE_TELEGRAM_LINK as string | undefined;
|
const byLang =
|
||||||
|
lang === 'ru' ? envVar('VITE_TELEGRAM_LINK_RU') : lang === 'en' ? envVar('VITE_TELEGRAM_LINK_EN') : undefined;
|
||||||
|
return byLang || envVar('VITE_TELEGRAM_LINK') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shareLink wraps a deep-link start parameter in a t.me Mini App link for the given
|
||||||
|
* bot language (the session's service language). Returns null when no base is
|
||||||
|
* configured, so callers can hide the share affordance.
|
||||||
|
*/
|
||||||
|
export function shareLink(param: string, lang = ''): string | null {
|
||||||
|
const base = telegramBase(lang);
|
||||||
if (!base) return null;
|
if (!base) return null;
|
||||||
const sep = base.includes('?') ? '&' : '?';
|
const sep = base.includes('?') ? '&' : '?';
|
||||||
return `${base}${sep}startapp=${encodeURIComponent(param)}`;
|
return `${base}${sep}startapp=${encodeURIComponent(param)}`;
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ export const en = {
|
|||||||
'friends.copy': 'Copy',
|
'friends.copy': 'Copy',
|
||||||
'friends.codeCopied': 'Code copied.',
|
'friends.codeCopied': 'Code copied.',
|
||||||
'friends.shareTelegram': 'Share via Telegram',
|
'friends.shareTelegram': 'Share via Telegram',
|
||||||
|
'friends.inviteText': "Let's be friends in Scrabble!",
|
||||||
|
'friends.linkCopied': 'Link copied.',
|
||||||
'friends.added': 'Added {name}.',
|
'friends.added': 'Added {name}.',
|
||||||
'friends.blockedList': 'Blocked players',
|
'friends.blockedList': 'Blocked players',
|
||||||
'friends.unblock': 'Unblock',
|
'friends.unblock': 'Unblock',
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'friends.copy': 'Копировать',
|
'friends.copy': 'Копировать',
|
||||||
'friends.codeCopied': 'Код скопирован.',
|
'friends.codeCopied': 'Код скопирован.',
|
||||||
'friends.shareTelegram': 'Поделиться через Telegram',
|
'friends.shareTelegram': 'Поделиться через Telegram',
|
||||||
|
'friends.inviteText': 'Давай дружить в Скрэббл!',
|
||||||
|
'friends.linkCopied': 'Ссылка скопирована.',
|
||||||
'friends.added': 'Добавлен(а) {name}.',
|
'friends.added': 'Добавлен(а) {name}.',
|
||||||
'friends.blockedList': 'Заблокированные',
|
'friends.blockedList': 'Заблокированные',
|
||||||
'friends.unblock': 'Разблокировать',
|
'friends.unblock': 'Разблокировать',
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const SESSION: Session = {
|
|||||||
displayName: 'You',
|
displayName: 'You',
|
||||||
// Both languages by default, so the mock-driven UI offers every variant.
|
// Both languages by default, so the mock-driven UI offers every variant.
|
||||||
supportedLanguages: ['en', 'ru'],
|
supportedLanguages: ['en', 'ru'],
|
||||||
|
serviceLanguage: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PROFILE: Profile = {
|
export const PROFILE: Profile = {
|
||||||
|
|||||||
@@ -234,6 +234,9 @@ export interface Session {
|
|||||||
// variants these languages support (en -> English; ru -> Russian + Эрудит). Empty
|
// variants these languages support (en -> English; ru -> Russian + Эрудит). Empty
|
||||||
// means ungated (all variants).
|
// means ungated (all variants).
|
||||||
supportedLanguages: string[];
|
supportedLanguages: string[];
|
||||||
|
// serviceLanguage is the en/ru tag of the Telegram bot this session was minted
|
||||||
|
// through (empty for a non-Telegram login); used to share a link to the same bot.
|
||||||
|
serviceLanguage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LinkResult is the outcome of an account link/merge step. status is
|
// LinkResult is the outcome of an account link/merge step. status is
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface TelegramWebApp {
|
|||||||
contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number };
|
contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number };
|
||||||
ready?: () => void;
|
ready?: () => void;
|
||||||
expand?: () => void;
|
expand?: () => void;
|
||||||
|
openTelegramLink?: (url: string) => void;
|
||||||
onEvent?: (event: string, handler: () => void) => void;
|
onEvent?: (event: string, handler: () => void) => void;
|
||||||
setHeaderColor?: (color: string) => void;
|
setHeaderColor?: (color: string) => void;
|
||||||
setBackgroundColor?: (color: string) => void;
|
setBackgroundColor?: (color: string) => void;
|
||||||
@@ -49,6 +50,19 @@ export function insideTelegram(): boolean {
|
|||||||
return !!w && typeof w.initData === 'string' && w.initData.length > 0;
|
return !!w && typeof w.initData === 'string' && w.initData.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shareTelegramLink opens Telegram's native "share to a chat" picker for url with a
|
||||||
|
* caption, through the Mini App SDK (https://t.me/share/url). Returns false outside
|
||||||
|
* Telegram or when the SDK lacks the method, so the caller can fall back to the Web
|
||||||
|
* Share API. Works consistently on iOS and Android within Telegram.
|
||||||
|
*/
|
||||||
|
export function shareTelegramLink(url: string, text: string): boolean {
|
||||||
|
const w = webApp();
|
||||||
|
if (!w?.openTelegramLink) return false;
|
||||||
|
w.openTelegramLink(`https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** TelegramLaunch is the data a Mini App launch carries. */
|
/** TelegramLaunch is the data a Mini App launch carries. */
|
||||||
export interface TelegramLaunch {
|
export interface TelegramLaunch {
|
||||||
initData: string;
|
initData: string;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { gateway } from '../lib/gateway';
|
import { gateway } from '../lib/gateway';
|
||||||
import { t } from '../lib/i18n/index.svelte';
|
import { t } from '../lib/i18n/index.svelte';
|
||||||
import { friendCodeParam, shareLink } from '../lib/deeplink';
|
import { friendCodeParam, shareLink } from '../lib/deeplink';
|
||||||
|
import { shareTelegramLink } from '../lib/telegram';
|
||||||
import type { AccountRef, FriendCode } from '../lib/model';
|
import type { AccountRef, FriendCode } from '../lib/model';
|
||||||
|
|
||||||
let friends = $state<AccountRef[]>([]);
|
let friends = $state<AccountRef[]>([]);
|
||||||
@@ -78,6 +79,28 @@
|
|||||||
// Clipboard may be unavailable (insecure context); leave the code on screen.
|
// Clipboard may be unavailable (insecure context); leave the code on screen.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shareInvite shares the friend-code deep link: inside Telegram via the native
|
||||||
|
// "share to chat" picker; on the web via the system share sheet; failing both, it
|
||||||
|
// copies the link to the clipboard.
|
||||||
|
async function shareInvite(url: string) {
|
||||||
|
const text = t('friends.inviteText');
|
||||||
|
if (shareTelegramLink(url, text)) return;
|
||||||
|
if (typeof navigator !== 'undefined' && navigator.share) {
|
||||||
|
try {
|
||||||
|
await navigator.share({ text, url });
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
return; // the user dismissed the share sheet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
showToast(t('friends.linkCopied'));
|
||||||
|
} catch {
|
||||||
|
// Clipboard unavailable (insecure context); nothing more to do.
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
@@ -97,7 +120,7 @@
|
|||||||
<button class="btn" onclick={redeem} disabled={!connection.online}>{t('friends.redeem')}</button>
|
<button class="btn" onclick={redeem} disabled={!connection.online}>{t('friends.redeem')}</button>
|
||||||
</div>
|
</div>
|
||||||
{#if code}
|
{#if code}
|
||||||
{@const tg = shareLink(friendCodeParam(code.code))}
|
{@const tg = shareLink(friendCodeParam(code.code), app.session?.serviceLanguage || app.locale)}
|
||||||
<div class="code" data-testid="friend-code">
|
<div class="code" data-testid="friend-code">
|
||||||
<div class="coderow">
|
<div class="coderow">
|
||||||
<button class="codeval" onclick={copyCode}>{code.code}</button>
|
<button class="codeval" onclick={copyCode}>{code.code}</button>
|
||||||
@@ -107,7 +130,7 @@
|
|||||||
{t('friends.codeHint')} · {t('friends.codeExpires', { time: codeTime(code.expiresAtUnix) })}
|
{t('friends.codeHint')} · {t('friends.codeExpires', { time: codeTime(code.expiresAtUnix) })}
|
||||||
</span>
|
</span>
|
||||||
{#if tg}
|
{#if tg}
|
||||||
<a class="link tgshare" href={tg} target="_blank" rel="noopener">{t('friends.shareTelegram')}</a>
|
<button type="button" class="link tgshare" onclick={() => shareInvite(tg)}>{t('friends.shareTelegram')}</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
Reference in New Issue
Block a user