fix(export): VK Android viewer+clipboard; no share title on iOS
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m56s

Round-5 on-device findings:

- VK Android's DownloadFile hangs regardless of Content-Length and
  Range/206 (PNG stalls ~80%, GCG at 0%) — the download route is
  abandoned there: the PNG opens in VK's native image viewer
  (VKWebAppShowImages — its save works on-device) and the GCG returns
  to the clipboard copy (the pre-URL route, always solid). VK iOS keeps
  VKWebAppDownloadFile (verified perfect); the desktop iframe keeps
  plain anchor downloads.
- The iOS share sheet no longer carries a title: iOS pasted the
  'game-<hash>.png' filename as accompanying text next to the image.
This commit is contained in:
Ilia Denisov
2026-07-02 23:32:52 +02:00
parent 8db18f2a2a
commit 77f222c209
7 changed files with 59 additions and 21 deletions
+19 -6
View File
@@ -23,7 +23,7 @@
import { alphabetLetters, hasAlphabet } from '../lib/alphabet';
import { hintsLeft } from '../lib/hints';
import { downloadUrl, shareOrDownloadGcg, shareUrlAsFile } from '../lib/share';
import { insideVK, vkCopyText, vkDownloadFile, vkPlatform } from '../lib/vk';
import { insideVK, vkAndroidWebView, vkCopyText, vkDownloadFile, vkPlatform, vkShowImages } from '../lib/vk';
import { getCachedGame, setCachedGame, setCachedDraft, type CachedGame } from '../lib/gamecache';
import { patchLobbyGame } from '../lib/lobbycache';
import { applyGameOver, applyMoveDelta, applyOpponentJoined, type DeltaResult } from '../lib/gamedelta';
@@ -939,8 +939,11 @@
// TG iOS our modal chooser → the OS share sheet (WKWebView has
// navigator.share; a native-popup callback could not open it — no
// user activation — so the chooser stays the app modal here).
// VK mobile our modal (VK has no native chooser) → VKWebAppDownloadFile for
// VK iOS our modal (VK has no native chooser) → VKWebAppDownloadFile for
// both formats — the client lands in its native share/preview flow.
// VK Android our modal → the PNG opens in VK's native image viewer (its save
// works; the Android DownloadFile hangs), the GCG copies to the
// clipboard (the pre-URL route, always solid there).
// VK desktop iframe our modal → plain anchor downloads (an ordinary browser).
// mobile browsers our modal → the OS share sheet with the fetched file.
// desktop browsers our modal → plain anchor download.
@@ -998,10 +1001,20 @@
const mime = kind === 'png' ? 'image/png' : 'text/plain';
if (insideTelegram() && !tgShareSheet && telegramDownloadFile(url, filename)) return;
if (insideVK()) {
// Desktop iframe: an ordinary browser, both formats download by anchor. Mobile
// clients take VKWebAppDownloadFile into VK's native share/preview flow; a failed
// bridge call falls back to the anchor.
if (!vkPlatform().startsWith('desktop') && (await vkDownloadFile(url, filename))) return;
// Per-VK-client delivery (each branch owner-verified): the iOS client's
// VKWebAppDownloadFile lands in a native share flow and is perfect; the ANDROID
// client's download hangs indefinitely, so the PNG opens in VK's native image
// viewer (its "save" works) and the GCG goes to the clipboard; the desktop
// iframe is an ordinary browser — plain anchor downloads.
if (vkAndroidWebView()) {
if (kind === 'png' && (await vkShowImages(url))) return;
if (kind === 'gcg') {
void exportGcgLegacy();
return;
}
} else if (!vkPlatform().startsWith('desktop') && (await vkDownloadFile(url, filename))) {
return;
}
downloadUrl(url, filename);
return;
}
+3 -1
View File
@@ -114,7 +114,9 @@ export async function shareUrlAsFile(url: string, filename: string, mime: string
const file = new File([await resp.blob()], filename, { type: mime });
if (nav.canShare({ files: [file] })) {
try {
await nav.share({ files: [file], title: filename });
// No title: iOS pastes it as accompanying text next to the image; the named
// file speaks for itself.
await nav.share({ files: [file] });
} catch {
/* cancelled — intentionally a no-op */
}
+15
View File
@@ -155,6 +155,21 @@ export async function vkShare(link: string): Promise<boolean> {
}
}
/**
* vkShowImages opens VK's native image viewer on url — on the Android client this is the
* PNG delivery: the viewer's own "save" works there, while VKWebAppDownloadFile hangs
* indefinitely (on-device finding). Resolves false on any failure, so the caller can
* fall back.
*/
export async function vkShowImages(url: string): Promise<boolean> {
try {
await (await bridge()).send('VKWebAppShowImages', { images: [url] });
return true;
} catch {
return false;
}
}
/**
* vkDownloadFile downloads url as filename through the VK client (VKWebAppDownloadFile) —
* the mobile in-app file delivery, where the webview ignores <a download>. Resolves false