Skip to content

Commit 0cd5ea9

Browse files
ci(release): publish latest release
1 parent 81c4491 commit 0cd5ea9

File tree

5 files changed

+238
-48
lines changed

5 files changed

+238
-48
lines changed

RELEASE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ Flashblocks on Unichain: This infra upgrade brings 200ms blocktimes to the netwo
44

55
Other changes:
66

7-
- Improved virtual keyboard behavior
7+
- Fixed visual glitches in token selector
88
- Various bug fixes and performance improvements

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
mobile/1.56
1+
extension/1.26.0

apps/extension/src/background/backgroundDappRequests.ts

Lines changed: 228 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
GetCapabilitiesRequest,
1010
RevokePermissionsRequest,
1111
} from 'src/app/features/dappRequests/types/DappRequestTypes'
12-
import { focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
12+
import { focusOrCreateDappRequestWindow, focusOrCreateOnboardingTab } from 'src/app/navigation/utils'
1313
import {
1414
DappBackgroundPortChannel,
1515
contentScriptToBackgroundMessageChannel,
@@ -22,7 +22,6 @@ import {
2222
ContentScriptUtilityMessageType,
2323
DappRequestMessage,
2424
} from 'src/background/messagePassing/types/requests'
25-
import { openSidePanel } from 'src/background/utils/chromeSidePanelUtils'
2625
import { checkAreMigrationsPending, readReduxStateFromStorage } from 'src/background/utils/persistedStateUtils'
2726
import { getFeatureFlaggedChainIds } from 'uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds'
2827
import { getEnabledChains, hexadecimalStringToInt, toSupportedChainId } from 'uniswap/src/features/chains/utils'
@@ -37,6 +36,20 @@ import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
3736
import { walletContextValue } from 'wallet/src/features/wallet/context'
3837
import { selectHasSmartWalletConsent } from 'wallet/src/features/wallet/selectors'
3938

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+
4053
const INACTIVITY_ALARM_NAME = 'inactivity'
4154
// TODO(EXT-546): add a setting to turn off the auto-lock setting
4255
const INACTIVITY_TIMEOUT_MINUTES = 60 * 24 // 1 day
@@ -93,39 +106,79 @@ export function initMessageBridge(): void {
93106
return
94107
}
95108

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"
99114

100-
if (sender?.tab?.id === undefined || sender.tab.url === undefined) {
115+
// Validate sender has required information
116+
if (!isValidSender(sender)) {
101117
logger.error(new Error('sender.tab id or url is not defined'), {
102118
tags: {
103-
file: 'background/background.ts',
119+
file: 'backgroundDappRequests.ts',
104120
function: 'dappMessageListener',
105121
},
106122
})
107123
return
108124
}
109125

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 })
122181
}
123-
124-
await handleSidebarRequest({
125-
request: message,
126-
windowId: sender.tab.windowId,
127-
senderTabInfo,
128-
})
129182
})
130183

131184
contentScriptUtilityMessageChannel.addMessageListener(ContentScriptUtilityMessageType.ErrorLog, async (message) => {
@@ -315,10 +368,58 @@ async function handleGetCapabilities({
315368
}
316369
}
317370

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+
}
321415
}
416+
417+
// Handle via sidebar (queue message for processing)
418+
await handleSidebarRequest({
419+
request: message,
420+
windowId,
421+
senderTabInfo,
422+
})
322423
}
323424

324425
async function handleSidebarRequest({
@@ -339,25 +440,111 @@ async function handleSidebarRequest({
339440
isSidebarClosed: !portChannel,
340441
}
341442

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) {
355448
logger.error(error, {
356449
tags: {
357450
file: 'backgroundDappRequests.ts',
358451
function: 'handleSidebarRequest',
359452
},
360453
})
454+
// Queue message if send fails
455+
queueMessageForPanel({ windowId, message: request, senderTabInfo })
361456
}
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, [])
362540
}
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)
363550
}

apps/extension/src/background/utils/chromeSidePanelUtils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import { logger } from 'utilities/src/logger/logger'
66
export async function openSidePanel(tabId: number | undefined, windowId: number): Promise<void> {
77
let hasError = false
88
try {
9-
await chrome.sidePanel.open({
10-
tabId,
11-
windowId,
12-
})
9+
// Chrome API accepts either tabId or windowId, prefer tabId for specific tab targeting
10+
if (tabId !== undefined) {
11+
await chrome.sidePanel.open({ tabId })
12+
} else {
13+
await chrome.sidePanel.open({ windowId })
14+
}
1315
} catch (error) {
1416
// TODO WALL-4313 - Backup for some broken chrome.sidePanel.open functionality
1517
// Consider removing this once the issue is resolved or leaving as fallback

packages/uniswap/src/features/transactions/TransactionDetails/UnichainPoweredMessage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useTranslation } from 'react-i18next'
22
import { Flex, Image, Text } from 'ui/src'
33
import { UNICHAIN_LOGO } from 'ui/src/assets'
4+
import { isInterfaceDesktop } from 'utilities/src/platform'
45

56
const UNICHAIN_LOGO_SIZE = 14
67
const UNICHAIN_LOGO_BORDER_RADIUS = 4.2
@@ -10,7 +11,7 @@ export function UnichainPoweredMessage({ swappedInTime }: { swappedInTime?: numb
1011
const { t } = useTranslation()
1112

1213
return (
13-
<Flex row centered gap="$spacing6" py="$spacing4" mb="$spacing4">
14+
<Flex row centered gap="$spacing6" py="$spacing4" mb={isInterfaceDesktop ? '$spacing8' : '$none'}>
1415
<Image
1516
source={UNICHAIN_LOGO}
1617
width={UNICHAIN_LOGO_SIZE}

0 commit comments

Comments
 (0)