Skip to content

Commit 0118437

Browse files
committed
feat(ui): implement cancel subscription functionality and enhance error handling in SubscriptionForm
1 parent 5279374 commit 0118437

File tree

3 files changed

+119
-7
lines changed

3 files changed

+119
-7
lines changed

apps/demo-dapp-with-react-ui/src/components/SubscriptionForm/SubscriptionForm.tsx

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import './style.scss';
44
import {
55
CreateSubscriptionV2Request,
66
CreateSubscriptionV2Response,
7+
CancelSubscriptionV2Request,
8+
CancelSubscriptionV2Response,
79
useTonConnectUI,
810
useTonWallet
911
} from '@tonconnect/ui-react';
@@ -36,6 +38,9 @@ export function SubscriptionForm() {
3638
const [subscriptionRes, setSubscriptionRes] = useState<CreateSubscriptionV2Response | null>(
3739
null
3840
);
41+
const [subscriptionError, setSubscriptionError] = useState<string | null>(null);
42+
const [cancelRes, setCancelRes] = useState<CancelSubscriptionV2Response | null>(null);
43+
const [cancelError, setCancelError] = useState<string | null>(null);
3944

4045
const wallet = useTonWallet();
4146
const [tonConnectUi] = useTonConnectUI();
@@ -48,10 +53,45 @@ export function SubscriptionForm() {
4853
// setSubscription(template);
4954
// };
5055

51-
const onSend = () =>
56+
const onSend = () => {
57+
setSubscriptionError(null);
5258
tonConnectUi
5359
.createSubscription(subscription, { version: 'v2' })
54-
.then(res => setSubscriptionRes(res));
60+
.then(res => {
61+
setSubscriptionRes(res);
62+
setSubscriptionError(null);
63+
})
64+
.catch(err => {
65+
setSubscriptionError(err instanceof Error ? err.message : String(err));
66+
setSubscriptionRes(null);
67+
});
68+
};
69+
70+
const onCancel = () => {
71+
if (!subscriptionRes?.boc) {
72+
console.error('No subscription response boc available');
73+
return;
74+
}
75+
76+
const cancelRequest: CancelSubscriptionV2Request = {
77+
validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes from now
78+
extensionAddress: subscriptionRes.boc,
79+
network: subscription.network,
80+
from: subscription.from
81+
};
82+
83+
setCancelError(null);
84+
tonConnectUi
85+
.cancelSubscription(cancelRequest, { version: 'v2' })
86+
.then(res => {
87+
setCancelRes(res);
88+
setCancelError(null);
89+
})
90+
.catch(err => {
91+
setCancelError(err instanceof Error ? err.message : String(err));
92+
setCancelRes(null);
93+
});
94+
};
5595

5696
return (
5797
<div className="create-subscription-form">
@@ -77,16 +117,42 @@ export function SubscriptionForm() {
77117
onAdd={onChange}
78118
onDelete={onChange}
79119
/>
120+
{subscriptionError && (
121+
<>
122+
<h4 style={{ color: 'red' }}>Create subscription error</h4>
123+
<div style={{ color: 'red', padding: '10px', border: '1px solid red' }}>
124+
{subscriptionError}
125+
</div>
126+
</>
127+
)}
80128
{subscriptionRes && (
81129
<>
82-
<h4>Create subscription response</h4>
130+
<h4 style={{ color: 'green' }}>Create subscription response</h4>
83131
<ReactJson name={false} src={subscriptionRes} theme="ocean" />
84132
</>
85133
)}
134+
{cancelError && (
135+
<>
136+
<h4 style={{ color: 'red' }}>Cancel subscription error</h4>
137+
<div style={{ color: 'red', padding: '10px', border: '1px solid red' }}>
138+
{cancelError}
139+
</div>
140+
</>
141+
)}
142+
{cancelRes && (
143+
<>
144+
<h4 style={{ color: 'green' }}>Cancel subscription response</h4>
145+
<ReactJson name={false} src={cancelRes} theme="ocean" />
146+
</>
147+
)}
86148
{wallet && (
87149
<div className="buttons-container">
88150
<button onClick={onSend}>Create subscription</button>
89-
{subscriptionRes && <button>Cancel subscription</button>}
151+
{subscriptionRes && (
152+
<button onClick={onCancel} disabled={!subscriptionRes?.boc}>
153+
Cancel subscription
154+
</button>
155+
)}
90156
</div>
91157
)}
92158
</div>

packages/sdk/src/ton-connect.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ConnectItem,
55
ConnectRequest,
66
CreateSubscriptionV2RpcResponseSuccess,
7+
CancelSubscriptionV2RpcResponseSuccess,
78
SendTransactionRpcResponseSuccess,
89
SignDataPayload,
910
SignDataRpcResponseSuccess,
@@ -71,7 +72,8 @@ import {
7172
validateSignDataPayload,
7273
validateConnectAdditionalRequest,
7374
validateTonProofItemReply,
74-
validateCreateSubscriptionV2Request
75+
validateCreateSubscriptionV2Request,
76+
validateCancelSubscriptionV2Request
7577
} from './validation/schemas';
7678
import { isQaModeEnabled } from './utils/qa-mode';
7779
import { normalizeBase64 } from './utils/base64';
@@ -605,7 +607,6 @@ export class TonConnect implements ITonConnect {
605607
throw new TonConnectError('Subscription V2 creation was aborted');
606608
}
607609

608-
// Validate the request
609610
const validationError = validateCreateSubscriptionV2Request(data);
610611
if (validationError) {
611612
throw new TonConnectError(validationError);
@@ -686,6 +687,11 @@ export class TonConnect implements ITonConnect {
686687
throw new TonConnectError('Subscription V2 cancellation was aborted');
687688
}
688689

690+
const validationError = validateCancelSubscriptionV2Request(data);
691+
if (validationError) {
692+
throw new TonConnectError(validationError);
693+
}
694+
689695
this.checkConnection();
690696
checkSubscriptionSupport(this.wallet!.device.features);
691697

@@ -719,7 +725,7 @@ export class TonConnect implements ITonConnect {
719725
}
720726

721727
const result = cancelSubscriptionV2Parser.convertFromRpcResponse(
722-
response as CreateSubscriptionV2RpcResponseSuccess
728+
response as CancelSubscriptionV2RpcResponseSuccess
723729
);
724730

725731
this.tracker.trackCancelSubscriptionV2Completed(this.wallet, data, result);

packages/sdk/src/validation/schemas.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,3 +554,43 @@ export function validateCreateSubscriptionV2Request(data: unknown): ValidationRe
554554

555555
return null;
556556
}
557+
558+
export function validateCancelSubscriptionV2Request(data: unknown): ValidationResult {
559+
if (!isValidObject(data)) {
560+
return 'CancelSubscriptionV2Request must be an object';
561+
}
562+
563+
const allowedKeys = ['validUntil', 'network', 'from', 'extensionAddress'];
564+
if (hasExtraProperties(data, allowedKeys)) {
565+
return 'CancelSubscriptionV2Request contains extra properties';
566+
}
567+
568+
if (data.validUntil === undefined) {
569+
return "Incorrect 'validUntil'";
570+
}
571+
if (!isValidNumber(data.validUntil)) {
572+
return "Incorrect 'validUntil'";
573+
}
574+
575+
const now = Math.floor(Date.now() / 1000);
576+
const fiveMinutesFromNow = now + 300;
577+
if (data.validUntil > fiveMinutesFromNow) {
578+
console.warn(`validUntil (${data.validUntil}) is more than 5 minutes from now (${now})`);
579+
}
580+
581+
if (data.network !== undefined) {
582+
if (!isValidNetwork(data.network)) {
583+
return "Invalid 'network' format";
584+
}
585+
}
586+
587+
if (data.from !== undefined && !isValidAddress(data.from)) {
588+
return "Invalid 'from' address format";
589+
}
590+
591+
if (!isValidAddress(data.extensionAddress)) {
592+
return "'extensionAddress' is required and must be a valid address";
593+
}
594+
595+
return null;
596+
}

0 commit comments

Comments
 (0)