Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/demo-dapp-with-react-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { Routes, Route } from 'react-router-dom';
import { Header } from './components/Header/Header';
import { TxForm } from './components/TxForm/TxForm';
import { Footer } from './components/Footer/Footer';
import { TonProofDemo } from './components/TonProofDemo/TonProofDemo';
import { CreateJettonDemo } from './components/CreateJettonDemo/CreateJettonDemo';
import { WalletBatchLimitsTester } from './components/WalletBatchLimitsTester/WalletBatchLimitsTester';
import { SignDataTester } from './components/SignDataTester/SignDataTester';
import { MerkleExample } from './components/MerkleExample/MerkleExample';
import { FindTransactionDemo } from './components/FindTransactionDemo/FindTransactionDemo';
import { TransferUsdt } from './components/TransferUsdt/TransferUsdt';
import { SubscriptionForm } from './components/SubscriptionForm/SubscriptionForm';

function HomePage() {
return (
Expand All @@ -21,9 +21,10 @@ function HomePage() {
<SignDataTester />
<TransferUsdt />
<CreateJettonDemo />
<TonProofDemo />
{/* <TonProofDemo /> */}
<FindTransactionDemo />
<MerkleExample />
<SubscriptionForm />
<Footer />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { useCallback, useState } from 'react';
import ReactJson from 'react-json-view';
import './style.scss';
import {
CreateSubscriptionV2Request,
CreateSubscriptionV2Response,
CancelSubscriptionV2Request,
CancelSubscriptionV2Response,
useTonConnectUI,
useTonWallet
} from '@tonconnect/ui-react';
import { Cell, loadMessage } from '@ton/core';

/**
* Parse extension address from BOC (external message)
* @param boc - Base64 encoded BOC string
* @returns Extension address in user-friendly format or error message
*/
function parseExtensionAddressFromBoc(boc: string): string {
try {
const slice = Cell.fromBase64(boc).beginParse();
const message = loadMessage(slice);

// Extract destination address from message info
if (message.info.type === 'external-out') {
return message.info.dest?.toString() || 'No destination address found';
} else if (message.info.type === 'internal') {
return message.info.dest.toString();
} else if (message.info.type === 'external-in') {
return 'External-in message (no destination)';
}

return 'Unknown message type';
} catch (error) {
return `Error parsing BOC: ${error instanceof Error ? error.message : String(error)}`;
}
}

const baseSubscriptionPayload: CreateSubscriptionV2Request = {
validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes from now
subscription: {
beneficiary: 'UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL',
id: 0,
period: 1209600, // 2 week
amount: '100000000',
firstChargeDate: Math.floor(Date.now() / 1000) + 86400, // 1 day from now
withdrawAddress: 'UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL',
withdrawMsgBody: 'asdsadasdasda',
metadata: {
logo: 'https://myapp.com/logo.png',
name: 'Example Subscription',
description: 'This is an example subscription service.',
link: 'https://myapp.com',
tos: 'https://myapp.com/tos',
merchant: 'Example Merchant',
website: 'https://myapp.com'
}
}
};

const baseCancelPayload: CancelSubscriptionV2Request = {
validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes from now
extensionAddress: ''
};

export function SubscriptionForm() {
const [subscription, setSubscription] =
useState<CreateSubscriptionV2Request>(baseSubscriptionPayload);
const [subscriptionRes, setSubscriptionRes] = useState<CreateSubscriptionV2Response | null>(
null
);
const [subscriptionError, setSubscriptionError] = useState<string | null>(null);

const [cancelPayload, setCancelPayload] =
useState<CancelSubscriptionV2Request>(baseCancelPayload);
const [cancelRes, setCancelRes] = useState<CancelSubscriptionV2Response | null>(null);
const [cancelError, setCancelError] = useState<string | null>(null);

const wallet = useTonWallet();
const [tonConnectUi] = useTonConnectUI();

const onSubscriptionChange = useCallback((value: object) => {
setSubscription((value as { updated_src: typeof subscription }).updated_src);
}, []);

const onCancelChange = useCallback((value: object) => {
setCancelPayload((value as { updated_src: typeof cancelPayload }).updated_src);
}, []);

// const loadTemplate = (template: CreateSubscriptionV2Request) => {
// setSubscription(template);
// };

const onSend = () => {
setSubscriptionError(null);

tonConnectUi
.createSubscription(subscription, { version: 'v2' })
.then(res => {
// TODO: remove res.extensionAddress property for release, only for testing purposes
const extensionAddress =
// @ts-ignore
res.extensionAddress ?? parseExtensionAddressFromBoc(res.boc);

setSubscriptionRes(res);
setSubscriptionError(null);
// Auto-fill extensionAddress in cancel form
if (extensionAddress) {
setCancelPayload(prev => ({
...prev,
extensionAddress: extensionAddress
}));
}
})
.catch(err => {
setSubscriptionError(err instanceof Error ? err.message : String(err));
setSubscriptionRes(null);
});
};

const onCancel = () => {
if (!cancelPayload.extensionAddress) {
setCancelError('No extensionAddress provided');
return;
}

setCancelError(null);
tonConnectUi
.cancelSubscription(cancelPayload, { version: 'v2' })
.then(res => {
setCancelRes(res);
setCancelError(null);
})
.catch(err => {
setCancelError(err instanceof Error ? err.message : String(err));
setCancelRes(null);
});
};

return (
<div className="create-subscription-form">
<h3>Subscriptions</h3>

{/* SUBSCRIBE BLOCK */}
<div className="subscription-block">
<h4 style={{ marginBottom: '10px' }}>Subscription Request Data</h4>

<ReactJson
name={false}
src={subscription}
theme="ocean"
onEdit={onSubscriptionChange}
onAdd={onSubscriptionChange}
onDelete={onSubscriptionChange}
/>

{subscriptionError && (
<>
<h4 style={{ color: 'red' }}>Error</h4>
<div style={{ color: 'red', padding: '10px', border: '1px solid red' }}>
{subscriptionError}
</div>
</>
)}

{subscriptionRes && (
<>
<h4 style={{ color: 'green' }}>Create subscription response</h4>
<ReactJson name={false} src={subscriptionRes} theme="ocean" />
<div
style={{
marginTop: '10px',
padding: '10px',
backgroundColor: '#f0f0f0',
borderRadius: '4px'
}}
>
<strong>Parsed Extension Address from BOC:</strong>
<div
style={{
fontFamily: 'monospace',
marginTop: '5px',
wordBreak: 'break-all'
}}
>
{parseExtensionAddressFromBoc(subscriptionRes.boc)}
</div>
</div>
</>
)}

{wallet && (
<div className="buttons-container">
<button onClick={onSend}>Create subscription</button>
</div>
)}
</div>

{/* UNSUBSCRIBE BLOCK */}
<div className="cancel-block">
<h4 style={{ marginBottom: '10px' }}>Cancel Request Data</h4>

<ReactJson
name={false}
src={cancelPayload}
theme="ocean"
onEdit={onCancelChange}
onAdd={onCancelChange}
onDelete={onCancelChange}
/>

{cancelError && (
<>
<h4 style={{ color: 'red' }}>Error</h4>
<div style={{ color: 'red', padding: '10px', border: '1px solid red' }}>
{cancelError}
</div>
</>
)}

{cancelRes && (
<>
<h4 style={{ color: 'green' }}>Cancel subscription response</h4>
<ReactJson name={false} src={cancelRes} theme="ocean" />
</>
)}

{wallet && (
<div className="buttons-container">
<button onClick={onCancel} disabled={!cancelPayload.extensionAddress}>
Cancel subscription
</button>
</div>
)}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.create-subscription-form {
flex: 1;
display: flex;
width: 100%;
flex-direction: column;
gap: 20px;
padding: 20px;
align-items: center;

h3 {
color: white;
opacity: 0.8;
font-size: 28px;
}

h4 {
color: white;
opacity: 0.8;
font-size: 20px;
margin: 0;
}

> div {
width: 100%;

span {
word-break: break-word;
}
}

.buttons-container {
display: flex;
gap: 10px;
margin-top: 16px;
justify-content: center;

> button {
border: none;
padding: 7px 15px;
border-radius: 15px;
cursor: pointer;

background-color: rgba(102, 170, 238, 0.91);
color: white;
font-size: 16px;
line-height: 20px;

transition: transform 0.1s ease-in-out;

&:hover {
transform: scale(1.03);
}

&:active {
transform: scale(0.97);
}
}
}

.verification-result {
margin-top: 16px;
padding: 10px;
border-radius: 4px;
font-weight: bold;

&.success {
background-color: rgba(0, 255, 0, 0.1);
color: #00ff00;
}

&.error {
background-color: rgba(255, 0, 0, 0.1);
color: #ff0000;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import { SendTransactionRpcRequest } from './send-transaction-rpc-request';
import { SignDataRpcRequest } from './sign-data-rpc-request';
import { RpcMethod } from '../../rpc-method';
import { DisconnectRpcRequest } from './disconnect-rpc-request';
import { CreateSubscriptionV2RpcRequest } from './create-subscription-v2-rpc-request';
import { CancelSubscriptionV2RpcRequest } from './cancel-subscription-v2-rpc-request';

export type RpcRequests = {
sendTransaction: SendTransactionRpcRequest;
signData: SignDataRpcRequest;
disconnect: DisconnectRpcRequest;
createSubscriptionV2: CreateSubscriptionV2RpcRequest;
cancelSubscriptionV2: CancelSubscriptionV2RpcRequest;
};

export type AppRequest<T extends RpcMethod> = RpcRequests[T];
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CancelSubscriptionV2RpcRequest {
method: 'cancelSubscriptionV2';
params: [string];
id: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CreateSubscriptionV2RpcRequest {
method: 'createSubscriptionV2';
params: [string];
id: string;
}
2 changes: 2 additions & 0 deletions packages/protocol/src/models/app-message/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { AppRequest, RpcRequests } from './app-request';
export { SendTransactionRpcRequest } from './send-transaction-rpc-request';
export { SignDataRpcRequest } from './sign-data-rpc-request';
export { DisconnectRpcRequest } from './disconnect-rpc-request';
export { CreateSubscriptionV2RpcRequest } from './create-subscription-v2-rpc-request';
export { CancelSubscriptionV2RpcRequest } from './cancel-subscription-v2-rpc-request';
14 changes: 13 additions & 1 deletion packages/protocol/src/models/feature.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export type Feature = SendTransactionFeatureDeprecated | SendTransactionFeature | SignDataFeature;
export type Feature =
| SendTransactionFeatureDeprecated
| SendTransactionFeature
| SignDataFeature
| SubscriptionFeature;

export type FeatureName = Exclude<Feature, 'SendTransaction'>['name'];

export type SendTransactionFeatureDeprecated = 'SendTransaction';
Expand All @@ -10,3 +15,10 @@ export type SendTransactionFeature = {

export type SignDataType = 'text' | 'binary' | 'cell';
export type SignDataFeature = { name: 'SignData'; types: SignDataType[] };

export type SubscriptionFeature = {
name: 'Subscription';
versions: {
v2: boolean;
};
};
Loading