From d76f1f40267ddc5ac0b23635d0f29a0a10870af0 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Mon, 29 Jun 2026 22:58:43 +0200 Subject: [PATCH] fix(vk): friend-code link is the plain vk.com/app link (VK strips iframe query payload) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contour diagnostic confirmed VK forwards only the signed vk_* params to the iframe — a custom payload on the app link is dropped (the '#' eaten by the vk.com SPA, a '?' query stripped), so the friend-code cannot ride the link. The VK share link is now just vk.com/app; the recipient enters the copied code by hand (VKWebAppCopyText works). The vkStartParam reader + bootVK routing stay as a no-op, ready for a post-moderation channel. Removes the temporary deep-link diagnostic. --- .claude/vk-games-integration.md | 13 ++++++++----- ui/src/lib/app.svelte.ts | 29 +++++++++-------------------- ui/src/lib/deeplink.test.ts | 6 +++--- ui/src/lib/deeplink.ts | 14 +++++++------- ui/src/screens/Friends.svelte | 3 +-- 5 files changed, 28 insertions(+), 37 deletions(-) diff --git a/.claude/vk-games-integration.md b/.claude/vk-games-integration.md index a399447..4859e87 100644 --- a/.claude/vk-games-integration.md +++ b/.claude/vk-games-integration.md @@ -90,11 +90,14 @@ The bridge talks to the embedding VK client over postMessage; it is NOT an exter it has no telegram.org-style load-hang risk. The SDK reads browser globals at import — we import it **lazily** so the pure URL helpers stay node-test-importable. -**Deep links** (direct-link launch): `https://vk.com/app#` forwards everything after the -`#` to the app as the **`hash` launch query parameter** (it is also in `location.hash`, but that -collides with our hash router, so read the `hash` query param). `?` query strings are NOT supported -for VK direct links. The friend-code invite is built as `vk.com/app#f` — the app id comes -from `vk_app_id` in the launch params (no `VITE_VK_APP_ID` needed) — and read back via `vkStartParam`. +**Deep links — NOT possible on VK (confirmed on the contour).** The VK iframe receives ONLY the signed +`vk_*` launch params (+ `sign`); VK strips any custom data from the app link. The documented +`vk.com/app#` form is eaten by the vk.com SPA (which owns the URL hash), and a +`vk.com/app?hash=` query is dropped (the diagnostic showed `rawSearch` with only `vk_*` +and an empty `hash`). So the friend-code invite link is just `vk.com/app` (`vkShareLink`, app id +from `vk_app_id`); the recipient enters the **copied code by hand** (`VKWebAppCopyText` works). The +`vkStartParam` reader + the `bootVK` routing stay as a no-op today, ready if a post-moderation VK +channel (e.g. an invite API) ever delivers a payload. ## 5. Test mode (to verify before moderation) diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index 58822d8..2a9de50 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -751,19 +751,12 @@ async function bootVK(): Promise { for (let attempt = 0; ; attempt++) { try { await adoptSession(await gateway.authVK(params, displayName)); - // A VK direct-link deep link (vk.com/app?hash=) rides in as the `hash` launch param. - const sp = vkStartParam(); - const diag = app.blocked ? 'blocked' : await routeStartParam(sp); - // TEMP VK deep-link diagnostic (remove after the contour catch): VK launches are not - // reproducible locally and the cold-boot path lands unexpectedly on the lobby, so surface - // the raw query, the parsed payload, the redeem outcome and the final route. - console.log('[VK deeplink]', { - rawSearch: typeof location !== 'undefined' ? location.search : '', - hash: sp, - outcome: diag, - route: router.route.name, - }); - showToast(`VK dl: "${sp || '∅'}" -> ${diag}`); + // VK forwards only the signed vk_* params to the iframe — a custom payload on the app link + // (whether after '#', eaten by the vk.com SPA, or as a '?' query, dropped) never reaches it, + // confirmed on the contour. So there is no friend-code auto-redeem on VK in test mode: the + // launch lands on the lobby. vkStartParam stays as the reader for a future (post-moderation) + // deep-link channel, and routes the lobby for an empty payload today. + if (!app.blocked) await routeStartParam(vkStartParam()); app.bootError = false; return; } catch (err) { @@ -825,12 +818,12 @@ export async function retryTelegramLaunch(): Promise { * specific game, the friends screen with a friend-code redemption, or the lobby * (where invitations surface as a badge). */ -async function routeStartParam(param: string): Promise { +async function routeStartParam(param: string): Promise { const link = parseStartParam(param); switch (link.kind) { case 'game': navigate(`/game/${link.id}`); - return 'game:' + link.id; + return; case 'friendCode': try { const friend = await gateway.friendCodeRedeem(link.code); @@ -843,7 +836,6 @@ async function routeStartParam(param: string): Promise { showToast(t('friends.added', { name: friend.displayName })); } void refreshNotifications(); - return 'friendCode ok: ' + friend.displayName; } catch (err) { const code = err instanceof GatewayError ? err.code : ''; if (code === 'self_relation') { @@ -851,22 +843,19 @@ async function routeStartParam(param: string): Promise { // scary "can't do that to yourself" error. navigate('/friends'); showToast(t('friends.selfInvite')); - return 'friendCode self_relation'; } 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; - return 'friendCode invalid/stale -> lobby'; } else { navigate('/friends'); handleError(err); - return 'friendCode err: ' + (code || 'unknown'); } } + return; default: navigate('/'); - return 'lobby (empty/unknown param)'; } } diff --git a/ui/src/lib/deeplink.test.ts b/ui/src/lib/deeplink.test.ts index f568f96..becb5f1 100644 --- a/ui/src/lib/deeplink.test.ts +++ b/ui/src/lib/deeplink.test.ts @@ -42,12 +42,12 @@ describe('vkShareLink', () => { it('returns null outside a VK launch (no vk_app_id)', () => { vi.stubGlobal('location', { search: '' }); - expect(vkShareLink('f123456')).toBeNull(); + expect(vkShareLink()).toBeNull(); }); - it('wraps the payload in a vk.com/app link as the hash query param', () => { + it('returns the plain vk.com/app link (VK drops a custom query payload, so the code is not carried)', () => { vi.stubGlobal('location', { search: '?vk_app_id=6736218&vk_user_id=1&sign=x' }); - expect(vkShareLink('f123456')).toBe('https://vk.com/app6736218?hash=f123456'); + expect(vkShareLink()).toBe('https://vk.com/app6736218'); }); }); diff --git a/ui/src/lib/deeplink.ts b/ui/src/lib/deeplink.ts index 9c8d9f2..19a2e5b 100644 --- a/ui/src/lib/deeplink.ts +++ b/ui/src/lib/deeplink.ts @@ -78,14 +78,14 @@ export function shareLink(param: string): string | null { } /** - * vkShareLink wraps a deep-link start parameter in a VK Mini App link - * (https://vk.com/app?hash=), read back by vk.ts vkStartParam (the `hash` query param). - * VK's documented '#' direct-link form is consumed by the vk.com SPA before it reaches the app, so - * we pass the payload as a query parameter instead. Returns null when the VK app id is unknown - * (outside a VK launch), so the caller falls back to the Telegram/web link. + * vkShareLink returns the VK Mini App link (https://vk.com/app) to share on VK, or null when the + * VK app id is unknown (outside a VK launch). VK forwards no custom payload through the app link to + * the iframe — the '#' form is eaten by the vk.com SPA and a '?' query is dropped (confirmed on the + * contour: only the signed vk_* params arrive) — so the link cannot carry the friend code; the + * recipient enters the copied code by hand. */ -export function vkShareLink(param: string): string | null { +export function vkShareLink(): string | null { const id = vkAppId(); if (!id) return null; - return `https://vk.com/app${id}?hash=${encodeURIComponent(param)}`; + return `https://vk.com/app${id}`; } diff --git a/ui/src/screens/Friends.svelte b/ui/src/screens/Friends.svelte index 9e56b75..141704c 100644 --- a/ui/src/screens/Friends.svelte +++ b/ui/src/screens/Friends.svelte @@ -201,8 +201,7 @@ {#if code} - {@const sharePayload = friendCodeParam(code.code)} - {@const shareUrl = insideVK() ? vkShareLink(sharePayload) : shareLink(sharePayload)} + {@const shareUrl = insideVK() ? vkShareLink() : shareLink(friendCodeParam(code.code))}