@blazium/ton-connect-mobile 1.1.0 → 1.1.2

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,209 @@
1
+ "use strict";
2
+ /**
3
+ * React integration layer for @tonconnect/ui-react compatibility
4
+ * Provides TonConnectUIProvider, hooks, and components compatible with @tonconnect/ui-react API
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.TonConnectUIProvider = TonConnectUIProvider;
41
+ exports.useTonConnectUI = useTonConnectUI;
42
+ exports.useTonWallet = useTonWallet;
43
+ exports.useTonConnectModal = useTonConnectModal;
44
+ exports.useTonConnectSDK = useTonConnectSDK;
45
+ const react_1 = __importStar(require("react"));
46
+ const index_1 = require("../index");
47
+ const TonConnectUIContext = (0, react_1.createContext)(null);
48
+ /**
49
+ * TonConnectUIProvider - React context provider for TON Connect
50
+ * Compatible with @tonconnect/ui-react API
51
+ */
52
+ function TonConnectUIProvider({ config, children, sdkInstance, }) {
53
+ const [sdk] = (0, react_1.useState)(() => sdkInstance || new index_1.TonConnectMobile(config));
54
+ const [walletState, setWalletState] = (0, react_1.useState)(null);
55
+ const [modalOpen, setModalOpen] = (0, react_1.useState)(false);
56
+ const [isConnecting, setIsConnecting] = (0, react_1.useState)(false);
57
+ // Update wallet state from SDK status
58
+ const updateWalletState = (0, react_1.useCallback)((status) => {
59
+ if (status.connected && status.wallet) {
60
+ setWalletState({
61
+ account: {
62
+ address: status.wallet.address,
63
+ chain: -239, // TON mainnet chain ID
64
+ publicKey: status.wallet.publicKey,
65
+ },
66
+ wallet: status.wallet,
67
+ connected: true,
68
+ });
69
+ }
70
+ else {
71
+ setWalletState({
72
+ account: null,
73
+ wallet: null,
74
+ connected: false,
75
+ });
76
+ }
77
+ }, []);
78
+ // Subscribe to SDK status changes
79
+ (0, react_1.useEffect)(() => {
80
+ // Set initial state
81
+ const initialStatus = sdk.getStatus();
82
+ updateWalletState(initialStatus);
83
+ // Subscribe to changes
84
+ const unsubscribe = sdk.onStatusChange((status) => {
85
+ updateWalletState(status);
86
+ // Close modal when connected
87
+ if (status.connected) {
88
+ setModalOpen(false);
89
+ setIsConnecting(false);
90
+ }
91
+ });
92
+ return () => {
93
+ unsubscribe();
94
+ // CRITICAL FIX: Cleanup SDK on unmount to prevent memory leaks
95
+ // Note: SDK has its own cleanup via destroy(), but we don't call it here
96
+ // to allow SDK to persist across component remounts (e.g., navigation)
97
+ };
98
+ }, [sdk, updateWalletState]);
99
+ // Open modal
100
+ const openModal = (0, react_1.useCallback)(async () => {
101
+ setModalOpen(true);
102
+ }, []);
103
+ // Close modal
104
+ const closeModal = (0, react_1.useCallback)(() => {
105
+ setModalOpen(false);
106
+ setIsConnecting(false);
107
+ }, []);
108
+ // Connect wallet
109
+ const connectWallet = (0, react_1.useCallback)(async () => {
110
+ // CRITICAL FIX: Use functional update to avoid race condition
111
+ setIsConnecting((prev) => {
112
+ if (prev) {
113
+ // Already connecting, return early
114
+ return prev;
115
+ }
116
+ return true;
117
+ });
118
+ // Wait for connection
119
+ try {
120
+ await sdk.connect();
121
+ // Status update will be handled by the subscription
122
+ }
123
+ catch (error) {
124
+ setIsConnecting(false);
125
+ throw error;
126
+ }
127
+ }, [sdk]);
128
+ // Disconnect
129
+ const disconnect = (0, react_1.useCallback)(async () => {
130
+ await sdk.disconnect();
131
+ setModalOpen(false);
132
+ }, [sdk]);
133
+ // Send transaction
134
+ const sendTransaction = (0, react_1.useCallback)(async (transaction) => {
135
+ const response = await sdk.sendTransaction(transaction);
136
+ return {
137
+ boc: response.boc,
138
+ signature: response.signature,
139
+ };
140
+ }, [sdk]);
141
+ // Sign data
142
+ const signData = (0, react_1.useCallback)(async (request) => {
143
+ const response = await sdk.signData(request.data, request.version);
144
+ return {
145
+ signature: response.signature,
146
+ timestamp: response.timestamp,
147
+ };
148
+ }, [sdk]);
149
+ // Create TonConnectUI instance
150
+ const tonConnectUI = {
151
+ openModal,
152
+ closeModal,
153
+ connectWallet,
154
+ disconnect,
155
+ sendTransaction,
156
+ signData,
157
+ wallet: walletState,
158
+ modalState: {
159
+ open: modalOpen,
160
+ },
161
+ uiVersion: '1.0.0',
162
+ };
163
+ const contextValue = {
164
+ tonConnectUI,
165
+ sdk,
166
+ };
167
+ return react_1.default.createElement(TonConnectUIContext.Provider, { value: contextValue }, children);
168
+ }
169
+ /**
170
+ * Hook to access TonConnectUI instance
171
+ * Compatible with @tonconnect/ui-react useTonConnectUI hook
172
+ */
173
+ function useTonConnectUI() {
174
+ const context = (0, react_1.useContext)(TonConnectUIContext);
175
+ if (!context) {
176
+ throw new Error('useTonConnectUI must be used within TonConnectUIProvider');
177
+ }
178
+ return context.tonConnectUI;
179
+ }
180
+ /**
181
+ * Hook to access wallet state
182
+ * Compatible with @tonconnect/ui-react useTonWallet hook
183
+ */
184
+ function useTonWallet() {
185
+ const tonConnectUI = useTonConnectUI();
186
+ return tonConnectUI.wallet;
187
+ }
188
+ /**
189
+ * Hook to access modal state
190
+ * Compatible with @tonconnect/ui-react useTonConnectModal hook
191
+ */
192
+ function useTonConnectModal() {
193
+ const tonConnectUI = useTonConnectUI();
194
+ return {
195
+ open: tonConnectUI.modalState.open,
196
+ close: tonConnectUI.closeModal,
197
+ openModal: tonConnectUI.openModal,
198
+ };
199
+ }
200
+ /**
201
+ * Hook to access SDK instance (for advanced usage)
202
+ */
203
+ function useTonConnectSDK() {
204
+ const context = (0, react_1.useContext)(TonConnectUIContext);
205
+ if (!context) {
206
+ throw new Error('useTonConnectSDK must be used within TonConnectUIProvider');
207
+ }
208
+ return context.sdk;
209
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * React integration exports
3
+ * Re-export all React components and hooks
4
+ */
5
+ export { TonConnectUIProvider, useTonConnectUI, useTonWallet, useTonConnectModal, useTonConnectSDK, } from './TonConnectUIProvider';
6
+ export type { TonConnectUIProviderProps, TonConnectUI, WalletState, Account, TransactionResponse, SignDataRequest, SignDataResponse, } from './TonConnectUIProvider';
7
+ export { TonConnectButton } from './TonConnectButton';
8
+ export type { TonConnectButtonProps } from './TonConnectButton';
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ /**
3
+ * React integration exports
4
+ * Re-export all React components and hooks
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.TonConnectButton = exports.useTonConnectSDK = exports.useTonConnectModal = exports.useTonWallet = exports.useTonConnectUI = exports.TonConnectUIProvider = void 0;
8
+ var TonConnectUIProvider_1 = require("./TonConnectUIProvider");
9
+ Object.defineProperty(exports, "TonConnectUIProvider", { enumerable: true, get: function () { return TonConnectUIProvider_1.TonConnectUIProvider; } });
10
+ Object.defineProperty(exports, "useTonConnectUI", { enumerable: true, get: function () { return TonConnectUIProvider_1.useTonConnectUI; } });
11
+ Object.defineProperty(exports, "useTonWallet", { enumerable: true, get: function () { return TonConnectUIProvider_1.useTonWallet; } });
12
+ Object.defineProperty(exports, "useTonConnectModal", { enumerable: true, get: function () { return TonConnectUIProvider_1.useTonConnectModal; } });
13
+ Object.defineProperty(exports, "useTonConnectSDK", { enumerable: true, get: function () { return TonConnectUIProvider_1.useTonConnectSDK; } });
14
+ var TonConnectButton_1 = require("./TonConnectButton");
15
+ Object.defineProperty(exports, "TonConnectButton", { enumerable: true, get: function () { return TonConnectButton_1.TonConnectButton; } });
@@ -75,10 +75,10 @@ export interface ConnectionRequestPayload {
75
75
  items: Array<{
76
76
  name: 'ton_addr';
77
77
  }>;
78
- /** Return URL scheme (for mobile apps) */
78
+ /** Return strategy - how wallet should return to the app */
79
+ returnStrategy?: 'back' | 'post_redirect' | 'none';
80
+ /** Return scheme for mobile apps (required by many wallets for proper callback handling) */
79
81
  returnScheme?: string;
80
- /** Return strategy */
81
- returnStrategy?: 'back' | 'none';
82
82
  }
