@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
package/src/core/protocol.ts
CHANGED
|
@@ -248,14 +248,23 @@ export function parseCallbackURL(url: string, scheme: string): {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// Extract encoded payload
|
|
251
|
-
|
|
251
|
+
let encoded = url.substring(expectedPrefix.length);
|
|
252
|
+
|
|
253
|
+
// CRITICAL FIX: Decode URL encoding first (wallet may URL-encode the payload)
|
|
254
|
+
try {
|
|
255
|
+
encoded = decodeURIComponent(encoded);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// If decodeURIComponent fails, try using the original encoded string
|
|
258
|
+
// Some wallets may not URL-encode the payload
|
|
259
|
+
console.log('[TON Connect] Payload not URL-encoded, using as-is');
|
|
260
|
+
}
|
|
252
261
|
|
|
253
262
|
// CRITICAL FIX: Validate base64 payload size (prevent DoS)
|
|
254
263
|
if (encoded.length === 0 || encoded.length > 5000) {
|
|
255
264
|
return { type: 'unknown', data: null };
|
|
256
265
|
}
|
|
257
266
|
|
|
258
|
-
// CRITICAL FIX: Validate base64 characters only
|
|
267
|
+
// CRITICAL FIX: Validate base64 characters only (after URL decoding)
|
|
259
268
|
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
260
269
|
return { type: 'unknown', data: null };
|
|
261
270
|
}
|
|
@@ -306,14 +315,20 @@ export function parseCallbackURL(url: string, scheme: string): {
|
|
|
306
315
|
|
|
307
316
|
/**
|
|
308
317
|
* Extract wallet info from connection response
|
|
318
|
+
* CRITICAL: This function assumes response has been validated by validateConnectionResponse
|
|
309
319
|
*/
|
|
310
320
|
export function extractWalletInfo(
|
|
311
321
|
response: ConnectionResponsePayload
|
|
312
322
|
): WalletInfo {
|
|
323
|
+
// CRITICAL FIX: Add null checks to prevent runtime errors
|
|
324
|
+
if (!response || !response.name || !response.address || !response.publicKey) {
|
|
325
|
+
throw new Error('Invalid connection response: missing required fields');
|
|
326
|
+
}
|
|
327
|
+
|
|
313
328
|
return {
|
|
314
329
|
name: response.name,
|
|
315
|
-
appName: response.appName,
|
|
316
|
-
version: response.version,
|
|
330
|
+
appName: response.appName || response.name,
|
|
331
|
+
version: response.version || 'unknown',
|
|
317
332
|
platform: response.platform || 'unknown',
|
|
318
333
|
address: response.address,
|
|
319
334
|
publicKey: response.publicKey,
|
|
@@ -360,13 +375,65 @@ export function validateTransactionRequest(
|
|
|
360
375
|
return { valid: false, error: 'Transaction must have at least one message' };
|
|
361
376
|
}
|
|
362
377
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
378
|
+
// CRITICAL: Validate each message
|
|
379
|
+
for (let i = 0; i < request.messages.length; i++) {
|
|
380
|
+
const msg = request.messages[i];
|
|
381
|
+
|
|
382
|
+
// Validate address
|
|
383
|
+
if (!msg.address || typeof msg.address !== 'string') {
|
|
384
|
+
return { valid: false, error: `Message ${i + 1}: Address is required and must be a string` };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// CRITICAL: Validate TON address format (EQ... or 0Q...)
|
|
388
|
+
if (!/^(EQ|0Q)[A-Za-z0-9_-]{46}$/.test(msg.address)) {
|
|
389
|
+
return { valid: false, error: `Message ${i + 1}: Invalid TON address format. Address must start with EQ or 0Q and be 48 characters long.` };
|
|
366
390
|
}
|
|
367
|
-
|
|
368
|
-
|
|
391
|
+
|
|
392
|
+
// Validate amount
|
|
393
|
+
if (!msg.amount || typeof msg.amount !== 'string') {
|
|
394
|
+
return { valid: false, error: `Message ${i + 1}: Amount is required and must be a string (nanotons)` };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// CRITICAL: Validate amount is a valid positive number (nanotons)
|
|
398
|
+
try {
|
|
399
|
+
const amount = BigInt(msg.amount);
|
|
400
|
+
if (amount <= 0n) {
|
|
401
|
+
return { valid: false, error: `Message ${i + 1}: Amount must be greater than 0` };
|
|
402
|
+
}
|
|
403
|
+
// Check for reasonable maximum (prevent overflow)
|
|
404
|
+
if (amount > BigInt('1000000000000000000')) { // 1 billion TON
|
|
405
|
+
return { valid: false, error: `Message ${i + 1}: Amount exceeds maximum allowed (1 billion TON)` };
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return { valid: false, error: `Message ${i + 1}: Amount must be a valid number string (nanotons)` };
|
|
369
409
|
}
|
|
410
|
+
|
|
411
|
+
// Validate payload if provided (must be base64)
|
|
412
|
+
if (msg.payload !== undefined && msg.payload !== null) {
|
|
413
|
+
if (typeof msg.payload !== 'string') {
|
|
414
|
+
return { valid: false, error: `Message ${i + 1}: Payload must be a base64 string` };
|
|
415
|
+
}
|
|
416
|
+
// Basic base64 validation
|
|
417
|
+
if (msg.payload.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.payload)) {
|
|
418
|
+
return { valid: false, error: `Message ${i + 1}: Payload must be valid base64 encoded` };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Validate stateInit if provided (must be base64)
|
|
423
|
+
if (msg.stateInit !== undefined && msg.stateInit !== null) {
|
|
424
|
+
if (typeof msg.stateInit !== 'string') {
|
|
425
|
+
return { valid: false, error: `Message ${i + 1}: StateInit must be a base64 string` };
|
|
426
|
+
}
|
|
427
|
+
// Basic base64 validation
|
|
428
|
+
if (msg.stateInit.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.stateInit)) {
|
|
429
|
+
return { valid: false, error: `Message ${i + 1}: StateInit must be valid base64 encoded` };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// CRITICAL: Limit maximum number of messages (prevent DoS)
|
|
435
|
+
if (request.messages.length > 255) {
|
|
436
|
+
return { valid: false, error: 'Transaction cannot have more than 255 messages' };
|
|
370
437
|
}
|
|
371
438
|
|
|
372
439
|
return { valid: true };
|
package/src/core/wallets.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
|
|
|
31
31
|
appName: 'Tonkeeper',
|
|
32
32
|
universalLink: 'https://app.tonkeeper.com/ton-connect',
|
|
33
33
|
deepLink: 'tonkeeper://',
|
|
34
|
-
platforms: ['ios', 'android'],
|
|
34
|
+
platforms: ['ios', 'android', 'web'], // CRITICAL FIX: Tonkeeper Web is supported
|
|
35
35
|
preferredReturnStrategy: 'post_redirect', // CRITICAL FIX: 'back' strategy may not send callback properly, use 'post_redirect'
|
|
36
36
|
requiresReturnScheme: true, // CRITICAL FIX: Mobile apps need returnScheme for proper callback handling
|
|
37
37
|
},
|
package/src/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ import { getWalletByName, getDefaultWallet, SUPPORTED_WALLETS, type WalletDefini
|
|
|
40
40
|
* Custom error classes
|
|
41
41
|
*/
|
|
42
42
|
export class TonConnectError extends Error {
|
|
43
|
-
constructor(message: string, public code?: string) {
|
|
43
|
+
constructor(message: string, public code?: string, public recoverySuggestion?: string) {
|
|
44
44
|
super(message);
|
|
45
45
|
this.name = 'TonConnectError';
|
|
46
46
|
}
|
|
@@ -48,35 +48,55 @@ export class TonConnectError extends Error {
|
|
|
48
48
|
|
|
49
49
|
export class ConnectionTimeoutError extends TonConnectError {
|
|
50
50
|
constructor() {
|
|
51
|
-
super(
|
|
51
|
+
super(
|
|
52
|
+
'Connection request timed out. The wallet did not respond in time.',
|
|
53
|
+
'CONNECTION_TIMEOUT',
|
|
54
|
+
'Please make sure the wallet app is installed and try again. If the issue persists, check your internet connection.'
|
|
55
|
+
);
|
|
52
56
|
this.name = 'ConnectionTimeoutError';
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export class TransactionTimeoutError extends TonConnectError {
|
|
57
61
|
constructor() {
|
|
58
|
-
super(
|
|
62
|
+
super(
|
|
63
|
+
'Transaction request timed out. The wallet did not respond in time.',
|
|
64
|
+
'TRANSACTION_TIMEOUT',
|
|
65
|
+
'Please check the wallet app and try again. Make sure you approve or reject the transaction in the wallet.'
|
|
66
|
+
);
|
|
59
67
|
this.name = 'TransactionTimeoutError';
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
export class UserRejectedError extends TonConnectError {
|
|
64
|
-
constructor() {
|
|
65
|
-
super(
|
|
72
|
+
constructor(message?: string) {
|
|
73
|
+
super(
|
|
74
|
+
message || 'User rejected the request',
|
|
75
|
+
'USER_REJECTED',
|
|
76
|
+
'The user cancelled the operation in the wallet app.'
|
|
77
|
+
);
|
|
66
78
|
this.name = 'UserRejectedError';
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
export class ConnectionInProgressError extends TonConnectError {
|
|
71
83
|
constructor() {
|
|
72
|
-
super(
|
|
84
|
+
super(
|
|
85
|
+
'Connection request already in progress',
|
|
86
|
+
'CONNECTION_IN_PROGRESS',
|
|
87
|
+
'Please wait for the current connection attempt to complete before trying again.'
|
|
88
|
+
);
|
|
73
89
|
this.name = 'ConnectionInProgressError';
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
export class TransactionInProgressError extends TonConnectError {
|
|
78
94
|
constructor() {
|
|
79
|
-
super(
|
|
95
|
+
super(
|
|
96
|
+
'Transaction request already in progress',
|
|
97
|
+
'TRANSACTION_IN_PROGRESS',
|
|
98
|
+
'Please wait for the current transaction to complete before sending another one.'
|
|
99
|
+
);
|
|
80
100
|
this.name = 'TransactionInProgressError';
|
|
81
101
|
}
|
|
82
102
|
}
|
|
@@ -235,8 +255,9 @@ export class TonConnectMobile {
|
|
|
235
255
|
console.log('[TON Connect] Parsed callback:', parsed.type, parsed.data ? 'has data' : 'no data');
|
|
236
256
|
|
|
237
257
|
// CRITICAL FIX: Check for sign data response first (before other handlers)
|
|
238
|
-
if (
|
|
239
|
-
|
|
258
|
+
// Note: We check if promise exists and hasn't timed out (timeout !== null means not timed out yet)
|
|
259
|
+
if (this.signDataPromise && this.signDataPromise.timeout !== null) {
|
|
260
|
+
// Sign data request is pending and hasn't timed out
|
|
240
261
|
if (parsed.type === 'error' && parsed.data) {
|
|
241
262
|
const errorData = parsed.data as ErrorResponse;
|
|
242
263
|
if (errorData?.error) {
|
|
@@ -327,12 +348,18 @@ export class TonConnectMobile {
|
|
|
327
348
|
this.notifyStatusChange();
|
|
328
349
|
|
|
329
350
|
// Resolve connection promise
|
|
351
|
+
// CRITICAL: Only resolve if promise still exists and hasn't timed out
|
|
330
352
|
if (this.connectionPromise) {
|
|
353
|
+
// Clear timeout if it exists
|
|
331
354
|
if (this.connectionPromise.timeout !== null) {
|
|
332
355
|
clearTimeout(this.connectionPromise.timeout);
|
|
333
356
|
}
|
|
334
|
-
|
|
357
|
+
// Store reference before clearing to prevent race conditions
|
|
358
|
+
const promise = this.connectionPromise;
|
|
359
|
+
// Clear promise first
|
|
335
360
|
this.connectionPromise = null;
|
|
361
|
+
// Then resolve
|
|
362
|
+
promise.resolve(wallet);
|
|
336
363
|
}
|
|
337
364
|
}
|
|
338
365
|
|
|
@@ -346,15 +373,21 @@ export class TonConnectMobile {
|
|
|
346
373
|
}
|
|
347
374
|
|
|
348
375
|
// Resolve transaction promise
|
|
376
|
+
// CRITICAL: Only resolve if promise still exists and hasn't timed out
|
|
349
377
|
if (this.transactionPromise) {
|
|
378
|
+
// Clear timeout if it exists
|
|
350
379
|
if (this.transactionPromise.timeout !== null) {
|
|
351
380
|
clearTimeout(this.transactionPromise.timeout);
|
|
352
381
|
}
|
|
353
|
-
|
|
382
|
+
// Store reference before clearing
|
|
383
|
+
const promise = this.transactionPromise;
|
|
384
|
+
// Clear promise first to prevent race conditions
|
|
385
|
+
this.transactionPromise = null;
|
|
386
|
+
// Then resolve
|
|
387
|
+
promise.resolve({
|
|
354
388
|
boc: response.boc,
|
|
355
389
|
signature: response.signature,
|
|
356
390
|
});
|
|
357
|
-
this.transactionPromise = null;
|
|
358
391
|
}
|
|
359
392
|
}
|
|
360
393
|
|
|
@@ -376,6 +409,14 @@ export class TonConnectMobile {
|
|
|
376
409
|
this.transactionPromise.reject(error);
|
|
377
410
|
this.transactionPromise = null;
|
|
378
411
|
}
|
|
412
|
+
// CRITICAL FIX: Also clear signDataPromise to prevent memory leaks
|
|
413
|
+
if (this.signDataPromise) {
|
|
414
|
+
if (this.signDataPromise.timeout !== null) {
|
|
415
|
+
clearTimeout(this.signDataPromise.timeout);
|
|
416
|
+
}
|
|
417
|
+
this.signDataPromise.reject(error);
|
|
418
|
+
this.signDataPromise = null;
|
|
419
|
+
}
|
|
379
420
|
}
|
|
380
421
|
|
|
381
422
|
/**
|
|
@@ -754,6 +795,38 @@ export class TonConnectMobile {
|
|
|
754
795
|
return this.currentWallet;
|
|
755
796
|
}
|
|
756
797
|
|
|
798
|
+
/**
|
|
799
|
+
* Check if a wallet is available on the current platform
|
|
800
|
+
* Note: This is a best-effort check and may not be 100% accurate
|
|
801
|
+
* CRITICAL FIX: On web, if wallet has universalLink, it's considered available
|
|
802
|
+
* because universal links can open in new tabs/windows
|
|
803
|
+
*/
|
|
804
|
+
async isWalletAvailable(walletName?: string): Promise<boolean> {
|
|
805
|
+
const wallet = walletName ? getWalletByName(walletName) : this.currentWallet;
|
|
806
|
+
if (!wallet) {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// CRITICAL FIX: Check adapter type to reliably detect web platform
|
|
811
|
+
// WebAdapter is only used on web, so this is the most reliable check
|
|
812
|
+
const isWeb = this.adapter.constructor.name === 'WebAdapter';
|
|
813
|
+
|
|
814
|
+
if (isWeb) {
|
|
815
|
+
// On web, if wallet has universalLink or supports web platform, it's available
|
|
816
|
+
// Universal links can open in a new tab on web
|
|
817
|
+
return wallet.platforms.includes('web') || !!wallet.universalLink;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// On mobile, we can't reliably check if wallet is installed
|
|
821
|
+
// Return true if wallet supports the current platform
|
|
822
|
+
// eslint-disable-next-line no-undef
|
|
823
|
+
const platform = typeof globalThis !== 'undefined' && (globalThis as any).Platform
|
|
824
|
+
? (globalThis as any).Platform.OS === 'ios' ? 'ios' : 'android'
|
|
825
|
+
: 'android';
|
|
826
|
+
|
|
827
|
+
return wallet.platforms.includes(platform);
|
|
828
|
+
}
|
|
829
|
+
|
|
757
830
|
/**
|
|
758
831
|
* Set preferred wallet for connections
|
|
759
832
|
*/
|
|
@@ -928,3 +1001,7 @@ export * from './types';
|
|
|
928
1001
|
export type { WalletDefinition } from './core/wallets';
|
|
929
1002
|
export { SUPPORTED_WALLETS, getWalletByName, getDefaultWallet, getWalletsForPlatform } from './core/wallets';
|
|
930
1003
|
|
|
1004
|
+
// Export utilities
|
|
1005
|
+
export * from './utils/transactionBuilder';
|
|
1006
|
+
export * from './utils/retry';
|
|
1007
|
+
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
|
7
7
|
import { TonConnectMobile, ConnectionStatus, WalletInfo, SendTransactionRequest } from '../index';
|
|
8
8
|
import type { TonConnectMobileConfig } from '../types';
|
|
9
|
+
import { WalletSelectionModal } from './WalletSelectionModal';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Account information (compatible with @tonconnect/ui-react)
|
|
@@ -108,7 +109,18 @@ export function TonConnectUIProvider({
|
|
|
108
109
|
children,
|
|
109
110
|
sdkInstance,
|
|
110
111
|
}: TonConnectUIProviderProps): JSX.Element {
|
|
111
|
-
|
|
112
|
+
// CRITICAL: Initialize SDK only once
|
|
113
|
+
const [sdk] = useState<TonConnectMobile>(() => {
|
|
114
|
+
if (sdkInstance) {
|
|
115
|
+
return sdkInstance;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return new TonConnectMobile(config);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('[TonConnectUIProvider] Failed to initialize SDK:', error);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
112
124
|
const [walletState, setWalletState] = useState<WalletState | null>(null);
|
|
113
125
|
const [modalOpen, setModalOpen] = useState(false);
|
|
114
126
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
@@ -160,8 +172,10 @@ export function TonConnectUIProvider({
|
|
|
160
172
|
|
|
161
173
|
// Open modal
|
|
162
174
|
const openModal = useCallback(async () => {
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
if (!walletState?.connected) {
|
|
176
|
+
setModalOpen(true);
|
|
177
|
+
}
|
|
178
|
+
}, [walletState?.connected]);
|
|
165
179
|
|
|
166
180
|
// Close modal
|
|
167
181
|
const closeModal = useCallback(() => {
|
|
@@ -199,11 +213,24 @@ export function TonConnectUIProvider({
|
|
|
199
213
|
// Send transaction
|
|
200
214
|
const sendTransaction = useCallback(
|
|
201
215
|
async (transaction: SendTransactionRequest): Promise<TransactionResponse> => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
216
|
+
try {
|
|
217
|
+
// Validate transaction before sending
|
|
218
|
+
if (!transaction || !transaction.messages || transaction.messages.length === 0) {
|
|
219
|
+
throw new Error('Invalid transaction: messages array is required and cannot be empty');
|
|
220
|
+
}
|
|
221
|
+
if (!transaction.validUntil || transaction.validUntil <= Date.now()) {
|
|
222
|
+
throw new Error('Invalid transaction: validUntil must be in the future');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const response = await sdk.sendTransaction(transaction);
|
|
226
|
+
return {
|
|
227
|
+
boc: response.boc,
|
|
228
|
+
signature: response.signature,
|
|
229
|
+
};
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('[TonConnectUIProvider] Transaction error:', error);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
207
234
|
},
|
|
208
235
|
[sdk]
|
|
209
236
|
);
|
|
@@ -211,11 +238,21 @@ export function TonConnectUIProvider({
|
|
|
211
238
|
// Sign data
|
|
212
239
|
const signData = useCallback(
|
|
213
240
|
async (request: SignDataRequest): Promise<SignDataResponse> => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
try {
|
|
242
|
+
// Validate request
|
|
243
|
+
if (!request || (!request.data && request.data !== '')) {
|
|
244
|
+
throw new Error('Invalid sign data request: data is required');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const response = await sdk.signData(request.data, request.version);
|
|
248
|
+
return {
|
|
249
|
+
signature: response.signature,
|
|
250
|
+
timestamp: response.timestamp,
|
|
251
|
+
};
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('[TonConnectUIProvider] Sign data error:', error);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
219
256
|
},
|
|
220
257
|
[sdk]
|
|
221
258
|
);
|
|
@@ -240,7 +277,16 @@ export function TonConnectUIProvider({
|
|
|
240
277
|
sdk,
|
|
241
278
|
};
|
|
242
279
|
|
|
243
|
-
return
|
|
280
|
+
return (
|
|
281
|
+
<TonConnectUIContext.Provider value={contextValue}>
|
|
282
|
+
{children}
|
|
283
|
+
{/* Auto-show wallet selection modal when modalOpen is true */}
|
|
284
|
+
<WalletSelectionModal
|
|
285
|
+
visible={modalOpen && !walletState?.connected}
|
|
286
|
+
onClose={closeModal}
|
|
287
|
+
/>
|
|
288
|
+
</TonConnectUIContext.Provider>
|
|
289
|
+
);
|
|
244
290
|
}
|
|
245
291
|
|
|
246
292
|
/**
|