@blazium/ton-connect-mobile 1.1.1 → 1.1.3
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/dist/core/protocol.d.ts +2 -2
- package/dist/core/protocol.js +16 -11
- package/dist/core/wallets.d.ts +4 -0
- package/dist/core/wallets.js +8 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +191 -4
- package/dist/react/TonConnectButton.d.ts +22 -0
- package/dist/react/TonConnectButton.js +105 -0
- package/dist/react/TonConnectUIProvider.d.ts +110 -0
- package/dist/react/TonConnectUIProvider.js +209 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +15 -0
- package/dist/react/package.json +4 -0
- package/dist/types/index.d.ts +4 -2
- package/package.json +14 -1
- package/src/core/protocol.ts +22 -11
- package/src/core/wallets.ts +12 -0
- package/src/index.ts +229 -5
- package/src/react/TonConnectButton.tsx +107 -0
- package/src/react/TonConnectUIProvider.tsx +290 -0
- package/src/react/index.ts +24 -0
- package/src/types/index.ts +4 -4
package/src/index.ts
CHANGED
|
@@ -22,11 +22,13 @@ import {
|
|
|
22
22
|
validateConnectionResponse,
|
|
23
23
|
validateTransactionRequest,
|
|
24
24
|
validateTransactionResponse,
|
|
25
|
+
decodeBase64URL,
|
|
25
26
|
} from './core/protocol';
|
|
26
27
|
import type {
|
|
27
28
|
ConnectionResponsePayload,
|
|
28
29
|
TransactionResponsePayload,
|
|
29
30
|
ErrorResponse,
|
|
31
|
+
ConnectionRequestPayload,
|
|
30
32
|
} from './types';
|
|
31
33
|
import { verifyConnectionProof, generateSessionId } from './core/crypto';
|
|
32
34
|
import { ExpoAdapter } from './adapters/expo';
|
|
@@ -99,6 +101,11 @@ export class TonConnectMobile {
|
|
|
99
101
|
reject: (error: Error) => void;
|
|
100
102
|
timeout: number | null;
|
|
101
103
|
} | null = null;
|
|
104
|
+
private signDataPromise: {
|
|
105
|
+
resolve: (response: { signature: string; timestamp: number }) => void;
|
|
106
|
+
reject: (error: Error) => void;
|
|
107
|
+
timeout: number | null;
|
|
108
|
+
} | null = null;
|
|
102
109
|
|
|
103
110
|
constructor(config: TonConnectMobileConfig) {
|
|
104
111
|
// Validate config
|
|
@@ -219,6 +226,41 @@ export class TonConnectMobile {
|
|
|
219
226
|
const parsed = parseCallbackURL(url, this.config.scheme);
|
|
220
227
|
console.log('[TON Connect] Parsed callback:', parsed.type, parsed.data ? 'has data' : 'no data');
|
|
221
228
|
|
|
229
|
+
// CRITICAL FIX: Check for sign data response first (before other handlers)
|
|
230
|
+
if (this.signDataPromise && !this.signDataPromise.timeout) {
|
|
231
|
+
// Sign data request is pending
|
|
232
|
+
if (parsed.type === 'error' && parsed.data) {
|
|
233
|
+
const errorData = parsed.data as ErrorResponse;
|
|
234
|
+
if (errorData?.error) {
|
|
235
|
+
const promise = this.signDataPromise;
|
|
236
|
+
this.signDataPromise = null;
|
|
237
|
+
if (errorData.error.code === 300) {
|
|
238
|
+
promise.reject(new UserRejectedError());
|
|
239
|
+
} else {
|
|
240
|
+
promise.reject(new TonConnectError(errorData.error.message || 'Sign data failed'));
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check for sign data response format
|
|
247
|
+
// Note: TON Connect protocol may return sign data in different format
|
|
248
|
+
// We check for signature field in the response
|
|
249
|
+
if (parsed.data && typeof parsed.data === 'object') {
|
|
250
|
+
const data = parsed.data as any;
|
|
251
|
+
if (data.signature && typeof data.signature === 'string') {
|
|
252
|
+
const promise = this.signDataPromise;
|
|
253
|
+
this.signDataPromise = null;
|
|
254
|
+
promise.resolve({
|
|
255
|
+
signature: data.signature,
|
|
256
|
+
timestamp: data.timestamp || Date.now(),
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Handle connection responses
|
|
222
264
|
if (parsed.type === 'connect' && parsed.data) {
|
|
223
265
|
this.handleConnectionResponse(parsed.data as ConnectionResponsePayload);
|
|
224
266
|
} else if (parsed.type === 'transaction' && parsed.data) {
|
|
@@ -349,11 +391,35 @@ export class TonConnectMobile {
|
|
|
349
391
|
// Build connection request URL (use wallet's universal link)
|
|
350
392
|
console.log('[TON Connect] Building connection request URL for wallet:', this.currentWallet.name);
|
|
351
393
|
console.log('[TON Connect] Using universal link:', this.currentWallet.universalLink);
|
|
352
|
-
|
|
353
|
-
console.log('[TON Connect]
|
|
394
|
+
console.log('[TON Connect] Wallet return strategy:', this.currentWallet.preferredReturnStrategy || 'back');
|
|
395
|
+
console.log('[TON Connect] Wallet requires returnScheme:', this.currentWallet.requiresReturnScheme !== false);
|
|
396
|
+
|
|
397
|
+
const url = buildConnectionRequest(
|
|
398
|
+
this.config.manifestUrl,
|
|
399
|
+
this.config.scheme,
|
|
400
|
+
this.currentWallet.universalLink,
|
|
401
|
+
this.currentWallet.preferredReturnStrategy,
|
|
402
|
+
this.currentWallet.requiresReturnScheme
|
|
403
|
+
);
|
|
354
404
|
|
|
355
|
-
// DEBUG:
|
|
356
|
-
|
|
405
|
+
// DEBUG: Decode and log the payload for debugging
|
|
406
|
+
try {
|
|
407
|
+
const urlParts = url.split('?');
|
|
408
|
+
if (urlParts.length > 1) {
|
|
409
|
+
const payload = urlParts[1];
|
|
410
|
+
// CRITICAL FIX: Handle URL encoding - payload might have additional encoding
|
|
411
|
+
const cleanPayload = decodeURIComponent(payload);
|
|
412
|
+
const decoded = decodeBase64URL<ConnectionRequestPayload>(cleanPayload);
|
|
413
|
+
console.log('[TON Connect] Connection request payload:', JSON.stringify(decoded, null, 2));
|
|
414
|
+
}
|
|
415
|
+
} catch (e: any) {
|
|
416
|
+
// Log decode errors for debugging but don't fail
|
|
417
|
+
console.log('[TON Connect] Could not decode payload for logging:', e?.message || e);
|
|
418
|
+
// This is just for logging, the actual URL is correct
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log('[TON Connect] Built URL:', url.substring(0, 100) + '...');
|
|
422
|
+
console.log('[TON Connect] Full URL:', url);
|
|
357
423
|
console.log('[TON Connect] Manifest URL:', this.config.manifestUrl);
|
|
358
424
|
console.log('[TON Connect] Return scheme:', this.config.scheme);
|
|
359
425
|
console.log('[TON Connect] Adapter type:', this.adapter.constructor.name);
|
|
@@ -437,7 +503,14 @@ export class TonConnectMobile {
|
|
|
437
503
|
}
|
|
438
504
|
|
|
439
505
|
// Build transaction request URL (use universal link for Android compatibility)
|
|
440
|
-
const url = buildTransactionRequest(
|
|
506
|
+
const url = buildTransactionRequest(
|
|
507
|
+
this.config.manifestUrl,
|
|
508
|
+
request,
|
|
509
|
+
this.config.scheme,
|
|
510
|
+
this.currentWallet.universalLink,
|
|
511
|
+
this.currentWallet.preferredReturnStrategy,
|
|
512
|
+
this.currentWallet.requiresReturnScheme
|
|
513
|
+
);
|
|
441
514
|
|
|
442
515
|
// Create promise for transaction
|
|
443
516
|
return new Promise<{ boc: string; signature: string }>((resolve, reject) => {
|
|
@@ -490,6 +563,156 @@ export class TonConnectMobile {
|
|
|
490
563
|
});
|
|
491
564
|
}
|
|
492
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Sign data (for authentication, etc.)
|
|
568
|
+
* Note: Not all wallets support signData. This is a TON Connect extension.
|
|
569
|
+
*/
|
|
570
|
+
async signData(data: string | Uint8Array, version: string = '1.0'): Promise<{ signature: string; timestamp: number }> {
|
|
571
|
+
// Check if connected
|
|
572
|
+
if (!this.currentStatus.connected || !this.currentStatus.wallet) {
|
|
573
|
+
throw new TonConnectError('Not connected to wallet. Call connect() first.');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Helper function to encode bytes to base64
|
|
577
|
+
const base64EncodeBytes = (bytes: Uint8Array): string => {
|
|
578
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
579
|
+
let result = '';
|
|
580
|
+
let i = 0;
|
|
581
|
+
|
|
582
|
+
while (i < bytes.length) {
|
|
583
|
+
const a = bytes[i++];
|
|
584
|
+
const b = i < bytes.length ? bytes[i++] : 0;
|
|
585
|
+
const c = i < bytes.length ? bytes[i++] : 0;
|
|
586
|
+
|
|
587
|
+
const bitmap = (a << 16) | (b << 8) | c;
|
|
588
|
+
|
|
589
|
+
result += chars.charAt((bitmap >> 18) & 63);
|
|
590
|
+
result += chars.charAt((bitmap >> 12) & 63);
|
|
591
|
+
result += i - 2 < bytes.length ? chars.charAt((bitmap >> 6) & 63) : '=';
|
|
592
|
+
result += i - 1 < bytes.length ? chars.charAt(bitmap & 63) : '=';
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return result;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// Helper function to get TextEncoder
|
|
599
|
+
const getTextEncoder = (): { encode(input: string): Uint8Array } => {
|
|
600
|
+
// eslint-disable-next-line no-undef
|
|
601
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).TextEncoder) {
|
|
602
|
+
// eslint-disable-next-line no-undef
|
|
603
|
+
return new (globalThis as any).TextEncoder();
|
|
604
|
+
}
|
|
605
|
+
// Fallback: manual encoding
|
|
606
|
+
return {
|
|
607
|
+
encode(input: string): Uint8Array {
|
|
608
|
+
const bytes = new Uint8Array(input.length);
|
|
609
|
+
for (let i = 0; i < input.length; i++) {
|
|
610
|
+
bytes[i] = input.charCodeAt(i);
|
|
611
|
+
}
|
|
612
|
+
return bytes;
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// Convert data to base64
|
|
618
|
+
let dataBase64: string;
|
|
619
|
+
if (typeof data === 'string') {
|
|
620
|
+
// Check if it's already base64
|
|
621
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
622
|
+
if (base64Regex.test(data) && data.length % 4 === 0) {
|
|
623
|
+
// Likely base64, use as-is
|
|
624
|
+
dataBase64 = data;
|
|
625
|
+
} else {
|
|
626
|
+
// Not base64, encode it
|
|
627
|
+
const encoder = getTextEncoder();
|
|
628
|
+
const bytes = encoder.encode(data);
|
|
629
|
+
dataBase64 = base64EncodeBytes(bytes);
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
// Uint8Array - convert to base64
|
|
633
|
+
dataBase64 = base64EncodeBytes(data);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Build sign data request
|
|
637
|
+
const payload = {
|
|
638
|
+
manifestUrl: this.config.manifestUrl,
|
|
639
|
+
data: dataBase64,
|
|
640
|
+
version,
|
|
641
|
+
returnStrategy: this.currentWallet.preferredReturnStrategy || 'back',
|
|
642
|
+
returnScheme: this.currentWallet.requiresReturnScheme !== false ? this.config.scheme : undefined,
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// Encode payload
|
|
646
|
+
const { encodeBase64URL } = require('./core/protocol');
|
|
647
|
+
const encoded = encodeBase64URL(payload);
|
|
648
|
+
|
|
649
|
+
// Build URL
|
|
650
|
+
const baseUrl = this.currentWallet.universalLink.endsWith('/ton-connect')
|
|
651
|
+
? this.currentWallet.universalLink
|
|
652
|
+
: `${this.currentWallet.universalLink}/ton-connect`;
|
|
653
|
+
const url = `${baseUrl}/sign-data?${encoded}`;
|
|
654
|
+
|
|
655
|
+
// Open wallet app and wait for response
|
|
656
|
+
return new Promise<{ signature: string; timestamp: number }>((resolve, reject) => {
|
|
657
|
+
let timeout: number | null = null;
|
|
658
|
+
let resolved = false;
|
|
659
|
+
|
|
660
|
+
// CRITICAL FIX: Check if sign data is already in progress
|
|
661
|
+
if (this.signDataPromise) {
|
|
662
|
+
throw new TonConnectError('Sign data request already in progress');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Create promise for sign data
|
|
666
|
+
const signDataPromise = {
|
|
667
|
+
resolve: (response: { signature: string; timestamp: number }) => {
|
|
668
|
+
if (timeout !== null) {
|
|
669
|
+
clearTimeout(timeout);
|
|
670
|
+
}
|
|
671
|
+
resolved = true;
|
|
672
|
+
if (this.signDataPromise === signDataPromise) {
|
|
673
|
+
this.signDataPromise = null;
|
|
674
|
+
}
|
|
675
|
+
resolve(response);
|
|
676
|
+
},
|
|
677
|
+
reject: (error: Error) => {
|
|
678
|
+
if (timeout !== null) {
|
|
679
|
+
clearTimeout(timeout);
|
|
680
|
+
}
|
|
681
|
+
resolved = true;
|
|
682
|
+
if (this.signDataPromise === signDataPromise) {
|
|
683
|
+
this.signDataPromise = null;
|
|
684
|
+
}
|
|
685
|
+
reject(error);
|
|
686
|
+
},
|
|
687
|
+
timeout: null as number | null,
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// Set timeout
|
|
691
|
+
timeout = setTimeout(() => {
|
|
692
|
+
if (!resolved && this.signDataPromise === signDataPromise) {
|
|
693
|
+
this.signDataPromise = null;
|
|
694
|
+
signDataPromise.reject(new TonConnectError('Sign data request timed out'));
|
|
695
|
+
}
|
|
696
|
+
}, this.config.transactionTimeout) as unknown as number;
|
|
697
|
+
|
|
698
|
+
signDataPromise.timeout = timeout;
|
|
699
|
+
|
|
700
|
+
// Store promise for callback handling
|
|
701
|
+
// CRITICAL FIX: Don't mutate handleCallback method - use a separate tracking mechanism
|
|
702
|
+
this.signDataPromise = signDataPromise;
|
|
703
|
+
|
|
704
|
+
// Open URL
|
|
705
|
+
this.adapter.openURL(url, this.config.skipCanOpenURLCheck).then(() => {
|
|
706
|
+
// URL opened, wait for callback
|
|
707
|
+
// Callback will be handled by handleCallback method checking signDataPromise
|
|
708
|
+
}).catch((error: Error) => {
|
|
709
|
+
// Clear promise on error
|
|
710
|
+
this.signDataPromise = null;
|
|
711
|
+
signDataPromise.reject(new TonConnectError(`Failed to open wallet: ${error?.message || String(error)}`));
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
493
716
|
/**
|
|
494
717
|
* Disconnect from wallet
|
|
495
718
|
*/
|
|
@@ -688,6 +911,7 @@ export class TonConnectMobile {
|
|
|
688
911
|
this.statusChangeCallbacks.clear();
|
|
689
912
|
this.connectionPromise = null;
|
|
690
913
|
this.transactionPromise = null;
|
|
914
|
+
this.signDataPromise = null;
|
|
691
915
|
}
|
|
692
916
|
}
|
|
693
917
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TonConnectButton component
|
|
3
|
+
* Compatible with @tonconnect/ui-react TonConnectButton
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, ViewStyle, TextStyle } from 'react-native';
|
|
8
|
+
import { useTonConnectUI, useTonWallet } from './index';
|
|
9
|
+
|
|
10
|
+
export interface TonConnectButtonProps {
|
|
11
|
+
/** Button text when disconnected */
|
|
12
|
+
text?: string;
|
|
13
|
+
/** Button text when connected */
|
|
14
|
+
connectedText?: string;
|
|
15
|
+
/** Custom styles */
|
|
16
|
+
style?: ViewStyle;
|
|
17
|
+
/** Custom text styles */
|
|
18
|
+
textStyle?: TextStyle;
|
|
19
|
+
/** Callback when button is pressed */
|
|
20
|
+
onPress?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* TonConnectButton - Button component for connecting/disconnecting wallet
|
|
25
|
+
* Compatible with @tonconnect/ui-react TonConnectButton
|
|
26
|
+
*/
|
|
27
|
+
export function TonConnectButton({
|
|
28
|
+
text = 'Connect Wallet',
|
|
29
|
+
connectedText = 'Disconnect',
|
|
30
|
+
style,
|
|
31
|
+
textStyle,
|
|
32
|
+
onPress,
|
|
33
|
+
}: TonConnectButtonProps): JSX.Element {
|
|
34
|
+
const tonConnectUI = useTonConnectUI();
|
|
35
|
+
const wallet = useTonWallet();
|
|
36
|
+
const isConnected = wallet?.connected || false;
|
|
37
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
38
|
+
|
|
39
|
+
const handlePress = async () => {
|
|
40
|
+
// CRITICAL FIX: Prevent multiple simultaneous presses
|
|
41
|
+
if (isLoading) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (onPress) {
|
|
46
|
+
onPress();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setIsLoading(true);
|
|
51
|
+
try {
|
|
52
|
+
if (isConnected) {
|
|
53
|
+
await tonConnectUI.disconnect();
|
|
54
|
+
} else {
|
|
55
|
+
// CRITICAL FIX: Only open modal, don't auto-connect
|
|
56
|
+
// The modal should handle wallet selection and connection
|
|
57
|
+
// This allows users to choose which wallet to connect
|
|
58
|
+
await tonConnectUI.openModal();
|
|
59
|
+
// Note: connectWallet() should be called by the modal/wallet selection UI
|
|
60
|
+
// Not automatically here, to allow wallet selection
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// CRITICAL FIX: Handle errors gracefully
|
|
64
|
+
console.error('TonConnectButton error:', error);
|
|
65
|
+
// Error is already handled by the SDK/UI, just reset loading state
|
|
66
|
+
} finally {
|
|
67
|
+
setIsLoading(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<TouchableOpacity
|
|
73
|
+
style={[styles.button, style, isLoading && styles.buttonDisabled]}
|
|
74
|
+
onPress={handlePress}
|
|
75
|
+
disabled={isLoading}
|
|
76
|
+
>
|
|
77
|
+
{isLoading ? (
|
|
78
|
+
<ActivityIndicator color="#ffffff" />
|
|
79
|
+
) : (
|
|
80
|
+
<Text style={[styles.buttonText, textStyle]}>
|
|
81
|
+
{isConnected ? connectedText : text}
|
|
82
|
+
</Text>
|
|
83
|
+
)}
|
|
84
|
+
</TouchableOpacity>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const styles = StyleSheet.create({
|
|
89
|
+
button: {
|
|
90
|
+
backgroundColor: '#0088cc',
|
|
91
|
+
paddingHorizontal: 24,
|
|
92
|
+
paddingVertical: 12,
|
|
93
|
+
borderRadius: 8,
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
justifyContent: 'center',
|
|
96
|
+
minHeight: 44,
|
|
97
|
+
},
|
|
98
|
+
buttonDisabled: {
|
|
99
|
+
opacity: 0.6,
|
|
100
|
+
},
|
|
101
|
+
buttonText: {
|
|
102
|
+
color: '#ffffff',
|
|
103
|
+
fontSize: 16,
|
|
104
|
+
fontWeight: '600',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React integration layer for @tonconnect/ui-react compatibility
|
|
3
|
+
* Provides TonConnectUIProvider, hooks, and components compatible with @tonconnect/ui-react API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
|
7
|
+
import { TonConnectMobile, ConnectionStatus, WalletInfo, SendTransactionRequest } from '../index';
|
|
8
|
+
import type { TonConnectMobileConfig } from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Account information (compatible with @tonconnect/ui-react)
|
|
12
|
+
*/
|
|
13
|
+
export interface Account {
|
|
14
|
+
address: string;
|
|
15
|
+
chain: number;
|
|
16
|
+
publicKey?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wallet state (compatible with @tonconnect/ui-react)
|
|
21
|
+
*/
|
|
22
|
+
export interface WalletState {
|
|
23
|
+
account: Account | null;
|
|
24
|
+
wallet: WalletInfo | null;
|
|
25
|
+
connected: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Transaction response (compatible with @tonconnect/ui-react)
|
|
30
|
+
*/
|
|
31
|
+
export interface TransactionResponse {
|
|
32
|
+
boc: string;
|
|
33
|
+
signature: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sign data request
|
|
38
|
+
*/
|
|
39
|
+
export interface SignDataRequest {
|
|
40
|
+
/** Data to sign (will be base64 encoded) */
|
|
41
|
+
data: string | Uint8Array;
|
|
42
|
+
/** Optional version */
|
|
43
|
+
version?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sign data response
|
|
48
|
+
*/
|
|
49
|
+
export interface SignDataResponse {
|
|
50
|
+
signature: string;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* TonConnect UI instance interface (compatible with @tonconnect/ui-react)
|
|
56
|
+
*/
|
|
57
|
+
export interface TonConnectUI {
|
|
58
|
+
/** Open connection modal */
|
|
59
|
+
openModal: () => Promise<void>;
|
|
60
|
+
/** Close connection modal */
|
|
61
|
+
closeModal: () => void;
|
|
62
|
+
/** Connect to wallet */
|
|
63
|
+
connectWallet: () => Promise<void>;
|
|
64
|
+
/** Disconnect from wallet */
|
|
65
|
+
disconnect: () => Promise<void>;
|
|
66
|
+
/** Send transaction */
|
|
67
|
+
sendTransaction: (transaction: SendTransactionRequest) => Promise<TransactionResponse>;
|
|
68
|
+
/** Sign data */
|
|
69
|
+
signData: (request: SignDataRequest) => Promise<SignDataResponse>;
|
|
70
|
+
/** Current wallet state */
|
|
71
|
+
wallet: WalletState | null;
|
|
72
|
+
/** Modal open state */
|
|
73
|
+
modalState: {
|
|
74
|
+
open: boolean;
|
|
75
|
+
};
|
|
76
|
+
/** UI kit version */
|
|
77
|
+
uiVersion: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Context value
|
|
82
|
+
*/
|
|
83
|
+
interface TonConnectUIContextValue {
|
|
84
|
+
tonConnectUI: TonConnectUI;
|
|
85
|
+
sdk: TonConnectMobile;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const TonConnectUIContext = createContext<TonConnectUIContextValue | null>(null);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* TonConnectUIProvider props
|
|
92
|
+
*/
|
|
93
|
+
export interface TonConnectUIProviderProps {
|
|
94
|
+
/** SDK configuration */
|
|
95
|
+
config: TonConnectMobileConfig;
|
|
96
|
+
/** Children */
|
|
97
|
+
children: ReactNode;
|
|
98
|
+
/** Optional SDK instance (for testing or custom instances) */
|
|
99
|
+
sdkInstance?: TonConnectMobile;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* TonConnectUIProvider - React context provider for TON Connect
|
|
104
|
+
* Compatible with @tonconnect/ui-react API
|
|
105
|
+
*/
|
|
106
|
+
export function TonConnectUIProvider({
|
|
107
|
+
config,
|
|
108
|
+
children,
|
|
109
|
+
sdkInstance,
|
|
110
|
+
}: TonConnectUIProviderProps): JSX.Element {
|
|
111
|
+
const [sdk] = useState<TonConnectMobile>(() => sdkInstance || new TonConnectMobile(config));
|
|
112
|
+
const [walletState, setWalletState] = useState<WalletState | null>(null);
|
|
113
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
114
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
115
|
+
|
|
116
|
+
// Update wallet state from SDK status
|
|
117
|
+
const updateWalletState = useCallback((status: ConnectionStatus) => {
|
|
118
|
+
if (status.connected && status.wallet) {
|
|
119
|
+
setWalletState({
|
|
120
|
+
account: {
|
|
121
|
+
address: status.wallet.address,
|
|
122
|
+
chain: -239, // TON mainnet chain ID
|
|
123
|
+
publicKey: status.wallet.publicKey,
|
|
124
|
+
},
|
|
125
|
+
wallet: status.wallet,
|
|
126
|
+
connected: true,
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
setWalletState({
|
|
130
|
+
account: null,
|
|
131
|
+
wallet: null,
|
|
132
|
+
connected: false,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
// Subscribe to SDK status changes
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
// Set initial state
|
|
140
|
+
const initialStatus = sdk.getStatus();
|
|
141
|
+
updateWalletState(initialStatus);
|
|
142
|
+
|
|
143
|
+
// Subscribe to changes
|
|
144
|
+
const unsubscribe = sdk.onStatusChange((status) => {
|
|
145
|
+
updateWalletState(status);
|
|
146
|
+
// Close modal when connected
|
|
147
|
+
if (status.connected) {
|
|
148
|
+
setModalOpen(false);
|
|
149
|
+
setIsConnecting(false);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return () => {
|
|
154
|
+
unsubscribe();
|
|
155
|
+
// CRITICAL FIX: Cleanup SDK on unmount to prevent memory leaks
|
|
156
|
+
// Note: SDK has its own cleanup via destroy(), but we don't call it here
|
|
157
|
+
// to allow SDK to persist across component remounts (e.g., navigation)
|
|
158
|
+
};
|
|
159
|
+
}, [sdk, updateWalletState]);
|
|
160
|
+
|
|
161
|
+
// Open modal
|
|
162
|
+
const openModal = useCallback(async () => {
|
|
163
|
+
setModalOpen(true);
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
// Close modal
|
|
167
|
+
const closeModal = useCallback(() => {
|
|
168
|
+
setModalOpen(false);
|
|
169
|
+
setIsConnecting(false);
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
// Connect wallet
|
|
173
|
+
const connectWallet = useCallback(async () => {
|
|
174
|
+
// CRITICAL FIX: Use functional update to avoid race condition
|
|
175
|
+
setIsConnecting((prev) => {
|
|
176
|
+
if (prev) {
|
|
177
|
+
// Already connecting, return early
|
|
178
|
+
return prev;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Wait for connection
|
|
184
|
+
try {
|
|
185
|
+
await sdk.connect();
|
|
186
|
+
// Status update will be handled by the subscription
|
|
187
|
+
} catch (error) {
|
|
188
|
+
setIsConnecting(false);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}, [sdk]);
|
|
192
|
+
|
|
193
|
+
// Disconnect
|
|
194
|
+
const disconnect = useCallback(async () => {
|
|
195
|
+
await sdk.disconnect();
|
|
196
|
+
setModalOpen(false);
|
|
197
|
+
}, [sdk]);
|
|
198
|
+
|
|
199
|
+
// Send transaction
|
|
200
|
+
const sendTransaction = useCallback(
|
|
201
|
+
async (transaction: SendTransactionRequest): Promise<TransactionResponse> => {
|
|
202
|
+
const response = await sdk.sendTransaction(transaction);
|
|
203
|
+
return {
|
|
204
|
+
boc: response.boc,
|
|
205
|
+
signature: response.signature,
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
[sdk]
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Sign data
|
|
212
|
+
const signData = useCallback(
|
|
213
|
+
async (request: SignDataRequest): Promise<SignDataResponse> => {
|
|
214
|
+
const response = await sdk.signData(request.data, request.version);
|
|
215
|
+
return {
|
|
216
|
+
signature: response.signature,
|
|
217
|
+
timestamp: response.timestamp,
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
[sdk]
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Create TonConnectUI instance
|
|
224
|
+
const tonConnectUI: TonConnectUI = {
|
|
225
|
+
openModal,
|
|
226
|
+
closeModal,
|
|
227
|
+
connectWallet,
|
|
228
|
+
disconnect,
|
|
229
|
+
sendTransaction,
|
|
230
|
+
signData,
|
|
231
|
+
wallet: walletState,
|
|
232
|
+
modalState: {
|
|
233
|
+
open: modalOpen,
|
|
234
|
+
},
|
|
235
|
+
uiVersion: '1.0.0',
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const contextValue: TonConnectUIContextValue = {
|
|
239
|
+
tonConnectUI,
|
|
240
|
+
sdk,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return <TonConnectUIContext.Provider value={contextValue}>{children}</TonConnectUIContext.Provider>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Hook to access TonConnectUI instance
|
|
248
|
+
* Compatible with @tonconnect/ui-react useTonConnectUI hook
|
|
249
|
+
*/
|
|
250
|
+
export function useTonConnectUI(): TonConnectUI {
|
|
251
|
+
const context = useContext(TonConnectUIContext);
|
|
252
|
+
if (!context) {
|
|
253
|
+
throw new Error('useTonConnectUI must be used within TonConnectUIProvider');
|
|
254
|
+
}
|
|
255
|
+
return context.tonConnectUI;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Hook to access wallet state
|
|
260
|
+
* Compatible with @tonconnect/ui-react useTonWallet hook
|
|
261
|
+
*/
|
|
262
|
+
export function useTonWallet(): WalletState | null {
|
|
263
|
+
const tonConnectUI = useTonConnectUI();
|
|
264
|
+
return tonConnectUI.wallet;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Hook to access modal state
|
|
269
|
+
* Compatible with @tonconnect/ui-react useTonConnectModal hook
|
|
270
|
+
*/
|
|
271
|
+
export function useTonConnectModal(): { open: boolean; close: () => void; openModal: () => Promise<void> } {
|
|
272
|
+
const tonConnectUI = useTonConnectUI();
|
|
273
|
+
return {
|
|
274
|
+
open: tonConnectUI.modalState.open,
|
|
275
|
+
close: tonConnectUI.closeModal,
|
|
276
|
+
openModal: tonConnectUI.openModal,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Hook to access SDK instance (for advanced usage)
|
|
282
|
+
*/
|
|
283
|
+
export function useTonConnectSDK(): TonConnectMobile {
|
|
284
|
+
const context = useContext(TonConnectUIContext);
|
|
285
|
+
if (!context) {
|
|
286
|
+
throw new Error('useTonConnectSDK must be used within TonConnectUIProvider');
|
|
287
|
+
}
|
|
288
|
+
return context.sdk;
|
|
289
|
+
}
|
|
290
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React integration exports
|
|
3
|
+
* Re-export all React components and hooks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
TonConnectUIProvider,
|
|
8
|
+
useTonConnectUI,
|
|
9
|
+
useTonWallet,
|
|
10
|
+
useTonConnectModal,
|
|
11
|
+
useTonConnectSDK,
|
|
12
|
+
} from './TonConnectUIProvider';
|
|
13
|
+
export type {
|
|
14
|
+
TonConnectUIProviderProps,
|
|
15
|
+
TonConnectUI,
|
|
16
|
+
WalletState,
|
|
17
|
+
Account,
|
|
18
|
+
TransactionResponse,
|
|
19
|
+
SignDataRequest,
|
|
20
|
+
SignDataResponse,
|
|
21
|
+
} from './TonConnectUIProvider';
|
|
22
|
+
export { TonConnectButton } from './TonConnectButton';
|
|
23
|
+
export type { TonConnectButtonProps } from './TonConnectButton';
|
|
24
|
+
|