feat(vk): native share/copy, auto theme, friend-code deep link, home-bar safe area
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m25s

Group B of the VK integration — the contour-verified follow-up to the launch+auth MVP:

- Share/copy inside the VK iframe go through VK Bridge: the friend-code invite shares via
  VKWebAppShare and copies via VKWebAppCopyText, since navigator.share is absent in the desktop
  iframe and navigator.clipboard is blocked there.
- The invite link is a VK Mini App direct link (vk.com/app<id>#f<code>) on VK instead of the
  Telegram link; the app id comes from vk_app_id in the launch params (no build arg needed). The
  recipient's launch routes the deep link from VK's `hash` launch query parameter.
- The app's "auto" theme follows the VK client's light/dark appearance (VKWebAppUpdateConfig),
  which the VK mobile webview's prefers-color-scheme does not track.
- The safe-area CSS vars default to env(safe-area-inset-*), so the VK mobile layout clears the
  home bar (and Capacitor/PWA too); Telegram still overrides them from its SDK.

vk.ts adds vkAppId/vkStartParam/vkShare/vkCopyText/vkOnScheme. Verified: svelte-check, 347 unit
(+ vkAppId/vkStartParam/vkShareLink), build, bundle-gate. The VK-Bridge behaviours need the live
contour (not reproducible headless).
This commit is contained in:
Ilia Denisov
2026-06-29 21:27:36 +02:00
parent 303348ed39
commit da17c18895
10 changed files with 178 additions and 26 deletions
+67
View File
@@ -64,3 +64,70 @@ export async function vkUserName(): Promise<string> {
return '';
}
}
/**
* vkAppId returns the launching VK app id (vk_app_id) from the signed launch parameters in the URL —
* so the app can build its own vk.com/app<id> links without a VK API call — or '' outside a VK launch.
*/
export function vkAppId(): string {
if (typeof location === 'undefined') return '';
return new URLSearchParams(location.search.replace(/^\?/, '')).get('vk_app_id') ?? '';
}
/**
* vkStartParam returns the VK direct-link deep-link payload. VK passes everything after the '#' in a
* vk.com/app<id>#<payload> link to the app as the `hash` query parameter (it is also in location.hash,
* but that collides with the app's hash router, so the query parameter is the safe source). Empty when
* the launch carried no deep link.
*/
export function vkStartParam(): string {
if (typeof location === 'undefined') return '';
return new URLSearchParams(location.search.replace(/^\?/, '')).get('hash') ?? '';
}
/**
* vkShare opens VK's native share dialog for link (VKWebAppShare) — the in-iframe replacement for
* navigator.share, which is unavailable in the desktop VK iframe. Resolves true when the share was
* handled, false on any failure or outside VK.
*/
export async function vkShare(link: string): Promise<boolean> {
try {
await (await bridge()).send('VKWebAppShare', { link });
return true;
} catch {
return false;
}
}
/**
* vkCopyText copies text to the clipboard via VKWebAppCopyText — which works inside the VK iframe,
* where navigator.clipboard is blocked. Resolves true on success, false on any failure or outside VK.
*/
export async function vkCopyText(text: string): Promise<boolean> {
try {
await (await bridge()).send('VKWebAppCopyText', { text });
return true;
} catch {
return false;
}
}
/**
* vkOnScheme subscribes to VK's appearance (VKWebAppUpdateConfig) and calls handler with the mapped
* 'light' | 'dark' scheme on launch and whenever the user switches the VK client theme — so the app's
* "auto" theme can follow the VK client instead of the (often wrong) webview prefers-color-scheme.
* A no-op outside VK.
*/
export async function vkOnScheme(handler: (scheme: 'light' | 'dark') => void): Promise<void> {
try {
const b = await bridge();
b.subscribe((e) => {
const detail = (e as { detail?: { type?: string; data?: { scheme?: string; appearance?: string } } }).detail;
if (detail?.type !== 'VKWebAppUpdateConfig') return;
const scheme = detail.data?.scheme ?? detail.data?.appearance ?? '';
handler(/dark|space_gray/i.test(scheme) ? 'dark' : 'light');
});
} catch {
// Outside VK / bridge unavailable: leave the app on its own theme.
}
}