@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.
@@ -0,0 +1,301 @@
1
+ /**
2
+ * WalletSelectionModal component
3
+ * Provides a beautiful wallet selection UI compatible with @tonconnect/ui-react
4
+ */
5
+
6
+ import React from 'react';
7
+ import {
8
+ Modal,
9
+ View,
10
+ Text,
11
+ TouchableOpacity,
12
+ ScrollView,
13
+ StyleSheet,
14
+ Platform,
15
+ } from 'react-native';
16
+ import { useTonConnectUI, useTonConnectSDK } from './index';
17
+ import type { WalletDefinition } from '../index';
18
+
19
+ export interface WalletSelectionModalProps {
20
+ /** Whether the modal is visible */
21
+ visible: boolean;
22
+ /** Callback when modal should close */
23
+ onClose: () => void;
24
+ /** Custom wallet list (optional, uses SDK's supported wallets by default) */
25
+ wallets?: WalletDefinition[];
26
+ /** Custom styles */
27
+ style?: any;
28
+ }
29
+
30
+ /**
31
+ * WalletSelectionModal - Beautiful wallet selection modal
32
+ * Compatible with @tonconnect/ui-react modal behavior
33
+ */
34
+ export function WalletSelectionModal({
35
+ visible,
36
+ onClose,
37
+ wallets: customWallets,
38
+ style,
39
+ }: WalletSelectionModalProps): JSX.Element {
40
+ const tonConnectUI = useTonConnectUI();
41
+ const sdk = useTonConnectSDK();
42
+ const [wallets, setWallets] = React.useState<WalletDefinition[]>([]);
43
+ const [connectingWallet, setConnectingWallet] = React.useState<string | null>(null);
44
+
45
+ // Load wallets
46
+ React.useEffect(() => {
47
+ if (customWallets) {
48
+ setWallets(customWallets);
49
+ } else {
50
+ const supportedWallets = sdk.getSupportedWallets();
51
+ // CRITICAL FIX: On web, show all wallets with universalLink (they can open in new tab)
52
+ // On mobile, filter by platform
53
+ const platform = Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'web';
54
+ let platformWallets: WalletDefinition[];
55
+ if (platform === 'web') {
56
+ // On web, show all wallets with universalLink (they can open in new tab)
57
+ platformWallets = supportedWallets.filter((w) => w.platforms.includes('web') || !!w.universalLink);
58
+ } else {
59
+ platformWallets = supportedWallets.filter((w) => w.platforms.includes(platform));
60
+ }
61
+ setWallets(platformWallets);
62
+ }
63
+ }, [sdk, customWallets]);
64
+
65
+ // Handle wallet selection
66
+ const handleSelectWallet = async (wallet: WalletDefinition) => {
67
+ // Prevent multiple simultaneous connection attempts
68
+ if (connectingWallet) {
69
+ return;
70
+ }
71
+
72
+ try {
73
+ setConnectingWallet(wallet.name);
74
+
75
+ // Set preferred wallet
76
+ try {
77
+ sdk.setPreferredWallet(wallet.name);
78
+ } catch (error) {
79
+ console.error('[WalletSelectionModal] Failed to set preferred wallet:', error);
80
+ // Continue anyway - SDK will use default wallet
81
+ }
82
+
83
+ // Close modal
84
+ onClose();
85
+
86
+ // Small delay to ensure modal closes
87
+ await new Promise<void>((resolve) => setTimeout(() => resolve(), 100));
88
+
89
+ // Connect
90
+ await tonConnectUI.connectWallet();
91
+ } catch (error) {
92
+ console.error('[WalletSelectionModal] Wallet connection error:', error);
93
+ setConnectingWallet(null);
94
+ // Error is handled by SDK/UI, just reset connecting state
95
+ }
96
+ };
97
+
98
+ return (
99
+ <Modal
100
+ visible={visible}
101
+ animationType="slide"
102
+ transparent={true}
103
+ onRequestClose={onClose}
104
+ >
105
+ <View style={[styles.overlay, style]}>
106
+ <View style={styles.modalContainer}>
107
+ {/* Header */}
108
+ <View style={styles.header}>
109
+ <Text style={styles.title}>Connect your TON wallet</Text>
110
+ <Text style={styles.subtitle}>Choose a wallet to connect to this app</Text>
111
+ <TouchableOpacity style={styles.closeButton} onPress={onClose}>
112
+ <Text style={styles.closeButtonText}>✕</Text>
113
+ </TouchableOpacity>
114
+ </View>
115
+
116
+ {/* Wallet List */}
117
+ <ScrollView style={styles.walletList} showsVerticalScrollIndicator={false}>
118
+ {wallets.length === 0 ? (
119
+ <View style={styles.emptyState}>
120
+ <Text style={styles.emptyStateText}>No wallets available</Text>
121
+ <Text style={styles.emptyStateSubtext}>
122
+ Please install a TON wallet app to continue
123
+ </Text>
124
+ </View>
125
+ ) : (
126
+ wallets.map((wallet) => {
127
+ const isConnecting = connectingWallet === wallet.name;
128
+ return (
129
+ <TouchableOpacity
130
+ key={wallet.name}
131
+ style={[styles.walletItem, isConnecting && styles.walletItemConnecting]}
132
+ onPress={() => handleSelectWallet(wallet)}
133
+ disabled={isConnecting}
134
+ >
135
+ <View style={styles.walletIconContainer}>
136
+ {/* Always use placeholder to avoid web image loading issues */}
137
+ <View style={styles.walletIconPlaceholder}>
138
+ <Text style={styles.walletIconText}>
139
+ {wallet.name.charAt(0).toUpperCase()}
140
+ </Text>
141
+ </View>
142
+ </View>
143
+ <View style={styles.walletInfo}>
144
+ <Text style={styles.walletName}>{wallet.name}</Text>
145
+ <Text style={styles.walletAppName}>{wallet.appName}</Text>
146
+ </View>
147
+ {isConnecting && (
148
+ <View style={styles.connectingIndicator}>
149
+ <Text style={styles.connectingText}>Connecting...</Text>
150
+ </View>
151
+ )}
152
+ </TouchableOpacity>
153
+ );
154
+ })
155
+ )}
156
+ </ScrollView>
157
+
158
+ {/* Footer */}
159
+ <View style={styles.footer}>
160
+ <Text style={styles.footerText}>
161
+ By connecting, you agree to the app's Terms of Service and Privacy Policy
162
+ </Text>
163
+ </View>
164
+ </View>
165
+ </View>
166
+ </Modal>
167
+ );
168
+ }
169
+
170
+ const styles = StyleSheet.create({
171
+ overlay: {
172
+ flex: 1,
173
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
174
+ justifyContent: 'flex-end',
175
+ },
176
+ modalContainer: {
177
+ backgroundColor: '#1a1a1a',
178
+ borderTopLeftRadius: 24,
179
+ borderTopRightRadius: 24,
180
+ maxHeight: '85%',
181
+ paddingBottom: Platform.OS === 'ios' ? 34 : 20,
182
+ },
183
+ header: {
184
+ padding: 24,
185
+ borderBottomWidth: 1,
186
+ borderBottomColor: '#2a2a2a',
187
+ position: 'relative',
188
+ },
189
+ title: {
190
+ fontSize: 28,
191
+ fontWeight: 'bold',
192
+ color: '#ffffff',
193
+ marginBottom: 8,
194
+ },
195
+ subtitle: {
196
+ fontSize: 15,
197
+ color: '#999999',
198
+ lineHeight: 20,
199
+ },
200
+ closeButton: {
201
+ position: 'absolute',
202
+ top: 24,
203
+ right: 24,
204
+ width: 32,
205
+ height: 32,
206
+ borderRadius: 16,
207
+ backgroundColor: '#2a2a2a',
208
+ justifyContent: 'center',
209
+ alignItems: 'center',
210
+ },
211
+ closeButtonText: {
212
+ color: '#ffffff',
213
+ fontSize: 18,
214
+ fontWeight: '600',
215
+ },
216
+ walletList: {
217
+ padding: 16,
218
+ maxHeight: 400,
219
+ },
220
+ walletItem: {
221
+ flexDirection: 'row',
222
+ alignItems: 'center',
223
+ padding: 16,
224
+ backgroundColor: '#2a2a2a',
225
+ borderRadius: 16,
226
+ marginBottom: 12,
227
+ minHeight: 72,
228
+ },
229
+ walletItemConnecting: {
230
+ opacity: 0.7,
231
+ },
232
+ walletIconContainer: {
233
+ marginRight: 16,
234
+ },
235
+ walletIcon: {
236
+ width: 48,
237
+ height: 48,
238
+ borderRadius: 12,
239
+ },
240
+ walletIconPlaceholder: {
241
+ width: 48,
242
+ height: 48,
243
+ borderRadius: 12,
244
+ backgroundColor: '#3a3a3a',
245
+ justifyContent: 'center',
246
+ alignItems: 'center',
247
+ },
248
+ walletIconText: {
249
+ color: '#ffffff',
250
+ fontSize: 20,
251
+ fontWeight: 'bold',
252
+ },
253
+ walletInfo: {
254
+ flex: 1,
255
+ },
256
+ walletName: {
257
+ fontSize: 17,
258
+ fontWeight: '600',
259
+ color: '#ffffff',
260
+ marginBottom: 4,
261
+ },
262
+ walletAppName: {
263
+ fontSize: 14,
264
+ color: '#999999',
265
+ },
266
+ connectingIndicator: {
267
+ marginLeft: 12,
268
+ },
269
+ connectingText: {
270
+ fontSize: 14,
271
+ color: '#0088cc',
272
+ fontWeight: '500',
273
+ },
274
+ emptyState: {
275
+ padding: 40,
276
+ alignItems: 'center',
277
+ },
278
+ emptyStateText: {
279
+ fontSize: 18,
280
+ fontWeight: '600',
281
+ color: '#ffffff',
282
+ marginBottom: 8,
283
+ },
284
+ emptyStateSubtext: {
285
+ fontSize: 14,
286
+ color: '#999999',
287
+ textAlign: 'center',
288
+ },
289
+ footer: {
290
+ padding: 16,
291
+ borderTopWidth: 1,
292
+ borderTopColor: '#2a2a2a',
293
+ },
294
+ footerText: {
295
+ fontSize: 12,
296
+ color: '#666666',
297
+ textAlign: 'center',
298
+ lineHeight: 16,
299
+ },
300
+ });
301
+
@@ -21,4 +21,6 @@ export type {
21
21
  } from './TonConnectUIProvider';
