From c1aad903a230af071f4b710043ed2a9aa178b067 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 15 Jul 2026 12:39:13 +0000 Subject: [PATCH 01/10] Add Crypo link INCY --- backend/package-lock.json | 10 +++++++++ backend/package.json | 1 + backend/src/modules/root/root.controller.ts | 21 +++++++++++++++++-- docker-compose.yml | 10 +++++++++ .../installation-guide.connector.tsx | 16 ++++++++++++-- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index db72c81e..088f5acf 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "7.2.6", "license": "AGPL-3.0-only", "dependencies": { + "@incy/link-encoder": "^1.0.1", "@nestjs/axios": "4.0.1", "@nestjs/common": "11.1.27", "@nestjs/config": "4.0.4", @@ -543,6 +544,15 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@incy/link-encoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@incy/link-encoder/-/link-encoder-1.0.1.tgz", + "integrity": "sha512-YIzvMv9JpIbN4SOk2BC6QhawmG+4A1Y5kWwiS2Gx8Lxp6ol8yp80Y2mXTFr2y6Yhx0LF4Px748pWsY79VRbZ6A==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index ef848a05..b7cc6304 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,7 @@ "lint:fix:imports": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --rule 'perfectionist/sort-imports: error'" }, "dependencies": { + "@incy/link-encoder": "^1.0.1", "@nestjs/axios": "4.0.1", "@nestjs/common": "11.1.27", "@nestjs/config": "4.0.4", diff --git a/backend/src/modules/root/root.controller.ts b/backend/src/modules/root/root.controller.ts index 5c6fc7bb..32ebddcd 100644 --- a/backend/src/modules/root/root.controller.ts +++ b/backend/src/modules/root/root.controller.ts @@ -1,6 +1,8 @@ import { Request, Response } from 'express'; -import { Get, Controller, Res, Req, Param, Logger } from '@nestjs/common'; +import { Get, Controller, Res, Req, Param, Query, Logger, BadRequestException } from '@nestjs/common'; + +import { encryptLink } from '@incy/link-encoder'; import { REQUEST_TEMPLATE_TYPE_VALUES, @@ -29,6 +31,21 @@ export class RootController { return await this.subpageConfigService.getSubscriptionPageConfig(user.su, request); } + @Get('api/incy-crypt-link') + getIncyCryptLink(@Query('url') url: string, @Query('name') name?: string) { + if (!url) { + throw new BadRequestException('url query param is required'); + } + + try { + const link = encryptLink(url, name ? { name } : undefined); + return { link }; + } catch (error) { + this.logger.error('Failed to encrypt INCY link', error as Error); + throw new BadRequestException('Failed to encrypt link'); + } + } + @Get([':shortUuid', ':shortUuid/:clientType']) async root( @ClientIp() clientIp: string, @@ -66,4 +83,4 @@ export class RootController { ); } } -} +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c1bb6707..10699c35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,17 @@ services: build: context: . dockerfile: Dockerfile + container_name: remnawave-subscription-page + hostname: remnawave-subscription-page + restart: always env_file: - .env ports: - '127.0.0.1:3010:3010' + networks: + - remnawave-network + +networks: + remnawave-network: + driver: bridge + external: true \ No newline at end of file diff --git a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx index 6bcf5359..56e18512 100644 --- a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx +++ b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx @@ -81,10 +81,22 @@ export const InstallationGuideConnector = (props: IProps) => { subscription.user.shortUuid ) - const handleButtonClick = (button: TSubscriptionPageButtonConfig) => { + const handleButtonClick = async (button: TSubscriptionPageButtonConfig) => { let formattedUrl: string | undefined - if (button.type === 'subscriptionLink' || button.type === 'copyButton') { + if (button.link === '{{INCY_CRYPT1_LINK}}') { + try { + const res = await fetch( + `/api/incy-crypt-link?url=${encodeURIComponent(subscriptionUrl)}&name=${encodeURIComponent(subscription.user.username)}` + ) + if (!res.ok) throw new Error('bad response') + const data = await res.json() + formattedUrl = data.link + } catch (e) { + console.error('Failed to get INCY link', e) + return + } + } else if (button.type === 'subscriptionLink' || button.type === 'copyButton') { formattedUrl = TemplateEngine.formatWithMetaInfo(button.link, { username: subscription.user.username, subscriptionUrl From 5a2334e4cfeb994cab250238b0372ae134d61c5e Mon Sep 17 00:00:00 2001 From: root Date: Wed, 15 Jul 2026 14:57:10 +0000 Subject: [PATCH 02/10] Add Security --- backend/package.json | 2 +- backend/src/modules/root/root.controller.ts | 39 +++-- backend/src/modules/root/root.service.ts | 30 +++- .../installation-guide.connector.tsx | 2 +- incy-crypt-backend.patch | 138 ++++++++++++++++++ 5 files changed, 194 insertions(+), 17 deletions(-) create mode 100644 incy-crypt-backend.patch diff --git a/backend/package.json b/backend/package.json index b7cc6304..2ebd4b8d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -95,4 +95,4 @@ "overrides": { "multer": "2.2.0" } -} +} \ No newline at end of file diff --git a/backend/src/modules/root/root.controller.ts b/backend/src/modules/root/root.controller.ts index 32ebddcd..6880474e 100644 --- a/backend/src/modules/root/root.controller.ts +++ b/backend/src/modules/root/root.controller.ts @@ -1,8 +1,15 @@ import { Request, Response } from 'express'; -import { Get, Controller, Res, Req, Param, Query, Logger, BadRequestException } from '@nestjs/common'; - -import { encryptLink } from '@incy/link-encoder'; +import { + Get, + Controller, + Res, + Req, + Param, + Query, + Logger, + BadRequestException, +} from '@nestjs/common'; import { REQUEST_TEMPLATE_TYPE_VALUES, @@ -32,18 +39,24 @@ export class RootController { } @Get('api/incy-crypt-link') - getIncyCryptLink(@Query('url') url: string, @Query('name') name?: string) { - if (!url) { - throw new BadRequestException('url query param is required'); + async getIncyCryptLink( + @ClientIp() clientIp: string, + @Req() request: Request, + @Query('shortUuid') shortUuid: string, + @Query('name') name?: string, + ) { + if (!shortUuid) { + throw new BadRequestException('shortUuid query param is required'); } - try { - const link = encryptLink(url, name ? { name } : undefined); - return { link }; - } catch (error) { - this.logger.error('Failed to encrypt INCY link', error as Error); - throw new BadRequestException('Failed to encrypt link'); - } + const link = await this.rootService.createIncyCryptoLink( + clientIp, + request, + shortUuid, + name, + ); + + return { link }; } @Get([':shortUuid', ':shortUuid/:clientType']) diff --git a/backend/src/modules/root/root.service.ts b/backend/src/modules/root/root.service.ts index 52b03d89..c293cdae 100644 --- a/backend/src/modules/root/root.service.ts +++ b/backend/src/modules/root/root.service.ts @@ -2,9 +2,10 @@ import { Request, Response } from 'express'; import { createHash } from 'node:crypto'; import { nanoid } from 'nanoid'; -import { Injectable } from '@nestjs/common'; +import { Injectable, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { Logger } from '@nestjs/common'; +import { encryptLink } from '@incy/link-encoder'; import { TRequestTemplateTypeKeys } from '@remnawave/backend-contract'; @@ -129,6 +130,31 @@ export class RootService { } } + public async createIncyCryptoLink( + clientIp: string, + req: Request, + shortUuid: string, + name?: string, + ): Promise { + const subscriptionDataResponse = await this.axiosService.getSubscriptionInfo( + clientIp, + shortUuid, + ); + + if (!subscriptionDataResponse.isOk || !subscriptionDataResponse.response) { + throw new BadRequestException('Invalid subscription'); + } + + const subscriptionUrl = `${req.protocol}://${req.get('host')}/${shortUuid}`; + + try { + return encryptLink(subscriptionUrl, name ? { name } : undefined); + } catch (error) { + this.logger.error(`Failed to encrypt INCY link: ${error}`); + throw new BadRequestException('Failed to encrypt link'); + } + } + private generateJwtForCookie(uuid: string | null): string { return this.jwtService.sign( { @@ -374,4 +400,4 @@ export class RootService { return true; } -} +} \ No newline at end of file diff --git a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx index 56e18512..de9c40e8 100644 --- a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx +++ b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx @@ -87,7 +87,7 @@ export const InstallationGuideConnector = (props: IProps) => { if (button.link === '{{INCY_CRYPT1_LINK}}') { try { const res = await fetch( - `/api/incy-crypt-link?url=${encodeURIComponent(subscriptionUrl)}&name=${encodeURIComponent(subscription.user.username)}` + `/api/incy-crypt-link?shortUuid=${encodeURIComponent(subscription.user.shortUuid)}&name=${encodeURIComponent(subscription.user.username)}` ) if (!res.ok) throw new Error('bad response') const data = await res.json() diff --git a/incy-crypt-backend.patch b/incy-crypt-backend.patch new file mode 100644 index 00000000..9edd1354 --- /dev/null +++ b/incy-crypt-backend.patch @@ -0,0 +1,138 @@ +diff --git a/backend/package.json b/backend/package.json +index ef848a0..b7cc630 100644 +--- a/backend/package.json ++++ b/backend/package.json +@@ -26,6 +26,7 @@ + "lint:fix:imports": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --rule 'perfectionist/sort-imports: error'" + }, + "dependencies": { ++ "@incy/link-encoder": "^1.0.1", + "@nestjs/axios": "4.0.1", + "@nestjs/common": "11.1.27", + "@nestjs/config": "4.0.4", +diff --git a/backend/src/modules/root/root.controller.ts b/backend/src/modules/root/root.controller.ts +index 5c6fc7b..30f1c17 100644 +--- a/backend/src/modules/root/root.controller.ts ++++ b/backend/src/modules/root/root.controller.ts +@@ -1,6 +1,15 @@ + import { Request, Response } from 'express'; + +-import { Get, Controller, Res, Req, Param, Logger } from '@nestjs/common'; ++import { ++ Get, ++ Controller, ++ Res, ++ Req, ++ Param, ++ Query, ++ Logger, ++ BadRequestException, ++} from '@nestjs/common'; + + import { + REQUEST_TEMPLATE_TYPE_VALUES, +@@ -29,6 +38,27 @@ export class RootController { + return await this.subpageConfigService.getSubscriptionPageConfig(user.su, request); + } + ++ @Get('api/incy-crypt-link') ++ async getIncyCryptLink( ++ @ClientIp() clientIp: string, ++ @Req() request: Request, ++ @Query('shortUuid') shortUuid: string, ++ @Query('name') name?: string, ++ ) { ++ if (!shortUuid) { ++ throw new BadRequestException('shortUuid query param is required'); ++ } ++ ++ const link = await this.rootService.createIncyCryptoLink( ++ clientIp, ++ request, ++ shortUuid, ++ name, ++ ); ++ ++ return { link }; ++ } ++ + @Get([':shortUuid', ':shortUuid/:clientType']) + async root( + @ClientIp() clientIp: string, +diff --git a/backend/src/modules/root/root.service.ts b/backend/src/modules/root/root.service.ts +index 52b03d8..18cc94b 100644 +--- a/backend/src/modules/root/root.service.ts ++++ b/backend/src/modules/root/root.service.ts +@@ -2,9 +2,10 @@ import { Request, Response } from 'express'; + import { createHash } from 'node:crypto'; + import { nanoid } from 'nanoid'; + +-import { Injectable } from '@nestjs/common'; ++import { Injectable, BadRequestException } from '@nestjs/common'; + import { JwtService } from '@nestjs/jwt'; + import { Logger } from '@nestjs/common'; ++import { encryptLink } from '@incy/link-encoder'; + + import { TRequestTemplateTypeKeys } from '@remnawave/backend-contract'; + +@@ -129,6 +130,31 @@ export class RootService { + } + } + ++ public async createIncyCryptoLink( ++ clientIp: string, ++ req: Request, ++ shortUuid: string, ++ name?: string, ++ ): Promise { ++ const subscriptionDataResponse = await this.axiosService.getSubscriptionInfo( ++ clientIp, ++ shortUuid, ++ ); ++ ++ if (!subscriptionDataResponse.isOk || !subscriptionDataResponse.response) { ++ throw new BadRequestException('Invalid subscription'); ++ } ++ ++ const subscriptionUrl = `${req.protocol}://${req.get('host')}/${shortUuid}`; ++ ++ try { ++ return encryptLink(subscriptionUrl, name ? { name } : undefined); ++ } catch (error) { ++ this.logger.error(`Failed to encrypt INCY link: ${error}`); ++ throw new BadRequestException('Failed to encrypt link'); ++ } ++ } ++ + private generateJwtForCookie(uuid: string | null): string { + return this.jwtService.sign( + { +diff --git a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx +index 6bcf535..de9c40e 100644 +--- a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx ++++ b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx +@@ -81,10 +81,22 @@ export const InstallationGuideConnector = (props: IProps) => { + subscription.user.shortUuid + ) + +- const handleButtonClick = (button: TSubscriptionPageButtonConfig) => { ++ const handleButtonClick = async (button: TSubscriptionPageButtonConfig) => { + let formattedUrl: string | undefined + +- if (button.type === 'subscriptionLink' || button.type === 'copyButton') { ++ if (button.link === '{{INCY_CRYPT1_LINK}}') { ++ try { ++ const res = await fetch( ++ `/api/incy-crypt-link?shortUuid=${encodeURIComponent(subscription.user.shortUuid)}&name=${encodeURIComponent(subscription.user.username)}` ++ ) ++ if (!res.ok) throw new Error('bad response') ++ const data = await res.json() ++ formattedUrl = data.link ++ } catch (e) { ++ console.error('Failed to get INCY link', e) ++ return ++ } ++ } else if (button.type === 'subscriptionLink' || button.type === 'copyButton') { + formattedUrl = TemplateEngine.formatWithMetaInfo(button.link, { + username: subscription.user.username, + subscriptionUrl From efc51141b385a0794457d21fe01a5f661d9539e9 Mon Sep 17 00:00:00 2001 From: densds <45933539+densds@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:13:21 +0300 Subject: [PATCH 03/10] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e19da8b9..6b96b8ec 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ ## Remnawave Subscription Page +Add INCY crypt link +Install npm in backend +npm install @incy/link-encoder + Learn more about Remnawave [here](https://remna.st/). # Contributors From c2cdca53fa096f41cddf543000b6cc748da477ac Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Jul 2026 08:28:09 +0000 Subject: [PATCH 04/10] del fix --- incy-crypt-backend.patch | 138 --------------------------------------- 1 file changed, 138 deletions(-) delete mode 100644 incy-crypt-backend.patch diff --git a/incy-crypt-backend.patch b/incy-crypt-backend.patch deleted file mode 100644 index 9edd1354..00000000 --- a/incy-crypt-backend.patch +++ /dev/null @@ -1,138 +0,0 @@ -diff --git a/backend/package.json b/backend/package.json -index ef848a0..b7cc630 100644 ---- a/backend/package.json -+++ b/backend/package.json -@@ -26,6 +26,7 @@ - "lint:fix:imports": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --rule 'perfectionist/sort-imports: error'" - }, - "dependencies": { -+ "@incy/link-encoder": "^1.0.1", - "@nestjs/axios": "4.0.1", - "@nestjs/common": "11.1.27", - "@nestjs/config": "4.0.4", -diff --git a/backend/src/modules/root/root.controller.ts b/backend/src/modules/root/root.controller.ts -index 5c6fc7b..30f1c17 100644 ---- a/backend/src/modules/root/root.controller.ts -+++ b/backend/src/modules/root/root.controller.ts -@@ -1,6 +1,15 @@ - import { Request, Response } from 'express'; - --import { Get, Controller, Res, Req, Param, Logger } from '@nestjs/common'; -+import { -+ Get, -+ Controller, -+ Res, -+ Req, -+ Param, -+ Query, -+ Logger, -+ BadRequestException, -+} from '@nestjs/common'; - - import { - REQUEST_TEMPLATE_TYPE_VALUES, -@@ -29,6 +38,27 @@ export class RootController { - return await this.subpageConfigService.getSubscriptionPageConfig(user.su, request); - } - -+ @Get('api/incy-crypt-link') -+ async getIncyCryptLink( -+ @ClientIp() clientIp: string, -+ @Req() request: Request, -+ @Query('shortUuid') shortUuid: string, -+ @Query('name') name?: string, -+ ) { -+ if (!shortUuid) { -+ throw new BadRequestException('shortUuid query param is required'); -+ } -+ -+ const link = await this.rootService.createIncyCryptoLink( -+ clientIp, -+ request, -+ shortUuid, -+ name, -+ ); -+ -+ return { link }; -+ } -+ - @Get([':shortUuid', ':shortUuid/:clientType']) - async root( - @ClientIp() clientIp: string, -diff --git a/backend/src/modules/root/root.service.ts b/backend/src/modules/root/root.service.ts -index 52b03d8..18cc94b 100644 ---- a/backend/src/modules/root/root.service.ts -+++ b/backend/src/modules/root/root.service.ts -@@ -2,9 +2,10 @@ import { Request, Response } from 'express'; - import { createHash } from 'node:crypto'; - import { nanoid } from 'nanoid'; - --import { Injectable } from '@nestjs/common'; -+import { Injectable, BadRequestException } from '@nestjs/common'; - import { JwtService } from '@nestjs/jwt'; - import { Logger } from '@nestjs/common'; -+import { encryptLink } from '@incy/link-encoder'; - - import { TRequestTemplateTypeKeys } from '@remnawave/backend-contract'; - -@@ -129,6 +130,31 @@ export class RootService { - } - } - -+ public async createIncyCryptoLink( -+ clientIp: string, -+ req: Request, -+ shortUuid: string, -+ name?: string, -+ ): Promise { -+ const subscriptionDataResponse = await this.axiosService.getSubscriptionInfo( -+ clientIp, -+ shortUuid, -+ ); -+ -+ if (!subscriptionDataResponse.isOk || !subscriptionDataResponse.response) { -+ throw new BadRequestException('Invalid subscription'); -+ } -+ -+ const subscriptionUrl = `${req.protocol}://${req.get('host')}/${shortUuid}`; -+ -+ try { -+ return encryptLink(subscriptionUrl, name ? { name } : undefined); -+ } catch (error) { -+ this.logger.error(`Failed to encrypt INCY link: ${error}`); -+ throw new BadRequestException('Failed to encrypt link'); -+ } -+ } -+ - private generateJwtForCookie(uuid: string | null): string { - return this.jwtService.sign( - { -diff --git a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx -index 6bcf535..de9c40e 100644 ---- a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx -+++ b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx -@@ -81,10 +81,22 @@ export const InstallationGuideConnector = (props: IProps) => { - subscription.user.shortUuid - ) - -- const handleButtonClick = (button: TSubscriptionPageButtonConfig) => { -+ const handleButtonClick = async (button: TSubscriptionPageButtonConfig) => { - let formattedUrl: string | undefined - -- if (button.type === 'subscriptionLink' || button.type === 'copyButton') { -+ if (button.link === '{{INCY_CRYPT1_LINK}}') { -+ try { -+ const res = await fetch( -+ `/api/incy-crypt-link?shortUuid=${encodeURIComponent(subscription.user.shortUuid)}&name=${encodeURIComponent(subscription.user.username)}` -+ ) -+ if (!res.ok) throw new Error('bad response') -+ const data = await res.json() -+ formattedUrl = data.link -+ } catch (e) { -+ console.error('Failed to get INCY link', e) -+ return -+ } -+ } else if (button.type === 'subscriptionLink' || button.type === 'copyButton') { - formattedUrl = TemplateEngine.formatWithMetaInfo(button.link, { - username: subscription.user.username, - subscriptionUrl From 3f7b8adb035e6579d8a52248771385852454dfae Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Jul 2026 09:14:48 +0000 Subject: [PATCH 05/10] fix: use formattedUrl for external buttons, add loading state, docs cleanup --- README.md | 17 +++++- .../installation-guide.connector.tsx | 60 +++++++++++++++---- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6b96b8ec..76180c87 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,19 @@ ## Remnawave Subscription Page -Add INCY crypt link -Install npm in backend -npm install @incy/link-encoder +### INCY encrypted subscription links + +This fork adds support for INCY encrypted deep links (`incy://crypt1/...`), similar to +the existing `HAPP_CRYPT4_LINK` placeholder for Happ. + +**Setup:** +1. Install the dependency in `backend`: `npm install @incy/link-encoder` +2. In your app-config JSON (Subscription Page settings), use the placeholder + `{{INCY_CRYPT1_LINK}}` as the `link` value for an INCY button. + +The backend validates that the subscription actually exists before encrypting the +link (via the same `getSubscriptionInfo` call used to serve the subscription page), +and builds the URL from the request's own host — the client never supplies an +arbitrary URL to encrypt. Learn more about Remnawave [here](https://remna.st/). diff --git a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx index de9c40e8..8356b313 100644 --- a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx +++ b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx @@ -16,7 +16,7 @@ import { } from '@mantine/core' import { notifications } from '@mantine/notifications' import { useClipboard } from '@mantine/hooks' -import { useState } from 'react' +import { useEffect, useState } from 'react' import clsx from 'clsx' import { constructSubscriptionUrl } from '@shared/utils/construct-subscription-url' @@ -81,21 +81,49 @@ export const InstallationGuideConnector = (props: IProps) => { subscription.user.shortUuid ) - const handleButtonClick = async (button: TSubscriptionPageButtonConfig) => { + const [incyCryptLink, setIncyCryptLink] = useState(undefined) + const [incyCryptLoading, setIncyCryptLoading] = useState(true) + + useEffect(() => { + let cancelled = false + setIncyCryptLoading(true) + + fetch( + `/api/incy-crypt-link?shortUuid=${encodeURIComponent(subscription.user.shortUuid)}&name=${encodeURIComponent(subscription.user.username)}` + ) + .then((res) => { + if (!res.ok) throw new Error('bad response') + return res.json() + }) + .then((data) => { + if (!cancelled) setIncyCryptLink(data.link) + }) + .catch((e) => { + console.error('Failed to pre-fetch INCY link', e) + }) + .finally(() => { + if (!cancelled) setIncyCryptLoading(false) + }) + + return () => { + cancelled = true + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [subscription.user.shortUuid]) + + const handleButtonClick = (button: TSubscriptionPageButtonConfig) => { let formattedUrl: string | undefined if (button.link === '{{INCY_CRYPT1_LINK}}') { - try { - const res = await fetch( - `/api/incy-crypt-link?shortUuid=${encodeURIComponent(subscription.user.shortUuid)}&name=${encodeURIComponent(subscription.user.username)}` - ) - if (!res.ok) throw new Error('bad response') - const data = await res.json() - formattedUrl = data.link - } catch (e) { - console.error('Failed to get INCY link', e) + if (!incyCryptLink) { + notifications.show({ + title: 'Error', + message: 'INCY link is not ready yet, please try again in a moment', + color: 'red' + }) return } + formattedUrl = incyCryptLink } else if (button.type === 'subscriptionLink' || button.type === 'copyButton') { formattedUrl = TemplateEngine.formatWithMetaInfo(button.link, { username: subscription.user.username, @@ -116,13 +144,17 @@ export const InstallationGuideConnector = (props: IProps) => { break } case 'external': { - window.open(button.link, '_blank') + if (formattedUrl) { + window.location.href = formattedUrl + } else { + window.open(button.link, '_blank') + } break } case 'subscriptionLink': { if (!formattedUrl) return - window.open(formattedUrl, '_blank') + window.location.href = formattedUrl break } default: @@ -141,6 +173,7 @@ export const InstallationGuideConnector = (props: IProps) => { {buttons.map((button, index) => (