Compare commits

..

6 Commits

Author SHA1 Message Date
owner 9291884da2 Merge pull request 'chore(docs): update legal info' (#284) from development into master
Reviewed-on: https://gitea.lan/developer/scrabble-game/pulls/284
2026-07-20 11:10:02 +00:00
owner 9cdd7138e3 Merge pull request 'chore(docs): update legal info' (#283) from chore/legal-info into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m14s
CI / conformance (push) Successful in 11s
CI / gate (push) Successful in 0s
CI / changes (pull_request) Successful in 3s
CI / deploy (push) Successful in 1m45s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 10s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
Reviewed-on: https://gitea.lan/developer/scrabble-game/pulls/283
2026-07-20 11:03:19 +00:00
Ilia Denisov 30a1baca12 chore(docs): update legal info
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m15s
CI / conformance (pull_request) Successful in 13s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m51s
2026-07-20 12:53:23 +02:00
developer bb0330fb9f Merge pull request 'release: VK Wallet public-offer link escapes the Mini App WebView (#281)' (#282) from development into master 2026-07-17 18:34:47 +00:00
developer 9eacb9b115 Merge pull request 'fix(vk): route the Wallet public-offer link out of the Mini App WebView' (#281) from feature/vk-offer-link-escape into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m14s
CI / conformance (push) Successful in 11s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m58s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
2026-07-17 18:27:06 +00:00
Ilia Denisov 584384487c fix(vk): route the Wallet public-offer link out of the Mini App WebView
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m15s
CI / conformance (pull_request) Successful in 12s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m56s
The "Public offer" link in the Wallet pointed at the same-origin static
/offer/ page with a plain target=_blank anchor and no click handler. The
Android VK client ignores target=_blank and navigates its own WebView to
the page, stranding the player with no way back to the game (iOS opens a
child browser, so it was unaffected). The existing onExternalLinkClick
router could not help: it escapes only cross-origin links, and /offer/ is
same-origin (a static legal page, not an SPA route).

Add onInAppPageLinkClick, which routes a same-origin, non-SPA page out
through the host escape hatch (Telegram openLink, VK away.php) while
bypassing the cross-origin gate, and attach it to the offer link. Outside
a Mini App host it is a no-op and the anchor's own target=_blank stands.

The "Rules" (About) and ad-banner links already route correctly
(cross-origin + a handler) and are left unchanged.
2026-07-17 20:20:22 +02:00
9 changed files with 76 additions and 14 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ In this Licence Agreement the following definitions, when capitalised, have the
**"Account"** — the User's personal account in the Game. **"Account"** — the User's personal account in the Game.
**"Company"** — Ilya Arkadyevich Denisov, TIN 290210610742, acting in accordance with the law of the Russian Federation, address: 236020, Kaliningrad, Pribrezhny district, Parkovaya St. 1, recipient — Ilya Arkadyevich Denisov. **"Company"** — Ilya A. Denisov, TIN 290210610742, acting in accordance with the law of the Russian Federation, address: 236020, Kaliningrad, Pribrezhny district, Parkovaya St. 1, recipient — Ilya A. Denisov.
**"Client part of the Game"** — the software required for the User to participate in the Game, which is installed on the User's computer or run on the User's computer in a browser when using the web version of the Game. The Client part of the Game is installed by the User on a personal computer or mobile device independently. The Client part of the Game may be distributed by the Company and/or its authorised persons both via the Internet and on physical media. The Client part of the Game distributed via the Internet is provided to the User free of charge with the right of reproduction, unless otherwise provided by this Licence Agreement. Copies of the Client part of the Game distributed on physical media may be provided to the User for a fee. **"Client part of the Game"** — the software required for the User to participate in the Game, which is installed on the User's computer or run on the User's computer in a browser when using the web version of the Game. The Client part of the Game is installed by the User on a personal computer or mobile device independently. The Client part of the Game may be distributed by the Company and/or its authorised persons both via the Internet and on physical media. The Client part of the Game distributed via the Internet is provided to the User free of charge with the right of reproduction, unless otherwise provided by this Licence Agreement. Copies of the Client part of the Game distributed on physical media may be provided to the User for a fee.
@@ -207,7 +207,7 @@ If the limitation or exclusion of liability is prohibited by applicable law, the
The Company cares about the protection of personal data. Personal data collected by the Company in the context of this document is subject to automated processing in accordance with applicable law. All information collected as part of providing the service is registered by the Company, which is the data controller. This is very important for the functioning of the services offered by the Company. The Company cares about the protection of personal data. Personal data collected by the Company in the context of this document is subject to automated processing in accordance with applicable law. All information collected as part of providing the service is registered by the Company, which is the data controller. This is very important for the functioning of the services offered by the Company.
To exercise one or more of their rights, the User must provide an identity document and contact the person responsible for data protection at the Company (via service support on Telegram: [@Erudit_GameBot](https://telegram.me/Erudit_GameBot), or send us your request in writing to: 236020, Kaliningrad, Pribrezhny district, Parkovaya St. 1, recipient — Ilya Arkadyevich Denisov). To exercise one or more of their rights, the User must provide an identity document and contact the person responsible for data protection at the Company (via service support on Telegram: [@Erudit_GameBot](https://telegram.me/Erudit_GameBot), or send us your request in writing to: 236020, Kaliningrad, Pribrezhny district, Parkovaya St. 1, recipient — Ilya A. Denisov).
In the event of a complaint, the User may contact the personal-data supervisory authority of their country of residence. In the event of a complaint, the User may contact the personal-data supervisory authority of their country of residence.
+1 -1
View File
@@ -20,7 +20,7 @@
**«Учётная запись»** — личный кабинет Пользователя в Игре. **«Учётная запись»** — личный кабинет Пользователя в Игре.
**«Компания»** — Денисов Илья Аркадьевич, ИНН 290210610742, действующий в соответствии с законодательством Российской Федерации, адрес: 236020, г. Калининград, мкр. Прибрежный, ул. Парковая, д. 1, получатель — Денисов Илья Аркадьевич. **«Компания»** — ИП Денисов Илья Аркадьевич, ИНН 290210610742, ОГРНИП 326390000053954, действующий в соответствии с законодательством Российской Федерации, адрес: 236020, г. Калининград, мкр. Прибрежный, ул. Парковая, д. 1, получатель — Денисов Илья Аркадьевич.
**«Клиентская часть Игры»** — программное обеспечение, необходимое Пользователю для участия в Игре, которое устанавливается на компьютер Пользователя либо запускается на компьютере Пользователя в браузере при использовании веб-версии Игры. Клиентская часть Игры устанавливается Пользователем на персональный компьютер или мобильное устройство самостоятельно. Клиентская часть Игры может распространяться Компанией и/или её уполномоченными лицами как через сеть Интернет, так и на материальных носителях. Клиентская часть Игры, распространяемая через Интернет, предоставляется Пользователю бесплатно с правом на воспроизведение, если иное не предусмотрено настоящим Лицензионным соглашением. Копии клиентской части Игры, распространяемые на материальных носителях, могут быть предоставлены Пользователю за плату. **«Клиентская часть Игры»** — программное обеспечение, необходимое Пользователю для участия в Игре, которое устанавливается на компьютер Пользователя либо запускается на компьютере Пользователя в браузере при использовании веб-версии Игры. Клиентская часть Игры устанавливается Пользователем на персональный компьютер или мобильное устройство самостоятельно. Клиентская часть Игры может распространяться Компанией и/или её уполномоченными лицами как через сеть Интернет, так и на материальных носителях. Клиентская часть Игры, распространяемая через Интернет, предоставляется Пользователю бесплатно с правом на воспроизведение, если иное не предусмотрено настоящим Лицензионным соглашением. Копии клиентской части Игры, распространяемые на материальных носителях, могут быть предоставлены Пользователю за плату.
+1 -1
View File
@@ -150,6 +150,6 @@ Disputes or disagreements on which the Parties have not reached agreement are su
## 11. Seller's details ## 11. Seller's details
Ilya Arkadyevich Denisov, TIN 290210610742. Ilya A. Denisov, TIN 290210610742.
Feedback on Telegram: [@Erudit_GameBot](https://telegram.me/Erudit_GameBot). Feedback on Telegram: [@Erudit_GameBot](https://telegram.me/Erudit_GameBot).
+1 -1
View File
@@ -150,6 +150,6 @@
## 11. Реквизиты Продавца ## 11. Реквизиты Продавца
Денисов Илья Аркадьевич, ИНН 290210610742. ИП Денисов Илья Аркадьевич, ИНН 290210610742, ОГРНИП 326390000053954.
Обратная связь в Telegram: [@Erudit_GameBot](https://telegram.me/Erudit_GameBot). Обратная связь в Telegram: [@Erudit_GameBot](https://telegram.me/Erudit_GameBot).
+3 -3
View File
@@ -2,7 +2,7 @@
## 1. About the service ## 1. About the service
**1.1.** The Game and its accompanying services are provided by Ilya Arkadyevich Denisov, TIN 290210610742 (hereinafter "we", "us", "our"). **1.1.** The Game and its accompanying services are provided by Ilya A. Denisov, TIN 290210610742 (hereinafter "we", "us", "our").
**1.2.** This Privacy Policy supplements the End-User Licence Agreement available at [«Terms»](/eula/) and must be read together with it. **1.2.** This Privacy Policy supplements the End-User Licence Agreement available at [«Terms»](/eula/) and must be read together with it.
@@ -133,8 +133,8 @@ The transfer of personal data to recipients (regardless of their legal status) i
## 11. Contact us ## 11. Contact us
**11.1.** If you have any questions, please contact Support on Telegram: [@Erudit_GameBot](https://telegram.me/Erudit_GameBot), or send us your request in writing to: 236020, Kaliningrad, Pribrezhny district, Parkovaya St. 1, recipient — Ilya Arkadyevich Denisov. Please refer to this Privacy Policy so that we can process your request effectively. We will try to respond to you within 30 days of receiving your request. **11.1.** If you have any questions, please contact Support on Telegram: [@Erudit_GameBot](https://telegram.me/Erudit_GameBot), or send us your request in writing to: 236020, Kaliningrad, Pribrezhny district, Parkovaya St. 1, recipient — Ilya A. Denisov. Please refer to this Privacy Policy so that we can process your request effectively. We will try to respond to you within 30 days of receiving your request.
**11.2.** All correspondence we receive from you (written or electronic requests) is classified as restricted-access information and may not be disclosed without your written consent. Personal data and other information about you may not be used without your consent for any purpose other than responding to the request, except in cases expressly provided for by law. **11.2.** All correspondence we receive from you (written or electronic requests) is classified as restricted-access information and may not be disclosed without your written consent. Personal data and other information about you may not be used without your consent for any purpose other than responding to the request, except in cases expressly provided for by law.
**11.3.** The email address of our data-protection officer: ilia.denissov@gmail.com. **11.3.** The email address of our data-protection officer: [ilia.denissov@gmail.com].
+2 -2
View File
@@ -2,7 +2,7 @@
## 1. О сервисе ## 1. О сервисе
**1.1.** Игру и сопутствующие ей сервисы предоставляет Денисов Илья Аркадьевич, ИНН 290210610742 (далее — «мы», «нас», «наш»). **1.1.** Игру и сопутствующие ей сервисы предоставляет Денисов Илья Аркадьевич, ИНН 290210610742, ОГРНИП 326390000053954 (далее — «мы», «нас», «наш»).
**1.2.** Настоящая Политика конфиденциальности дополняет Пользовательское соглашение, доступное по ссылке [«Условия»](/eula/), и должна рассматриваться совместно с ним. **1.2.** Настоящая Политика конфиденциальности дополняет Пользовательское соглашение, доступное по ссылке [«Условия»](/eula/), и должна рассматриваться совместно с ним.
@@ -137,4 +137,4 @@
**11.2.** Вся полученная нами от вас корреспонденция (письменные или электронные запросы) классифицируется как информация с ограниченным доступом и не может быть разглашена без вашего письменного согласия. Персональные данные и другая информация о вас не могут быть использованы без вашего согласия для каких-либо целей, кроме как для ответа на запрос, за исключением случаев, прямо предусмотренных законом. **11.2.** Вся полученная нами от вас корреспонденция (письменные или электронные запросы) классифицируется как информация с ограниченным доступом и не может быть разглашена без вашего письменного согласия. Персональные данные и другая информация о вас не могут быть использованы без вашего согласия для каких-либо целей, кроме как для ответа на запрос, за исключением случаев, прямо предусмотренных законом.
**11.3.** Адрес электронной почты нашего лица, ответственного за защиту данных: ilia.denissov@gmail.com. **11.3.** Адрес электронной почты нашего лица, ответственного за защиту данных: [ilia.denissov@gmail.com].
+48 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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>