83
83
  /**
84
84
  * Connection response payload (received from wallet)
@@ -135,7 +135,7 @@ export interface TransactionRequestPayload {
135
135
  /** Return URL scheme (for mobile apps) */
136
136
  returnScheme?: string;
137
137
  /** Return strategy */
138
- returnStrategy?: 'back' | 'none';
138
+ returnStrategy?: 'back' | 'post_redirect' | 'none';
139
139
  }
140
140
  /**
141
141
  * Transaction response payload (received from wallet)
package/package.json CHANGED
@@ -1,9 +1,19 @@
1
1
  {
2
2
  "name": "@blazium/ton-connect-mobile",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Production-ready TON Connect Mobile SDK for React Native and Expo. Implements the real TonConnect protocol for mobile applications using deep links and callbacks.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./react": {
13
+ "types": "./dist/react/index.d.ts",
14
+ "default": "./dist/react/index.js"
15
+ }
16
+ },
7
17
  "repository": {
8
18
  "type": "git",
9
19
  "url": "git+https://github.com/blaziumdev/ton-connect-mobile.git"
@@ -31,6 +41,7 @@
31
41
  "react-native-get-random-values": "^1.9.0"
32
42
  },
33
43
  "peerDependencies": {
44
+ "react": "*",
34
45
  "react-native": "*"
35
46
  },
36
47
  "peerDependenciesMeta": {
@@ -130,15 +130,25 @@ export function decodeBase64URL<T>(encoded: string): T {
130
130
  export function buildConnectionRequest(
131
131
  manifestUrl: string,
132
132
  returnScheme: string,
133
- walletUniversalLink?: string
133
+ walletUniversalLink?: string,
134
+ returnStrategy?: 'back' | 'post_redirect' | 'none',
135
+ requiresReturnScheme?: boolean
134
136
  ): string {
137
+ // Build payload with required fields
135
138
  const payload: ConnectionRequestPayload = {
136
139
  manifestUrl,
137
140
  items: [{ name: 'ton_addr' }],
138
- returnScheme, // CRITICAL: Wallet needs to know the callback scheme for mobile apps
139
- returnStrategy: 'back',
141
+ returnStrategy: returnStrategy || 'back',
140
142
  };
141
143
 
144
+ // CRITICAL FIX: Many wallets (Tonhub, MyTonWallet, Telegram Wallet) require returnScheme
145
+ // in the payload to properly handle mobile app callbacks. While not in the official
146
+ // protocol spec, it's a de-facto requirement for mobile apps.
147
+ if (requiresReturnScheme !== false) {
148
+ // Default to true if not specified - safer to include it
149
+ payload.returnScheme = returnScheme;
150
+ }
151
+
142
152
  const encoded = encodeBase64URL(payload);
143
153
 
144
154
  // Use custom wallet universal link if provided
@@ -160,7 +170,9 @@ export function buildTransactionRequest(
160
170
  manifestUrl: string,
161
171
  request: SendTransactionRequest,
162
172
  returnScheme: string,
163
- walletUniversalLink?: string
173
+ walletUniversalLink?: string,
174
+ returnStrategy?: 'back' | 'post_redirect' | 'none',
175
+ requiresReturnScheme?: boolean
164
176
  ): string {
165
177
  const payload: TransactionRequestPayload = {
166
178
  manifestUrl,
@@ -175,10 +187,14 @@ export function buildTransactionRequest(
175
187
  network: request.network,
176
188
  from: request.from,
177
189
  },
178
- returnScheme, // CRITICAL: Wallet needs to know the callback scheme for mobile apps
179
- returnStrategy: 'back',
190
+ returnStrategy: returnStrategy || 'back',
180
191
  };
181
192
 
193
+ // CRITICAL FIX: Include returnScheme for mobile wallets that require it
194
+ if (requiresReturnScheme !== false) {
195
+ payload.returnScheme = returnScheme;
196
+ }
197
+
182
198
  const encoded = encodeBase64URL(payload);
183
199
 
184
200
  // Use custom wallet universal link if provided
@@ -16,6 +16,10 @@ export interface WalletDefinition {
16
16
  iconUrl?: string;
17
17
  /** Platform support */
