Merge pull request 'release: VK Wallet public-offer link escapes the Mini App WebView (#281)' (#282) from development into master
This commit was merged in pull request #282.
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { openExternalUrl } from './links';
|
import { onInAppPageLinkClick, openExternalUrl } from './links';
|
||||||
|
|
||||||
|
// clickInAppPage fires onInAppPageLinkClick on a fake anchor whose (already absolute) href is the
|
||||||
|
// same-origin /offer/ page, and reports whether the click's default navigation was suppressed.
|
||||||
|
function clickInAppPage(href = 'https://app.example/offer/') {
|
||||||
|
const preventDefault = vi.fn();
|
||||||
|
const a = { href, target: '_blank' };
|
||||||
|
onInAppPageLinkClick({ target: { closest: () => a }, preventDefault } as unknown as MouseEvent);
|
||||||
|
return preventDefault;
|
||||||
|
}
|
||||||
|
|
||||||
describe('openExternalUrl', () => {
|
describe('openExternalUrl', () => {
|
||||||
afterEach(() => vi.unstubAllGlobals());
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
@@ -27,3 +36,41 @@ describe('openExternalUrl', () => {
|
|||||||
expect(open).toHaveBeenCalledWith('https://telegram.me/some_bot', '_blank');
|
expect(open).toHaveBeenCalledWith('https://telegram.me/some_bot', '_blank');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onInAppPageLinkClick', () => {
|
||||||
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
|
|
||||||
|
it('routes the same-origin offer page out through the VK away redirect', () => {
|
||||||
|
const open = vi.fn();
|
||||||
|
vi.stubGlobal('location', {
|
||||||
|
pathname: '/vk/',
|
||||||
|
search: '?vk_user_id=1&vk_platform=mobile_android&sign=x',
|
||||||
|
href: 'https://app.example/vk/',
|
||||||
|
origin: 'https://app.example',
|
||||||
|
});
|
||||||
|
vi.stubGlobal('window', { open });
|
||||||
|
const preventDefault = clickInAppPage();
|
||||||
|
// Same-origin, yet still escaped: the cross-origin gate is bypassed for a non-SPA page.
|
||||||
|
expect(open).toHaveBeenCalledWith(
|
||||||
|
`https://vk.com/away.php?to=${encodeURIComponent('https://app.example/offer/')}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
expect(preventDefault).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('routes the offer page through the Telegram in-app browser', () => {
|
||||||
|
const openLink = vi.fn();
|
||||||
|
vi.stubGlobal('window', { Telegram: { WebApp: { openLink } } });
|
||||||
|
const preventDefault = clickInAppPage();
|
||||||
|
expect(openLink).toHaveBeenCalledWith('https://app.example/offer/');
|
||||||
|
expect(preventDefault).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the anchor to its own navigation outside a Mini App host', () => {
|
||||||
|
const open = vi.fn();
|
||||||
|
vi.stubGlobal('window', { open });
|
||||||
|
const preventDefault = clickInAppPage();
|
||||||
|
expect(open).not.toHaveBeenCalled();
|
||||||
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+16
-1
@@ -4,7 +4,7 @@
|
|||||||
// below route an external URL through the platform's escape hatch (Telegram's openLink, VK's
|
// below route an external URL through the platform's escape hatch (Telegram's openLink, VK's
|
||||||
// away.php redirect) and leave the anchor's own navigation / window.open in effect elsewhere.
|
// away.php redirect) and leave the anchor's own navigation / window.open in effect elsewhere.
|
||||||
|
|
||||||
import { routeExternalLinkInTelegram } from './telegram';
|
import { routeExternalLinkInTelegram, telegramOpenExternalLink } from './telegram';
|
||||||
import { routeExternalLinkInVK, vkOpenExternalUrl } from './vk';
|
import { routeExternalLinkInVK, vkOpenExternalUrl } from './vk';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,6 +20,21 @@ export function onExternalLinkClick(e: MouseEvent): void {
|
|||||||
if (routeExternalLinkInTelegram(a) || routeExternalLinkInVK(a)) e.preventDefault();
|
if (routeExternalLinkInTelegram(a) || routeExternalLinkInVK(a)) e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onInAppPageLinkClick is the anchor click handler for a link to a standalone, same-origin page
|
||||||
|
* that is not part of the SPA — the static legal /offer/ page. Such a page shares the app's origin
|
||||||
|
* yet must still leave the Mini App WebView: opened in place it replaces the game, and the Android
|
||||||
|
* VK client then strands the user there with no way back. Unlike onExternalLinkClick it therefore
|
||||||
|
* skips the cross-origin test (the page is same-origin by design) and routes the click straight
|
||||||
|
* through the Telegram in-app browser (openLink) or VK's away.php redirect. Outside those hosts it
|
||||||
|
* is a no-op and the anchor's own target=_blank opens the page in a new tab / child browser.
|
||||||
|
*/
|
||||||
|
export function onInAppPageLinkClick(e: MouseEvent): void {
|
||||||
|
const a = (e.target as HTMLElement | null)?.closest('a');
|
||||||
|
if (!a) return;
|
||||||
|
if (telegramOpenExternalLink(a.href) || vkOpenExternalUrl(a.href)) e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* openExternalUrl opens an external URL from code (no anchor to click): through VK's external
|
* openExternalUrl opens an external URL from code (no anchor to click): through VK's external
|
||||||
* redirect inside the Android VK client, else a plain new tab. Telegram callers try their native
|
* redirect inside the Android VK client, else a plain new tab. Telegram callers try their native
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { t, i18n, type MessageKey } from '../lib/i18n/index.svelte';
|
import { t, i18n, type MessageKey } from '../lib/i18n/index.svelte';
|
||||||
import { executionContext, formatAmount, needsWebSpendWarning, spendableChips, type SpendContext } from '../lib/wallet';
|
import { executionContext, formatAmount, needsWebSpendWarning, spendableChips, type SpendContext } from '../lib/wallet';
|
||||||
import { isGooglePlayBuild, purchasesHidden } from '../lib/distribution';
|
import { isGooglePlayBuild, purchasesHidden } from '../lib/distribution';
|
||||||
import { onExternalLinkClick, openExternalUrl } from '../lib/links';
|
import { onExternalLinkClick, onInAppPageLinkClick, openExternalUrl } from '../lib/links';
|
||||||
import { gatewayOrigin } from '../lib/origin';
|
import { gatewayOrigin } from '../lib/origin';
|
||||||
import { vkPlatform, vkShowOrderBox } from '../lib/vk';
|
import { vkPlatform, vkShowOrderBox } from '../lib/vk';
|
||||||
import { telegramOpenInvoice } from '../lib/telegram';
|
import { telegramOpenInvoice } from '../lib/telegram';
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{#if packs.length > 0}
|
{#if packs.length > 0}
|
||||||
<p class="offer" data-testid="offer">
|
<p class="offer" data-testid="offer">
|
||||||
<a href="/offer/" target="_blank" rel="noopener noreferrer">{t('wallet.offer')}</a>
|
<a href="/offer/" target="_blank" rel="noopener noreferrer" onclick={onInAppPageLinkClick}>{t('wallet.offer')}</a>
|
||||||
</p>
|
</p>
|
||||||
{:else if !loading}
|
{:else if !loading}
|
||||||
<p class="empty">{t('wallet.empty')}</p>
|
<p class="empty">{t('wallet.empty')}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user