22
22
  export { TonConnectButton } from './TonConnectButton';
23
23
  export type { TonConnectButtonProps } from './TonConnectButton';
24
+ export { WalletSelectionModal } from './WalletSelectionModal';
25
+ export type { WalletSelectionModalProps } from './WalletSelectionModal';
24
26
 
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Retry Utilities
3
+ * Helper functions for retrying operations with exponential backoff
4
+ */
5
+
6
+ export interface RetryOptions {
7
+ /** Maximum number of retry attempts (default: 3) */
8
+ maxAttempts?: number;
9
+ /** Initial delay in milliseconds (default: 1000) */
10
+ initialDelay?: number;
11
+ /** Maximum delay in milliseconds (default: 10000) */
12
+ maxDelay?: number;
13
+ /** Multiplier for exponential backoff (default: 2) */
14
+ multiplier?: number;
15
+ /** Function to determine if error should be retried (default: retry all errors) */
16
+ shouldRetry?: (error: Error) => boolean;
17
+ }
18
+
19
+ /**
20
+ * Retry a function with exponential backoff
21
+ * @param fn - Function to retry
22
+ * @param options - Retry options
23
+ * @returns Promise that resolves with the function result
24
+ */
25
+ export async function retry<T>(
26
+ fn: () => Promise<T>,
27
+ options: RetryOptions = {}
28
+ ): Promise<T> {
29
+ const {
30
+ maxAttempts = 3,
31
+ initialDelay = 1000,
32
+ maxDelay = 10000,
33
+ multiplier = 2,
34
+ shouldRetry = () => true,
35
+ } = options;
36
+
37
+ let lastError: Error | null = null;
38
+ let delay = initialDelay;
39
+
40
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
41
+ try {
42
+ return await fn();
43
+ } catch (error) {
44
+ lastError = error instanceof Error ? error : new Error(String(error));
45
+
46
+ // Don't retry if shouldRetry returns false
47
+ if (!shouldRetry(lastError)) {
48
+ throw lastError;
49
+ }
50
+
51
+ // Don't retry on last attempt
52
+ if (attempt === maxAttempts) {
53
+ throw lastError;
54
+ }
55
+
56
+ // Wait before retrying
57
+ await new Promise<void>((resolve) => setTimeout(() => resolve(), delay));
58
+
59
+ // Increase delay for next attempt (exponential backoff)
60
+ delay = Math.min(delay * multiplier, maxDelay);
61
+ }
62
+ }
63
+
64
+ // This should never be reached, but TypeScript needs it
65
+ throw lastError || new Error('Retry failed');
66
+ }
67
+
68
+ /**
69
+ * Retry with custom delay function
70
+ * @param fn - Function to retry
71
+ * @param getDelay - Function that returns delay for each attempt (attempt number, last error)
72
+ * @param maxAttempts - Maximum number of attempts
73
+ * @returns Promise that resolves with the function result
74
+ */
75
+ export async function retryWithCustomDelay<T>(
76
+ fn: () => Promise<T>,
77
+ getDelay: (attempt: number, error: Error | null) => number,
78
+ maxAttempts: number = 3
79
+ ): Promise<T> {
80
+ let lastError: Error | null = null;
81
+
82
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
83
+ try {
84
+ return await fn();
85
+ } catch (error) {
86
+ lastError = error instanceof Error ? error : new Error(String(error));
87
+
88
+ // Don't retry on last attempt
89
+ if (attempt === maxAttempts) {
90
+ throw lastError;
91
+ }
92
+
93
+ // Wait before retrying
94
+ const delay = getDelay(attempt, lastError);
95
+ await new Promise<void>((resolve) => setTimeout(() => resolve(), delay));
96
+ }
97
+ }
98
+
99
+ throw lastError || new Error('Retry failed');
100
+ }
101
+
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Transaction Builder Utilities
3
+ * Helper functions for building TON Connect transaction requests
4
+ */
5
+
6
+ import type { SendTransactionRequest, TransactionMessage } from '../types';
7
+
8
+ /**
9
+ * Convert TON amount to nanotons
10
+ * @param tonAmount - Amount in TON (e.g., 1.5 for 1.5 TON)
11
+ * @returns Amount in nanotons as string
12
+ */
13
+ export function tonToNano(tonAmount: number | string): string {
14
+ const amount = typeof tonAmount === 'string' ? parseFloat(tonAmount) : tonAmount;
15
+ if (isNaN(amount) || amount < 0) {
16
+ throw new Error('Invalid TON amount');
17
+ }
18
+ const nanotons = Math.floor(amount * 1_000_000_000);
19
+ return nanotons.toString();
20
+ }
21
+
22
+ /**
23
+ * Convert nanotons to TON
24
+ * @param nanotons - Amount in nanotons as string
25
+ * @returns Amount in TON as number
26
+ */
27
+ export function nanoToTon(nanotons: string): number {
28
+ const nano = BigInt(nanotons);
29
+ return Number(nano) / 1_000_000_000;
30
+ }
31
+
32
+ /**
33
+ * Build a simple TON transfer transaction
34
+ * @param to - Recipient address (EQ... format)
35
+ * @param amount - Amount in TON (will be converted to nanotons)
36
+ * @param validUntil - Optional expiration timestamp (default: 5 minutes from now)
37
+ * @returns Transaction request
38
+ */
39
+ export function buildTransferTransaction(
40
+ to: string,
41
+ amount: number | string,
42
+ validUntil?: number
43
+ ): SendTransactionRequest {
44
+ // Validate address
45
+ if (!to || typeof to !== 'string') {
46
+ throw new Error('Recipient address is required');
47
+ }
48
+ if (!isValidTonAddress(to)) {
49
+ throw new Error(`Invalid TON address format: ${to}`);
50
+ }
51
+
52
+ // Validate amount
53
+ const nanoAmount = tonToNano(amount);
54
+ if (BigInt(nanoAmount) <= 0n) {
55
+ throw new Error('Transaction amount must be greater than 0');
56
+ }
57
+
58
+ // Validate validUntil
59
+ const expiration = validUntil || Date.now() + 5 * 60 * 1000; // 5 minutes default
60
+ if (expiration <= Date.now()) {
61
+ throw new Error('Transaction expiration must be in the future');
62
+ }
63
+
64
+ return {
65
+ validUntil: expiration,
66
+ messages: [
67
+ {
68
+ address: to,
69
+ amount: nanoAmount,
70
+ },
71
+ ],
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Build a transaction with multiple recipients
77
+ * @param transfers - Array of {to, amount} transfers
78
+ * @param validUntil - Optional expiration timestamp (default: 5 minutes from now)
79
+ * @returns Transaction request
80
+ */
81
+ export function buildMultiTransferTransaction(
82
+ transfers: Array<{ to: string; amount: number | string }>,
83
+ validUntil?: number
84
+ ): SendTransactionRequest {
85
+ // Validate transfers array
86
+ if (!transfers || !Array.isArray(transfers) || transfers.length === 0) {
87
+ throw new Error('Transfers array is required and cannot be empty');
88
+ }
89
+ if (transfers.length > 255) {
90
+ throw new Error('Maximum 255 transfers allowed per transaction');
91
+ }
92
+
93
+ // Validate each transfer
94
+ const messages = transfers.map((transfer, index) => {
95
+ if (!transfer.to || typeof transfer.to !== 'string') {
96
+ throw new Error(`Transfer ${index + 1}: Recipient address is required`);
97
+ }
98
+ if (!isValidTonAddress(transfer.to)) {
99
+ throw new Error(`Transfer ${index + 1}: Invalid TON address format: ${transfer.to}`);
100
+ }
101
+ const nanoAmount = tonToNano(transfer.amount);
102
+ if (BigInt(nanoAmount) <= 0n) {
103
+ throw new Error(`Transfer ${index + 1}: Amount must be greater than 0`);
104
+ }
105
+ return {
106
+ address: transfer.to,
107
+ amount: nanoAmount,
108
+ };
109
+ });
110
+
111
+ // Validate validUntil
112
+ const expiration = validUntil || Date.now() + 5 * 60 * 1000;
113
+ if (expiration <= Date.now()) {
114
+ throw new Error('Transaction expiration must be in the future');
115
+ }
116
+
117
+ return {
118
+ validUntil: expiration,
119
+ messages,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Build a transaction with custom payload
125
+ * @param to - Recipient address
126
+ * @param amount - Amount in TON
127
+ * @param payload - Base64 encoded payload
128
+ * @param validUntil - Optional expiration timestamp
129
+ * @returns Transaction request
130
+ */
131
+ export function buildTransactionWithPayload(
132
+ to: string,
133
+ amount: number | string,
134
+ payload: string,
135
+ validUntil?: number
136
+ ): SendTransactionRequest {
137
+ return {
138
+ validUntil: validUntil || Date.now() + 5 * 60 * 1000,
139
+ messages: [
140
+ {
141
+ address: to,
142
+ amount: tonToNano(amount),
143
+ payload,
144
+ },
145
+ ],
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Build a transaction with state init (for contract deployment)
151
+ * @param to - Recipient address
152
+ * @param amount - Amount in TON
153
+ * @param stateInit - Base64 encoded state init
154
+ * @param validUntil - Optional expiration timestamp
155
+ * @returns Transaction request
156
+ */
157
+ export function buildTransactionWithStateInit(
158
+ to: string,
159
+ amount: number | string,
160
+ stateInit: string,
161
+ validUntil?: number
162
+ ): SendTransactionRequest {
163
+ return {
164
+ validUntil: validUntil || Date.now() + 5 * 60 * 1000,
165
+ messages: [
166
+ {
167
+ address: to,
168
+ amount: tonToNano(amount),
169
+ stateInit,
170
+ },
171
+ ],
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Validate TON address format
177
+ * @param address - Address to validate
178
+ * @returns true if address is valid
179
+ */
180
+ export function isValidTonAddress(address: string): boolean {
181
+ // TON addresses start with EQ or 0Q and are 48 characters long (base64)
182
+ return /^(EQ|0Q)[A-Za-z0-9_-]{46}$/.test(address);
183
+ }
184
+
185
+ /**
186
+ * Format TON address for display (with ellipsis)
187
+ * @param address - Full address
188
+ * @param startLength - Characters to show at start (default: 6)
189
+ * @param endLength - Characters to show at end (default: 4)
190
+ * @returns Formatted address
191
+ */
192
+ export function formatTonAddress(
193
+ address: string,
194
+ startLength: number = 6,
195
+ endLength: number = 4
196
+ ): string {
197
+ if (address.length <= startLength + endLength) {
198
+ return address;
199
+ }
200
+ return `${address.substring(0, startLength)}...${address.substring(address.length - endLength)}`;
201
+ }
202
+