@blazium/ton-connect-mobile 1.1.5 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +424 -145
- package/dist/adapters/expo.js +3 -3
- package/dist/adapters/react-native.js +3 -3
- package/dist/core/protocol.d.ts +1 -0
- package/dist/core/protocol.js +69 -9
- package/dist/core/wallets.js +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +64 -12
- package/dist/react/TonConnectUIProvider.js +54 -14
- package/dist/react/WalletSelectionModal.d.ts +20 -0
- package/dist/react/WalletSelectionModal.js +228 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +3 -1
- package/dist/utils/retry.d.ts +31 -0
- package/dist/utils/retry.js +67 -0
- package/dist/utils/transactionBuilder.d.ts +67 -0
- package/dist/utils/transactionBuilder.js +174 -0
- package/package.json +1 -1
- package/src/adapters/expo.ts +3 -3
- package/src/adapters/react-native.ts +3 -3
- package/src/core/protocol.ts +76 -9
- package/src/core/wallets.ts +1 -1
- package/src/index.ts +89 -12
- package/src/react/TonConnectUIProvider.tsx +60 -14
- package/src/react/WalletSelectionModal.tsx +301 -0
- package/src/react/index.ts +2 -0
- package/src/utils/retry.ts +101 -0
- package/src/utils/transactionBuilder.ts +202 -0
|
@@ -55,15 +55,15 @@ class ReactNativeAdapter {
|
|
|
55
55
|
else {
|
|
56
56
|
console.log('[ReactNativeAdapter] Skipping canOpenURL check (Android compatibility)');
|
|
57
57
|
}
|
|
58
|
-
// CRITICAL FIX: Android
|
|
59
|
-
//
|
|
58
|
+
// CRITICAL FIX: On Android, canOpenURL() may not recognize tonconnect:// protocol
|
|
59
|
+
// So we call openURL() directly. If it fails, it will throw an error.
|
|
60
60
|
await Linking.openURL(url);
|
|
61
61
|
console.log('[ReactNativeAdapter] URL opened successfully');
|
|
62
62
|
return true;
|
|
63
63
|
}
|
|
64
64
|
catch (error) {
|
|
65
65
|
console.error('[ReactNativeAdapter] Error in openURL:', error);
|
|
66
|
-
// Android
|
|
66
|
+
// On Android, if tonconnect:// protocol is not recognized or wallet is not installed, it will throw an error
|
|
67
67
|
const errorMessage = error?.message || String(error);
|
|
68
68
|
if (errorMessage.includes('No Activity found') || errorMessage.includes('No app found') || errorMessage.includes('Cannot open URL')) {
|
|
69
69
|
throw new Error('No TON wallet app found. Please install Tonkeeper or another TON Connect compatible wallet from Google Play Store.');
|
package/dist/core/protocol.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare function parseCallbackURL(url: string, scheme: string): {
|
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
37
|
* Extract wallet info from connection response
|
|
38
|
+
* CRITICAL: This function assumes response has been validated by validateConnectionResponse
|
|
38
39
|
*/
|
|
39
40
|
export declare function extractWalletInfo(response: ConnectionResponsePayload): WalletInfo;
|
|
40
41
|
/**
|
package/dist/core/protocol.js
CHANGED
|
@@ -195,12 +195,21 @@ function parseCallbackURL(url, scheme) {
|
|
|
195
195
|
return { type: 'unknown', data: null };
|
|
196
196
|
}
|
|
197
197
|
// Extract encoded payload
|
|
198
|
-
|
|
198
|
+
let encoded = url.substring(expectedPrefix.length);
|
|
199
|
+
// CRITICAL FIX: Decode URL encoding first (wallet may URL-encode the payload)
|
|
200
|
+
try {
|
|
201
|
+
encoded = decodeURIComponent(encoded);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
// If decodeURIComponent fails, try using the original encoded string
|
|
205
|
+
// Some wallets may not URL-encode the payload
|
|
206
|
+
console.log('[TON Connect] Payload not URL-encoded, using as-is');
|
|
207
|
+
}
|
|
199
208
|
// CRITICAL FIX: Validate base64 payload size (prevent DoS)
|
|
200
209
|
if (encoded.length === 0 || encoded.length > 5000) {
|
|
201
210
|
return { type: 'unknown', data: null };
|
|
202
211
|
}
|
|
203
|
-
// CRITICAL FIX: Validate base64 characters only
|
|
212
|
+
// CRITICAL FIX: Validate base64 characters only (after URL decoding)
|
|
204
213
|
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
205
214
|
return { type: 'unknown', data: null };
|
|
206
215
|
}
|
|
@@ -241,12 +250,17 @@ function parseCallbackURL(url, scheme) {
|
|
|
241
250
|
}
|
|
242
251
|
/**
|
|
243
252
|
* Extract wallet info from connection response
|
|
253
|
+
* CRITICAL: This function assumes response has been validated by validateConnectionResponse
|
|
244
254
|
*/
|
|
245
255
|
function extractWalletInfo(response) {
|
|
256
|
+
// CRITICAL FIX: Add null checks to prevent runtime errors
|
|
257
|
+
if (!response || !response.name || !response.address || !response.publicKey) {
|
|
258
|
+
throw new Error('Invalid connection response: missing required fields');
|
|
259
|
+
}
|
|
246
260
|
return {
|
|
247
261
|
name: response.name,
|
|
248
|
-
appName: response.appName,
|
|
249
|
-
version: response.version,
|
|
262
|
+
appName: response.appName || response.name,
|
|
263
|
+
version: response.version || 'unknown',
|
|
250
264
|
platform: response.platform || 'unknown',
|
|
251
265
|
address: response.address,
|
|
252
266
|
publicKey: response.publicKey,
|
|
@@ -280,13 +294,59 @@ function validateTransactionRequest(request) {
|
|
|
280
294
|
if (!request.messages || request.messages.length === 0) {
|
|
281
295
|
return { valid: false, error: 'Transaction must have at least one message' };
|
|
282
296
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
297
|
+
// CRITICAL: Validate each message
|
|
298
|
+
for (let i = 0; i < request.messages.length; i++) {
|
|
299
|
+
const msg = request.messages[i];
|
|
300
|
+
// Validate address
|
|
301
|
+
if (!msg.address || typeof msg.address !== 'string') {
|
|
302
|
+
return { valid: false, error: `Message ${i + 1}: Address is required and must be a string` };
|
|
303
|
+
}
|
|
304
|
+
// CRITICAL: Validate TON address format (EQ... or 0Q...)
|
|
305
|
+
if (!/^(EQ|0Q)[A-Za-z0-9_-]{46}$/.test(msg.address)) {
|
|
306
|
+
return { valid: false, error: `Message ${i + 1}: Invalid TON address format. Address must start with EQ or 0Q and be 48 characters long.` };
|
|
307
|
+
}
|
|
308
|
+
// Validate amount
|
|
309
|
+
if (!msg.amount || typeof msg.amount !== 'string') {
|
|
310
|
+
return { valid: false, error: `Message ${i + 1}: Amount is required and must be a string (nanotons)` };
|
|
311
|
+
}
|
|
312
|
+
// CRITICAL: Validate amount is a valid positive number (nanotons)
|
|
313
|
+
try {
|
|
314
|
+
const amount = BigInt(msg.amount);
|
|
315
|
+
if (amount <= 0n) {
|
|
316
|
+
return { valid: false, error: `Message ${i + 1}: Amount must be greater than 0` };
|
|
317
|
+
}
|
|
318
|
+
// Check for reasonable maximum (prevent overflow)
|
|
319
|
+
if (amount > BigInt('1000000000000000000')) { // 1 billion TON
|
|
320
|
+
return { valid: false, error: `Message ${i + 1}: Amount exceeds maximum allowed (1 billion TON)` };
|
|
321
|
+
}
|
|
286
322
|
}
|
|
287
|
-
|
|
288
|
-
return { valid: false, error:
|
|
323
|
+
catch (error) {
|
|
324
|
+
return { valid: false, error: `Message ${i + 1}: Amount must be a valid number string (nanotons)` };
|
|
289
325
|
}
|
|
326
|
+
// Validate payload if provided (must be base64)
|
|
327
|
+
if (msg.payload !== undefined && msg.payload !== null) {
|
|
328
|
+
if (typeof msg.payload !== 'string') {
|
|
329
|
+
return { valid: false, error: `Message ${i + 1}: Payload must be a base64 string` };
|
|
330
|
+
}
|
|
331
|
+
// Basic base64 validation
|
|
332
|
+
if (msg.payload.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.payload)) {
|
|
333
|
+
return { valid: false, error: `Message ${i + 1}: Payload must be valid base64 encoded` };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Validate stateInit if provided (must be base64)
|
|
337
|
+
if (msg.stateInit !== undefined && msg.stateInit !== null) {
|
|
338
|
+
if (typeof msg.stateInit !== 'string') {
|
|
339
|
+
return { valid: false, error: `Message ${i + 1}: StateInit must be a base64 string` };
|
|
340
|
+
}
|
|
341
|
+
// Basic base64 validation
|
|
342
|
+
if (msg.stateInit.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.stateInit)) {
|
|
343
|
+
return { valid: false, error: `Message ${i + 1}: StateInit must be valid base64 encoded` };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// CRITICAL: Limit maximum number of messages (prevent DoS)
|
|
348
|
+
if (request.messages.length > 255) {
|
|
349
|
+
return { valid: false, error: 'Transaction cannot have more than 255 messages' };
|
|
290
350
|
}
|
|
291
351
|
return { valid: true };
|
|
292
352
|
}
|
package/dist/core/wallets.js
CHANGED
|
@@ -17,7 +17,7 @@ exports.SUPPORTED_WALLETS = [
|
|
|
17
17
|
appName: 'Tonkeeper',
|
|
18
18
|
universalLink: 'https://app.tonkeeper.com/ton-connect',
|
|
19
19
|
deepLink: 'tonkeeper://',
|
|
20
|
-
platforms: ['ios', 'android'],
|
|
20
|
+
platforms: ['ios', 'android', 'web'], // CRITICAL FIX: Tonkeeper Web is supported
|
|
21
21
|
preferredReturnStrategy: 'post_redirect', // CRITICAL FIX: 'back' strategy may not send callback properly, use 'post_redirect'
|
|
22
22
|
requiresReturnScheme: true, // CRITICAL FIX: Mobile apps need returnScheme for proper callback handling
|
|
23
23
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { type WalletDefinition } from './core/wallets';
|
|
|
9
9
|
*/
|
|
10
10
|
export declare class TonConnectError extends Error {
|
|
11
11
|
code?: string | undefined;
|
|
12
|
-
|
|
12
|
+
recoverySuggestion?: string | undefined;
|
|
13
|
+
constructor(message: string, code?: string | undefined, recoverySuggestion?: string | undefined);
|
|
13
14
|
}
|
|
14
15
|
export declare class ConnectionTimeoutError extends TonConnectError {
|
|
15
16
|
constructor();
|
|
@@ -18,7 +19,7 @@ export declare class TransactionTimeoutError extends TonConnectError {
|
|
|
18
19
|
constructor();
|
|
19
20
|
}
|
|
20
21
|
export declare class UserRejectedError extends TonConnectError {
|
|
21
|
-
constructor();
|
|
22
|
+
constructor(message?: string);
|
|
22
23
|
}
|
|
23
24
|
export declare class ConnectionInProgressError extends TonConnectError {
|
|
24
25
|
constructor();
|
|
@@ -99,6 +100,13 @@ export declare class TonConnectMobile {
|
|
|
99
100
|
* Get current wallet being used
|
|
100
101
|
*/
|
|
101
102
|
getCurrentWallet(): WalletDefinition;
|
|
103
|
+
/**
|
|
104
|
+
* Check if a wallet is available on the current platform
|
|
105
|
+
* Note: This is a best-effort check and may not be 100% accurate
|
|
106
|
+
* CRITICAL FIX: On web, if wallet has universalLink, it's considered available
|
|
107
|
+
* because universal links can open in new tabs/windows
|
|
108
|
+
*/
|
|
109
|
+
isWalletAvailable(walletName?: string): Promise<boolean>;
|
|
102
110
|
/**
|
|
103
111
|
* Set preferred wallet for connections
|
|
104
112
|
*/
|
|
@@ -135,3 +143,5 @@ export declare class TonConnectMobile {
|
|
|
135
143
|
export * from './types';
|
|
136
144
|
export type { WalletDefinition } from './core/wallets';
|
|
137
145
|
export { SUPPORTED_WALLETS, getWalletByName, getDefaultWallet, getWalletsForPlatform } from './core/wallets';
|
|
146
|
+
export * from './utils/transactionBuilder';
|
|
147
|
+
export * from './utils/retry';
|
package/dist/index.js
CHANGED
|
@@ -29,44 +29,45 @@ const wallets_1 = require("./core/wallets");
|
|
|
29
29
|
* Custom error classes
|
|
30
30
|
*/
|
|
31
31
|
class TonConnectError extends Error {
|
|
32
|
-
constructor(message, code) {
|
|
32
|
+
constructor(message, code, recoverySuggestion) {
|
|
33
33
|
super(message);
|
|
34
34
|
this.code = code;
|
|
35
|
+
this.recoverySuggestion = recoverySuggestion;
|
|
35
36
|
this.name = 'TonConnectError';
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
exports.TonConnectError = TonConnectError;
|
|
39
40
|
class ConnectionTimeoutError extends TonConnectError {
|
|
40
41
|
constructor() {
|
|
41
|
-
super('Connection request timed out', 'CONNECTION_TIMEOUT');
|
|
42
|
+
super('Connection request timed out. The wallet did not respond in time.', 'CONNECTION_TIMEOUT', 'Please make sure the wallet app is installed and try again. If the issue persists, check your internet connection.');
|
|
42
43
|
this.name = 'ConnectionTimeoutError';
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
exports.ConnectionTimeoutError = ConnectionTimeoutError;
|
|
46
47
|
class TransactionTimeoutError extends TonConnectError {
|
|
47
48
|
constructor() {
|
|
48
|
-
super('Transaction request timed out', 'TRANSACTION_TIMEOUT');
|
|
49
|
+
super('Transaction request timed out. The wallet did not respond in time.', 'TRANSACTION_TIMEOUT', 'Please check the wallet app and try again. Make sure you approve or reject the transaction in the wallet.');
|
|
49
50
|
this.name = 'TransactionTimeoutError';
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
exports.TransactionTimeoutError = TransactionTimeoutError;
|
|
53
54
|
class UserRejectedError extends TonConnectError {
|
|
54
|
-
constructor() {
|
|
55
|
-
super('User rejected the request', 'USER_REJECTED');
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message || 'User rejected the request', 'USER_REJECTED', 'The user cancelled the operation in the wallet app.');
|
|
56
57
|
this.name = 'UserRejectedError';
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
exports.UserRejectedError = UserRejectedError;
|
|
60
61
|
class ConnectionInProgressError extends TonConnectError {
|
|
61
62
|
constructor() {
|
|
62
|
-
super('Connection request already in progress', 'CONNECTION_IN_PROGRESS');
|
|
63
|
+
super('Connection request already in progress', 'CONNECTION_IN_PROGRESS', 'Please wait for the current connection attempt to complete before trying again.');
|
|
63
64
|
this.name = 'ConnectionInProgressError';
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
exports.ConnectionInProgressError = ConnectionInProgressError;
|
|
67
68
|
class TransactionInProgressError extends TonConnectError {
|
|
68
69
|
constructor() {
|
|
69
|
-
super('Transaction request already in progress', 'TRANSACTION_IN_PROGRESS');
|
|
70
|
+
super('Transaction request already in progress', 'TRANSACTION_IN_PROGRESS', 'Please wait for the current transaction to complete before sending another one.');
|
|
70
71
|
this.name = 'TransactionInProgressError';
|
|
71
72
|
}
|
|
72
73
|
}
|
|
@@ -197,8 +198,9 @@ class TonConnectMobile {
|
|
|
197
198
|
const parsed = (0, protocol_1.parseCallbackURL)(url, this.config.scheme);
|
|
198
199
|
console.log('[TON Connect] Parsed callback:', parsed.type, parsed.data ? 'has data' : 'no data');
|
|
199
200
|
// CRITICAL FIX: Check for sign data response first (before other handlers)
|
|
200
|
-
if (
|
|
201
|
-
|
|
201
|
+
// Note: We check if promise exists and hasn't timed out (timeout !== null means not timed out yet)
|
|
202
|
+
if (this.signDataPromise && this.signDataPromise.timeout !== null) {
|
|
203
|
+
// Sign data request is pending and hasn't timed out
|
|
202
204
|
if (parsed.type === 'error' && parsed.data) {
|
|
203
205
|
const errorData = parsed.data;
|
|
204
206
|
if (errorData?.error) {
|
|
@@ -283,12 +285,18 @@ class TonConnectMobile {
|
|
|
283
285
|
this.currentStatus = { connected: true, wallet };
|
|
284
286
|
this.notifyStatusChange();
|
|
285
287
|
// Resolve connection promise
|
|
288
|
+
// CRITICAL: Only resolve if promise still exists and hasn't timed out
|
|
286
289
|
if (this.connectionPromise) {
|
|
290
|
+
// Clear timeout if it exists
|
|
287
291
|
if (this.connectionPromise.timeout !== null) {
|
|
288
292
|
clearTimeout(this.connectionPromise.timeout);
|
|
289
293
|
}
|
|
290
|
-
|
|
294
|
+
// Store reference before clearing to prevent race conditions
|
|
295
|
+
const promise = this.connectionPromise;
|
|
296
|
+
// Clear promise first
|
|
291
297
|
this.connectionPromise = null;
|
|
298
|
+
// Then resolve
|
|
299
|
+
promise.resolve(wallet);
|
|
292
300
|
}
|
|
293
301
|
}
|
|
294
302
|
/**
|
|
@@ -300,15 +308,21 @@ class TonConnectMobile {
|
|
|
300
308
|
return;
|
|
301
309
|
}
|
|
302
310
|
// Resolve transaction promise
|
|
311
|
+
// CRITICAL: Only resolve if promise still exists and hasn't timed out
|
|
303
312
|
if (this.transactionPromise) {
|
|
313
|
+
// Clear timeout if it exists
|
|
304
314
|
if (this.transactionPromise.timeout !== null) {
|
|
305
315
|
clearTimeout(this.transactionPromise.timeout);
|
|
306
316
|
}
|
|
307
|
-
|
|
317
|
+
// Store reference before clearing
|
|
318
|
+
const promise = this.transactionPromise;
|
|
319
|
+
// Clear promise first to prevent race conditions
|
|
320
|
+
this.transactionPromise = null;
|
|
321
|
+
// Then resolve
|
|
322
|
+
promise.resolve({
|
|
308
323
|
boc: response.boc,
|
|
309
324
|
signature: response.signature,
|
|
310
325
|
});
|
|
311
|
-
this.transactionPromise = null;
|
|
312
326
|
}
|
|
313
327
|
}
|
|
314
328
|
/**
|
|
@@ -329,6 +343,14 @@ class TonConnectMobile {
|
|
|
329
343
|
this.transactionPromise.reject(error);
|
|
330
344
|
this.transactionPromise = null;
|
|
331
345
|
}
|
|
346
|
+
// CRITICAL FIX: Also clear signDataPromise to prevent memory leaks
|
|
347
|
+
if (this.signDataPromise) {
|
|
348
|
+
if (this.signDataPromise.timeout !== null) {
|
|
349
|
+
clearTimeout(this.signDataPromise.timeout);
|
|
350
|
+
}
|
|
351
|
+
this.signDataPromise.reject(error);
|
|
352
|
+
this.signDataPromise = null;
|
|
353
|
+
}
|
|
332
354
|
}
|
|
333
355
|
/**
|
|
334
356
|
* Connect to wallet
|
|
@@ -645,6 +667,33 @@ class TonConnectMobile {
|
|
|
645
667
|
getCurrentWallet() {
|
|
646
668
|
return this.currentWallet;
|
|
647
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* Check if a wallet is available on the current platform
|
|
672
|
+
* Note: This is a best-effort check and may not be 100% accurate
|
|
673
|
+
* CRITICAL FIX: On web, if wallet has universalLink, it's considered available
|
|
674
|
+
* because universal links can open in new tabs/windows
|
|
675
|
+
*/
|
|
676
|
+
async isWalletAvailable(walletName) {
|
|
677
|
+
const wallet = walletName ? (0, wallets_1.getWalletByName)(walletName) : this.currentWallet;
|
|
678
|
+
if (!wallet) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
// CRITICAL FIX: Check adapter type to reliably detect web platform
|
|
682
|
+
// WebAdapter is only used on web, so this is the most reliable check
|
|
683
|
+
const isWeb = this.adapter.constructor.name === 'WebAdapter';
|
|
684
|
+
if (isWeb) {
|
|
685
|
+
// On web, if wallet has universalLink or supports web platform, it's available
|
|
686
|
+
// Universal links can open in a new tab on web
|
|
687
|
+
return wallet.platforms.includes('web') || !!wallet.universalLink;
|
|
688
|
+
}
|
|
689
|
+
// On mobile, we can't reliably check if wallet is installed
|
|
690
|
+
// Return true if wallet supports the current platform
|
|
691
|
+
// eslint-disable-next-line no-undef
|
|
692
|
+
const platform = typeof globalThis !== 'undefined' && globalThis.Platform
|
|
693
|
+
? globalThis.Platform.OS === 'ios' ? 'ios' : 'android'
|
|
694
|
+
: 'android';
|
|
695
|
+
return wallet.platforms.includes(platform);
|
|
696
|
+
}
|
|
648
697
|
/**
|
|
649
698
|
* Set preferred wallet for connections
|
|
650
699
|
*/
|
|
@@ -805,3 +854,6 @@ Object.defineProperty(exports, "SUPPORTED_WALLETS", { enumerable: true, get: fun
|
|
|
805
854
|
Object.defineProperty(exports, "getWalletByName", { enumerable: true, get: function () { return wallets_2.getWalletByName; } });
|
|
806
855
|
Object.defineProperty(exports, "getDefaultWallet", { enumerable: true, get: function () { return wallets_2.getDefaultWallet; } });
|
|
807
856
|
Object.defineProperty(exports, "getWalletsForPlatform", { enumerable: true, get: function () { return wallets_2.getWalletsForPlatform; } });
|
|
857
|
+
// Export utilities
|
|
858
|
+
__exportStar(require("./utils/transactionBuilder"), exports);
|
|
859
|
+
__exportStar(require("./utils/retry"), exports);
|
|
@@ -44,13 +44,26 @@ exports.useTonConnectModal = useTonConnectModal;
|
|
|
44
44
|
exports.useTonConnectSDK = useTonConnectSDK;
|
|
45
45
|
const react_1 = __importStar(require("react"));
|
|
46
46
|
const index_1 = require("../index");
|
|
47
|
+
const WalletSelectionModal_1 = require("./WalletSelectionModal");
|
|
47
48
|
const TonConnectUIContext = (0, react_1.createContext)(null);
|
|
48
49
|
/**
|
|
49
50
|
* TonConnectUIProvider - React context provider for TON Connect
|
|
50
51
|
* Compatible with @tonconnect/ui-react API
|
|
51
52
|
*/
|
|
52
53
|
function TonConnectUIProvider({ config, children, sdkInstance, }) {
|
|
53
|
-
|
|
54
|
+
// CRITICAL: Initialize SDK only once
|
|
55
|
+
const [sdk] = (0, react_1.useState)(() => {
|
|
56
|
+
if (sdkInstance) {
|
|
57
|
+
return sdkInstance;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return new index_1.TonConnectMobile(config);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error('[TonConnectUIProvider] Failed to initialize SDK:', error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
54
67
|
const [walletState, setWalletState] = (0, react_1.useState)(null);
|
|
55
68
|
const [modalOpen, setModalOpen] = (0, react_1.useState)(false);
|
|
56
69
|
const [isConnecting, setIsConnecting] = (0, react_1.useState)(false);
|
|
@@ -98,8 +111,10 @@ function TonConnectUIProvider({ config, children, sdkInstance, }) {
|
|
|
98
111
|
}, [sdk, updateWalletState]);
|
|
99
112
|
// Open modal
|
|
100
113
|
const openModal = (0, react_1.useCallback)(async () => {
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
if (!walletState?.connected) {
|
|
115
|
+
setModalOpen(true);
|
|
116
|
+
}
|
|
117
|
+
}, [walletState?.connected]);
|
|
103
118
|
// Close modal
|
|
104
119
|
const closeModal = (0, react_1.useCallback)(() => {
|
|
105
120
|
setModalOpen(false);
|
|
@@ -132,19 +147,42 @@ function TonConnectUIProvider({ config, children, sdkInstance, }) {
|
|
|
132
147
|
}, [sdk]);
|
|
133
148
|
// Send transaction
|
|
134
149
|
const sendTransaction = (0, react_1.useCallback)(async (transaction) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
try {
|
|
151
|
+
// Validate transaction before sending
|
|
152
|
+
if (!transaction || !transaction.messages || transaction.messages.length === 0) {
|
|
153
|
+
throw new Error('Invalid transaction: messages array is required and cannot be empty');
|
|
154
|
+
}
|
|
155
|
+
if (!transaction.validUntil || transaction.validUntil <= Date.now()) {
|
|
156
|
+
throw new Error('Invalid transaction: validUntil must be in the future');
|
|
157
|
+
}
|
|
158
|
+
const response = await sdk.sendTransaction(transaction);
|
|
159
|
+
return {
|
|
160
|
+
boc: response.boc,
|
|
161
|
+
signature: response.signature,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('[TonConnectUIProvider] Transaction error:', error);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
140
168
|
}, [sdk]);
|
|
141
169
|
// Sign data
|
|
142
170
|
const signData = (0, react_1.useCallback)(async (request) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
171
|
+
try {
|
|
172
|
+
// Validate request
|
|
173
|
+
if (!request || (!request.data && request.data !== '')) {
|
|
174
|
+
throw new Error('Invalid sign data request: data is required');
|
|
175
|
+
}
|
|
176
|
+
const response = await sdk.signData(request.data, request.version);
|
|
177
|
+
return {
|
|
178
|
+
signature: response.signature,
|
|
179
|
+
timestamp: response.timestamp,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error('[TonConnectUIProvider] Sign data error:', error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
148
186
|
}, [sdk]);
|
|
149
187
|
// Create TonConnectUI instance
|
|
150
188
|
const tonConnectUI = {
|
|
@@ -164,7 +202,9 @@ function TonConnectUIProvider({ config, children, sdkInstance, }) {
|
|
|
164
202
|
tonConnectUI,
|
|
165
203
|
sdk,
|
|
166
204
|
};
|
|
167
|
-
return react_1.default.createElement(TonConnectUIContext.Provider, { value: contextValue },
|
|
205
|
+
return (react_1.default.createElement(TonConnectUIContext.Provider, { value: contextValue },
|
|
206
|
+
children,
|
|
207
|
+
react_1.default.createElement(WalletSelectionModal_1.WalletSelectionModal, { visible: modalOpen && !walletState?.connected, onClose: closeModal })));
|
|
168
208
|
}
|
|
169
209
|
/**
|
|
170
210
|
* Hook to access TonConnectUI instance
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WalletSelectionModal component
|
|
3
|
+
* Provides a beautiful wallet selection UI compatible with @tonconnect/ui-react
|
|
4
|
+
*/
|
|
5
|
+
import type { WalletDefinition } from '../index';
|
|
6
|
+
export interface WalletSelectionModalProps {
|
|
7
|
+
/** Whether the modal is visible */
|
|
8
|
+
visible: boolean;
|
|
9
|
+
/** Callback when modal should close */
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
/** Custom wallet list (optional, uses SDK's supported wallets by default) */
|
|
12
|
+
wallets?: WalletDefinition[];
|
|
13
|
+
/** Custom styles */
|
|
14
|
+
style?: any;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* WalletSelectionModal - Beautiful wallet selection modal
|
|
18
|
+
* Compatible with @tonconnect/ui-react modal behavior
|
|
19
|
+
*/
|
|
20
|
+
export declare function WalletSelectionModal({ visible, onClose, wallets: customWallets, style, }: WalletSelectionModalProps): JSX.Element;
|