From c36081e48ec8e67ead095a4fb8aa1fd695138368 Mon Sep 17 00:00:00 2001 From: Dmitrii Liulekin Date: Sun, 26 Oct 2025 19:54:05 -0300 Subject: [PATCH 01/13] feat: support tonConnect subscriptions --- packages/core/src/entries/tonConnect.ts | 169 ++++++++- .../src/service/tonConnect/connectService.ts | 45 +++ .../InstallSubscriptionV2Notification.tsx | 322 ++++++++++++++++++ .../RemoveSubscriptionV2Notification.tsx | 275 +++++++++++++++ .../connect/TonConnectRequestNotification.tsx | 30 ++ .../connect/WebTonConnectSubscription.tsx | 20 ++ .../pro/ProInstallExtensionNotification.tsx | 4 +- .../pro/ProRemoveExtensionNotification.tsx | 7 +- .../src/components/pro/ProActiveWallet.tsx | 6 + .../subscription/useCancelSubscription.ts | 8 +- .../subscription/useCreateSubscription.ts | 24 +- 11 files changed, 878 insertions(+), 32 deletions(-) create mode 100644 packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx create mode 100644 packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx diff --git a/packages/core/src/entries/tonConnect.ts b/packages/core/src/entries/tonConnect.ts index f633d7041..d33367c95 100644 --- a/packages/core/src/entries/tonConnect.ts +++ b/packages/core/src/entries/tonConnect.ts @@ -199,10 +199,19 @@ const signDataFeatureSchema = z.object({ }); export type SignDataFeature = z.infer; +const subscriptionFeatureSchema = z.object({ + name: z.literal('Subscription'), + versions: z.object({ + v2: z.boolean() + }) +}); +export type SubscriptionFeature = z.infer; + export const featureSchema = z.union([ sendTransactionFeatureDeprecatedSchema, sendTransactionFeatureSchema, - signDataFeatureSchema + signDataFeatureSchema, + subscriptionFeatureSchema ]); export type Feature = z.infer; @@ -268,9 +277,78 @@ const keyPairSchema = z.object({ }); export type KeyPair = z.infer; -const rpcMethodSchema = z.enum(['disconnect', 'sendTransaction', 'signData']); +const rpcMethodSchema = z.enum([ + 'disconnect', + 'sendTransaction', + 'signData', + 'createSubscriptionV2', + 'cancelSubscriptionV2' +]); export type RpcMethod = z.infer; +const createSubscriptionV2RpcRequestSchema = z.object({ + id: z.string(), + method: z.literal('createSubscriptionV2'), + params: z.tuple([z.string()]) +}); +export type CreateSubscriptionV2RpcRequest = z.infer; + +const cancelSubscriptionV2RpcRequestSchema = z.object({ + id: z.string(), + method: z.literal('cancelSubscriptionV2'), + params: z.tuple([z.string()]) +}); +export type CancelSubscriptionV2RpcRequest = z.infer; + +export enum SUBSCRIPTION_V2_ERROR_CODES { + UNKNOWN_ERROR = 0, + BAD_REQUEST_ERROR = 1, + UNKNOWN_APP_ERROR = 100, + USER_REJECTS_ERROR = 300, + METHOD_NOT_SUPPORTED = 400, + EXTENSION_NOT_FOUND = 404 +} + +const createSubscriptionV2RpcResponseSuccessSchema = z.object({ + id: z.string(), + result: z.object({ + boc: z.string() + }) +}); +export type CreateSubscriptionV2RpcResponseSuccess = z.infer< + typeof createSubscriptionV2RpcResponseSuccessSchema +>; + +const createSubscriptionV2RpcResponseErrorSchema = z.object({ + id: z.string(), + error: z.object({ + code: z.nativeEnum(SUBSCRIPTION_V2_ERROR_CODES), + message: z.string() + }) +}); +export type CreateSubscriptionV2RpcResponseError = z.infer< + typeof createSubscriptionV2RpcResponseErrorSchema +>; + +const cancelSubscriptionV2RpcResponseSuccessSchema = z.object({ + id: z.string(), + result: z.object({}) +}); +export type CancelSubscriptionV2RpcResponseSuccess = z.infer< + typeof cancelSubscriptionV2RpcResponseSuccessSchema +>; + +const cancelSubscriptionV2RpcResponseErrorSchema = z.object({ + id: z.string(), + error: z.object({ + code: z.nativeEnum(SUBSCRIPTION_V2_ERROR_CODES), + message: z.string() + }) +}); +export type CancelSubscriptionV2RpcResponseError = z.infer< + typeof cancelSubscriptionV2RpcResponseErrorSchema +>; + export enum SEND_TRANSACTION_ERROR_CODES { UNKNOWN_ERROR = 0, BAD_REQUEST_ERROR = 1, @@ -365,7 +443,9 @@ export type SignDataRequestPayload = z.infer; @@ -411,6 +491,14 @@ const rpcResponsesSchema = z.object({ disconnect: z.object({ error: disconnectRpcResponseErrorSchema, success: disconnectRpcResponseSuccessSchema + }), + createSubscriptionV2: z.object({ + error: createSubscriptionV2RpcResponseErrorSchema, + success: createSubscriptionV2RpcResponseSuccessSchema + }), + cancelSubscriptionV2: z.object({ + error: cancelSubscriptionV2RpcResponseErrorSchema, + success: cancelSubscriptionV2RpcResponseSuccessSchema }) }); export type RpcResponses = z.infer; @@ -424,7 +512,11 @@ export const walletResponseSchema = z.union([ signDataRpcResponseSuccessSchema, signDataRpcResponseErrorSchema, disconnectRpcResponseSuccessSchema, - disconnectRpcResponseErrorSchema + disconnectRpcResponseErrorSchema, + createSubscriptionV2RpcResponseSuccessSchema, + createSubscriptionV2RpcResponseErrorSchema, + cancelSubscriptionV2RpcResponseSuccessSchema, + cancelSubscriptionV2RpcResponseErrorSchema ]); export type WalletResponse = WalletResponseSuccess | WalletResponseError; @@ -439,7 +531,9 @@ assertTypesEqual>(true); export const appRequestSchema = z.union([ sendTransactionRpcRequestSchema, signDataRpcRequestSchema, - disconnectRpcRequestSchema + disconnectRpcRequestSchema, + createSubscriptionV2RpcRequestSchema, + cancelSubscriptionV2RpcRequestSchema ]); export type AppRequest = RpcRequests[T]; assertTypesEqual, z.infer>(true); @@ -476,9 +570,39 @@ export interface SignDatAppRequest< payload: SignDataRequestPayload; } +export interface CreateSubscriptionV2AppRequest< + T extends AccountConnection['type'] = AccountConnection['type'] +> { + id: string; + connection: T extends 'http' + ? AccountConnectionHttp + : T extends 'injected' + ? AccountConnectionInjected + : AccountConnection; + kind: 'createSubscriptionV2'; + payload: CreateSubscriptionV2Payload; +} + +export interface CancelSubscriptionV2AppRequest< + T extends AccountConnection['type'] = AccountConnection['type'] +> { + id: string; + connection: T extends 'http' + ? AccountConnectionHttp + : T extends 'injected' + ? AccountConnectionInjected + : AccountConnection; + kind: 'cancelSubscriptionV2'; + payload: any; +} + export type TonConnectAppRequestPayload< T extends AccountConnection['type'] = AccountConnection['type'] -> = SendTransactionAppRequest | SignDatAppRequest; +> = + | SendTransactionAppRequest + | SignDatAppRequest + | CreateSubscriptionV2AppRequest + | CancelSubscriptionV2AppRequest; export interface InjectedWalletInfo { name: string; @@ -497,3 +621,36 @@ export interface ITonConnectInjectedBridge { send(message: AppRequest): Promise>; listen(callback: (event: WalletEvent) => void): () => void; } + +export type SubscriptionMetadataSource = z.infer; + +export const subscriptionMetadataSchema = z.object({ + logo: z.string().url(), + name: z.string(), + description: z.string(), + link: z.string().url(), + tos: z.string().url(), + merchant: z.string(), + website: z.string().url() +}); + +export const subscriptionSchema = z.object({ + beneficiary: z.string(), + id: z.number(), + period: z.number().int().positive(), + amount: z.string(), + firstChargeDate: z.number(), + withdrawAddress: z.string(), + withdrawMsgBody: z.string(), + metadata: subscriptionMetadataSchema +}); + +export const createSubscriptionV2PayloadSchema = z.object({ + validUntil: z.number(), + subscription: subscriptionSchema, + from: rawAddressSchema.optional(), + network: tonConnectNetworkSchema, + valid_until: z.number() +}); + +export type CreateSubscriptionV2Payload = z.infer; diff --git a/packages/core/src/service/tonConnect/connectService.ts b/packages/core/src/service/tonConnect/connectService.ts index ff8b6b74b..c06f6283f 100644 --- a/packages/core/src/service/tonConnect/connectService.ts +++ b/packages/core/src/service/tonConnect/connectService.ts @@ -16,6 +16,7 @@ import { SEND_TRANSACTION_ERROR_CODES, SendTransactionRpcResponseError, SendTransactionRpcResponseSuccess, + SUBSCRIPTION_V2_ERROR_CODES, TonAddressItemReply, TonConnectEventPayload, TonConnectNetwork, @@ -211,6 +212,10 @@ export const getDeviceInfo = ( { name: 'SignData', types: ['text', 'binary', 'cell'] + }, + { + name: 'Subscription', + versions: { v2: true } } ] }; @@ -608,3 +613,43 @@ export async function checkTonConnectFromAndNetwork( } } } + +export const createSubscriptionV2SuccessResponse = (id: string, boc: string) => { + return { + id, + result: { boc } + }; +}; + +export const createSubscriptionV2ErrorResponse = ( + id: string, + message = 'User rejected subscription' +) => { + return { + id, + error: { + code: SUBSCRIPTION_V2_ERROR_CODES.USER_REJECTS_ERROR, + message + } + }; +}; + +export const cancelSubscriptionV2SuccessResponse = (id: string, boc: string) => { + return { + id, + result: { boc } + }; +}; + +export const cancelSubscriptionV2ErrorResponse = ( + id: string, + message = 'User rejected cancellation' +) => { + return { + id, + error: { + code: SUBSCRIPTION_V2_ERROR_CODES.USER_REJECTS_ERROR, + message + } + }; +}; diff --git a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx new file mode 100644 index 000000000..8080f244e --- /dev/null +++ b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx @@ -0,0 +1,322 @@ +import { + CreateSubscriptionV2Payload, + SubscriptionMetadataSource +} from '@tonkeeper/core/dist/entries/tonConnect'; +import React, { FC, useEffect, useMemo } from 'react'; +import styled from 'styled-components'; +import { useTranslation } from '../../hooks/translation'; +import { SpinnerIcon } from '../Icon'; +import { Notification, NotificationFooter, NotificationFooterPortal } from '../Notification'; +import { Body2, Body3, Label2 } from '../Text'; +import { Button } from '../fields/Button'; +import { ConfirmMainButtonProps } from '../transfer/common'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { ListBlock, ListItem, ListItemPayload } from '../List'; +import { secondsToUnitCount } from '@tonkeeper/core/dist/utils/pro'; +import { useFormatFiat, useRate } from '../../state/rates'; +import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; +import { ErrorBoundary } from '../shared/ErrorBoundary'; +import { fallbackRenderOver } from '../Error'; +import { + useCreateSubscription, + useEstimateDeploySubscription +} from '../../hooks/blockchain/subscription'; +import { + ConfirmView, + ConfirmViewAdditionalBottomSlot, + ConfirmViewButtons, + ConfirmViewButtonsSlot, + ConfirmViewDetailsSlot, + ConfirmViewHeadingSlot +} from '../transfer/ConfirmView'; +import { ProActiveWallet } from '../pro/ProActiveWallet'; +import { ProSubscriptionHeader } from '../pro/ProSubscriptionHeader'; +import { + CryptoCurrency, + SubscriptionExtension, + SubscriptionExtensionMetadata, + SubscriptionExtensionStatus, + SubscriptionExtensionVersion +} from '@tonkeeper/core/dist/pro'; +import { useProCompatibleAccountsWallets } from '../../state/wallet'; +import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; +import { toNano } from '@ton/core'; + +interface IProInstallExtensionProps { + isOpen: boolean; + onClose: (boc?: string) => void; + extensionData?: SubscriptionExtension; +} + +function toSubscriptionMetadata(src: SubscriptionMetadataSource): SubscriptionExtensionMetadata { + return { + l: src.logo, + n: src.name, + u: src.link, + m: src.merchant, + w: src.website, + ...(src.description ? { d: src.description } : {}), + ...(src.tos ? { t: src.tos } : {}) + }; +} + +export const InstallSubscriptionV2Notification: FC<{ + params: CreateSubscriptionV2Payload | null; + handleClose: (boc?: string) => void; +}> = ({ params, handleClose }) => { + const subscription = params?.subscription; + + if (!subscription || !params?.from) return null; + + const extensionData: SubscriptionExtension = { + version: SubscriptionExtensionVersion.V2, + status: SubscriptionExtensionStatus.NOT_INITIALIZED, + admin: subscription.beneficiary, + recipient: subscription.beneficiary, + subscription_id: subscription.id, + first_charging_date: 0, + last_charging_date: 0, + grace_period: 0, + payment_per_period: subscription.amount, + currency: CryptoCurrency.TON, + created_at: Date.now(), + deploy_value: toNano('0.1').toString(), + destroy_value: toNano('0.05').toString(), + caller_fee: toNano('0.05').toString(), + payer: params.from, + contract: '', + period: subscription.period, + metadata: toSubscriptionMetadata(subscription.metadata) + }; + + return ( + <> + handleClose()} + hideButton + backShadow + > + {() => ( + + {extensionData && ( + + )} + + )} + + + ); +}; + +const ProInstallExtensionNotificationContent: FC< + Required> +> = ({ onClose, extensionData }) => { + const { t } = useTranslation(); + const deployMutation = useCreateSubscription(); + const estimateFeeMutation = useEstimateDeploySubscription(); + const { + data: estimation, + error: estimationError, + isLoading: isEstimating + } = estimateFeeMutation; + + const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); + + const accountWallet = accountsWallets.find( + accWallet => accWallet.wallet.id === extensionData.payer + ); + + const selectedWallet = accountWallet?.wallet; + + const { data: rate } = useRate(CryptoCurrency.TON); + + const { fiatAmount: fiatEquivalent } = useFormatFiat( + rate, + formatDecimals(extensionData.payment_per_period) + ); + const { fiatAmount: feeEquivalent } = useFormatFiat( + rate, + formatDecimals(estimation?.fee?.extra?.stringWeiAmount ?? 0) + ); + + useEffect(() => { + if (!selectedWallet) return; + + estimateFeeMutation.mutate({ + selectedWallet, + ...extensionData + }); + }, [selectedWallet]); + + const price = useMemo( + () => + new AssetAmount({ + asset: TON_ASSET, + weiAmount: extensionData.payment_per_period + }), + [extensionData?.payment_per_period] + ); + + const deployMutate = async () => { + if (!selectedWallet) { + throw new Error('Selected wallet is required!'); + } + + const boc = await deployMutation.mutateAsync({ + selectedWallet, + ...extensionData + }); + + setTimeout(() => { + onClose(boc.toString()); + }, 1500); + + return !!boc; + }; + + const { + unit: periodUnit, + count: periodCount, + form: periodForm + } = secondsToUnitCount(extensionData.period); + + return ( + onClose()} + estimation={{ ...estimateFeeMutation }} + {...deployMutation} + mutateAsync={deployMutate} + > + + + + + + + + + + + + {t('price')} + + {price.toStringAssetRelativeAmount()} + {`≈ ${fiatEquivalent}`} + + + + + + + {t('interval')} + + {t(`every_${periodUnit}_${periodForm}`, { + count: periodCount + })} + + + + + + + {t('swap_blockchain_fee')} + + + {isEstimating && } + {!!estimationError && <>—} + {estimation?.fee?.extra && + estimateFeeMutation.data.fee.extra.toStringAssetRelativeAmount()} + + {!estimationError && !isEstimating && ( + {`≈ ${feeEquivalent}`} + )} + + + + + + + + + + + + + + + ); +}; + +export const ConfirmMainButton: ConfirmMainButtonProps = props => { + const { isLoading, isDisabled, onClick } = props; + + const { t } = useTranslation(); + + return ( + + ); +}; + +const Body2Styled = styled(Body2)` + color: ${props => props.theme.textSecondary}; +`; + +const Body3Styled = styled(Body3)` + color: ${props => props.theme.textSecondary}; +`; + +const ProSubscriptionHeaderStyled = styled(ProSubscriptionHeader)` + margin-bottom: 0; +`; + +const NotificationStyled = styled(Notification)` + max-width: 650px; + + @media (pointer: fine) { + &:hover { + [data-swipe-button] { + color: ${p => p.theme.textSecondary}; + } + } + } +`; + +const ListItemStyled = styled(ListItem)` + &:not(:first-child) > div { + padding-top: 10px; + } +`; + +const ListItemPayloadStyled = styled(ListItemPayload)<{ alignItems?: string }>` + padding-top: 10px; + padding-bottom: 10px; + + align-items: ${({ alignItems }) => alignItems ?? 'center'}; +`; + +const FiatEquivalentWrapper = styled.div` + display: grid; + justify-items: end; +`; diff --git a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx new file mode 100644 index 000000000..63cba8a26 --- /dev/null +++ b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx @@ -0,0 +1,275 @@ +import React, { FC, useEffect, useMemo } from 'react'; +import styled from 'styled-components'; +import { useTranslation } from '../../hooks/translation'; +import { SpinnerIcon } from '../Icon'; +import { Notification, NotificationFooter, NotificationFooterPortal } from '../Notification'; +import { Body2, Body3, Label2 } from '../Text'; +import { Button } from '../fields/Button'; +import { ConfirmMainButtonProps } from '../transfer/common'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { ListItem, ListItemPayload } from '../List'; +import { useFormatFiat, useRate } from '../../state/rates'; +import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; +import { ErrorBoundary } from '../shared/ErrorBoundary'; +import { fallbackRenderOver } from '../Error'; +import { + useCancelSubscription, + useEstimateRemoveExtension +} from '../../hooks/blockchain/subscription'; +import { + ConfirmView, + ConfirmViewButtons, + ConfirmViewButtonsSlot, + ConfirmViewDetailsSlot, + ConfirmViewHeadingSlot +} from '../transfer/ConfirmView'; +import { ProSubscriptionHeader } from '../pro/ProSubscriptionHeader'; +import { CryptoCurrency } from '@tonkeeper/core/dist/pro'; +import { useProCompatibleAccountsWallets } from '../../state/wallet'; +import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; +import { useToast } from '../../hooks/useNotification'; +import { useDateTimeFormat } from '../../hooks/useDateTimeFormat'; +import { hexToRGBA } from '../../libs/css'; +import { toNano } from '@ton/core'; + +export const RemoveSubscriptionV2Notification: FC<{ + params: any; + handleClose: (boc?: string) => void; +}> = ({ params, handleClose }) => { + return ( + <> + handleClose()} + hideButton + backShadow + > + {() => ( + + {!!params?.subscription && ( + + )} + + )} + + + ); +}; + +const ProRemoveSubscriptionV2NotificationContent: FC<{ + params: any; + onClose: (boc?: string) => void; +}> = ({ onClose, params }) => { + const extensionContract = ''; + const destroyValue = toNano('0.05').toString(); + + const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); + + const accountWallet = accountsWallets.find(accWallet => accWallet.wallet.id === params?.from); + + const selectedWallet = accountWallet?.wallet; + const finalExpiresDate = new Date(); + + const toast = useToast(); + const { t } = useTranslation(); + const formatDate = useDateTimeFormat(); + const { data: rate } = useRate(CryptoCurrency.TON); + + const removeMutation = useCancelSubscription(); + const estimateFeeMutation = useEstimateRemoveExtension(); + const { + data: estimation, + error: estimationError, + isLoading: isEstimating + } = estimateFeeMutation; + + const { fiatAmount: feeEquivalent } = useFormatFiat( + rate, + formatDecimals(estimation?.fee?.extra?.stringWeiAmount ?? 0) + ); + + useEffect(() => { + if (!removeMutation.isSuccess || !finalExpiresDate) return; + + toast( + `${t('extension_cancellation_success')} ${formatDate(finalExpiresDate, { + day: 'numeric', + month: 'short', + year: 'numeric', + inputUnit: 'seconds' + })}` + ); + }, [removeMutation.isSuccess]); + + useEffect(() => { + if (!selectedWallet) return; + + estimateFeeMutation.mutate({ + selectedWallet, + extensionContract, + destroyValue + }); + }, [selectedWallet]); + + const removeMutate = async () => { + if (!selectedWallet) { + throw new Error('Selected wallet not found!'); + } + + const boc = await removeMutation.mutateAsync({ + selectedWallet, + extensionContract, + destroyValue + }); + + setTimeout(() => { + onClose(boc.toString()); + }, 1500); + + return !!boc; + }; + + const deployReserve = useMemo( + () => + new AssetAmount({ + asset: TON_ASSET, + weiAmount: destroyValue + }), + [destroyValue] + ); + + return ( + onClose()} + estimation={{ ...estimateFeeMutation }} + {...removeMutation} + mutateAsync={removeMutate} + > + + + + + + {finalExpiresDate && ( + + + {t('will_be_active_until')} + + {formatDate(finalExpiresDate, { + day: 'numeric', + month: 'short', + year: 'numeric', + inputUnit: 'seconds' + })} + + + + )} + + + + {t('swap_blockchain_fee')} + + + {isEstimating && } + {!!estimationError && <>—} + {estimation?.fee?.extra && + estimation.fee.extra.toStringAssetRelativeAmount()} + + {!estimationError && !isEstimating && ( + {`≈ ${feeEquivalent}`} + )} + + + + + + + + + + + + + + ); +}; + +export const ConfirmMainButton: ConfirmMainButtonProps = props => { + const { isLoading, isDisabled, onClick } = props; + + const { t } = useTranslation(); + + return ( + + {t('cancel_subscription')} + + ); +}; + +const Body2Styled = styled(Body2)` + color: ${props => props.theme.textSecondary}; +`; + +const Body3Styled = styled(Body3)` + color: ${props => props.theme.textSecondary}; +`; + +const ProSubscriptionHeaderStyled = styled(ProSubscriptionHeader)` + margin-bottom: 0; +`; + +const NotificationStyled = styled(Notification)` + max-width: 650px; + + @media (pointer: fine) { + &:hover { + [data-swipe-button] { + color: ${p => p.theme.textSecondary}; + } + } + } +`; + +const ListItemStyled = styled(ListItem)` + &:not(:first-child) > div { + padding-top: 10px; + } +`; + +const ListItemPayloadStyled = styled(ListItemPayload)` + padding-top: 10px; + padding-bottom: 10px; +`; + +const FiatEquivalentWrapper = styled.div` + display: grid; + justify-items: end; +`; + +const CancelButtonStyled = styled(Button)` + color: ${p => p.theme.accentRed}; + background-color: ${({ theme }) => hexToRGBA(theme.accentRed, 0.16)}; + transition: background-color 0.1s ease-in; + + &:enabled:hover { + background-color: ${({ theme }) => hexToRGBA(theme.accentRed, 0.12)}; + } +`; diff --git a/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx b/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx index 6ee32e6e1..63f9ec101 100644 --- a/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx +++ b/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx @@ -9,6 +9,10 @@ import { FC } from 'react'; import { TonTransactionNotification } from './TonTransactionNotification'; import { SignDataNotification } from './SignDataNotification'; import { + cancelSubscriptionV2ErrorResponse, + cancelSubscriptionV2SuccessResponse, + createSubscriptionV2ErrorResponse, + createSubscriptionV2SuccessResponse, sendTransactionErrorResponse, sendTransactionSuccessResponse } from '@tonkeeper/core/dist/service/tonConnect/connectService'; @@ -16,6 +20,8 @@ import { useTrackerTonConnectSendSuccess, useTrackTonConnectActionRequest } from '../../hooks/analytics/events-hooks'; +import { InstallSubscriptionV2Notification } from './InstallSubscriptionV2Notification'; +import { RemoveSubscriptionV2Notification } from './RemoveSubscriptionV2Notification'; export const TonConnectRequestNotification: FC<{ request: TonConnectAppRequestPayload | undefined; @@ -72,6 +78,30 @@ export const TonConnectRequestNotification: FC<{ } }} /> + { + if (request) { + handleClose( + boc + ? createSubscriptionV2SuccessResponse(request.id, boc) + : createSubscriptionV2ErrorResponse(request.id) + ); + } + }} + /> + { + if (request) { + handleClose( + boc + ? cancelSubscriptionV2SuccessResponse(request.id, boc) + : cancelSubscriptionV2ErrorResponse(request.id) + ); + } + }} + /> ); }; diff --git a/packages/uikit/src/components/connect/WebTonConnectSubscription.tsx b/packages/uikit/src/components/connect/WebTonConnectSubscription.tsx index 1e0ecd596..8224470be 100644 --- a/packages/uikit/src/components/connect/WebTonConnectSubscription.tsx +++ b/packages/uikit/src/components/connect/WebTonConnectSubscription.tsx @@ -96,6 +96,26 @@ const WebTonConnectSubscription = () => { }; return openNotification(params.connection.clientSessionId, value); } + case 'createSubscriptionV2': { + setRequest(undefined); + const value: TonConnectAppRequestPayload = { + connection: params.connection, + id: params.request.id, + kind: 'createSubscriptionV2', + payload: JSON.parse(params.request.params[0]) + }; + return openNotification(params.connection.clientSessionId, value); + } + case 'cancelSubscriptionV2': { + setRequest(undefined); + const value: TonConnectAppRequestPayload = { + connection: params.connection, + id: params.request.id, + kind: 'cancelSubscriptionV2', + payload: JSON.parse(params.request.params[0]) + }; + return openNotification(params.connection.clientSessionId, value); + } default: { return badRequestResponse({ ...params, diff --git a/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx b/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx index 1ab2efaf9..facf32dbc 100644 --- a/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx +++ b/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx @@ -114,10 +114,12 @@ const ProInstallExtensionNotificationContent: FC< throw new Error('Selected wallet is required!'); } - return deployMutation.mutateAsync({ + const result = deployMutation.mutateAsync({ selectedWallet: targetAuth.wallet, ...extensionData }); + + return !!result; }; const { diff --git a/packages/uikit/src/components/desktop/pro/ProRemoveExtensionNotification.tsx b/packages/uikit/src/components/desktop/pro/ProRemoveExtensionNotification.tsx index 9bbb2121a..15777763e 100644 --- a/packages/uikit/src/components/desktop/pro/ProRemoveExtensionNotification.tsx +++ b/packages/uikit/src/components/desktop/pro/ProRemoveExtensionNotification.tsx @@ -118,13 +118,16 @@ const ProRemoveExtensionNotificationContent: FC< }); }, [selectedWallet]); - const removeMutate = async () => - removeMutation.mutateAsync({ + const removeMutate = async () => { + const boc = await removeMutation.mutateAsync({ selectedWallet, extensionContract, destroyValue }); + return !!boc; + }; + const deployReserve = useMemo( () => new AssetAmount({ diff --git a/packages/uikit/src/components/pro/ProActiveWallet.tsx b/packages/uikit/src/components/pro/ProActiveWallet.tsx index 497bb7871..573ad5a5c 100644 --- a/packages/uikit/src/components/pro/ProActiveWallet.tsx +++ b/packages/uikit/src/components/pro/ProActiveWallet.tsx @@ -14,6 +14,7 @@ import { useAtomValue } from '../../libs/useAtom'; interface IProps { title?: ReactNode; belowCaption?: ReactNode; + rawAddress?: string; isCurrentSubscription?: ReactNode; onDisconnect?: () => Promise; isLoading: boolean; @@ -24,6 +25,7 @@ export const ProActiveWallet: FC = props => { const { onDisconnect, isLoading, + rawAddress, title, belowCaption, isCurrentSubscription, @@ -34,6 +36,10 @@ export const ProActiveWallet: FC = props => { const targetAuth = useAtomValue(subscriptionFormTempAuth$); const { account, wallet } = useControllableAccountAndWalletByWalletId( (() => { + if (rawAddress) { + return rawAddress; + } + const currentAuth = subscription?.auth; if (targetAuth && !isCurrentSubscription) { diff --git a/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts b/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts index 23a24938b..531df0482 100644 --- a/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts +++ b/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts @@ -6,7 +6,7 @@ import { SubscriptionEncoder } from '@tonkeeper/core/dist/service/ton-blockchain import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; import { QueryKey } from '../../../libs/queryKey'; import { CancelParams } from './commonTypes'; -import { Address } from '@ton/core'; +import { Address, Cell } from '@ton/core'; import { WalletMessageSender } from '@tonkeeper/core/dist/service/ton-blockchain/sender'; export const useCancelSubscription = () => { @@ -15,7 +15,7 @@ export const useCancelSubscription = () => { const client = useQueryClient(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - return useMutation(async subscriptionParams => { + return useMutation(async subscriptionParams => { if (!subscriptionParams) throw new Error('No params'); const { selectedWallet, extensionContract, destroyValue } = subscriptionParams; @@ -40,10 +40,10 @@ export const useCancelSubscription = () => { const outgoingMsg = encoder.encodeDestructAction(extensionAddress, BigInt(destroyValue)); - await sender.send(outgoingMsg); + const boc = await sender.send(outgoingMsg); await client.invalidateQueries([QueryKey.pro]); - return true; + return boc; }); }; diff --git a/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts b/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts index b590e1587..8f94eb200 100644 --- a/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts +++ b/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts @@ -1,5 +1,4 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { Address } from '@ton/core'; import { useActiveApi, useProCompatibleAccountsWallets } from '../../../state/wallet'; import { getSigner } from '../../../state/mnemonic'; import { useAppSdk } from '../../appSdk'; @@ -10,23 +9,22 @@ import { } from '@tonkeeper/core/dist/service/ton-blockchain/encoder/subscription-encoder'; import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; import { WalletMessageSender } from '@tonkeeper/core/dist/service/ton-blockchain/sender'; -import { useTranslation } from '../../translation'; import { QueryKey } from '../../../libs/queryKey'; import { MetaEncryptionSerializedMap } from '@tonkeeper/core/dist/entries/wallet'; import { AppKey } from '@tonkeeper/core/dist/Keys'; import { metaEncryptionMapSerializer } from '@tonkeeper/core/dist/utils/metadata'; +import { Cell } from '@ton/core'; export const useCreateSubscription = () => { const sdk = useAppSdk(); const api = useActiveApi(); - const { t } = useTranslation(); const client = useQueryClient(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - return useMutation(async subscriptionParams => { + return useMutation(async subscriptionParams => { if (!subscriptionParams) throw new Error('No params'); - const { admin, subscription_id, contract, selectedWallet } = subscriptionParams; + const { selectedWallet } = subscriptionParams; const accountWallet = accountsWallets.find( accWallet => accWallet.wallet.id === selectedWallet.id @@ -58,26 +56,14 @@ export const useCreateSubscription = () => { const sender = new WalletMessageSender(api, selectedWallet, signer); const encoder = new SubscriptionEncoder(selectedWallet); - const beneficiary = Address.parse(admin); - const subscriptionId = subscription_id; - - const extensionAddress = encoder.getExtensionAddress({ - beneficiary, - subscriptionId - }); - const outgoingMsg = await encoder.encodeCreateSubscriptionV2( prepareSubscriptionParamsForEncoder(subscriptionParams, metaEncryptionMap) ); - if (!extensionAddress.equals(Address.parse(contract))) { - throw new Error('Contract extension addresses do not match!'); - } - - await sender.send(outgoingMsg); + const boc = await sender.send(outgoingMsg); await client.invalidateQueries([QueryKey.metaEncryptionData]); - return true; + return boc; }); }; From 17e3c54a54a492f48fc10ba780947e0b9200f05b Mon Sep 17 00:00:00 2001 From: siandreev Date: Mon, 27 Oct 2025 13:45:19 +0100 Subject: [PATCH 02/13] fix: add unsubscribe legacy plugin banners for mobile & desktop layouts --- apps/desktop/src/app/App.tsx | 2 + .../src/app/app-content/WideContent.tsx | 2 + apps/web/src/AppDesktop.tsx | 4 + .../CancelLegacySubscriptionNotification.tsx | 86 +++++++++++ .../DesktopCancelLegacySubscriptionBanner.tsx | 133 ++++++++++++++++++ .../MobileCancelLegacySubscriptionBanner.tsx | 65 +++++++++ packages/uikit/src/libs/queryKey.ts | 4 +- .../mobile-pro-pages/MobileProHomePage.tsx | 2 + packages/uikit/src/pages/home/Home.tsx | 2 + packages/uikit/src/state/plugins.ts | 28 ++++ 10 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx create mode 100644 packages/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner.tsx create mode 100644 packages/uikit/src/components/legacy-plugins/MobileCancelLegacySubscriptionBanner.tsx create mode 100644 packages/uikit/src/state/plugins.ts diff --git a/apps/desktop/src/app/App.tsx b/apps/desktop/src/app/App.tsx index 234629fc3..29ec13e54 100644 --- a/apps/desktop/src/app/App.tsx +++ b/apps/desktop/src/app/App.tsx @@ -85,6 +85,7 @@ import { CryptoStrategyInstaller } from '@tonkeeper/uikit/dist/components/pro/Cr import { localesList } from '@tonkeeper/locales/localesList'; import { useAppCountryInfo } from '@tonkeeper/uikit/dist/state/country'; import { SecureWalletNotification } from '@tonkeeper/uikit/dist/components/desktop/SecureWalletNotification'; +import { DesktopCancelLegacySubscriptionBanner } from '@tonkeeper/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner'; const queryClient = new QueryClient({ defaultOptions: { @@ -426,6 +427,7 @@ const WalletContent = () => { + ); diff --git a/apps/mobile/src/app/app-content/WideContent.tsx b/apps/mobile/src/app/app-content/WideContent.tsx index d4ec1927c..731075b6f 100644 --- a/apps/mobile/src/app/app-content/WideContent.tsx +++ b/apps/mobile/src/app/app-content/WideContent.tsx @@ -37,6 +37,7 @@ import { SplashScreen } from '@capacitor/splash-screen'; import { useRealtimeUpdatesInvalidation } from '@tonkeeper/uikit/dist/hooks/realtime'; import DashboardPage from '@tonkeeper/uikit/dist/desktop-pages/dashboard'; import { SecureWalletNotification } from '@tonkeeper/uikit/dist/components/desktop/SecureWalletNotification'; +import { DesktopCancelLegacySubscriptionBanner } from '@tonkeeper/uikit/dist/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner'; const FullSizeWrapper = styled(Container)` max-width: 800px; @@ -191,6 +192,7 @@ const WalletContent = () => { + ); diff --git a/apps/web/src/AppDesktop.tsx b/apps/web/src/AppDesktop.tsx index c09122765..52ffc13c4 100644 --- a/apps/web/src/AppDesktop.tsx +++ b/apps/web/src/AppDesktop.tsx @@ -39,6 +39,9 @@ import { import { DesktopMultisigOrdersPage } from "@tonkeeper/uikit/dist/desktop-pages/multisig-orders/DesktopMultisigOrders"; import { UrlTonConnectSubscription } from "./components/UrlTonConnectSubscription"; import { useRealtimeUpdatesInvalidation } from '@tonkeeper/uikit/dist/hooks/realtime'; +import { + DesktopCancelLegacySubscriptionBanner +} from "@tonkeeper/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner"; const DesktopAccountSettingsPage = React.lazy( () => import('@tonkeeper/uikit/dist/desktop-pages/settings/DesktopAccountSettingsPage') @@ -280,6 +283,7 @@ const WalletContent = () => { + ); diff --git a/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx b/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx new file mode 100644 index 000000000..aa911dc5e --- /dev/null +++ b/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx @@ -0,0 +1,86 @@ +import { FC, useCallback, useEffect } from 'react'; +import { Notification } from '../Notification'; +import { ConfirmView } from '../transfer/ConfirmView'; +import { + useCancelSubscription, + useEstimateRemoveExtension +} from '../../hooks/blockchain/subscription'; +import { useActiveWallet } from '../../state/wallet'; +import { isStandardTonWallet, WalletVersion } from '@tonkeeper/core/dist/entries/wallet'; +import { toNano } from '@ton/core'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { useAppSdk } from '../../hooks/appSdk'; +import { useTranslation } from '../../hooks/translation'; + +export default CancelLegacySubscriptionNotification; + +export const CancelLegacySubscriptionNotification: FC<{ + onClose: () => void; + pluginAddress: string | undefined; +}> = ({ onClose, pluginAddress }) => { + return ( + + {() => + !!pluginAddress && ( + + ) + } + + ); +}; + +const destroyValue = toNano(0.05).toString(); +const assetAmount = new AssetAmount({ weiAmount: destroyValue, asset: TON_ASSET }); + +const CancelLegacySubscription: FC<{ pluginAddress: string; onClose: () => void }> = ({ + pluginAddress, + onClose +}) => { + const wallet = useActiveWallet(); + const estimation = useEstimateRemoveExtension(); + const unsubscribeMutation = useCancelSubscription(); + const sdk = useAppSdk(); + const { t } = useTranslation(); + + const unsubscribeCallback = useCallback(() => { + if (!isStandardTonWallet(wallet) || wallet.version !== WalletVersion.V4R1) { + throw new Error('Unexpected wallet is used to unsubscribe from legacy subscription'); + } + + return unsubscribeMutation + .mutateAsync({ + selectedWallet: wallet, + extensionContract: pluginAddress, + destroyValue + }) + .then(succeed => { + if (succeed) { + sdk.topMessage(t('unsubscribe_legacy_plugin_success_toast')); + } + return succeed; + }); + }, [wallet, pluginAddress, t]); + + useEffect(() => { + if (!isStandardTonWallet(wallet) || wallet.version !== WalletVersion.V4R1) { + console.error('Unexpected wallet is used to unsubscribe from legacy subscription'); + return onClose(); + } + estimation.mutate({ + selectedWallet: wallet, + extensionContract: pluginAddress, + destroyValue + }); + }, []); + + return ( + + ); +}; diff --git a/packages/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner.tsx b/packages/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner.tsx new file mode 100644 index 000000000..ff5b6b556 --- /dev/null +++ b/packages/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner.tsx @@ -0,0 +1,133 @@ +import { useWalletLegacyPlugins } from '../../state/plugins'; +import styled from 'styled-components'; +import { Body2, Label2 } from '../Text'; +import { useTranslation } from '../../hooks/translation'; +import { Button } from '../fields/Button'; +import React, { FC, Suspense, useState } from 'react'; + +const CancelLegacySubscriptionNotification = React.lazy( + () => import('./CancelLegacySubscriptionNotification') +); + +const Banner = styled.div` + position: absolute; + bottom: 16px; + left: 16px; + right: 16px; + + background: ${p => p.theme.backgroundContent}; + border-radius: ${p => p.theme.cornerSmall}; + padding: 0 16px; + display: flex; + align-items: center; + + > svg { + flex-shrink: 0; + } +`; + +const ColumnText = styled.div` + padding: 14px 16px; + display: flex; + flex-direction: column; + + ${Body2} { + color: ${p => p.theme.textSecondary}; + } +`; + +const WarnIcon = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const ButtonsBlock = styled.div` + display: flex; + gap: 8px; + margin-left: auto; +`; + +export const DesktopCancelLegacySubscriptionBanner: FC<{ className?: string }> = ({ + className +}) => { + const { data: legacyPlugins } = useWalletLegacyPlugins(); + const { t } = useTranslation(); + const [pluginToUnsubscribe, setPluginToUnsubscribe] = useState(); + + if (!legacyPlugins?.length) { + return null; + } + + return ( + <> + + + + {t('unsubscribe_legacy_plugin_banner_title')} + {t('unsubscribe_legacy_plugin_banner_subtitle')} + + + + + + + + setPluginToUnsubscribe(undefined)} + /> + + + ); +}; diff --git a/packages/uikit/src/components/legacy-plugins/MobileCancelLegacySubscriptionBanner.tsx b/packages/uikit/src/components/legacy-plugins/MobileCancelLegacySubscriptionBanner.tsx new file mode 100644 index 000000000..b0a119258 --- /dev/null +++ b/packages/uikit/src/components/legacy-plugins/MobileCancelLegacySubscriptionBanner.tsx @@ -0,0 +1,65 @@ +import { useWalletLegacyPlugins } from '../../state/plugins'; +import styled from 'styled-components'; +import { Body2, Label1 } from '../Text'; +import { useTranslation } from '../../hooks/translation'; +import { ButtonFlat } from '../fields/Button'; +import React, { FC, Suspense, useState } from 'react'; + +const CancelLegacySubscriptionNotification = React.lazy( + () => import('./CancelLegacySubscriptionNotification') +); + +const Banner = styled.div` + width: 100%; + margin: 16px; + + background: ${p => p.theme.accentOrange}; + border-radius: ${p => p.theme.cornerSmall}; + padding: 12px 16px; + display: flex; + align-items: center; + + color: ${p => p.theme.constantBlack}; +`; + +const ColumnText = styled.div` + display: flex; + flex-direction: column; + ${Body2} { + color: ${p => p.theme.textSecondary}; + } + margin-bottom: 4px; +`; + +export const MobileCancelLegacySubscriptionBanner: FC<{ className?: string }> = ({ className }) => { + const { data: legacyPlugins } = useWalletLegacyPlugins(); + const { t } = useTranslation(); + const [pluginToUnsubscribe, setPluginToUnsubscribe] = useState(); + + if (!legacyPlugins?.length) { + return null; + } + + return ( + <> + + + {t('unsubscribe_legacy_plugin_banner_title')} + {t('unsubscribe_legacy_plugin_banner_subtitle')} + + setPluginToUnsubscribe(legacyPlugins![0].address)} + > + {t('disable')} + + + + setPluginToUnsubscribe(undefined)} + /> + + + ); +}; diff --git a/packages/uikit/src/libs/queryKey.ts b/packages/uikit/src/libs/queryKey.ts index 9a9ea61ff..d5b273d1c 100644 --- a/packages/uikit/src/libs/queryKey.ts +++ b/packages/uikit/src/libs/queryKey.ts @@ -89,7 +89,9 @@ export enum QueryKey { appCountryInfo = 'appCountryInfo', trc20TrxDefaultFee = 'trc20TrxDefaultFee', - trc20FreeTransfersConfig = 'trc20FreeTransfersConfig' + trc20FreeTransfersConfig = 'trc20FreeTransfersConfig', + + legacyPlugins = 'legacyPlugins' } export enum JettonKey { diff --git a/packages/uikit/src/mobile-pro-pages/MobileProHomePage.tsx b/packages/uikit/src/mobile-pro-pages/MobileProHomePage.tsx index 8b75721a6..dad21c1f2 100644 --- a/packages/uikit/src/mobile-pro-pages/MobileProHomePage.tsx +++ b/packages/uikit/src/mobile-pro-pages/MobileProHomePage.tsx @@ -30,6 +30,7 @@ import { DesktopViewHeader, DesktopViewPageLayout } from '../components/desktop/ import { TwoFARecoveryStartedBanner } from '../components/settings/two-fa/TwoFARecoveryStartedBanner'; import { IfFeatureEnabled } from '../components/shared/IfFeatureEnabled'; import { FLAGGED_FEATURE } from '../state/tonendpoint'; +import { MobileCancelLegacySubscriptionBanner } from '../components/legacy-plugins/MobileCancelLegacySubscriptionBanner'; const MobileProHomeActionsStyled = styled(MobileProHomeActions)` margin: 0 8px 16px; @@ -104,6 +105,7 @@ export const MobileProHomePage = () => { + diff --git a/packages/uikit/src/pages/home/Home.tsx b/packages/uikit/src/pages/home/Home.tsx index 5efd32775..d7d75828d 100644 --- a/packages/uikit/src/pages/home/Home.tsx +++ b/packages/uikit/src/pages/home/Home.tsx @@ -12,6 +12,7 @@ import { usePreFetchRates } from '../../state/rates'; import { useWalletFilteredNftList } from '../../state/nft'; import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; import { FLAGGED_FEATURE, useIsFeatureEnabled } from '../../state/tonendpoint'; +import { MobileCancelLegacySubscriptionBanner } from '../../components/legacy-plugins/MobileCancelLegacySubscriptionBanner'; const HomeAssets: FC<{ assets: AssetAmount[]; @@ -41,6 +42,7 @@ const Home = () => { return ( <> + diff --git a/packages/uikit/src/state/plugins.ts b/packages/uikit/src/state/plugins.ts new file mode 100644 index 000000000..6d450b7e5 --- /dev/null +++ b/packages/uikit/src/state/plugins.ts @@ -0,0 +1,28 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from '../libs/queryKey'; +import { useActiveAccount, useActiveApi } from './wallet'; +import { WalletApi } from '@tonkeeper/core/dist/tonApiV2'; + +export const useWalletLegacyPlugins = () => { + const api = useActiveApi(); + const activeAccount = useActiveAccount(); + const wallet = activeAccount.activeTonWallet; + const isSuitableAccount = activeAccount.type === 'mnemonic' || activeAccount.type === 'mam'; + + return useQuery( + [QueryKey.legacyPlugins, isSuitableAccount, wallet.rawAddress, api], + async () => { + if (!isSuitableAccount) { + return []; + } + + const data = await new WalletApi(api.tonApiV2).getWalletInfo({ + accountId: wallet.rawAddress + }); + return data.plugins.filter(plugin => plugin.type === 'subscription_v1'); + }, + { + refetchInterval: data => (!!data?.length ? 30_000 : 0) + } + ); +}; From 0b29b70218916ba84c45e20180454636ddde0381 Mon Sep 17 00:00:00 2001 From: siandreev Date: Mon, 27 Oct 2025 16:12:51 +0100 Subject: [PATCH 03/13] fix: improve layout --- apps/desktop/src/app/App.tsx | 3 +- apps/mobile/package.json | 2 +- apps/web/src/AppDesktop.tsx | 3 +- packages/locales/src/tonkeeper-web/ar.json | 4 ++ packages/locales/src/tonkeeper-web/bg.json | 4 ++ packages/locales/src/tonkeeper-web/bn.json | 4 ++ packages/locales/src/tonkeeper-web/de.json | 4 ++ packages/locales/src/tonkeeper-web/en.json | 4 ++ packages/locales/src/tonkeeper-web/es.json | 4 ++ packages/locales/src/tonkeeper-web/fa.json | 4 ++ packages/locales/src/tonkeeper-web/fr.json | 4 ++ packages/locales/src/tonkeeper-web/hi.json | 4 ++ packages/locales/src/tonkeeper-web/id.json | 4 ++ packages/locales/src/tonkeeper-web/it.json | 4 ++ packages/locales/src/tonkeeper-web/pa.json | 4 ++ packages/locales/src/tonkeeper-web/pt.json | 4 ++ packages/locales/src/tonkeeper-web/ru-RU.json | 4 ++ packages/locales/src/tonkeeper-web/tr-TR.json | 4 ++ packages/locales/src/tonkeeper-web/uk.json | 7 ++++ packages/locales/src/tonkeeper-web/uz.json | 4 ++ packages/locales/src/tonkeeper-web/vi.json | 4 ++ .../locales/src/tonkeeper-web/zh-Hans-CN.json | 4 ++ .../locales/src/tonkeeper-web/zh-Hant.json | 4 ++ packages/locales/src/tonkeeper/uk.json | 19 ++++++---- .../CancelLegacySubscriptionNotification.tsx | 19 ++++++---- .../DesktopCancelLegacySubscriptionBanner.tsx | 16 +++++--- .../MobileCancelLegacySubscriptionBanner.tsx | 38 ++++++++++--------- .../src/components/transfer/ConfirmView.tsx | 9 ++--- .../mobile-pro-pages/MobileProHomePage.tsx | 9 ++++- packages/uikit/src/state/pro.ts | 10 +++++ 30 files changed, 165 insertions(+), 46 deletions(-) diff --git a/apps/desktop/src/app/App.tsx b/apps/desktop/src/app/App.tsx index 29ec13e54..688be7552 100644 --- a/apps/desktop/src/app/App.tsx +++ b/apps/desktop/src/app/App.tsx @@ -85,7 +85,7 @@ import { CryptoStrategyInstaller } from '@tonkeeper/uikit/dist/components/pro/Cr import { localesList } from '@tonkeeper/locales/localesList'; import { useAppCountryInfo } from '@tonkeeper/uikit/dist/state/country'; import { SecureWalletNotification } from '@tonkeeper/uikit/dist/components/desktop/SecureWalletNotification'; -import { DesktopCancelLegacySubscriptionBanner } from '@tonkeeper/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner'; +import { DesktopCancelLegacySubscriptionBanner } from '@tonkeeper/uikit/dist/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner'; const queryClient = new QueryClient({ defaultOptions: { @@ -226,6 +226,7 @@ const WalletLayout = styled.div` `; const WalletLayoutBody = styled.div` + position: relative; flex: 1; display: flex; max-height: calc(100% - ${desktopHeaderContainerHeight}); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index cc9510eed..c82ea3f56 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@tonkeeper/mobile", - "version": "4.3.1", + "version": "4.3.2", "license": "Apache-2.0", "description": "Your tablet wallet on The Open Network", "type": "module", diff --git a/apps/web/src/AppDesktop.tsx b/apps/web/src/AppDesktop.tsx index 52ffc13c4..49005701d 100644 --- a/apps/web/src/AppDesktop.tsx +++ b/apps/web/src/AppDesktop.tsx @@ -41,7 +41,7 @@ import { UrlTonConnectSubscription } from "./components/UrlTonConnectSubscriptio import { useRealtimeUpdatesInvalidation } from '@tonkeeper/uikit/dist/hooks/realtime'; import { DesktopCancelLegacySubscriptionBanner -} from "@tonkeeper/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner"; +} from "@tonkeeper/uikit/dist/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner"; const DesktopAccountSettingsPage = React.lazy( () => import('@tonkeeper/uikit/dist/desktop-pages/settings/DesktopAccountSettingsPage') @@ -140,6 +140,7 @@ const WalletLayout = styled.div` `; const WalletLayoutBody = styled.div` + position: relative; flex: 1; display: flex; max-height: calc(100% - ${desktopHeaderContainerHeight}); diff --git a/packages/locales/src/tonkeeper-web/ar.json b/packages/locales/src/tonkeeper-web/ar.json index 969463692..6af24b4fc 100644 --- a/packages/locales/src/tonkeeper-web/ar.json +++ b/packages/locales/src/tonkeeper-web/ar.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "بعد حذف جميع المحافظ، ستصبح ميزات PRO غير متاحة. سيتم استعادتها بعد تسجيل الدخول إلى محفظة تحتوي على اشتراك نشط.", "deleting_wallet_warning": "بعد حذف المحفظة، ستصبح ميزات PRO غير متاحة. سيتم استعادتها بعد تسجيل الدخول باستخدام محفظة تحتوي على اشتراك نشط.", "desktop_only": "سطح المكتب فقط", + "disable": "تعطيل", "disabled": "معطل", "disconnect": "الغاء ربط", "disconnect_all_apps": "الغاء ربط كل التطبيقات", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "رمز الاستجابة السريعة (QR) غير المتوقع", "unknown_operation": "عملية غير معروفة", "Unlock": "فتح القفل", + "unsubscribe_legacy_plugin_banner_subtitle": "قد يستمر في سحب الأموال من محفظتك. عَطِّلْهُ لتفادي دفعات إضافية.", + "unsubscribe_legacy_plugin_banner_title": "لديك {count} اشتراك منتهي من @donate", + "unsubscribe_legacy_plugin_success_toast": "سيتم تأكيد إلغاء الاشتراك في البلوكشين خلال بضع دقائق", "unsupported_two_fa": "لا يتم دعم المحافظ التي تستخدم التحقق بخطوتين مؤقتًا. استخدم محفظة بدون التحقق بخطوتين أو قم بتعطيلها.\n", "unused": { "Edit_jettons": "تعديل", diff --git a/packages/locales/src/tonkeeper-web/bg.json b/packages/locales/src/tonkeeper-web/bg.json index e9ce902b2..db7161bf3 100644 --- a/packages/locales/src/tonkeeper-web/bg.json +++ b/packages/locales/src/tonkeeper-web/bg.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "След изтриване на всички портфейли, PRO функциите ще станат недостъпни. Те ще бъдат възстановени след влизане в портфейл с активен абонамент.", "deleting_wallet_warning": "След изтриване на портфейла PRO функциите ще станат недостъпни. Те ще бъдат възстановени след влизане с портфейл с активен абонамент.", "desktop_only": "Само за настолен компютър", + "disable": "Деактивиране", "disabled": "Деактивирано", "disconnect": "Прекъсване", "disconnect_all_apps": "Прекъснете всички приложения", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Неочакван QR код", "unknown_operation": "Неизвестна операция", "Unlock": "Отключи", + "unsubscribe_legacy_plugin_banner_subtitle": "Може да продължи да таксува портфейла ви. Деактивирайте го, за да избегнете допълнителни плащания.", + "unsubscribe_legacy_plugin_banner_title": "Имате {count} изтекъл абонамент от @donate", + "unsubscribe_legacy_plugin_success_toast": "Отмяната на абонамента ще бъде потвърдена в блокчейна след няколко минути", "unsupported_two_fa": "Портфейли с 2FA временно не се поддържат. Използвайте портфейл без 2FA или го деактивирайте.\n", "unused": { "add_wallet_existing_multisig_description": "%{number} портфейла, управлявани от вашия списък с портфейли", diff --git a/packages/locales/src/tonkeeper-web/bn.json b/packages/locales/src/tonkeeper-web/bn.json index 9fc971535..f0f4acbb3 100644 --- a/packages/locales/src/tonkeeper-web/bn.json +++ b/packages/locales/src/tonkeeper-web/bn.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "সমস্ত ওয়ালেট মুছে ফেলার পর, PRO বৈশিষ্ট্যগুলি আর উপলব্ধ থাকবে না। সক্রিয় সাবস্ক্রিপশন থাকা কোনও ওয়ালেটে লগ ইন করলে এগুলি পুনরুদ্ধার হবে।", "deleting_wallet_warning": "ওয়ালেট মুছে ফেললে, PRO বৈশিষ্ট্যগুলি আর ব্যবহারযোগ্য থাকবে না। একটি সক্রিয় সাবস্ক্রিপশন থাকা ওয়ালেট দিয়ে লগ ইন করলে এগুলো পুনরুদ্ধার করা হবে।", "desktop_only": "শুধুমাত্র ডেস্কটপ", + "disable": "নিষ্ক্রিয়", "disabled": "নিষ্ক্রিয়", "disconnect": "সংযোগ বিচ্ছিন্ন করুন", "disconnect_all_apps": "সমস্ত অ্যাপস সংযোগ বিচ্ছিন্ন করুন", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "অপ্রত্যাশিত QR কোড", "unknown_operation": "অজানা অপারেশন", "Unlock": "আনলক করুন", + "unsubscribe_legacy_plugin_banner_subtitle": "এটি আপনার ওয়ালেট থেকে চার্জ করতে পারে। পুনরায় অর্থপ্রদান এড়াতে এটি নিষ্ক্রিয় করুন।", + "unsubscribe_legacy_plugin_banner_title": "আপনার @donate থেকে {count} মেয়াদোত্তীর্ণ সদস্যতা আছে", + "unsubscribe_legacy_plugin_success_toast": "গ্রাহকতা বাতিল কয়েক মিনিটের মধ্যে ব্লকচেইনে নিশ্চিত হবে", "unsupported_two_fa": "২এফএ ওয়ালেটগুলো আপাতত সমর্থিত নয়। ২এফএ ছাড়া একটি ওয়ালেট ব্যবহার করুন বা এটিকে নিষ্ক্রিয় করুন।\n", "unused": { "Edit_jettons": "সম্পাদনা করুন", diff --git a/packages/locales/src/tonkeeper-web/de.json b/packages/locales/src/tonkeeper-web/de.json index 8db7e2c4c..3d17ab9d6 100644 --- a/packages/locales/src/tonkeeper-web/de.json +++ b/packages/locales/src/tonkeeper-web/de.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Nach dem Löschen aller Wallets sind die PRO-Funktionen nicht mehr verfügbar. Sie werden wiederhergestellt, nachdem Sie sich mit einer Wallet mit aktivem Abonnement angemeldet haben.", "deleting_wallet_warning": "Nach dem Löschen der Wallet sind die PRO-Funktionen nicht mehr verfügbar. Sie werden wiederhergestellt, sobald Sie sich mit einer Wallet mit aktivem Abonnement anmelden.", "desktop_only": "Nur für Desktop", + "disable": "Deaktivieren", "disabled": "Deaktiviert", "disconnect": "Trennen", "disconnect_all_apps": "Alle Apps trennen", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Unerwarteter QR-Code", "unknown_operation": "Unbekannte Operation", "Unlock": "Entsperren", + "unsubscribe_legacy_plugin_banner_subtitle": "Es könnte weiterhin Ihre Geldbörse belasten. Deaktivieren Sie es, um weitere Zahlungen zu vermeiden.", + "unsubscribe_legacy_plugin_banner_title": "Sie haben {count} abgelaufene Abonnement von @donate", + "unsubscribe_legacy_plugin_success_toast": "Das Abonnement wird in wenigen Minuten in der Blockchain bestätigt werden", "unsupported_two_fa": "2FA-Wallets werden vorübergehend nicht unterstützt. Verwenden Sie ein Wallet ohne 2FA oder deaktivieren Sie es.\n", "unused": { "add_wallet_existing_multisig_description": "%{number} Wallets werden von Ihrer Wallet-Liste verwaltet", diff --git a/packages/locales/src/tonkeeper-web/en.json b/packages/locales/src/tonkeeper-web/en.json index 90b0828ed..b73de43bc 100644 --- a/packages/locales/src/tonkeeper-web/en.json +++ b/packages/locales/src/tonkeeper-web/en.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "After deleting all wallets, PRO features will become unavailable. They will be restored after logging in with a wallet that has an active subscription.", "deleting_wallet_warning": "After deleting the wallet, PRO features will become unavailable. They will be restored after logging in with a wallet that has an active subscription.", "desktop_only": "DESKTOP ONLY", + "disable": "Disable", "disabled": "Disabled", "disconnect": "Disconnect", "disconnect_all_apps": "Disconnect All Apps", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Unexpected QR Code", "unknown_operation": "Unknown Operation", "Unlock": "Unlock", + "unsubscribe_legacy_plugin_banner_subtitle": "It may continue charging your wallet. Disable it to avoid further payments.", + "unsubscribe_legacy_plugin_banner_title": "You have {count} expired subscription from @donate", + "unsubscribe_legacy_plugin_success_toast": "Subscription cancellation will be confirmed in blockchain in few minutes ", "unsupported_two_fa": "2FA wallets temporarily aren’t supported. Use a wallet without 2FA or disable it.\n", "unused": { "1_month": "1 month", diff --git a/packages/locales/src/tonkeeper-web/es.json b/packages/locales/src/tonkeeper-web/es.json index 8fcaa3a28..274b06bb1 100644 --- a/packages/locales/src/tonkeeper-web/es.json +++ b/packages/locales/src/tonkeeper-web/es.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Después de eliminar todas las billeteras, las funciones PRO dejarán de estar disponibles. Se restaurarán después de iniciar sesión con una billetera que tenga una suscripción activa.", "deleting_wallet_warning": "Después de eliminar la billetera, las funciones PRO dejarán de estar disponibles. Se restaurarán después de iniciar sesión con una billetera que tenga una suscripción activa.", "desktop_only": "Solo para escritorio", + "disable": "Desactivar", "disabled": "Desactivado", "disconnect": "Desconectar", "disconnect_all_apps": "Desconectar Todas las Aplicaciones", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Código QR inesperado", "unknown_operation": "Operación desconocida", "Unlock": "Desbloquear", + "unsubscribe_legacy_plugin_banner_subtitle": "Puede seguir cobrando a su cartera. Desactívelo para evitar más pagos.", + "unsubscribe_legacy_plugin_banner_title": "Tienes {count} suscripción expirada de @donate", + "unsubscribe_legacy_plugin_success_toast": "La cancelación de la suscripción será confirmada en blockchain en unos minutos", "unsupported_two_fa": "Las carteras con 2FA no son soportadas temporalmente. Utiliza una cartera sin 2FA o desactívala.\n", "unused": { "Edit_jettons": "Editar", diff --git a/packages/locales/src/tonkeeper-web/fa.json b/packages/locales/src/tonkeeper-web/fa.json index 64a94f72c..f36f99357 100644 --- a/packages/locales/src/tonkeeper-web/fa.json +++ b/packages/locales/src/tonkeeper-web/fa.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "پس از حذف همه کیف‌پول‌ها، قابلیت‌های PRO دیگر در دسترس نخواهند بود. این قابلیت‌ها پس از ورود به کیف‌پولی با اشتراک فعال بازیابی می‌شوند.", "deleting_wallet_warning": "پس از حذف کیف پول، امکانات PRO دیگر در دسترس نخواهند بود. این امکانات پس از ورود با کیف پولی که اشتراک فعال دارد، بازیابی خواهند شد.", "desktop_only": "فقط دسکتاپ", + "disable": "غیرفعال کردن", "disabled": "غیرفعال", "disconnect": "قطع کردن", "disconnect_all_apps": "قطع کردن تمام اپلیکیشن‌ها", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "کد QR نامعتبر", "unknown_operation": "عملیات ناشناخته", "Unlock": "باز کردن", + "unsubscribe_legacy_plugin_banner_subtitle": "این ممکن است همچنان کیف پول شما را شارژ کند. برای جلوگیری از پرداخت‌های بیشتر آن را غیرفعال کنید.", + "unsubscribe_legacy_plugin_banner_title": "شما {count} اشتراک منقضی شده از @donate دارید", + "unsubscribe_legacy_plugin_success_toast": "تأیید لغو اشتراک در بلاکچین تا چند دقیقه دیگر انجام خواهد شد", "unsupported_two_fa": "کیف پول‌های دارای 2FA به‌طور موقت پشتیبانی نمی‌شوند. از کیف پولی بدون 2FA استفاده کنید یا آن را غیرفعال نمایید.\n", "unused": { "Edit_jettons": "ویرایش", diff --git a/packages/locales/src/tonkeeper-web/fr.json b/packages/locales/src/tonkeeper-web/fr.json index 32454aa31..dc4c7851b 100644 --- a/packages/locales/src/tonkeeper-web/fr.json +++ b/packages/locales/src/tonkeeper-web/fr.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Après avoir supprimé tous les portefeuilles, les fonctionnalités PRO deviendront indisponibles. Elles seront rétablies après connexion à un portefeuille avec un abonnement actif.", "deleting_wallet_warning": "Après avoir supprimé le portefeuille, les fonctionnalités PRO deviendront indisponibles. Elles seront restaurées après connexion avec un portefeuille ayant un abonnement actif.", "desktop_only": "Uniquement sur ordinateur", + "disable": "Désactiver", "disabled": "Désactivé", "disconnect": "Déconnecter", "disconnect_all_apps": "Déconnecter toutes les applications", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "QR code inattendu", "unknown_operation": "Opération inconnue", "Unlock": "Déverrouiller", + "unsubscribe_legacy_plugin_banner_subtitle": "Il peut continuer à facturer votre portefeuille. Désactivez-le pour éviter d'autres paiements.", + "unsubscribe_legacy_plugin_banner_title": "Vous avez {count} abonnement expiré de @donate", + "unsubscribe_legacy_plugin_success_toast": "L'annulation de l'abonnement sera confirmée sur la blockchain dans quelques minutes", "unsupported_two_fa": "Les portefeuilles 2FA ne sont temporairement pas supportés. Utilisez un portefeuille sans 2FA ou désactivez-le.\n", "update": "Mettre à jour", "update_click_to_install": "Cliquez pour installer la dernière version", diff --git a/packages/locales/src/tonkeeper-web/hi.json b/packages/locales/src/tonkeeper-web/hi.json index 4a4500d13..0eba0a2fd 100644 --- a/packages/locales/src/tonkeeper-web/hi.json +++ b/packages/locales/src/tonkeeper-web/hi.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "सभी वॉलेट हटाने के बाद, PRO फीचर्स उपलब्ध नहीं रहेंगे। वे एक सक्रिय सदस्यता वाले वॉलेट से लॉग इन करने पर पुनः उपलब्ध हो जाएंगे।", "deleting_wallet_warning": "वॉलेट हटाने के बाद PRO सुविधाएँ उपलब्ध नहीं रहेंगी। यह सुविधाएँ एक सक्रिय सदस्यता वाले वॉलेट के साथ लॉगिन करने पर पुनः सक्षम हो जाएंगी।", "desktop_only": "केवल डेस्कटॉप", + "disable": "अक्षम", "disabled": "अक्षम", "disconnect": "डिस्कनेक्ट करें", "disconnect_all_apps": "सभी ऐप्स डिस्कनेक्ट करें", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "अप्रत्याशित क्यूआर कोड", "unknown_operation": "अज्ञात ऑपरेशन", "Unlock": "अनलॉक करें", + "unsubscribe_legacy_plugin_banner_subtitle": "\"यह आपकी वॉलेट से चार्ज करना जारी रख सकता है। आगे भुगतान से बचने के लिए इसे निष्क्रिय करें।\"", + "unsubscribe_legacy_plugin_banner_title": "आपके पास @donate से {count} समाप्त सदस्यता है", + "unsubscribe_legacy_plugin_success_toast": "सदस्यता रद्दीकरण की पुष्टि ब्लॉकचेन में कुछ ही मिनटों में होगी", "unsupported_two_fa": "2FA वॉलेट्स अस्थायी रूप से समर्थित नहीं हैं। बिना 2FA के वॉलेट का उपयोग करें या इसे अक्षम करें।\n", "unused": { "Edit_jettons": "संपादित करें", diff --git a/packages/locales/src/tonkeeper-web/id.json b/packages/locales/src/tonkeeper-web/id.json index dc7f5c69e..f7f7b0b8b 100644 --- a/packages/locales/src/tonkeeper-web/id.json +++ b/packages/locales/src/tonkeeper-web/id.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Setelah menghapus semua dompet, fitur PRO akan menjadi tidak tersedia. Fitur tersebut akan dipulihkan setelah login dengan dompet yang memiliki langganan aktif.", "deleting_wallet_warning": "Setelah dompet dihapus, fitur PRO akan menjadi tidak tersedia. Fitur tersebut akan dipulihkan setelah masuk dengan dompet yang memiliki langganan aktif.", "desktop_only": "Hanya untuk Desktop", + "disable": "Nonaktifkan", "disabled": "Dinonaktifkan", "disconnect": "Putuskan sambungan", "disconnect_all_apps": "Putuskan Sambungan Semua Aplikasi", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Kode QR tidak sesuai", "unknown_operation": "Operasi tidak dikenal", "Unlock": "Membuka Kunci", + "unsubscribe_legacy_plugin_banner_subtitle": "Ini dapat terus mengenakan biaya ke dompet Anda. Nonaktifkan untuk menghindari pembayaran lebih lanjut.", + "unsubscribe_legacy_plugin_banner_title": "Anda memiliki {count} langganan kedaluwarsa dari @donate", + "unsubscribe_legacy_plugin_success_toast": "Pembatalan langganan akan dikonfirmasi di blockchain dalam beberapa menit", "unsupported_two_fa": "Dompet 2FA sementara tidak didukung. Gunakan dompet tanpa 2FA atau nonaktifkan.\n", "unused": { "Edit_jettons": "Edit", diff --git a/packages/locales/src/tonkeeper-web/it.json b/packages/locales/src/tonkeeper-web/it.json index bbc90f824..952d05e30 100644 --- a/packages/locales/src/tonkeeper-web/it.json +++ b/packages/locales/src/tonkeeper-web/it.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Dopo aver eliminato tutti i wallet, le funzionalità PRO non saranno più disponibili. Verranno ripristinate dopo l’accesso con un wallet che dispone di un abbonamento attivo.", "deleting_wallet_warning": "Dopo aver eliminato il portafoglio, le funzionalità PRO non saranno più disponibili. Verranno ripristinate dopo l'accesso con un portafoglio che ha un abbonamento attivo.", "desktop_only": "Solo su desktop", + "disable": "Disabilitare", "disabled": "Disabilitato", "disconnect": "Disconnetti", "disconnect_all_apps": "Disconnetti tutte le app", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Codice QR inatteso", "unknown_operation": "Operazione sconosciuta", "Unlock": "Sblocca", + "unsubscribe_legacy_plugin_banner_subtitle": "Potrebbe continuare a prelevare denaro dal tuo portafoglio. Disattivalo per evitare ulteriori pagamenti.", + "unsubscribe_legacy_plugin_banner_title": "Hai {count} abbonamento scaduto da @donate", + "unsubscribe_legacy_plugin_success_toast": "La cancellazione dell'abbonamento sarà confermata sulla blockchain entro pochi minuti", "unsupported_two_fa": "I portafogli 2FA temporaneamente non sono supportati. Usa un portafoglio senza 2FA o disabilitalo.\n", "update": "Aggiorna", "update_click_to_install": "Clicca per installare la versione più recente", diff --git a/packages/locales/src/tonkeeper-web/pa.json b/packages/locales/src/tonkeeper-web/pa.json index 957c811eb..5f37a515f 100644 --- a/packages/locales/src/tonkeeper-web/pa.json +++ b/packages/locales/src/tonkeeper-web/pa.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "ਸਾਰੇ ਵਾਲਿਟ ਮਿਟਾਉਣ ਤੋਂ ਬਾਅਦ, PRO ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਉਪਲੱਬਧ ਨਹੀਂ ਰਹਿਣਗੀਆਂ। ਇਹ ਉਹ ਦੁਬਾਰਾ ਤਦ ਉਪਲੱਬਧ ਹੋਣਗੀਆਂ ਜਦੋਂ ਤੁਸੀਂ ਇੱਕ ਸਰਗਰਮ ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਵਾਲੇ ਵਾਲਿਟ ਨਾਲ ਲੌਗ ਇਨ ਕਰਦੇ ਹੋ।", "deleting_wallet_warning": "ਵਾਲਟ ਨੂੰ ਹਟਾਉਣ ਤੋਂ ਬਾਅਦ, PRO ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਉਪਲਬਧ ਨਹੀਂ ਰਹਿਣਗੀਆਂ। ਇਹਾਂ ਨੂੰ ਇੱਕ ਸਰਗਰਮ ਸਭਸਕ੍ਰਿਪਸ਼ਨ ਵਾਲੇ ਵਾਲਟ ਨਾਲ ਲਾਗਇਨ ਕਰਨ ਉਪਰੰਤ ਮੁੜ ਬਹਾਲ ਕਰ ਦਿੱਤਾ ਜਾਵੇਗਾ।", "desktop_only": "ਕੇਵਲ ਡੈਸਕਟਾਪ", + "disable": "ਅਣਚਾਲੂ", "disabled": "ਅਸਮਰਥ", "disconnect": "ਡਿਸਕਨੈਕਟ ਕਰੋ", "disconnect_all_apps": "ਸਭ ਐਪਸ ਦਾ ਕੁਨੈਕਸ਼ਨ ਤੋੜੋ", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "ਅਣਪੇक्षित QR ਕੋਡ", "unknown_operation": "ਅਣਜਾਣ ਓਪਰੇਸ਼ਨ", "Unlock": "ਲਾਕ ਖੋਲ੍ਹੋ", + "unsubscribe_legacy_plugin_banner_subtitle": "ਇਹ ਤੁਹਾਡੇ ਵੌਲੇਟ ਨੂੰ ਚਾਰਜ ਕਰਦਾ ਰਹਿ ਸਕਦਾ ਹੈ। ਹੋਰ ਭੁਗਤਾਨਾਂ ਤੋਂ ਬਚਣ ਲਈ ਇਸਨੂੰ ਬੰਦ ਕਰੋ।", + "unsubscribe_legacy_plugin_banner_title": "ਤੁਹਾਡੇ ਕੋਲ {count} ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਮਿਆਦ ਪੁੱਗ ਚੁੱਕਾ ਹੈ @donate ਤੋਂ", + "unsubscribe_legacy_plugin_success_toast": "ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਰੱਦ ਕੀਤੀ ਜਾਣ ਦੀ ਪੁਸ਼ਟੀ ਬਲੌਕਚੇਨ ਵਿੱਚ ਕੁਝ ਮਿੰਟਾਂ ਵਿੱਚ ਕੀਤੀ ਜਾਵੇਗੀ", "unsupported_two_fa": "2FA ਬਟੂਆਂ ਨੂੰ ਅਸਥਾਈ ਤੌਰ ਤੇ ਸਮਰਥਨ ਨਹੀਂ ਕੀਤਾ ਗਿਆ।\n2FA ਤੋਂ ਬਿਨਾ ਬਟੂਆ ਵਰਤੋਂ ਵਿੱਚ ਲਿਆਵੋ ਜਾਂ ਇਸਨੂੰ ਬੰਦ ਕਰੋ।\n", "update": "ਅੱਪਡੇਟ ਕਰੋ", "update_click_to_install": "ਆਖਰੀ ਸੰਸਕਰਣ ਨੂੰ ਇੰਸਟਾਲ ਕਰਨ ਲਈ ਕਲਿੱਕ ਕਰੋ", diff --git a/packages/locales/src/tonkeeper-web/pt.json b/packages/locales/src/tonkeeper-web/pt.json index e6bfec081..a7e2b1809 100644 --- a/packages/locales/src/tonkeeper-web/pt.json +++ b/packages/locales/src/tonkeeper-web/pt.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Após excluir todas as carteiras, os recursos PRO ficarão indisponíveis. Eles serão restaurados após o login com uma carteira que possua uma assinatura ativa.", "deleting_wallet_warning": "Após excluir a carteira, os recursos PRO ficarão indisponíveis. Eles serão restaurados após o login com uma carteira que tenha uma assinatura ativa.", "desktop_only": "Somente para desktop", + "disable": "Desativar", "disabled": "Desativado", "disconnect": "Desconectar", "disconnect_all_apps": "Desconectar todos os aplicativos", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Código QR inesperado", "unknown_operation": "Operação desconhecida", "Unlock": "Desbloquear", + "unsubscribe_legacy_plugin_banner_subtitle": "Ele pode continuar debitando sua carteira. Desative-o para evitar cobranças futuras.", + "unsubscribe_legacy_plugin_banner_title": "Você tem {count} assinatura expirada de @doar", + "unsubscribe_legacy_plugin_success_toast": "O cancelamento da assinatura será confirmado no blockchain em poucos minutos", "unsupported_two_fa": "Carteiras 2FA temporariamente não são suportadas. Utilize uma carteira sem 2FA ou desative-o.\n", "update": "Atualizar", "update_click_to_install": "Clique para instalar a versão mais recente", diff --git a/packages/locales/src/tonkeeper-web/ru-RU.json b/packages/locales/src/tonkeeper-web/ru-RU.json index 2ea61b132..7f48f1a21 100644 --- a/packages/locales/src/tonkeeper-web/ru-RU.json +++ b/packages/locales/src/tonkeeper-web/ru-RU.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "После удаления всех кошельков PRO-функции станут недоступны. Они вернутся после входа в кошелёк с активной подпиской.", "deleting_wallet_warning": "После удаления кошелька PRO-функции станут недоступны. Они вернутся после входа в кошелёк с активной подпиской.", "desktop_only": "ТОЛЬКО ПК", + "disable": "Отключить", "disabled": "Отключено", "disconnect": "Отключить", "disconnect_all_apps": "Отключите все приложения", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Неожиданный QR-код", "unknown_operation": "Неизвестная операция", "Unlock": "Разблокировать", + "unsubscribe_legacy_plugin_banner_subtitle": "Он может продолжать списывать средства. Отключите его, чтобы избежать дальнейших платежей.", + "unsubscribe_legacy_plugin_banner_title": "У вас {count} истекшая подписка на @donate", + "unsubscribe_legacy_plugin_success_toast": "Отмена подписки будет подтверждена в блокчейне через несколько минут", "unsupported_two_fa": "Кошельки с двухфакторной аутентификацией временно не поддерживаются. Используйте кошелек без 2FA или отключите ее.\n", "unused": { "1_month": "1 месяц", diff --git a/packages/locales/src/tonkeeper-web/tr-TR.json b/packages/locales/src/tonkeeper-web/tr-TR.json index 9eb7c8fba..0a595b79b 100644 --- a/packages/locales/src/tonkeeper-web/tr-TR.json +++ b/packages/locales/src/tonkeeper-web/tr-TR.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Tüm cüzdanlar silindikten sonra, PRO özellikler kullanılamaz hale gelir. Aktif aboneliği olan bir cüzdanla giriş yaptıktan sonra tekrar kullanılabilir olacaklardır.", "deleting_wallet_warning": "Cüzdan silindikten sonra PRO özellikler kullanılamaz hale gelecektir. Aktif aboneliği olan bir cüzdanla giriş yaptıktan sonra bu özellikler geri yüklenecektir.", "desktop_only": "Yalnızca masaüstü", + "disable": "Devre Dışı Bırak", "disabled": "Devre Dışı", "disconnect": "Bağlantıyı kesin", "disconnect_all_apps": "Tüm uygulamaların bağlantısını kesin", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Beklenmeyen QR Kodu", "unknown_operation": "Bilinmeyen işlem", "Unlock": "Kilidi Aç", + "unsubscribe_legacy_plugin_banner_subtitle": "Cüzdanınızdan ücret almaya devam edebilir. Daha fazla ödeme yapılmasını önlemek için devre dışı bırakın.", + "unsubscribe_legacy_plugin_banner_title": "{count} adet süresi dolmuş aboneliğiniz var @donate", + "unsubscribe_legacy_plugin_success_toast": "Aboneliğin iptali birkaç dakika içerisinde blok zincirde onaylanacaktır", "unsupported_two_fa": "2FA cüzdanlar geçici olarak desteklenmiyor. 2FA olmayan bir cüzdan kullanın veya devre dışı bırakın.", "unused": { "Edit_jettons": "Düzenle", diff --git a/packages/locales/src/tonkeeper-web/uk.json b/packages/locales/src/tonkeeper-web/uk.json index 8afe877a4..94cbf6627 100644 --- a/packages/locales/src/tonkeeper-web/uk.json +++ b/packages/locales/src/tonkeeper-web/uk.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Після видалення всіх гаманців функції PRO стануть недоступними. Вони будуть відновлені після входу в гаманець з активною підпискою.", "deleting_wallet_warning": "Після видалення гаманця функції PRO стануть недоступними. Вони будуть відновлені після входу в гаманець з активною підпискою.", "desktop_only": "Тільки для ПК", + "disable": "Вимкнути", "disabled": "Вимкнено", "disconnect": "Відключити", "disconnect_all_apps": "Відключіть усі додатки", @@ -693,8 +694,14 @@ "Unexpected_QR_Code": "Неочікуваний QR-код", "unknown_operation": "Невідома операція", "Unlock": "Розблокувати", + "unsubscribe_legacy_plugin_banner_subtitle": "Він може продовжити стягувати кошти з вашого рахунку. Вимкніть, щоб уникнути додаткових платежів.", + "unsubscribe_legacy_plugin_banner_title": "У вас {count} прострочених підписок від @donate", + "unsubscribe_legacy_plugin_success_toast": "Скасування підписки буде підтверджено в блокчейні через кілька хвилин", "unsupported_two_fa": "Гаманці з двофакторною автентифікацією тимчасово не підтримуються. Використовуйте гаманець без 2FA або вимкніть його.\n", "unused": { + "1_month": "1 місяць", + "1_year": "1 рік", + "add_wallet_existing_multisig_title": "Існуючий гаманець Multisig", "Edit_jettons": "Редагувати", "Enable_storing_config": "Збереження конфігурації", "ge_header_history": "Історія", diff --git a/packages/locales/src/tonkeeper-web/uz.json b/packages/locales/src/tonkeeper-web/uz.json index 257f049fa..38aa8a817 100644 --- a/packages/locales/src/tonkeeper-web/uz.json +++ b/packages/locales/src/tonkeeper-web/uz.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Barcha hamyonlarni o‘chirib tashlaganingizdan so‘ng PRO funksiyalari ishlamaydi. Faol obunasi bor hamyon bilan tizimga kirganingizdan so‘ng ular tiklanadi.", "deleting_wallet_warning": "Hamyon o‘chirib tashlangandan so‘ng PRO funksiyalari mavjud bo‘lmaydi. Ular faollashtirilgan obunaga ega hamyon bilan tizimga kirilgandan so‘ng qayta tiklanadi.", "desktop_only": "Faqat kompyuterda", + "disable": "O‘chirish", "disabled": "O‘chirilgan", "disconnect": "Ochirish", "disconnect_all_apps": "Barcha ilovalarni o'chirish", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Kutilmagan QR kodi", "unknown_operation": "Nomaʼlum operatsiya", "Unlock": "Qulfni ochish", + "unsubscribe_legacy_plugin_banner_subtitle": "U bu sizning hamyoningizdan to'lovni davom ettirish mumkin. Kelgusida to'lovlardan qochish uchun uni o'chiring.", + "unsubscribe_legacy_plugin_banner_title": "Sizda @donate'dan {count} eskirgan obuna mavjud", + "unsubscribe_legacy_plugin_success_toast": "Obunani bekor qilish blokcheynda bir necha daqiqada tasdiqlanadi", "unsupported_two_fa": "Ikki faktorli autentifikatsiya hamyonlari vaqtincha qo'llab-quvvatlanmaydi. Ikkita autentifikatsiyasiz hamyon ishlating yoki uni o'chiring.\n", "unused": { "add_wallet_existing_multisig_description": "%{number} hamyonlar sizning hamyon ro'yxatingiz tomonidan boshqariladi", diff --git a/packages/locales/src/tonkeeper-web/vi.json b/packages/locales/src/tonkeeper-web/vi.json index d251688a0..9da38a7b5 100644 --- a/packages/locales/src/tonkeeper-web/vi.json +++ b/packages/locales/src/tonkeeper-web/vi.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "Sau khi xóa tất cả ví, các tính năng PRO sẽ không khả dụng. Chúng sẽ được khôi phục sau khi đ��ng nhập bằng ví có đăng ký còn hiệu lực.", "deleting_wallet_warning": "Sau khi xóa ví, các tính năng PRO sẽ không còn khả dụng. Chúng sẽ được khôi phục sau khi đăng nhập bằng ví có đăng ký đang hoạt động.", "desktop_only": "Chỉ dành cho máy tính để bàn", + "disable": "Tắt", "disabled": "Đã tắt", "disconnect": "Ngắt kết nối", "disconnect_all_apps": "Ngắt kết nối tất cả ứng dụng", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "Mã QR không mong đợi", "unknown_operation": "Hoạt động không xác định", "Unlock": "Mở khóa", + "unsubscribe_legacy_plugin_banner_subtitle": "Nó có thể tiếp tục tính phí ví của bạn. Vô hiệu hóa nó để tránh các khoản thanh toán thêm.", + "unsubscribe_legacy_plugin_banner_title": "Bạn có {count} đăng ký hết hạn từ @donate", + "unsubscribe_legacy_plugin_success_toast": "Việc hủy đăng ký sẽ được xác nhận trên blockchain trong vài phút", "unsupported_two_fa": "Ví 2FA tạm thời không được hỗ trợ. Sử dụng ví không có 2FA hoặc vô hiệu hóa nó.\n", "update": "Cập nhật", "update_click_to_install": "Nhấp để cài đặt phiên bản mới nhất", diff --git a/packages/locales/src/tonkeeper-web/zh-Hans-CN.json b/packages/locales/src/tonkeeper-web/zh-Hans-CN.json index 86a6c7235..6092551e9 100644 --- a/packages/locales/src/tonkeeper-web/zh-Hans-CN.json +++ b/packages/locales/src/tonkeeper-web/zh-Hans-CN.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "删除所有钱包后,PRO功能将不可用。使用具有有效订阅的钱包登录后,这些功能将恢复。", "deleting_wallet_warning": "删除钱包后,PRO 功能将不可用。使用具有有效订阅的钱包登录后,这些功能将被恢复。", "desktop_only": "仅限桌面", + "disable": "禁用", "disabled": "已禁用", "disconnect": "断开连接", "disconnect_all_apps": "断开所有应用程序", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "非預期的二维码", "unknown_operation": "未知操作", "Unlock": "解锁", + "unsubscribe_legacy_plugin_banner_subtitle": "它 可能会继续 收费。禁用 它 以 避免 进一步 付款。", + "unsubscribe_legacy_plugin_banner_title": "您有 {count} 个已过期的订阅 从 @donate", + "unsubscribe_legacy_plugin_success_toast": "订阅取消将在区块链中确认几分钟后", "unsupported_two_fa": "暂不支持 2FA 钱包。请使用不带 2FA 的钱包或禁用 2FA。\n", "unused": { "add_wallet_existing_multisig_description": "%{number} 个钱包由您的钱包列表管理", diff --git a/packages/locales/src/tonkeeper-web/zh-Hant.json b/packages/locales/src/tonkeeper-web/zh-Hant.json index 63e11b700..ea84994b7 100644 --- a/packages/locales/src/tonkeeper-web/zh-Hant.json +++ b/packages/locales/src/tonkeeper-web/zh-Hant.json @@ -147,6 +147,7 @@ "deleting_wallets_warning": "刪除所有錢包後,PRO 功能將不可用。登入具有有效訂閱的錢包後,這些功能將恢復。", "deleting_wallet_warning": "刪除錢包後,PRO 功能將無法使用。使用具有有效訂閱的錢包登入後,這些功能將恢復。", "desktop_only": "僅限桌面", + "disable": "停用", "disabled": "已停用", "disconnect": "斷開連接", "disconnect_all_apps": "斷開連接所有應用程式", @@ -693,6 +694,9 @@ "Unexpected_QR_Code": "出現未預期的二維碼", "unknown_operation": "未知操作", "Unlock": "解鎖", + "unsubscribe_legacy_plugin_banner_subtitle": "它 可能 會 繼續 向 你 的 賬戶 收費。 禁用 它 以 避免 更多 付款。", + "unsubscribe_legacy_plugin_banner_title": "你有 {count} 個過期的訂閱來自 @donate", + "unsubscribe_legacy_plugin_success_toast": "訂閱取消將在幾分鐘內於 blockchain 中確認", "unsupported_two_fa": "2FA 錢包暫時不支援。請使用沒有 2FA 的錢包或停用 2FA。\n", "unused": { "add_wallet_existing_multisig_description": "%{number} 個錢包由您的錢包列表管理", diff --git a/packages/locales/src/tonkeeper/uk.json b/packages/locales/src/tonkeeper/uk.json index 290a9730e..4df9d87d3 100644 --- a/packages/locales/src/tonkeeper/uk.json +++ b/packages/locales/src/tonkeeper/uk.json @@ -141,7 +141,7 @@ "exchange_title": "Купити TON", "import_add_wallet": "Додати гаманець", "import_add_wallet_description": "Створіть новий гаманець або додайте існуючий.", - "import_existing_wallet": "Існуючий Гаманець", + "import_existing_wallet": "Імпортувати існуючий гаманець", "import_existing_wallet_description": "Імпорт гаманця за допомогою 24 секретних фраз відновлення", "import_new_wallet": "Новий Гаманець", "import_new_wallet_description": "Створіть новий гаманець", @@ -555,12 +555,11 @@ "description": { "empty": "Обмінюйте через Tonkeeper, надсилайте токени та NFT.", "less_10": "У вас достатньо заряду батареї для менше ніж 10 транзакцій.", - "other": { - "few": "{count} заряди", - "many": "{count} зарядів", - "one": "{count} заряд", - "other": "{count} зарядів" - } + "other": "У вас достатньо заряду акумулятора для виконання більше ніж % транзакцій.", + "other.few": "{count} заряди", + "other.many": "{count} зарядів", + "other.one": "{count} заряд", + "other.other": "{count} зарядів" }, "max_input_amount": "Максимальна сума {amount}", "ok": "OK", @@ -870,6 +869,7 @@ "info_about_inactive_title": "Неактивний контракт", "intro_item3_caption": "-", "intro_item3_title": "-", + "it_expires_on": "Термін дії закінчується", "jetton_buy_tether": "Купити USD₮", "jetton_id_copied": "ID токена скопійовано", "jetton_locked_till": "Заблоковано до {date}", @@ -1096,6 +1096,8 @@ "title": "Ви впевнені, що хочете відкрити зовнішнє посилання?" } }, + "pro_subscription_is_active": "Підписка Pro активна.", + "pro_trial_is_active": "Пробна версія активна.", "receive_address_title": "Або використовуйте адресу гаманця", "receive_copy": "Копіювати", "receiveModal": { @@ -1212,6 +1214,9 @@ "send_title": "Відправити {currency}", "settings_appearance": "Зовнішній вигляд", "settings_bank_card": "Банківська картка", + "settings_delete_alert_button": "Видалити обліковий запис та дані", + "settings_delete_alert_caption": "Ця дія призведе до видалення вашого акаунту та всіх даних з цього додатку.", + "settings_delete_alert_title": "Ви впевнені, що хочете видалити свій акаунт?", "settings_delete_signer_account": "Видалити акаунт?", "settings_delete_watch_account": "Видалити обліковий запис для перегляду?", "settings_delete_watch_account_button": "Видалити", diff --git a/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx b/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx index aa911dc5e..b05d624d5 100644 --- a/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx +++ b/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx @@ -1,6 +1,6 @@ import { FC, useCallback, useEffect } from 'react'; import { Notification } from '../Notification'; -import { ConfirmView } from '../transfer/ConfirmView'; +import { ConfirmView, ConfirmViewHeading, ConfirmViewHeadingSlot } from '../transfer/ConfirmView'; import { useCancelSubscription, useEstimateRemoveExtension @@ -13,14 +13,12 @@ import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; import { useAppSdk } from '../../hooks/appSdk'; import { useTranslation } from '../../hooks/translation'; -export default CancelLegacySubscriptionNotification; - export const CancelLegacySubscriptionNotification: FC<{ onClose: () => void; pluginAddress: string | undefined; }> = ({ onClose, pluginAddress }) => { return ( - + {() => !!pluginAddress && ( @@ -44,7 +42,7 @@ const CancelLegacySubscription: FC<{ pluginAddress: string; onClose: () => void const { t } = useTranslation(); const unsubscribeCallback = useCallback(() => { - if (!isStandardTonWallet(wallet) || wallet.version !== WalletVersion.V4R1) { + if (!isStandardTonWallet(wallet) || wallet.version !== WalletVersion.V4R2) { throw new Error('Unexpected wallet is used to unsubscribe from legacy subscription'); } @@ -63,7 +61,7 @@ const CancelLegacySubscription: FC<{ pluginAddress: string; onClose: () => void }, [wallet, pluginAddress, t]); useEffect(() => { - if (!isStandardTonWallet(wallet) || wallet.version !== WalletVersion.V4R1) { + if (!isStandardTonWallet(wallet) || wallet.version !== WalletVersion.V4R2) { console.error('Unexpected wallet is used to unsubscribe from legacy subscription'); return onClose(); } @@ -81,6 +79,13 @@ const CancelLegacySubscription: FC<{ pluginAddress: string; onClose: () => void estimation={estimation} {...unsubscribeMutation} mutateAsync={unsubscribeCallback} - /> + > + + + + ); }; diff --git a/packages/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner.tsx b/packages/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner.tsx index ff5b6b556..ff0472da4 100644 --- a/packages/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner.tsx +++ b/packages/uikit/src/components/legacy-plugins/DesktopCancelLegacySubscriptionBanner.tsx @@ -4,10 +4,9 @@ import { Body2, Label2 } from '../Text'; import { useTranslation } from '../../hooks/translation'; import { Button } from '../fields/Button'; import React, { FC, Suspense, useState } from 'react'; - -const CancelLegacySubscriptionNotification = React.lazy( - () => import('./CancelLegacySubscriptionNotification') -); +import { CancelLegacySubscriptionNotification } from './CancelLegacySubscriptionNotification'; +import { useOpenSupport } from '../../state/pro'; +import { useAppSdk } from '../../hooks/appSdk'; const Banner = styled.div` position: absolute; @@ -97,6 +96,7 @@ export const DesktopCancelLegacySubscriptionBanner: FC<{ className?: string }> = className }) => { const { data: legacyPlugins } = useWalletLegacyPlugins(); + const openSupport = useOpenSupport(); const { t } = useTranslation(); const [pluginToUnsubscribe, setPluginToUnsubscribe] = useState(); @@ -109,11 +109,15 @@ export const DesktopCancelLegacySubscriptionBanner: FC<{ className?: string }> = - {t('unsubscribe_legacy_plugin_banner_title')} + + {t('unsubscribe_legacy_plugin_banner_title', { + count: legacyPlugins.length + })} + {t('unsubscribe_legacy_plugin_banner_subtitle')} - + diff --git a/packages/uikit/src/components/legacy-plugins/MobileCancelLegacySubscriptionBanner.tsx b/packages/uikit/src/components/legacy-plugins/MobileCancelLegacySubscriptionBanner.tsx index 52d495968..74336fd96 100644 --- a/packages/uikit/src/components/legacy-plugins/MobileCancelLegacySubscriptionBanner.tsx +++ b/packages/uikit/src/components/legacy-plugins/MobileCancelLegacySubscriptionBanner.tsx @@ -52,7 +52,13 @@ export const MobileCancelLegacySubscriptionBanner: FC< count: legacyPlugins.length })} - {t('unsubscribe_legacy_plugin_banner_subtitle')} + + {t( + legacyPlugins.length === 1 + ? 'unsubscribe_legacy_plugin_banner_subtitle' + : 'unsubscribe_legacy_plugin_many_banner_subtitle' + )} + setPluginToUnsubscribe(legacyPlugins![0].address)}> {t('disable')} From b1f307e6012be85fd154db0285edea6ac29f5f0d Mon Sep 17 00:00:00 2001 From: Dmitrii Liulekin Date: Mon, 27 Oct 2025 14:13:36 -0300 Subject: [PATCH 05/13] feat: update hardcoded data --- packages/core/src/entries/tonConnect.ts | 6 +++--- .../subscription-encoder/subscription-encoder.ts | 4 +--- .../connect/InstallSubscriptionV2Notification.tsx | 11 ++++++----- .../connect/RemoveSubscriptionV2Notification.tsx | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/core/src/entries/tonConnect.ts b/packages/core/src/entries/tonConnect.ts index d33367c95..872479270 100644 --- a/packages/core/src/entries/tonConnect.ts +++ b/packages/core/src/entries/tonConnect.ts @@ -639,9 +639,9 @@ export const subscriptionSchema = z.object({ id: z.number(), period: z.number().int().positive(), amount: z.string(), - firstChargeDate: z.number(), - withdrawAddress: z.string(), - withdrawMsgBody: z.string(), + first_charge_date: z.number(), + withdraw_address: z.string(), + withdraw_msg_body: z.string(), metadata: subscriptionMetadataSchema }); diff --git a/packages/core/src/service/ton-blockchain/encoder/subscription-encoder/subscription-encoder.ts b/packages/core/src/service/ton-blockchain/encoder/subscription-encoder/subscription-encoder.ts index 1325adfed..c7ba7d296 100644 --- a/packages/core/src/service/ton-blockchain/encoder/subscription-encoder/subscription-encoder.ts +++ b/packages/core/src/service/ton-blockchain/encoder/subscription-encoder/subscription-encoder.ts @@ -158,8 +158,6 @@ export class SubscriptionEncoder { private encodeWithdrawMsgBody(message?: string): Cell { if (!message) return Cell.EMPTY; - const boc = Buffer.from(message, 'hex'); - - return Cell.fromBoc(boc)[0]; + return beginCell().storeBuffer(Buffer.from(message, 'utf8')).endCell(); } } diff --git a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx index 8080f244e..d3aa520f7 100644 --- a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx +++ b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx @@ -73,7 +73,7 @@ export const InstallSubscriptionV2Notification: FC<{ version: SubscriptionExtensionVersion.V2, status: SubscriptionExtensionStatus.NOT_INITIALIZED, admin: subscription.beneficiary, - recipient: subscription.beneficiary, + recipient: subscription.withdraw_address, subscription_id: subscription.id, first_charging_date: 0, last_charging_date: 0, @@ -81,12 +81,13 @@ export const InstallSubscriptionV2Notification: FC<{ payment_per_period: subscription.amount, currency: CryptoCurrency.TON, created_at: Date.now(), - deploy_value: toNano('0.1').toString(), - destroy_value: toNano('0.05').toString(), - caller_fee: toNano('0.05').toString(), + deploy_value: toNano('0.11').toString(), + destroy_value: toNano('0.08').toString(), + caller_fee: '10000000', payer: params.from, contract: '', period: subscription.period, + withdraw_msg_body: subscription.withdraw_msg_body, metadata: toSubscriptionMetadata(subscription.metadata) }; @@ -175,7 +176,7 @@ const ProInstallExtensionNotificationContent: FC< }); setTimeout(() => { - onClose(boc.toString()); + onClose(boc.toBoc().toString('base64')); }, 1500); return !!boc; diff --git a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx index 63cba8a26..1ab2e6278 100644 --- a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx +++ b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx @@ -129,7 +129,7 @@ const ProRemoveSubscriptionV2NotificationContent: FC<{ }); setTimeout(() => { - onClose(boc.toString()); + onClose(boc.toBoc().toString('base64')); }, 1500); return !!boc; From ae6b9bc6f76ef698810e046b2d3f6db310dbd767 Mon Sep 17 00:00:00 2001 From: siandreev Date: Tue, 28 Oct 2025 11:03:38 +0100 Subject: [PATCH 06/13] chore(web): bump version --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index d6213b2ab..ee8731d3f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@tonkeeper/web", - "version": "4.3.1", + "version": "4.3.2", "author": "Ton APPS UK Limited ", "description": "Your web wallet on The Open Network", "dependencies": { From c0550118fab0bdf0eabace6423abf9e7f7f506f0 Mon Sep 17 00:00:00 2001 From: siandreev Date: Fri, 31 Oct 2025 11:06:16 +0100 Subject: [PATCH 07/13] chore: release 4.3.2 --- apps/desktop/package.json | 2 +- apps/extension/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d3366a6b8..fc6952dc8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@tonkeeper/desktop", "license": "Apache-2.0", - "version": "4.3.1", + "version": "4.3.2", "description": "Your desktop wallet on The Open Network", "main": ".webpack/main", "repository": { diff --git a/apps/extension/package.json b/apps/extension/package.json index b86190b8f..3d1f68c86 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,6 +1,6 @@ { "name": "@tonkeeper/extension", - "version": "4.3.1", + "version": "4.3.2", "author": "Ton APPS UK Limited ", "description": "Your extension wallet on The Open Network", "dependencies": { diff --git a/package.json b/package.json index 83e726036..dfb782f2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tonkeeper-web", - "version": "4.3.1", + "version": "4.3.2", "repository": { "type": "git", "url": "https://github.com/tonkeeper/tonkeeper-web.git" From 350144d1ba7d8c6e1df96e454e453159248e1a1e Mon Sep 17 00:00:00 2001 From: Dmitrii Liulekin Date: Fri, 31 Oct 2025 20:17:11 -0300 Subject: [PATCH 08/13] fix: apply tonconnect fixes --- packages/core/src/entries/tonConnect.ts | 11 ++- .../src/service/tonConnect/connectService.ts | 12 ++- .../InstallSubscriptionV2Notification.tsx | 20 ++-- .../RemoveSubscriptionV2Notification.tsx | 19 ++-- .../connect/TonConnectRequestNotification.tsx | 6 +- .../pro/ProInstallExtensionNotification.tsx | 6 +- .../subscription/useCancelSubscription.ts | 6 +- .../subscription/useCreateSubscription.ts | 93 ++++++++++++------- 8 files changed, 113 insertions(+), 60 deletions(-) diff --git a/packages/core/src/entries/tonConnect.ts b/packages/core/src/entries/tonConnect.ts index 872479270..e80e28153 100644 --- a/packages/core/src/entries/tonConnect.ts +++ b/packages/core/src/entries/tonConnect.ts @@ -593,7 +593,7 @@ export interface CancelSubscriptionV2AppRequest< ? AccountConnectionInjected : AccountConnection; kind: 'cancelSubscriptionV2'; - payload: any; + payload: CancelSubscriptionV2Payload; } export type TonConnectAppRequestPayload< @@ -653,4 +653,13 @@ export const createSubscriptionV2PayloadSchema = z.object({ valid_until: z.number() }); +export const cancelSubscriptionV2PayloadSchema = z.object({ + validUntil: z.number(), + extensionAddress: z.string(), + from: rawAddressSchema.optional(), + network: tonConnectNetworkSchema, + valid_until: z.number() +}); + export type CreateSubscriptionV2Payload = z.infer; +export type CancelSubscriptionV2Payload = z.infer; diff --git a/packages/core/src/service/tonConnect/connectService.ts b/packages/core/src/service/tonConnect/connectService.ts index c06f6283f..8a5180285 100644 --- a/packages/core/src/service/tonConnect/connectService.ts +++ b/packages/core/src/service/tonConnect/connectService.ts @@ -614,10 +614,18 @@ export async function checkTonConnectFromAndNetwork( } } -export const createSubscriptionV2SuccessResponse = (id: string, boc: string) => { +export interface ICreateSubscriptionV2Response { + boc: string; + extensionAddress: string; +} + +export const createSubscriptionV2SuccessResponse = ( + id: string, + result: ICreateSubscriptionV2Response +) => { return { id, - result: { boc } + result }; }; diff --git a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx index d3aa520f7..4e8be35d1 100644 --- a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx +++ b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx @@ -42,10 +42,11 @@ import { import { useProCompatibleAccountsWallets } from '../../state/wallet'; import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; import { toNano } from '@ton/core'; +import { ICreateSubscriptionV2Response } from '@tonkeeper/core/dist/service/tonConnect/connectService'; interface IProInstallExtensionProps { isOpen: boolean; - onClose: (boc?: string) => void; + onClose: (result?: ICreateSubscriptionV2Response) => void; extensionData?: SubscriptionExtension; } @@ -63,7 +64,7 @@ function toSubscriptionMetadata(src: SubscriptionMetadataSource): SubscriptionEx export const InstallSubscriptionV2Notification: FC<{ params: CreateSubscriptionV2Payload | null; - handleClose: (boc?: string) => void; + handleClose: (result?: ICreateSubscriptionV2Response) => void; }> = ({ params, handleClose }) => { const subscription = params?.subscription; @@ -170,16 +171,21 @@ const ProInstallExtensionNotificationContent: FC< throw new Error('Selected wallet is required!'); } - const boc = await deployMutation.mutateAsync({ - selectedWallet, - ...extensionData + const result = await deployMutation.mutateAsync({ + subscriptionParams: { + selectedWallet, + ...extensionData + }, + options: { + disableAddressCheck: true + } }); setTimeout(() => { - onClose(boc.toBoc().toString('base64')); + onClose(result); }, 1500); - return !!boc; + return !!result; }; const { diff --git a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx index 1ab2e6278..b930e2df4 100644 --- a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx +++ b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx @@ -32,9 +32,10 @@ import { useToast } from '../../hooks/useNotification'; import { useDateTimeFormat } from '../../hooks/useDateTimeFormat'; import { hexToRGBA } from '../../libs/css'; import { toNano } from '@ton/core'; +import { CancelSubscriptionV2Payload } from '@tonkeeper/core/dist/entries/tonConnect'; export const RemoveSubscriptionV2Notification: FC<{ - params: any; + params: CancelSubscriptionV2Payload | null; handleClose: (boc?: string) => void; }> = ({ params, handleClose }) => { return ( @@ -49,7 +50,7 @@ export const RemoveSubscriptionV2Notification: FC<{ - {!!params?.subscription && ( + {!!params?.extensionAddress && ( void; }> = ({ onClose, params }) => { - const extensionContract = ''; + const { extensionAddress, from } = params; const destroyValue = toNano('0.05').toString(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - const accountWallet = accountsWallets.find(accWallet => accWallet.wallet.id === params?.from); + const accountWallet = accountsWallets.find(accWallet => accWallet.wallet.id === from); const selectedWallet = accountWallet?.wallet; const finalExpiresDate = new Date(); @@ -111,9 +112,9 @@ const ProRemoveSubscriptionV2NotificationContent: FC<{ if (!selectedWallet) return; estimateFeeMutation.mutate({ + destroyValue, selectedWallet, - extensionContract, - destroyValue + extensionContract: extensionAddress }); }, [selectedWallet]); @@ -123,13 +124,13 @@ const ProRemoveSubscriptionV2NotificationContent: FC<{ } const boc = await removeMutation.mutateAsync({ + destroyValue, selectedWallet, - extensionContract, - destroyValue + extensionContract: extensionAddress }); setTimeout(() => { - onClose(boc.toBoc().toString('base64')); + onClose(boc); }, 1500); return !!boc; diff --git a/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx b/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx index 63f9ec101..7f5f89c2a 100644 --- a/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx +++ b/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx @@ -80,11 +80,11 @@ export const TonConnectRequestNotification: FC<{ /> { + handleClose={result => { if (request) { handleClose( - boc - ? createSubscriptionV2SuccessResponse(request.id, boc) + result + ? createSubscriptionV2SuccessResponse(request.id, result) : createSubscriptionV2ErrorResponse(request.id) ); } diff --git a/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx b/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx index facf32dbc..7bb54889d 100644 --- a/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx +++ b/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx @@ -115,8 +115,10 @@ const ProInstallExtensionNotificationContent: FC< } const result = deployMutation.mutateAsync({ - selectedWallet: targetAuth.wallet, - ...extensionData + subscriptionParams: { + selectedWallet: targetAuth.wallet, + ...extensionData + } }); return !!result; diff --git a/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts b/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts index 531df0482..614c302f2 100644 --- a/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts +++ b/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts @@ -6,7 +6,7 @@ import { SubscriptionEncoder } from '@tonkeeper/core/dist/service/ton-blockchain import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; import { QueryKey } from '../../../libs/queryKey'; import { CancelParams } from './commonTypes'; -import { Address, Cell } from '@ton/core'; +import { Address } from '@ton/core'; import { WalletMessageSender } from '@tonkeeper/core/dist/service/ton-blockchain/sender'; export const useCancelSubscription = () => { @@ -15,7 +15,7 @@ export const useCancelSubscription = () => { const client = useQueryClient(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - return useMutation(async subscriptionParams => { + return useMutation(async subscriptionParams => { if (!subscriptionParams) throw new Error('No params'); const { selectedWallet, extensionContract, destroyValue } = subscriptionParams; @@ -44,6 +44,6 @@ export const useCancelSubscription = () => { await client.invalidateQueries([QueryKey.pro]); - return boc; + return boc.toBoc().toString('base64'); }); }; diff --git a/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts b/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts index 8f94eb200..8f755b791 100644 --- a/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts +++ b/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts @@ -1,4 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Address } from '@ton/core'; import { useActiveApi, useProCompatibleAccountsWallets } from '../../../state/wallet'; import { getSigner } from '../../../state/mnemonic'; import { useAppSdk } from '../../appSdk'; @@ -13,7 +14,14 @@ import { QueryKey } from '../../../libs/queryKey'; import { MetaEncryptionSerializedMap } from '@tonkeeper/core/dist/entries/wallet'; import { AppKey } from '@tonkeeper/core/dist/Keys'; import { metaEncryptionMapSerializer } from '@tonkeeper/core/dist/utils/metadata'; -import { Cell } from '@ton/core'; +import { ICreateSubscriptionV2Response } from '@tonkeeper/core/dist/service/tonConnect/connectService'; + +interface ICreateSubscriptionProps { + subscriptionParams: SubscriptionEncodingParams; + options?: { + disableAddressCheck?: boolean; + }; +} export const useCreateSubscription = () => { const sdk = useAppSdk(); @@ -21,49 +29,68 @@ export const useCreateSubscription = () => { const client = useQueryClient(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - return useMutation(async subscriptionParams => { - if (!subscriptionParams) throw new Error('No params'); + return useMutation( + async ({ subscriptionParams, options }) => { + if (!subscriptionParams) throw new Error('No params'); - const { selectedWallet } = subscriptionParams; + const { disableAddressCheck } = options ?? {}; - const accountWallet = accountsWallets.find( - accWallet => accWallet.wallet.id === selectedWallet.id - ); - const account = accountWallet?.account; - const accountId = account?.id; + const { admin, selectedWallet, contract, subscription_id } = subscriptionParams; - if (!accountId) throw new Error('Account id is required!'); + const accountWallet = accountsWallets.find( + accWallet => accWallet.wallet.id === selectedWallet.id + ); + const account = accountWallet?.account; + const accountId = account?.id; - let metaEncryptionMap = await sdk.storage - .get(AppKey.META_ENCRYPTION_MAP) - .then(metaEncryptionMapSerializer); + if (!accountId) throw new Error('Account id is required!'); - const shouldCreateMetaKeys = !metaEncryptionMap?.[selectedWallet.rawAddress]; + let metaEncryptionMap = await sdk.storage + .get(AppKey.META_ENCRYPTION_MAP) + .then(metaEncryptionMapSerializer); - const signer = await getSigner(sdk, accountId, { - walletId: selectedWallet.id, - shouldCreateMetaKeys - }).catch(() => null); + const shouldCreateMetaKeys = !metaEncryptionMap?.[selectedWallet.rawAddress]; - if (!signer || signer.type !== 'cell') throw new Error('Signer is incorrect!'); + const signer = await getSigner(sdk, accountId, { + walletId: selectedWallet.id, + shouldCreateMetaKeys + }).catch(() => null); - if (shouldCreateMetaKeys) { - metaEncryptionMap = await sdk.storage - .get(AppKey.META_ENCRYPTION_MAP) - .then(metaEncryptionMapSerializer); - } + if (!signer || signer.type !== 'cell') throw new Error('Signer is incorrect!'); - const sender = new WalletMessageSender(api, selectedWallet, signer); - const encoder = new SubscriptionEncoder(selectedWallet); + if (shouldCreateMetaKeys) { + metaEncryptionMap = await sdk.storage + .get(AppKey.META_ENCRYPTION_MAP) + .then(metaEncryptionMapSerializer); + } - const outgoingMsg = await encoder.encodeCreateSubscriptionV2( - prepareSubscriptionParamsForEncoder(subscriptionParams, metaEncryptionMap) - ); + const sender = new WalletMessageSender(api, selectedWallet, signer); + const encoder = new SubscriptionEncoder(selectedWallet); - const boc = await sender.send(outgoingMsg); + const beneficiary = Address.parse(admin); + const subscriptionId = subscription_id; - await client.invalidateQueries([QueryKey.metaEncryptionData]); + const extensionAddress = encoder.getExtensionAddress({ + beneficiary, + subscriptionId + }); - return boc; - }); + const outgoingMsg = await encoder.encodeCreateSubscriptionV2( + prepareSubscriptionParamsForEncoder(subscriptionParams, metaEncryptionMap) + ); + + if (!disableAddressCheck && !extensionAddress.equals(Address.parse(contract))) { + throw new Error('Contract extension addresses do not match!'); + } + + const boc = await sender.send(outgoingMsg); + + await client.invalidateQueries([QueryKey.metaEncryptionData]); + + return { + boc: boc.toBoc().toString('base64'), + extensionAddress: extensionAddress.toString() + }; + } + ); }; From 900322b784aaf98f2fd03c7466d7cce420c22808 Mon Sep 17 00:00:00 2001 From: Dmitrii Liulekin Date: Sun, 26 Oct 2025 19:54:05 -0300 Subject: [PATCH 09/13] feat: support tonConnect subscriptions --- packages/core/src/entries/tonConnect.ts | 169 ++++++++- .../src/service/tonConnect/connectService.ts | 45 +++ .../InstallSubscriptionV2Notification.tsx | 322 ++++++++++++++++++ .../RemoveSubscriptionV2Notification.tsx | 275 +++++++++++++++ .../connect/TonConnectRequestNotification.tsx | 30 ++ .../connect/WebTonConnectSubscription.tsx | 20 ++ .../pro/ProInstallExtensionNotification.tsx | 4 +- .../pro/ProRemoveExtensionNotification.tsx | 7 +- .../src/components/pro/ProActiveWallet.tsx | 6 + .../subscription/useCancelSubscription.ts | 8 +- .../subscription/useCreateSubscription.ts | 24 +- 11 files changed, 878 insertions(+), 32 deletions(-) create mode 100644 packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx create mode 100644 packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx diff --git a/packages/core/src/entries/tonConnect.ts b/packages/core/src/entries/tonConnect.ts index f633d7041..d33367c95 100644 --- a/packages/core/src/entries/tonConnect.ts +++ b/packages/core/src/entries/tonConnect.ts @@ -199,10 +199,19 @@ const signDataFeatureSchema = z.object({ }); export type SignDataFeature = z.infer; +const subscriptionFeatureSchema = z.object({ + name: z.literal('Subscription'), + versions: z.object({ + v2: z.boolean() + }) +}); +export type SubscriptionFeature = z.infer; + export const featureSchema = z.union([ sendTransactionFeatureDeprecatedSchema, sendTransactionFeatureSchema, - signDataFeatureSchema + signDataFeatureSchema, + subscriptionFeatureSchema ]); export type Feature = z.infer; @@ -268,9 +277,78 @@ const keyPairSchema = z.object({ }); export type KeyPair = z.infer; -const rpcMethodSchema = z.enum(['disconnect', 'sendTransaction', 'signData']); +const rpcMethodSchema = z.enum([ + 'disconnect', + 'sendTransaction', + 'signData', + 'createSubscriptionV2', + 'cancelSubscriptionV2' +]); export type RpcMethod = z.infer; +const createSubscriptionV2RpcRequestSchema = z.object({ + id: z.string(), + method: z.literal('createSubscriptionV2'), + params: z.tuple([z.string()]) +}); +export type CreateSubscriptionV2RpcRequest = z.infer; + +const cancelSubscriptionV2RpcRequestSchema = z.object({ + id: z.string(), + method: z.literal('cancelSubscriptionV2'), + params: z.tuple([z.string()]) +}); +export type CancelSubscriptionV2RpcRequest = z.infer; + +export enum SUBSCRIPTION_V2_ERROR_CODES { + UNKNOWN_ERROR = 0, + BAD_REQUEST_ERROR = 1, + UNKNOWN_APP_ERROR = 100, + USER_REJECTS_ERROR = 300, + METHOD_NOT_SUPPORTED = 400, + EXTENSION_NOT_FOUND = 404 +} + +const createSubscriptionV2RpcResponseSuccessSchema = z.object({ + id: z.string(), + result: z.object({ + boc: z.string() + }) +}); +export type CreateSubscriptionV2RpcResponseSuccess = z.infer< + typeof createSubscriptionV2RpcResponseSuccessSchema +>; + +const createSubscriptionV2RpcResponseErrorSchema = z.object({ + id: z.string(), + error: z.object({ + code: z.nativeEnum(SUBSCRIPTION_V2_ERROR_CODES), + message: z.string() + }) +}); +export type CreateSubscriptionV2RpcResponseError = z.infer< + typeof createSubscriptionV2RpcResponseErrorSchema +>; + +const cancelSubscriptionV2RpcResponseSuccessSchema = z.object({ + id: z.string(), + result: z.object({}) +}); +export type CancelSubscriptionV2RpcResponseSuccess = z.infer< + typeof cancelSubscriptionV2RpcResponseSuccessSchema +>; + +const cancelSubscriptionV2RpcResponseErrorSchema = z.object({ + id: z.string(), + error: z.object({ + code: z.nativeEnum(SUBSCRIPTION_V2_ERROR_CODES), + message: z.string() + }) +}); +export type CancelSubscriptionV2RpcResponseError = z.infer< + typeof cancelSubscriptionV2RpcResponseErrorSchema +>; + export enum SEND_TRANSACTION_ERROR_CODES { UNKNOWN_ERROR = 0, BAD_REQUEST_ERROR = 1, @@ -365,7 +443,9 @@ export type SignDataRequestPayload = z.infer; @@ -411,6 +491,14 @@ const rpcResponsesSchema = z.object({ disconnect: z.object({ error: disconnectRpcResponseErrorSchema, success: disconnectRpcResponseSuccessSchema + }), + createSubscriptionV2: z.object({ + error: createSubscriptionV2RpcResponseErrorSchema, + success: createSubscriptionV2RpcResponseSuccessSchema + }), + cancelSubscriptionV2: z.object({ + error: cancelSubscriptionV2RpcResponseErrorSchema, + success: cancelSubscriptionV2RpcResponseSuccessSchema }) }); export type RpcResponses = z.infer; @@ -424,7 +512,11 @@ export const walletResponseSchema = z.union([ signDataRpcResponseSuccessSchema, signDataRpcResponseErrorSchema, disconnectRpcResponseSuccessSchema, - disconnectRpcResponseErrorSchema + disconnectRpcResponseErrorSchema, + createSubscriptionV2RpcResponseSuccessSchema, + createSubscriptionV2RpcResponseErrorSchema, + cancelSubscriptionV2RpcResponseSuccessSchema, + cancelSubscriptionV2RpcResponseErrorSchema ]); export type WalletResponse = WalletResponseSuccess | WalletResponseError; @@ -439,7 +531,9 @@ assertTypesEqual>(true); export const appRequestSchema = z.union([ sendTransactionRpcRequestSchema, signDataRpcRequestSchema, - disconnectRpcRequestSchema + disconnectRpcRequestSchema, + createSubscriptionV2RpcRequestSchema, + cancelSubscriptionV2RpcRequestSchema ]); export type AppRequest = RpcRequests[T]; assertTypesEqual, z.infer>(true); @@ -476,9 +570,39 @@ export interface SignDatAppRequest< payload: SignDataRequestPayload; } +export interface CreateSubscriptionV2AppRequest< + T extends AccountConnection['type'] = AccountConnection['type'] +> { + id: string; + connection: T extends 'http' + ? AccountConnectionHttp + : T extends 'injected' + ? AccountConnectionInjected + : AccountConnection; + kind: 'createSubscriptionV2'; + payload: CreateSubscriptionV2Payload; +} + +export interface CancelSubscriptionV2AppRequest< + T extends AccountConnection['type'] = AccountConnection['type'] +> { + id: string; + connection: T extends 'http' + ? AccountConnectionHttp + : T extends 'injected' + ? AccountConnectionInjected + : AccountConnection; + kind: 'cancelSubscriptionV2'; + payload: any; +} + export type TonConnectAppRequestPayload< T extends AccountConnection['type'] = AccountConnection['type'] -> = SendTransactionAppRequest | SignDatAppRequest; +> = + | SendTransactionAppRequest + | SignDatAppRequest + | CreateSubscriptionV2AppRequest + | CancelSubscriptionV2AppRequest; export interface InjectedWalletInfo { name: string; @@ -497,3 +621,36 @@ export interface ITonConnectInjectedBridge { send(message: AppRequest): Promise>; listen(callback: (event: WalletEvent) => void): () => void; } + +export type SubscriptionMetadataSource = z.infer; + +export const subscriptionMetadataSchema = z.object({ + logo: z.string().url(), + name: z.string(), + description: z.string(), + link: z.string().url(), + tos: z.string().url(), + merchant: z.string(), + website: z.string().url() +}); + +export const subscriptionSchema = z.object({ + beneficiary: z.string(), + id: z.number(), + period: z.number().int().positive(), + amount: z.string(), + firstChargeDate: z.number(), + withdrawAddress: z.string(), + withdrawMsgBody: z.string(), + metadata: subscriptionMetadataSchema +}); + +export const createSubscriptionV2PayloadSchema = z.object({ + validUntil: z.number(), + subscription: subscriptionSchema, + from: rawAddressSchema.optional(), + network: tonConnectNetworkSchema, + valid_until: z.number() +}); + +export type CreateSubscriptionV2Payload = z.infer; diff --git a/packages/core/src/service/tonConnect/connectService.ts b/packages/core/src/service/tonConnect/connectService.ts index ff8b6b74b..c06f6283f 100644 --- a/packages/core/src/service/tonConnect/connectService.ts +++ b/packages/core/src/service/tonConnect/connectService.ts @@ -16,6 +16,7 @@ import { SEND_TRANSACTION_ERROR_CODES, SendTransactionRpcResponseError, SendTransactionRpcResponseSuccess, + SUBSCRIPTION_V2_ERROR_CODES, TonAddressItemReply, TonConnectEventPayload, TonConnectNetwork, @@ -211,6 +212,10 @@ export const getDeviceInfo = ( { name: 'SignData', types: ['text', 'binary', 'cell'] + }, + { + name: 'Subscription', + versions: { v2: true } } ] }; @@ -608,3 +613,43 @@ export async function checkTonConnectFromAndNetwork( } } } + +export const createSubscriptionV2SuccessResponse = (id: string, boc: string) => { + return { + id, + result: { boc } + }; +}; + +export const createSubscriptionV2ErrorResponse = ( + id: string, + message = 'User rejected subscription' +) => { + return { + id, + error: { + code: SUBSCRIPTION_V2_ERROR_CODES.USER_REJECTS_ERROR, + message + } + }; +}; + +export const cancelSubscriptionV2SuccessResponse = (id: string, boc: string) => { + return { + id, + result: { boc } + }; +}; + +export const cancelSubscriptionV2ErrorResponse = ( + id: string, + message = 'User rejected cancellation' +) => { + return { + id, + error: { + code: SUBSCRIPTION_V2_ERROR_CODES.USER_REJECTS_ERROR, + message + } + }; +}; diff --git a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx new file mode 100644 index 000000000..8080f244e --- /dev/null +++ b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx @@ -0,0 +1,322 @@ +import { + CreateSubscriptionV2Payload, + SubscriptionMetadataSource +} from '@tonkeeper/core/dist/entries/tonConnect'; +import React, { FC, useEffect, useMemo } from 'react'; +import styled from 'styled-components'; +import { useTranslation } from '../../hooks/translation'; +import { SpinnerIcon } from '../Icon'; +import { Notification, NotificationFooter, NotificationFooterPortal } from '../Notification'; +import { Body2, Body3, Label2 } from '../Text'; +import { Button } from '../fields/Button'; +import { ConfirmMainButtonProps } from '../transfer/common'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { ListBlock, ListItem, ListItemPayload } from '../List'; +import { secondsToUnitCount } from '@tonkeeper/core/dist/utils/pro'; +import { useFormatFiat, useRate } from '../../state/rates'; +import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; +import { ErrorBoundary } from '../shared/ErrorBoundary'; +import { fallbackRenderOver } from '../Error'; +import { + useCreateSubscription, + useEstimateDeploySubscription +} from '../../hooks/blockchain/subscription'; +import { + ConfirmView, + ConfirmViewAdditionalBottomSlot, + ConfirmViewButtons, + ConfirmViewButtonsSlot, + ConfirmViewDetailsSlot, + ConfirmViewHeadingSlot +} from '../transfer/ConfirmView'; +import { ProActiveWallet } from '../pro/ProActiveWallet'; +import { ProSubscriptionHeader } from '../pro/ProSubscriptionHeader'; +import { + CryptoCurrency, + SubscriptionExtension, + SubscriptionExtensionMetadata, + SubscriptionExtensionStatus, + SubscriptionExtensionVersion +} from '@tonkeeper/core/dist/pro'; +import { useProCompatibleAccountsWallets } from '../../state/wallet'; +import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; +import { toNano } from '@ton/core'; + +interface IProInstallExtensionProps { + isOpen: boolean; + onClose: (boc?: string) => void; + extensionData?: SubscriptionExtension; +} + +function toSubscriptionMetadata(src: SubscriptionMetadataSource): SubscriptionExtensionMetadata { + return { + l: src.logo, + n: src.name, + u: src.link, + m: src.merchant, + w: src.website, + ...(src.description ? { d: src.description } : {}), + ...(src.tos ? { t: src.tos } : {}) + }; +} + +export const InstallSubscriptionV2Notification: FC<{ + params: CreateSubscriptionV2Payload | null; + handleClose: (boc?: string) => void; +}> = ({ params, handleClose }) => { + const subscription = params?.subscription; + + if (!subscription || !params?.from) return null; + + const extensionData: SubscriptionExtension = { + version: SubscriptionExtensionVersion.V2, + status: SubscriptionExtensionStatus.NOT_INITIALIZED, + admin: subscription.beneficiary, + recipient: subscription.beneficiary, + subscription_id: subscription.id, + first_charging_date: 0, + last_charging_date: 0, + grace_period: 0, + payment_per_period: subscription.amount, + currency: CryptoCurrency.TON, + created_at: Date.now(), + deploy_value: toNano('0.1').toString(), + destroy_value: toNano('0.05').toString(), + caller_fee: toNano('0.05').toString(), + payer: params.from, + contract: '', + period: subscription.period, + metadata: toSubscriptionMetadata(subscription.metadata) + }; + + return ( + <> + handleClose()} + hideButton + backShadow + > + {() => ( + + {extensionData && ( + + )} + + )} + + + ); +}; + +const ProInstallExtensionNotificationContent: FC< + Required> +> = ({ onClose, extensionData }) => { + const { t } = useTranslation(); + const deployMutation = useCreateSubscription(); + const estimateFeeMutation = useEstimateDeploySubscription(); + const { + data: estimation, + error: estimationError, + isLoading: isEstimating + } = estimateFeeMutation; + + const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); + + const accountWallet = accountsWallets.find( + accWallet => accWallet.wallet.id === extensionData.payer + ); + + const selectedWallet = accountWallet?.wallet; + + const { data: rate } = useRate(CryptoCurrency.TON); + + const { fiatAmount: fiatEquivalent } = useFormatFiat( + rate, + formatDecimals(extensionData.payment_per_period) + ); + const { fiatAmount: feeEquivalent } = useFormatFiat( + rate, + formatDecimals(estimation?.fee?.extra?.stringWeiAmount ?? 0) + ); + + useEffect(() => { + if (!selectedWallet) return; + + estimateFeeMutation.mutate({ + selectedWallet, + ...extensionData + }); + }, [selectedWallet]); + + const price = useMemo( + () => + new AssetAmount({ + asset: TON_ASSET, + weiAmount: extensionData.payment_per_period + }), + [extensionData?.payment_per_period] + ); + + const deployMutate = async () => { + if (!selectedWallet) { + throw new Error('Selected wallet is required!'); + } + + const boc = await deployMutation.mutateAsync({ + selectedWallet, + ...extensionData + }); + + setTimeout(() => { + onClose(boc.toString()); + }, 1500); + + return !!boc; + }; + + const { + unit: periodUnit, + count: periodCount, + form: periodForm + } = secondsToUnitCount(extensionData.period); + + return ( + onClose()} + estimation={{ ...estimateFeeMutation }} + {...deployMutation} + mutateAsync={deployMutate} + > + + + + + + + + + + + + {t('price')} + + {price.toStringAssetRelativeAmount()} + {`≈ ${fiatEquivalent}`} + + + + + + + {t('interval')} + + {t(`every_${periodUnit}_${periodForm}`, { + count: periodCount + })} + + + + + + + {t('swap_blockchain_fee')} + + + {isEstimating && } + {!!estimationError && <>—} + {estimation?.fee?.extra && + estimateFeeMutation.data.fee.extra.toStringAssetRelativeAmount()} + + {!estimationError && !isEstimating && ( + {`≈ ${feeEquivalent}`} + )} + + + + + + + + + + + + + + + ); +}; + +export const ConfirmMainButton: ConfirmMainButtonProps = props => { + const { isLoading, isDisabled, onClick } = props; + + const { t } = useTranslation(); + + return ( + + ); +}; + +const Body2Styled = styled(Body2)` + color: ${props => props.theme.textSecondary}; +`; + +const Body3Styled = styled(Body3)` + color: ${props => props.theme.textSecondary}; +`; + +const ProSubscriptionHeaderStyled = styled(ProSubscriptionHeader)` + margin-bottom: 0; +`; + +const NotificationStyled = styled(Notification)` + max-width: 650px; + + @media (pointer: fine) { + &:hover { + [data-swipe-button] { + color: ${p => p.theme.textSecondary}; + } + } + } +`; + +const ListItemStyled = styled(ListItem)` + &:not(:first-child) > div { + padding-top: 10px; + } +`; + +const ListItemPayloadStyled = styled(ListItemPayload)<{ alignItems?: string }>` + padding-top: 10px; + padding-bottom: 10px; + + align-items: ${({ alignItems }) => alignItems ?? 'center'}; +`; + +const FiatEquivalentWrapper = styled.div` + display: grid; + justify-items: end; +`; diff --git a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx new file mode 100644 index 000000000..63cba8a26 --- /dev/null +++ b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx @@ -0,0 +1,275 @@ +import React, { FC, useEffect, useMemo } from 'react'; +import styled from 'styled-components'; +import { useTranslation } from '../../hooks/translation'; +import { SpinnerIcon } from '../Icon'; +import { Notification, NotificationFooter, NotificationFooterPortal } from '../Notification'; +import { Body2, Body3, Label2 } from '../Text'; +import { Button } from '../fields/Button'; +import { ConfirmMainButtonProps } from '../transfer/common'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { ListItem, ListItemPayload } from '../List'; +import { useFormatFiat, useRate } from '../../state/rates'; +import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; +import { ErrorBoundary } from '../shared/ErrorBoundary'; +import { fallbackRenderOver } from '../Error'; +import { + useCancelSubscription, + useEstimateRemoveExtension +} from '../../hooks/blockchain/subscription'; +import { + ConfirmView, + ConfirmViewButtons, + ConfirmViewButtonsSlot, + ConfirmViewDetailsSlot, + ConfirmViewHeadingSlot +} from '../transfer/ConfirmView'; +import { ProSubscriptionHeader } from '../pro/ProSubscriptionHeader'; +import { CryptoCurrency } from '@tonkeeper/core/dist/pro'; +import { useProCompatibleAccountsWallets } from '../../state/wallet'; +import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; +import { useToast } from '../../hooks/useNotification'; +import { useDateTimeFormat } from '../../hooks/useDateTimeFormat'; +import { hexToRGBA } from '../../libs/css'; +import { toNano } from '@ton/core'; + +export const RemoveSubscriptionV2Notification: FC<{ + params: any; + handleClose: (boc?: string) => void; +}> = ({ params, handleClose }) => { + return ( + <> + handleClose()} + hideButton + backShadow + > + {() => ( + + {!!params?.subscription && ( + + )} + + )} + + + ); +}; + +const ProRemoveSubscriptionV2NotificationContent: FC<{ + params: any; + onClose: (boc?: string) => void; +}> = ({ onClose, params }) => { + const extensionContract = ''; + const destroyValue = toNano('0.05').toString(); + + const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); + + const accountWallet = accountsWallets.find(accWallet => accWallet.wallet.id === params?.from); + + const selectedWallet = accountWallet?.wallet; + const finalExpiresDate = new Date(); + + const toast = useToast(); + const { t } = useTranslation(); + const formatDate = useDateTimeFormat(); + const { data: rate } = useRate(CryptoCurrency.TON); + + const removeMutation = useCancelSubscription(); + const estimateFeeMutation = useEstimateRemoveExtension(); + const { + data: estimation, + error: estimationError, + isLoading: isEstimating + } = estimateFeeMutation; + + const { fiatAmount: feeEquivalent } = useFormatFiat( + rate, + formatDecimals(estimation?.fee?.extra?.stringWeiAmount ?? 0) + ); + + useEffect(() => { + if (!removeMutation.isSuccess || !finalExpiresDate) return; + + toast( + `${t('extension_cancellation_success')} ${formatDate(finalExpiresDate, { + day: 'numeric', + month: 'short', + year: 'numeric', + inputUnit: 'seconds' + })}` + ); + }, [removeMutation.isSuccess]); + + useEffect(() => { + if (!selectedWallet) return; + + estimateFeeMutation.mutate({ + selectedWallet, + extensionContract, + destroyValue + }); + }, [selectedWallet]); + + const removeMutate = async () => { + if (!selectedWallet) { + throw new Error('Selected wallet not found!'); + } + + const boc = await removeMutation.mutateAsync({ + selectedWallet, + extensionContract, + destroyValue + }); + + setTimeout(() => { + onClose(boc.toString()); + }, 1500); + + return !!boc; + }; + + const deployReserve = useMemo( + () => + new AssetAmount({ + asset: TON_ASSET, + weiAmount: destroyValue + }), + [destroyValue] + ); + + return ( + onClose()} + estimation={{ ...estimateFeeMutation }} + {...removeMutation} + mutateAsync={removeMutate} + > + + + + + + {finalExpiresDate && ( + + + {t('will_be_active_until')} + + {formatDate(finalExpiresDate, { + day: 'numeric', + month: 'short', + year: 'numeric', + inputUnit: 'seconds' + })} + + + + )} + + + + {t('swap_blockchain_fee')} + + + {isEstimating && } + {!!estimationError && <>—} + {estimation?.fee?.extra && + estimation.fee.extra.toStringAssetRelativeAmount()} + + {!estimationError && !isEstimating && ( + {`≈ ${feeEquivalent}`} + )} + + + + + + + + + + + + + + ); +}; + +export const ConfirmMainButton: ConfirmMainButtonProps = props => { + const { isLoading, isDisabled, onClick } = props; + + const { t } = useTranslation(); + + return ( + + {t('cancel_subscription')} + + ); +}; + +const Body2Styled = styled(Body2)` + color: ${props => props.theme.textSecondary}; +`; + +const Body3Styled = styled(Body3)` + color: ${props => props.theme.textSecondary}; +`; + +const ProSubscriptionHeaderStyled = styled(ProSubscriptionHeader)` + margin-bottom: 0; +`; + +const NotificationStyled = styled(Notification)` + max-width: 650px; + + @media (pointer: fine) { + &:hover { + [data-swipe-button] { + color: ${p => p.theme.textSecondary}; + } + } + } +`; + +const ListItemStyled = styled(ListItem)` + &:not(:first-child) > div { + padding-top: 10px; + } +`; + +const ListItemPayloadStyled = styled(ListItemPayload)` + padding-top: 10px; + padding-bottom: 10px; +`; + +const FiatEquivalentWrapper = styled.div` + display: grid; + justify-items: end; +`; + +const CancelButtonStyled = styled(Button)` + color: ${p => p.theme.accentRed}; + background-color: ${({ theme }) => hexToRGBA(theme.accentRed, 0.16)}; + transition: background-color 0.1s ease-in; + + &:enabled:hover { + background-color: ${({ theme }) => hexToRGBA(theme.accentRed, 0.12)}; + } +`; diff --git a/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx b/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx index 6ee32e6e1..63f9ec101 100644 --- a/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx +++ b/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx @@ -9,6 +9,10 @@ import { FC } from 'react'; import { TonTransactionNotification } from './TonTransactionNotification'; import { SignDataNotification } from './SignDataNotification'; import { + cancelSubscriptionV2ErrorResponse, + cancelSubscriptionV2SuccessResponse, + createSubscriptionV2ErrorResponse, + createSubscriptionV2SuccessResponse, sendTransactionErrorResponse, sendTransactionSuccessResponse } from '@tonkeeper/core/dist/service/tonConnect/connectService'; @@ -16,6 +20,8 @@ import { useTrackerTonConnectSendSuccess, useTrackTonConnectActionRequest } from '../../hooks/analytics/events-hooks'; +import { InstallSubscriptionV2Notification } from './InstallSubscriptionV2Notification'; +import { RemoveSubscriptionV2Notification } from './RemoveSubscriptionV2Notification'; export const TonConnectRequestNotification: FC<{ request: TonConnectAppRequestPayload | undefined; @@ -72,6 +78,30 @@ export const TonConnectRequestNotification: FC<{ } }} /> + { + if (request) { + handleClose( + boc + ? createSubscriptionV2SuccessResponse(request.id, boc) + : createSubscriptionV2ErrorResponse(request.id) + ); + } + }} + /> + { + if (request) { + handleClose( + boc + ? cancelSubscriptionV2SuccessResponse(request.id, boc) + : cancelSubscriptionV2ErrorResponse(request.id) + ); + } + }} + /> ); }; diff --git a/packages/uikit/src/components/connect/WebTonConnectSubscription.tsx b/packages/uikit/src/components/connect/WebTonConnectSubscription.tsx index 1e0ecd596..8224470be 100644 --- a/packages/uikit/src/components/connect/WebTonConnectSubscription.tsx +++ b/packages/uikit/src/components/connect/WebTonConnectSubscription.tsx @@ -96,6 +96,26 @@ const WebTonConnectSubscription = () => { }; return openNotification(params.connection.clientSessionId, value); } + case 'createSubscriptionV2': { + setRequest(undefined); + const value: TonConnectAppRequestPayload = { + connection: params.connection, + id: params.request.id, + kind: 'createSubscriptionV2', + payload: JSON.parse(params.request.params[0]) + }; + return openNotification(params.connection.clientSessionId, value); + } + case 'cancelSubscriptionV2': { + setRequest(undefined); + const value: TonConnectAppRequestPayload = { + connection: params.connection, + id: params.request.id, + kind: 'cancelSubscriptionV2', + payload: JSON.parse(params.request.params[0]) + }; + return openNotification(params.connection.clientSessionId, value); + } default: { return badRequestResponse({ ...params, diff --git a/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx b/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx index 1ab2efaf9..facf32dbc 100644 --- a/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx +++ b/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx @@ -114,10 +114,12 @@ const ProInstallExtensionNotificationContent: FC< throw new Error('Selected wallet is required!'); } - return deployMutation.mutateAsync({ + const result = deployMutation.mutateAsync({ selectedWallet: targetAuth.wallet, ...extensionData }); + + return !!result; }; const { diff --git a/packages/uikit/src/components/desktop/pro/ProRemoveExtensionNotification.tsx b/packages/uikit/src/components/desktop/pro/ProRemoveExtensionNotification.tsx index 9bbb2121a..15777763e 100644 --- a/packages/uikit/src/components/desktop/pro/ProRemoveExtensionNotification.tsx +++ b/packages/uikit/src/components/desktop/pro/ProRemoveExtensionNotification.tsx @@ -118,13 +118,16 @@ const ProRemoveExtensionNotificationContent: FC< }); }, [selectedWallet]); - const removeMutate = async () => - removeMutation.mutateAsync({ + const removeMutate = async () => { + const boc = await removeMutation.mutateAsync({ selectedWallet, extensionContract, destroyValue }); + return !!boc; + }; + const deployReserve = useMemo( () => new AssetAmount({ diff --git a/packages/uikit/src/components/pro/ProActiveWallet.tsx b/packages/uikit/src/components/pro/ProActiveWallet.tsx index 497bb7871..573ad5a5c 100644 --- a/packages/uikit/src/components/pro/ProActiveWallet.tsx +++ b/packages/uikit/src/components/pro/ProActiveWallet.tsx @@ -14,6 +14,7 @@ import { useAtomValue } from '../../libs/useAtom'; interface IProps { title?: ReactNode; belowCaption?: ReactNode; + rawAddress?: string; isCurrentSubscription?: ReactNode; onDisconnect?: () => Promise; isLoading: boolean; @@ -24,6 +25,7 @@ export const ProActiveWallet: FC = props => { const { onDisconnect, isLoading, + rawAddress, title, belowCaption, isCurrentSubscription, @@ -34,6 +36,10 @@ export const ProActiveWallet: FC = props => { const targetAuth = useAtomValue(subscriptionFormTempAuth$); const { account, wallet } = useControllableAccountAndWalletByWalletId( (() => { + if (rawAddress) { + return rawAddress; + } + const currentAuth = subscription?.auth; if (targetAuth && !isCurrentSubscription) { diff --git a/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts b/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts index 23a24938b..531df0482 100644 --- a/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts +++ b/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts @@ -6,7 +6,7 @@ import { SubscriptionEncoder } from '@tonkeeper/core/dist/service/ton-blockchain import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; import { QueryKey } from '../../../libs/queryKey'; import { CancelParams } from './commonTypes'; -import { Address } from '@ton/core'; +import { Address, Cell } from '@ton/core'; import { WalletMessageSender } from '@tonkeeper/core/dist/service/ton-blockchain/sender'; export const useCancelSubscription = () => { @@ -15,7 +15,7 @@ export const useCancelSubscription = () => { const client = useQueryClient(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - return useMutation(async subscriptionParams => { + return useMutation(async subscriptionParams => { if (!subscriptionParams) throw new Error('No params'); const { selectedWallet, extensionContract, destroyValue } = subscriptionParams; @@ -40,10 +40,10 @@ export const useCancelSubscription = () => { const outgoingMsg = encoder.encodeDestructAction(extensionAddress, BigInt(destroyValue)); - await sender.send(outgoingMsg); + const boc = await sender.send(outgoingMsg); await client.invalidateQueries([QueryKey.pro]); - return true; + return boc; }); }; diff --git a/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts b/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts index b590e1587..8f94eb200 100644 --- a/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts +++ b/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts @@ -1,5 +1,4 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { Address } from '@ton/core'; import { useActiveApi, useProCompatibleAccountsWallets } from '../../../state/wallet'; import { getSigner } from '../../../state/mnemonic'; import { useAppSdk } from '../../appSdk'; @@ -10,23 +9,22 @@ import { } from '@tonkeeper/core/dist/service/ton-blockchain/encoder/subscription-encoder'; import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; import { WalletMessageSender } from '@tonkeeper/core/dist/service/ton-blockchain/sender'; -import { useTranslation } from '../../translation'; import { QueryKey } from '../../../libs/queryKey'; import { MetaEncryptionSerializedMap } from '@tonkeeper/core/dist/entries/wallet'; import { AppKey } from '@tonkeeper/core/dist/Keys'; import { metaEncryptionMapSerializer } from '@tonkeeper/core/dist/utils/metadata'; +import { Cell } from '@ton/core'; export const useCreateSubscription = () => { const sdk = useAppSdk(); const api = useActiveApi(); - const { t } = useTranslation(); const client = useQueryClient(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - return useMutation(async subscriptionParams => { + return useMutation(async subscriptionParams => { if (!subscriptionParams) throw new Error('No params'); - const { admin, subscription_id, contract, selectedWallet } = subscriptionParams; + const { selectedWallet } = subscriptionParams; const accountWallet = accountsWallets.find( accWallet => accWallet.wallet.id === selectedWallet.id @@ -58,26 +56,14 @@ export const useCreateSubscription = () => { const sender = new WalletMessageSender(api, selectedWallet, signer); const encoder = new SubscriptionEncoder(selectedWallet); - const beneficiary = Address.parse(admin); - const subscriptionId = subscription_id; - - const extensionAddress = encoder.getExtensionAddress({ - beneficiary, - subscriptionId - }); - const outgoingMsg = await encoder.encodeCreateSubscriptionV2( prepareSubscriptionParamsForEncoder(subscriptionParams, metaEncryptionMap) ); - if (!extensionAddress.equals(Address.parse(contract))) { - throw new Error('Contract extension addresses do not match!'); - } - - await sender.send(outgoingMsg); + const boc = await sender.send(outgoingMsg); await client.invalidateQueries([QueryKey.metaEncryptionData]); - return true; + return boc; }); }; From 5834563212520d6f8fe6692be4361656e78d43b2 Mon Sep 17 00:00:00 2001 From: Dmitrii Liulekin Date: Mon, 27 Oct 2025 14:13:36 -0300 Subject: [PATCH 10/13] feat: update hardcoded data --- packages/core/src/entries/tonConnect.ts | 6 +++--- .../subscription-encoder/subscription-encoder.ts | 4 +--- .../connect/InstallSubscriptionV2Notification.tsx | 11 ++++++----- .../connect/RemoveSubscriptionV2Notification.tsx | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/core/src/entries/tonConnect.ts b/packages/core/src/entries/tonConnect.ts index d33367c95..872479270 100644 --- a/packages/core/src/entries/tonConnect.ts +++ b/packages/core/src/entries/tonConnect.ts @@ -639,9 +639,9 @@ export const subscriptionSchema = z.object({ id: z.number(), period: z.number().int().positive(), amount: z.string(), - firstChargeDate: z.number(), - withdrawAddress: z.string(), - withdrawMsgBody: z.string(), + first_charge_date: z.number(), + withdraw_address: z.string(), + withdraw_msg_body: z.string(), metadata: subscriptionMetadataSchema }); diff --git a/packages/core/src/service/ton-blockchain/encoder/subscription-encoder/subscription-encoder.ts b/packages/core/src/service/ton-blockchain/encoder/subscription-encoder/subscription-encoder.ts index 1325adfed..c7ba7d296 100644 --- a/packages/core/src/service/ton-blockchain/encoder/subscription-encoder/subscription-encoder.ts +++ b/packages/core/src/service/ton-blockchain/encoder/subscription-encoder/subscription-encoder.ts @@ -158,8 +158,6 @@ export class SubscriptionEncoder { private encodeWithdrawMsgBody(message?: string): Cell { if (!message) return Cell.EMPTY; - const boc = Buffer.from(message, 'hex'); - - return Cell.fromBoc(boc)[0]; + return beginCell().storeBuffer(Buffer.from(message, 'utf8')).endCell(); } } diff --git a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx index 8080f244e..d3aa520f7 100644 --- a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx +++ b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx @@ -73,7 +73,7 @@ export const InstallSubscriptionV2Notification: FC<{ version: SubscriptionExtensionVersion.V2, status: SubscriptionExtensionStatus.NOT_INITIALIZED, admin: subscription.beneficiary, - recipient: subscription.beneficiary, + recipient: subscription.withdraw_address, subscription_id: subscription.id, first_charging_date: 0, last_charging_date: 0, @@ -81,12 +81,13 @@ export const InstallSubscriptionV2Notification: FC<{ payment_per_period: subscription.amount, currency: CryptoCurrency.TON, created_at: Date.now(), - deploy_value: toNano('0.1').toString(), - destroy_value: toNano('0.05').toString(), - caller_fee: toNano('0.05').toString(), + deploy_value: toNano('0.11').toString(), + destroy_value: toNano('0.08').toString(), + caller_fee: '10000000', payer: params.from, contract: '', period: subscription.period, + withdraw_msg_body: subscription.withdraw_msg_body, metadata: toSubscriptionMetadata(subscription.metadata) }; @@ -175,7 +176,7 @@ const ProInstallExtensionNotificationContent: FC< }); setTimeout(() => { - onClose(boc.toString()); + onClose(boc.toBoc().toString('base64')); }, 1500); return !!boc; diff --git a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx index 63cba8a26..1ab2e6278 100644 --- a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx +++ b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx @@ -129,7 +129,7 @@ const ProRemoveSubscriptionV2NotificationContent: FC<{ }); setTimeout(() => { - onClose(boc.toString()); + onClose(boc.toBoc().toString('base64')); }, 1500); return !!boc; From 085fd324e83f2122cc914eaac147b05578d24487 Mon Sep 17 00:00:00 2001 From: Dmitrii Liulekin Date: Fri, 31 Oct 2025 20:17:11 -0300 Subject: [PATCH 11/13] fix: apply tonconnect fixes --- packages/core/src/entries/tonConnect.ts | 11 ++- .../src/service/tonConnect/connectService.ts | 12 ++- .../InstallSubscriptionV2Notification.tsx | 20 ++-- .../RemoveSubscriptionV2Notification.tsx | 19 ++-- .../connect/TonConnectRequestNotification.tsx | 6 +- .../pro/ProInstallExtensionNotification.tsx | 6 +- .../subscription/useCancelSubscription.ts | 6 +- .../subscription/useCreateSubscription.ts | 93 ++++++++++++------- 8 files changed, 113 insertions(+), 60 deletions(-) diff --git a/packages/core/src/entries/tonConnect.ts b/packages/core/src/entries/tonConnect.ts index 872479270..e80e28153 100644 --- a/packages/core/src/entries/tonConnect.ts +++ b/packages/core/src/entries/tonConnect.ts @@ -593,7 +593,7 @@ export interface CancelSubscriptionV2AppRequest< ? AccountConnectionInjected : AccountConnection; kind: 'cancelSubscriptionV2'; - payload: any; + payload: CancelSubscriptionV2Payload; } export type TonConnectAppRequestPayload< @@ -653,4 +653,13 @@ export const createSubscriptionV2PayloadSchema = z.object({ valid_until: z.number() }); +export const cancelSubscriptionV2PayloadSchema = z.object({ + validUntil: z.number(), + extensionAddress: z.string(), + from: rawAddressSchema.optional(), + network: tonConnectNetworkSchema, + valid_until: z.number() +}); + export type CreateSubscriptionV2Payload = z.infer; +export type CancelSubscriptionV2Payload = z.infer; diff --git a/packages/core/src/service/tonConnect/connectService.ts b/packages/core/src/service/tonConnect/connectService.ts index c06f6283f..8a5180285 100644 --- a/packages/core/src/service/tonConnect/connectService.ts +++ b/packages/core/src/service/tonConnect/connectService.ts @@ -614,10 +614,18 @@ export async function checkTonConnectFromAndNetwork( } } -export const createSubscriptionV2SuccessResponse = (id: string, boc: string) => { +export interface ICreateSubscriptionV2Response { + boc: string; + extensionAddress: string; +} + +export const createSubscriptionV2SuccessResponse = ( + id: string, + result: ICreateSubscriptionV2Response +) => { return { id, - result: { boc } + result }; }; diff --git a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx index d3aa520f7..4e8be35d1 100644 --- a/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx +++ b/packages/uikit/src/components/connect/InstallSubscriptionV2Notification.tsx @@ -42,10 +42,11 @@ import { import { useProCompatibleAccountsWallets } from '../../state/wallet'; import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; import { toNano } from '@ton/core'; +import { ICreateSubscriptionV2Response } from '@tonkeeper/core/dist/service/tonConnect/connectService'; interface IProInstallExtensionProps { isOpen: boolean; - onClose: (boc?: string) => void; + onClose: (result?: ICreateSubscriptionV2Response) => void; extensionData?: SubscriptionExtension; } @@ -63,7 +64,7 @@ function toSubscriptionMetadata(src: SubscriptionMetadataSource): SubscriptionEx export const InstallSubscriptionV2Notification: FC<{ params: CreateSubscriptionV2Payload | null; - handleClose: (boc?: string) => void; + handleClose: (result?: ICreateSubscriptionV2Response) => void; }> = ({ params, handleClose }) => { const subscription = params?.subscription; @@ -170,16 +171,21 @@ const ProInstallExtensionNotificationContent: FC< throw new Error('Selected wallet is required!'); } - const boc = await deployMutation.mutateAsync({ - selectedWallet, - ...extensionData + const result = await deployMutation.mutateAsync({ + subscriptionParams: { + selectedWallet, + ...extensionData + }, + options: { + disableAddressCheck: true + } }); setTimeout(() => { - onClose(boc.toBoc().toString('base64')); + onClose(result); }, 1500); - return !!boc; + return !!result; }; const { diff --git a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx index 1ab2e6278..b930e2df4 100644 --- a/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx +++ b/packages/uikit/src/components/connect/RemoveSubscriptionV2Notification.tsx @@ -32,9 +32,10 @@ import { useToast } from '../../hooks/useNotification'; import { useDateTimeFormat } from '../../hooks/useDateTimeFormat'; import { hexToRGBA } from '../../libs/css'; import { toNano } from '@ton/core'; +import { CancelSubscriptionV2Payload } from '@tonkeeper/core/dist/entries/tonConnect'; export const RemoveSubscriptionV2Notification: FC<{ - params: any; + params: CancelSubscriptionV2Payload | null; handleClose: (boc?: string) => void; }> = ({ params, handleClose }) => { return ( @@ -49,7 +50,7 @@ export const RemoveSubscriptionV2Notification: FC<{ - {!!params?.subscription && ( + {!!params?.extensionAddress && ( void; }> = ({ onClose, params }) => { - const extensionContract = ''; + const { extensionAddress, from } = params; const destroyValue = toNano('0.05').toString(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - const accountWallet = accountsWallets.find(accWallet => accWallet.wallet.id === params?.from); + const accountWallet = accountsWallets.find(accWallet => accWallet.wallet.id === from); const selectedWallet = accountWallet?.wallet; const finalExpiresDate = new Date(); @@ -111,9 +112,9 @@ const ProRemoveSubscriptionV2NotificationContent: FC<{ if (!selectedWallet) return; estimateFeeMutation.mutate({ + destroyValue, selectedWallet, - extensionContract, - destroyValue + extensionContract: extensionAddress }); }, [selectedWallet]); @@ -123,13 +124,13 @@ const ProRemoveSubscriptionV2NotificationContent: FC<{ } const boc = await removeMutation.mutateAsync({ + destroyValue, selectedWallet, - extensionContract, - destroyValue + extensionContract: extensionAddress }); setTimeout(() => { - onClose(boc.toBoc().toString('base64')); + onClose(boc); }, 1500); return !!boc; diff --git a/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx b/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx index 63f9ec101..7f5f89c2a 100644 --- a/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx +++ b/packages/uikit/src/components/connect/TonConnectRequestNotification.tsx @@ -80,11 +80,11 @@ export const TonConnectRequestNotification: FC<{ /> { + handleClose={result => { if (request) { handleClose( - boc - ? createSubscriptionV2SuccessResponse(request.id, boc) + result + ? createSubscriptionV2SuccessResponse(request.id, result) : createSubscriptionV2ErrorResponse(request.id) ); } diff --git a/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx b/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx index facf32dbc..7bb54889d 100644 --- a/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx +++ b/packages/uikit/src/components/desktop/pro/ProInstallExtensionNotification.tsx @@ -115,8 +115,10 @@ const ProInstallExtensionNotificationContent: FC< } const result = deployMutation.mutateAsync({ - selectedWallet: targetAuth.wallet, - ...extensionData + subscriptionParams: { + selectedWallet: targetAuth.wallet, + ...extensionData + } }); return !!result; diff --git a/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts b/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts index 531df0482..614c302f2 100644 --- a/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts +++ b/packages/uikit/src/hooks/blockchain/subscription/useCancelSubscription.ts @@ -6,7 +6,7 @@ import { SubscriptionEncoder } from '@tonkeeper/core/dist/service/ton-blockchain import { backwardCompatibilityFilter } from '@tonkeeper/core/dist/service/proService'; import { QueryKey } from '../../../libs/queryKey'; import { CancelParams } from './commonTypes'; -import { Address, Cell } from '@ton/core'; +import { Address } from '@ton/core'; import { WalletMessageSender } from '@tonkeeper/core/dist/service/ton-blockchain/sender'; export const useCancelSubscription = () => { @@ -15,7 +15,7 @@ export const useCancelSubscription = () => { const client = useQueryClient(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - return useMutation(async subscriptionParams => { + return useMutation(async subscriptionParams => { if (!subscriptionParams) throw new Error('No params'); const { selectedWallet, extensionContract, destroyValue } = subscriptionParams; @@ -44,6 +44,6 @@ export const useCancelSubscription = () => { await client.invalidateQueries([QueryKey.pro]); - return boc; + return boc.toBoc().toString('base64'); }); }; diff --git a/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts b/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts index 8f94eb200..8f755b791 100644 --- a/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts +++ b/packages/uikit/src/hooks/blockchain/subscription/useCreateSubscription.ts @@ -1,4 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Address } from '@ton/core'; import { useActiveApi, useProCompatibleAccountsWallets } from '../../../state/wallet'; import { getSigner } from '../../../state/mnemonic'; import { useAppSdk } from '../../appSdk'; @@ -13,7 +14,14 @@ import { QueryKey } from '../../../libs/queryKey'; import { MetaEncryptionSerializedMap } from '@tonkeeper/core/dist/entries/wallet'; import { AppKey } from '@tonkeeper/core/dist/Keys'; import { metaEncryptionMapSerializer } from '@tonkeeper/core/dist/utils/metadata'; -import { Cell } from '@ton/core'; +import { ICreateSubscriptionV2Response } from '@tonkeeper/core/dist/service/tonConnect/connectService'; + +interface ICreateSubscriptionProps { + subscriptionParams: SubscriptionEncodingParams; + options?: { + disableAddressCheck?: boolean; + }; +} export const useCreateSubscription = () => { const sdk = useAppSdk(); @@ -21,49 +29,68 @@ export const useCreateSubscription = () => { const client = useQueryClient(); const accountsWallets = useProCompatibleAccountsWallets(backwardCompatibilityFilter); - return useMutation(async subscriptionParams => { - if (!subscriptionParams) throw new Error('No params'); + return useMutation( + async ({ subscriptionParams, options }) => { + if (!subscriptionParams) throw new Error('No params'); - const { selectedWallet } = subscriptionParams; + const { disableAddressCheck } = options ?? {}; - const accountWallet = accountsWallets.find( - accWallet => accWallet.wallet.id === selectedWallet.id - ); - const account = accountWallet?.account; - const accountId = account?.id; + const { admin, selectedWallet, contract, subscription_id } = subscriptionParams; - if (!accountId) throw new Error('Account id is required!'); + const accountWallet = accountsWallets.find( + accWallet => accWallet.wallet.id === selectedWallet.id + ); + const account = accountWallet?.account; + const accountId = account?.id; - let metaEncryptionMap = await sdk.storage - .get(AppKey.META_ENCRYPTION_MAP) - .then(metaEncryptionMapSerializer); + if (!accountId) throw new Error('Account id is required!'); - const shouldCreateMetaKeys = !metaEncryptionMap?.[selectedWallet.rawAddress]; + let metaEncryptionMap = await sdk.storage + .get(AppKey.META_ENCRYPTION_MAP) + .then(metaEncryptionMapSerializer); - const signer = await getSigner(sdk, accountId, { - walletId: selectedWallet.id, - shouldCreateMetaKeys - }).catch(() => null); + const shouldCreateMetaKeys = !metaEncryptionMap?.[selectedWallet.rawAddress]; - if (!signer || signer.type !== 'cell') throw new Error('Signer is incorrect!'); + const signer = await getSigner(sdk, accountId, { + walletId: selectedWallet.id, + shouldCreateMetaKeys + }).catch(() => null); - if (shouldCreateMetaKeys) { - metaEncryptionMap = await sdk.storage - .get(AppKey.META_ENCRYPTION_MAP) - .then(metaEncryptionMapSerializer); - } + if (!signer || signer.type !== 'cell') throw new Error('Signer is incorrect!'); - const sender = new WalletMessageSender(api, selectedWallet, signer); - const encoder = new SubscriptionEncoder(selectedWallet); + if (shouldCreateMetaKeys) { + metaEncryptionMap = await sdk.storage + .get(AppKey.META_ENCRYPTION_MAP) + .then(metaEncryptionMapSerializer); + } - const outgoingMsg = await encoder.encodeCreateSubscriptionV2( - prepareSubscriptionParamsForEncoder(subscriptionParams, metaEncryptionMap) - ); + const sender = new WalletMessageSender(api, selectedWallet, signer); + const encoder = new SubscriptionEncoder(selectedWallet); - const boc = await sender.send(outgoingMsg); + const beneficiary = Address.parse(admin); + const subscriptionId = subscription_id; - await client.invalidateQueries([QueryKey.metaEncryptionData]); + const extensionAddress = encoder.getExtensionAddress({ + beneficiary, + subscriptionId + }); - return boc; - }); + const outgoingMsg = await encoder.encodeCreateSubscriptionV2( + prepareSubscriptionParamsForEncoder(subscriptionParams, metaEncryptionMap) + ); + + if (!disableAddressCheck && !extensionAddress.equals(Address.parse(contract))) { + throw new Error('Contract extension addresses do not match!'); + } + + const boc = await sender.send(outgoingMsg); + + await client.invalidateQueries([QueryKey.metaEncryptionData]); + + return { + boc: boc.toBoc().toString('base64'), + extensionAddress: extensionAddress.toString() + }; + } + ); }; From 407d7a3dccdaf8c794e1b7094416220ba27d56ba Mon Sep 17 00:00:00 2001 From: Dmitrii Liulekin Date: Mon, 3 Nov 2025 07:15:13 -0300 Subject: [PATCH 12/13] chore: bump version --- apps/desktop/package.json | 2 +- apps/extension/package.json | 2 +- apps/mobile/package.json | 2 +- apps/web/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index fc6952dc8..c53e5a9ef 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@tonkeeper/desktop", "license": "Apache-2.0", - "version": "4.3.2", + "version": "4.3.3", "description": "Your desktop wallet on The Open Network", "main": ".webpack/main", "repository": { diff --git a/apps/extension/package.json b/apps/extension/package.json index 3d1f68c86..f8d1b90d8 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,6 +1,6 @@ { "name": "@tonkeeper/extension", - "version": "4.3.2", + "version": "4.3.3", "author": "Ton APPS UK Limited ", "description": "Your extension wallet on The Open Network", "dependencies": { diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c82ea3f56..987f8994d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@tonkeeper/mobile", - "version": "4.3.2", + "version": "4.3.3", "license": "Apache-2.0", "description": "Your tablet wallet on The Open Network", "type": "module", diff --git a/apps/web/package.json b/apps/web/package.json index ee8731d3f..86c745c63 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@tonkeeper/web", - "version": "4.3.2", + "version": "4.3.3", "author": "Ton APPS UK Limited ", "description": "Your web wallet on The Open Network", "dependencies": { From 9ac93e74c79dc0c2d2feaa9edf89afe195d643e4 Mon Sep 17 00:00:00 2001 From: Dmitrii Liulekin Date: Mon, 3 Nov 2025 07:25:28 -0300 Subject: [PATCH 13/13] fix: change returnType --- .../legacy-plugins/CancelLegacySubscriptionNotification.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx b/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx index b05d624d5..62828ba79 100644 --- a/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx +++ b/packages/uikit/src/components/legacy-plugins/CancelLegacySubscriptionNotification.tsx @@ -56,7 +56,7 @@ const CancelLegacySubscription: FC<{ pluginAddress: string; onClose: () => void if (succeed) { sdk.topMessage(t('unsubscribe_legacy_plugin_success_toast')); } - return succeed; + return !!succeed; }); }, [wallet, pluginAddress, t]);