18
18
  platforms: ('ios' | 'android' | 'web')[];
19
+ /** Preferred return strategy for this wallet */
20
+ preferredReturnStrategy?: 'back' | 'post_redirect' | 'none';
21
+ /** Whether this wallet requires returnScheme in payload */
22
+ requiresReturnScheme?: boolean;
19
23
  }
20
24
 
21
25
  /**
@@ -28,6 +32,8 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
28
32
  universalLink: 'https://app.tonkeeper.com/ton-connect',
29
33
  deepLink: 'tonkeeper://',
30
34
  platforms: ['ios', 'android'],
35
+ preferredReturnStrategy: 'back',
36
+ requiresReturnScheme: false, // Tonkeeper can infer from manifest
31
37
  },
32
38
  {
33
39
  name: 'MyTonWallet',
@@ -35,6 +41,8 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
35
41
  universalLink: 'https://connect.mytonwallet.org',
36
42
  deepLink: 'mytonwallet://',
37
43
  platforms: ['ios', 'android', 'web'],
44
+ preferredReturnStrategy: 'post_redirect',
45
+ requiresReturnScheme: true, // MyTonWallet requires explicit returnScheme
38
46
  },
39
47
  {
40
48
  name: 'Wallet in Telegram',
@@ -42,6 +50,8 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
42
50
  universalLink: 'https://wallet.tonapi.io/ton-connect',
43
51
  deepLink: 'tg://',
44
52
  platforms: ['ios', 'android'],
53
+ preferredReturnStrategy: 'post_redirect',
54
+ requiresReturnScheme: true, // Telegram Wallet requires explicit returnScheme
45
55
  },
46
56
  {
47
57
  name: 'Tonhub',
@@ -49,6 +59,8 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
49
59
  universalLink: 'https://tonhub.com/ton-connect',
50
60
  deepLink: 'tonhub://',
51
61
  platforms: ['ios', 'android'],
62
+ preferredReturnStrategy: 'post_redirect',
63
+ requiresReturnScheme: true, // Tonhub requires explicit returnScheme for proper callback
52
64
  },
53
65
  ];
54
66
 
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,32 @@ 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
- const url = buildConnectionRequest(this.config.manifestUrl, this.config.scheme, this.currentWallet.universalLink);
353
- console.log('[TON Connect] Built URL:', url.substring(0, 100) + '...');
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: Log the URL being opened
356
- console.log('[TON Connect] Opening URL:', url);
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
+ const decoded = decodeBase64URL<ConnectionRequestPayload>(payload);
411
+ console.log('[TON Connect] Connection request payload:', JSON.stringify(decoded, null, 2));
412
+ }
413
+ } catch (e) {
414
+ // Ignore decode errors in logging
415
+ console.log('[TON Connect] Could not decode payload for logging:', e);
416
+ }
417
+
418
+ console.log('[TON Connect] Built URL:', url.substring(0, 100) + '...');
419
+ console.log('[TON Connect] Full URL:', url);
357
420
  console.log('[TON Connect] Manifest URL:', this.config.manifestUrl);
358
421
  console.log('[TON Connect] Return scheme:', this.config.scheme);
359
422
  console.log('[TON Connect] Adapter type:', this.adapter.constructor.name);
@@ -437,7 +500,14 @@ export class TonConnectMobile {
437
500
  }
438
501
 
439
502
  // Build transaction request URL (use universal link for Android compatibility)
440
- const url = buildTransactionRequest(this.config.manifestUrl, request, this.config.scheme, this.currentWallet.universalLink);
503
+ const url = buildTransactionRequest(
504
+ this.config.manifestUrl,
505
+ request,
506
+ this.config.scheme,
507
+ this.currentWallet.universalLink,
508
+ this.currentWallet.preferredReturnStrategy,
509
+ this.currentWallet.requiresReturnScheme
510
+ );
441
511
 
442
512
  // Create promise for transaction
443
513
  return new Promise<{ boc: string; signature: string }>((resolve, reject) => {
@@ -490,6 +560,156 @@ export class TonConnectMobile {
490
560
  });
491
561
  }
492
562
 
563
+ /**
564
+ * Sign data (for authentication, etc.)
565
+ * Note: Not all wallets support signData. This is a TON Connect extension.
566
+ */
567
+ async signData(data: string | Uint8Array, version: string = '1.0'): Promise<{ signature: string; timestamp: number }> {
568
+ // Check if connected
569
+ if (!this.currentStatus.connected || !this.currentStatus.wallet) {
570
+ throw new TonConnectError('Not connected to wallet. Call connect() first.');
571
+ }
572
+
573
+ // Helper function to encode bytes to base64
574
+ const base64EncodeBytes = (bytes: Uint8Array): string => {
575
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
576
+ let result = '';
577
+ let i = 0;
578
+
579
+ while (i < bytes.length) {
580
+ const a = bytes[i++];
581
+ const b = i < bytes.length ? bytes[i++] : 0;
582
+ const c = i < bytes.length ? bytes[i++] : 0;
583
+
584
+ const bitmap = (a << 16) | (b << 8) | c;
585
+
586
+ result += chars.charAt((bitmap >> 18) & 63);
587
+ result += chars.charAt((bitmap >> 12) & 63);
588
+ result += i - 2 < bytes.length ? chars.charAt((bitmap >> 6) & 63) : '=';
589
+ result += i - 1 < bytes.length ? chars.charAt(bitmap & 63) : '=';
590
+ }
591
+
592
+ return result;
593
+ };
594
+
595
+ // Helper function to get TextEncoder
596
+ const getTextEncoder = (): { encode(input: string): Uint8Array } => {
597
+ // eslint-disable-next-line no-undef
598
+ if (typeof globalThis !== 'undefined' && (globalThis as any).TextEncoder) {
599
+ // eslint-disable-next-line no-undef
600
+ return new (globalThis as any).TextEncoder();
601
+ }
602
+ // Fallback: manual encoding
603
+ return {
604
+ encode(input: string): Uint8Array {
605
+ const bytes = new Uint8Array(input.length);
606
+ for (let i = 0; i < input.length; i++) {
607
+ bytes[i] = input.charCodeAt(i);
608
+ }
609
+ return bytes;
610
+ },
611
+ };
612
+ };
613
+
614
+ // Convert data to base64
615
+ let dataBase64: string;
616
+ if (typeof data === 'string') {
617
+ // Check if it's already base64
618
+ const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
619
+ if (base64Regex.test(data) && data.length % 4 === 0) {
620
+ // Likely base64, use as-is
621
+ dataBase64 = data;
622
+ } else {
623
+ // Not base64, encode it
624
+ const encoder = getTextEncoder();
625
+ const bytes = encoder.encode(data);
626
+ dataBase64 = base64EncodeBytes(bytes);
627
+ }
628
+ } else {
629
+ // Uint8Array - convert to base64
630
+ dataBase64 = base64EncodeBytes(data);
631
+ }
632
+
633
+ // Build sign data request
634
+ const payload = {
635
+ manifestUrl: this.config.manifestUrl,
636
+ data: dataBase64,
637
+ version,
638
+ returnStrategy: this.currentWallet.preferredReturnStrategy || 'back',
639
+ returnScheme: this.currentWallet.requiresReturnScheme !== false ? this.config.scheme : undefined,
640
+ };
641
+
642
+ // Encode payload
643
+ const { encodeBase64URL } = require('./core/protocol');
644
+ const encoded = encodeBase64URL(payload);
645
+
646
+ // Build URL
647
+ const baseUrl = this.currentWallet.universalLink.endsWith('/ton-connect')
648
+ ? this.currentWallet.universalLink
649
+ : `${this.currentWallet.universalLink}/ton-connect`;
650
+ const url = `${baseUrl}/sign-data?${encoded}`;
651
+
652
+ // Open wallet app and wait for response
653
+ return new Promise<{ signature: string; timestamp: number }>((resolve, reject) => {
654
+ let timeout: number | null = null;
655
+ let resolved = false;
656
+
657
+ // CRITICAL FIX: Check if sign data is already in progress
658
+ if (this.signDataPromise) {
659
+ throw new TonConnectError('Sign data request already in progress');
660
+ }
661
+
662
+ // Create promise for sign data
663
+ const signDataPromise = {
664
+ resolve: (response: { signature: string; timestamp: number }) => {
665
+ if (timeout !== null) {
666
+ clearTimeout(timeout);
667
+ }
668
+ resolved = true;
669
+ if (this.signDataPromise === signDataPromise) {
670
+ this.signDataPromise = null;
671
+ }
672
+ resolve(response);
673
+ },
674
+ reject: (error: Error) => {
675
+ if (timeout !== null) {
676
+ clearTimeout(timeout);
677
+ }
678
+ resolved = true;
679
+ if (this.signDataPromise === signDataPromise) {
680
+ this.signDataPromise = null;
681
+ }
682
+ reject(error);
683
+ },
684
+ timeout: null as number | null,
685
+ };
686
+
687
+ // Set timeout
688
+ timeout = setTimeout(() => {
689
+ if (!resolved && this.signDataPromise === signDataPromise) {
690
+ this.signDataPromise = null;
691
+ signDataPromise.reject(new TonConnectError('Sign data request timed out'));
692
+ }
693
+ }, this.config.transactionTimeout) as unknown as number;
694
+
695
+ signDataPromise.timeout = timeout;
696
+
697
+ // Store promise for callback handling
698
+ // CRITICAL FIX: Don't mutate handleCallback method - use a separate tracking mechanism
699
+ this.signDataPromise = signDataPromise;
700
+
701
+ // Open URL
702
+ this.adapter.openURL(url, this.config.skipCanOpenURLCheck).then(() => {
703
+ // URL opened, wait for callback
704
+ // Callback will be handled by handleCallback method checking signDataPromise
705
+ }).catch((error: Error) => {
706
+ // Clear promise on error
707
+ this.signDataPromise = null;
708
+ signDataPromise.reject(new TonConnectError(`Failed to open wallet: ${error?.message || String(error)}`));
709
+ });
710
+ });
711
+ }
712
+
493
713
  /**
494
714
  * Disconnect from wallet
495
715
  */
@@ -688,6 +908,7 @@ export class TonConnectMobile {
688
908
  this.statusChangeCallbacks.clear();
689
909
  this.connectionPromise = null;
690
910
  this.transactionPromise = null;
911
+ this.signDataPromise = null;
691
912
  }
692
913
  }
693
914