9
9
GetCapabilitiesRequest ,
10
10
RevokePermissionsRequest ,
11
11
} from 'src/app/features/dappRequests/types/DappRequestTypes'
12
- import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
12
+ import { focusOrCreateDappRequestWindow , focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
13
13
import {
14
14
DappBackgroundPortChannel ,
15
15
contentScriptToBackgroundMessageChannel ,
@@ -22,7 +22,6 @@ import {
22
22
ContentScriptUtilityMessageType ,
23
23
DappRequestMessage ,
24
24
} from 'src/background/messagePassing/types/requests'
25
- import { openSidePanel } from 'src/background/utils/chromeSidePanelUtils'
26
25
import { checkAreMigrationsPending , readReduxStateFromStorage } from 'src/background/utils/persistedStateUtils'
27
26
import { getFeatureFlaggedChainIds } from 'uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds'
28
27
import { getEnabledChains , hexadecimalStringToInt , toSupportedChainId } from 'uniswap/src/features/chains/utils'
@@ -37,6 +36,20 @@ import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
37
36
import { walletContextValue } from 'wallet/src/features/wallet/context'
38
37
import { selectHasSmartWalletConsent } from 'wallet/src/features/wallet/selectors'
39
38
39
+ // Request classification constants for determining which requests need user interaction
40
+ const REQUEST_CLASSIFICATION = {
41
+ interactive : new Set ( [
42
+ DappRequestType . RequestAccount ,
43
+ DappRequestType . SendTransaction ,
44
+ DappRequestType . SignMessage ,
45
+ DappRequestType . SignTypedData ,
46
+ DappRequestType . UniswapOpenSidebar ,
47
+ DappRequestType . RequestPermissions ,
48
+ DappRequestType . SendCalls ,
49
+ ] ) ,
50
+ silent : new Set ( [ DappRequestType . ChangeChain , DappRequestType . RevokePermissions , DappRequestType . GetCapabilities ] ) ,
51
+ } as const
52
+
40
53
const INACTIVITY_ALARM_NAME = 'inactivity'
41
54
// TODO(EXT-546): add a setting to turn off the auto-lock setting
42
55
const INACTIVITY_TIMEOUT_MINUTES = 60 * 24 // 1 day
@@ -93,39 +106,79 @@ export function initMessageBridge(): void {
93
106
return
94
107
}
95
108
96
- contentScriptToBackgroundMessageChannel . addAllMessageListener ( async ( message , sender ) => {
97
- // The side panel needs to be opened here because it has to be in response to a user action.
98
- // Further down in the chain it will be opened in response to a message from the background script.
109
+ contentScriptToBackgroundMessageChannel . addAllMessageListener ( ( message , sender ) => {
110
+ // CRITICAL: This listener must NOT be async to preserve user gesture context.
111
+ // Chrome's sidePanel.open() API requires execution within ~1ms of a user gesture.
112
+ // Using async/await here breaks the gesture context and causes the error:
113
+ // "sidePanel.open() may only be called in response to a user gesture"
99
114
100
- if ( sender ?. tab ?. id === undefined || sender . tab . url === undefined ) {
115
+ // Validate sender has required information
116
+ if ( ! isValidSender ( sender ) ) {
101
117
logger . error ( new Error ( 'sender.tab id or url is not defined' ) , {
102
118
tags : {
103
- file : 'background/background .ts' ,
119
+ file : 'backgroundDappRequests .ts' ,
104
120
function : 'dappMessageListener' ,
105
121
} ,
106
122
} )
107
123
return
108
124
}
109
125
110
- const senderTabInfo = {
111
- id : sender . tab . id ,
112
- url : sender . tab . url ,
113
- favIconUrl : sender . tab . favIconUrl ,
114
- }
115
-
116
- const isSidebarActive = Boolean ( windowIdToSidebarPortMap . get ( sender . tab . windowId . toString ( ) ) )
117
- if ( ! isSidebarActive ) {
118
- const handled = await handleSilentBackgroundRequest ( message , senderTabInfo )
119
- if ( handled ) {
120
- return
121
- }
126
+ const requestType = message . type
127
+ const windowId = sender . tab . windowId
128
+ const windowIdString = windowId . toString ( )
129
+ const isSidebarActive = Boolean ( windowIdToSidebarPortMap . get ( windowIdString ) )
130
+
131
+ // CRITICAL: Open side panel synchronously to preserve user gesture context.
132
+ // This must happen immediately, before any async operations.
133
+ if ( requiresSidePanel ( requestType ) && ! isSidebarActive ) {
134
+ openSidePanelSync ( {
135
+ tabId : sender . tab . id ,
136
+ windowId,
137
+ onSuccess : ( ) => {
138
+ // Process request after panel opens (async operations safe here)
139
+ handleRequestAsync ( { message, sender } )
140
+ } ,
141
+ onError : ( error , fallbackOpened ) => {
142
+ // Panel failed to open, but fallback might have succeeded
143
+ logger . error ( error , {
144
+ tags : {
145
+ file : 'backgroundDappRequests.ts' ,
146
+ function : 'initMessageBridge' ,
147
+ } ,
148
+ extra : {
149
+ action : 'openSidePanel' ,
150
+ fallbackOpened,
151
+ } ,
152
+ } )
153
+
154
+ // Revalidate sender in error callback context
155
+ if ( ! isValidSender ( sender ) ) {
156
+ logger . error ( new Error ( 'Sender tab info unexpectedly invalid in error callback' ) , {
157
+ tags : {
158
+ file : 'backgroundDappRequests.ts' ,
159
+ function : 'initMessageBridge' ,
160
+ } ,
161
+ } )
162
+ return
163
+ }
164
+
165
+ // Queue the message for when panel/popup eventually connects
166
+ // This works for both side panel and popup window
167
+ queueMessageForPanel ( {
168
+ windowId,
169
+ message,
170
+ senderTabInfo : {
171
+ id : sender . tab . id ,
172
+ url : sender . tab . url ,
173
+ favIconUrl : sender . tab . favIconUrl ,
174
+ } ,
175
+ } )
176
+ } ,
177
+ } )
178
+ } else {
179
+ // Non-interactive request or panel already open - async handling is safe
180
+ handleRequestAsync ( { message, sender } )
122
181
}
123
-
124
- await handleSidebarRequest ( {
125
- request : message ,
126
- windowId : sender . tab . windowId ,
127
- senderTabInfo,
128
- } )
129
182
} )
130
183
131
184
contentScriptUtilityMessageChannel . addMessageListener ( ContentScriptUtilityMessageType . ErrorLog , async ( message ) => {
@@ -315,10 +368,58 @@ async function handleGetCapabilities({
315
368
}
316
369
}
317
370
318
- class ExpectedNoPortError extends Error {
319
- constructor ( ) {
320
- super ( 'No port in storage to post message to' )
371
+ /**
372
+ * Handles dapp requests asynchronously after the side panel has been opened (if needed).
373
+ * This function contains the original async logic that was previously in the message listener.
374
+ * Moving it here allows us to open the side panel synchronously while preserving all existing behavior.
375
+ */
376
+ async function handleRequestAsync ( {
377
+ message,
378
+ sender,
379
+ } : {
380
+ message : DappRequest
381
+ sender : chrome . runtime . MessageSender
382
+ } ) : Promise < void > {
383
+ // Revalidate sender
384
+ if ( ! isValidSender ( sender ) ) {
385
+ logger . error ( new Error ( 'Invalid sender tab info in handleRequestAsync' ) , {
386
+ tags : {
387
+ file : 'backgroundDappRequests.ts' ,
388
+ function : 'handleRequestAsync' ,
389
+ } ,
390
+ extra : {
391
+ hasTab : ! ! sender . tab ,
392
+ hasId : sender . tab ?. id !== undefined ,
393
+ hasUrl : ! ! sender . tab ?. url ,
394
+ } ,
395
+ } )
396
+ return
397
+ }
398
+
399
+ const senderTabInfo : SenderTabInfo = {
400
+ id : sender . tab . id ,
401
+ url : sender . tab . url ,
402
+ favIconUrl : sender . tab . favIconUrl ,
403
+ }
404
+
405
+ const windowId = sender . tab . windowId
406
+ const windowIdString = windowId . toString ( )
407
+ const isSidebarActive = Boolean ( windowIdToSidebarPortMap . get ( windowIdString ) )
408
+
409
+ // Try to handle silently if sidebar is not active
410
+ if ( ! isSidebarActive ) {
411
+ const handled = await handleSilentBackgroundRequest ( message , senderTabInfo )
412
+ if ( handled ) {
413
+ return
414
+ }
321
415
}
416
+
417
+ // Handle via sidebar (queue message for processing)
418
+ await handleSidebarRequest ( {
419
+ request : message ,
420
+ windowId,
421
+ senderTabInfo,
422
+ } )
322
423
}
323
424
324
425
async function handleSidebarRequest ( {
@@ -339,25 +440,111 @@ async function handleSidebarRequest({
339
440
isSidebarClosed : ! portChannel ,
340
441
}
341
442
342
- try {
343
- if ( ! portChannel ) {
344
- throw new ExpectedNoPortError ( )
345
- }
346
-
347
- await portChannel . sendMessage ( message )
348
- } catch ( error ) {
349
- await openSidePanel ( senderTabInfo . id , windowId )
350
-
351
- windowIdToPendingRequestsMap . set ( windowIdString , windowIdToPendingRequestsMap . get ( windowIdString ) ?? [ ] )
352
- windowIdToPendingRequestsMap . get ( windowIdString ) ?. push ( message )
353
-
354
- if ( ! ( error instanceof ExpectedNoPortError ) ) {
443
+ if ( portChannel ) {
444
+ // Port exists, send message directly
445
+ try {
446
+ await portChannel . sendMessage ( message )
447
+ } catch ( error ) {
355
448
logger . error ( error , {
356
449
tags : {
357
450
file : 'backgroundDappRequests.ts' ,
358
451
function : 'handleSidebarRequest' ,
359
452
} ,
360
453
} )
454
+ // Queue message if send fails
455
+ queueMessageForPanel ( { windowId, message : request , senderTabInfo } )
361
456
}
457
+ } else {
458
+ // IMPORTANT: No port channel means the panel is opening or about to open.
459
+ // We do NOT call openSidePanel here because it was already opened synchronously
460
+ // in the message listener to preserve the user gesture context.
461
+ // Just queue the message - it will be processed when the panel connects.
462
+ queueMessageForPanel ( { windowId, message : request , senderTabInfo } )
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Determines if a request requires the side panel to be opened for user interaction
468
+ */
469
+ function requiresSidePanel ( requestType : DappRequestType ) : boolean {
470
+ return REQUEST_CLASSIFICATION . interactive . has ( requestType )
471
+ }
472
+
473
+ /**
474
+ * Validates that the sender has all required tab information
475
+ */
476
+ function isValidSender ( sender ?: chrome . runtime . MessageSender ) : sender is chrome . runtime . MessageSender & {
477
+ tab : chrome . tabs . Tab & { id : number ; url : string }
478
+ } {
479
+ return sender ?. tab ?. id !== undefined && sender . tab . url !== undefined
480
+ }
481
+
482
+ /**
483
+ * Opens the side panel synchronously to preserve user gesture context.
484
+ * Must be called within ~1ms of user gesture.
485
+ * Falls back to opening a popup window if side panel fails.
486
+ */
487
+ function openSidePanelSync ( {
488
+ tabId,
489
+ windowId,
490
+ onSuccess,
491
+ onError,
492
+ } : {
493
+ tabId : number
494
+ windowId : number
495
+ onSuccess : ( ) => void
496
+ onError : ( error : chrome . runtime . LastError , fallbackOpened : boolean ) => void
497
+ } ) : void {
498
+ chrome . sidePanel . open ( { tabId } , ( ) => {
499
+ const lastError = chrome . runtime . lastError
500
+ if ( lastError ) {
501
+ // Try fallback to popup window - still in sync callback to preserve gesture
502
+ focusOrCreateDappRequestWindow ( tabId , windowId )
503
+ . then ( ( ) => {
504
+ // Fallback succeeded - notify that we opened a window instead
505
+ onError ( lastError , true )
506
+ } )
507
+ . catch ( ( fallbackError ) => {
508
+ // Even fallback failed
509
+ logger . error ( fallbackError , {
510
+ tags : {
511
+ file : 'backgroundDappRequests.ts' ,
512
+ function : 'openSidePanelSync' ,
513
+ } ,
514
+ extra : { action : 'fallbackToPopupWindow' } ,
515
+ } )
516
+ onError ( lastError , false )
517
+ } )
518
+ } else {
519
+ onSuccess ( )
520
+ }
521
+ } )
522
+ }
523
+
524
+ /**
525
+ * Queues a message for processing when the side panel connects
526
+ */
527
+ function queueMessageForPanel ( {
528
+ windowId,
529
+ message,
530
+ senderTabInfo,
531
+ } : {
532
+ windowId : number
533
+ message : DappRequest
534
+ senderTabInfo : SenderTabInfo
535
+ } ) : void {
536
+ const windowIdString = windowId . toString ( )
537
+
538
+ if ( ! windowIdToPendingRequestsMap . has ( windowIdString ) ) {
539
+ windowIdToPendingRequestsMap . set ( windowIdString , [ ] )
362
540
}
541
+
542
+ const queuedMessage : DappRequestMessage = {
543
+ type : BackgroundToSidePanelRequestType . DappRequestReceived ,
544
+ dappRequest : message ,
545
+ senderTabInfo,
546
+ isSidebarClosed : true ,
547
+ }
548
+
549
+ windowIdToPendingRequestsMap . get ( windowIdString ) ?. push ( queuedMessage )
363
550
}
0 commit comments