Skip to content

Commit

Permalink
Fix the onboarding flow for the Chrome MV3 build (#2509)
Browse files Browse the repository at this point in the history
The onboarding code has some logic to display a welcome banner the
first time the user performs a DuckDuckGo search after installing the
extension. Until now, that code did not work for Chrome MV3 builds,
since it attempted to inject a script into the "main world" by
appending a <script> element - that does not work with MV3 due to the
stricter Content-Security-Policy used. Let's use the
scripting.executeScript API instead to inject the required code into
the "main world".

Note: This code is somewhat convoluted and could likely be simplified
      in the future. But for now, with the upcoming MV3 migration
      deadline, let's make the minimum changes required to get this
      working!
  • Loading branch information
kzar authored Apr 17, 2024
1 parent ecf7c31 commit 2e4441d
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 22 deletions.
26 changes: 8 additions & 18 deletions integration-test/onboarding.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from './helpers/playwrightHarness'
import backgroundWait from './helpers/backgroundWait'

test.describe('onboarding', () => {
test('should manage the onboarding state and inject a script that calls window.onFirstSearchPostExtensionInstall on the first search post extension', async ({ context, backgroundPage, page }) => {
test('should manage the onboarding state and inject a script that calls window.onFirstSearchPostExtensionInstall on the first search post extension', async ({ manifestVersion, context, backgroundPage, page }) => {
await backgroundWait.forExtensionLoaded(context)

const params = await backgroundPage.evaluate(() => {
Expand All @@ -18,11 +18,13 @@ test.describe('onboarding', () => {
await page.bringToFront()
await page.goto('https://duckduckgo.com/?q=hello')

const hasScriptHandle = await page.waitForFunction(() => {
const scripts = document.querySelectorAll('script:not([src])')
return Array.from(scripts).some((s) => s.textContent.includes('window.onFirstSearchPostExtensionInstall'))
}, { polling: 'mutation' })
expect(hasScriptHandle).toBeTruthy()
if (manifestVersion === 2) {
const hasScriptHandle = await page.waitForFunction(() => {
const scripts = document.querySelectorAll('script:not([src])')
return Array.from(scripts).some((s) => s.textContent.includes('window.onFirstSearchPostExtensionInstall'))
}, { polling: 'mutation' })
expect(hasScriptHandle).toBeTruthy()
}

const nextParams = await backgroundPage.evaluate(() => {
return {
Expand All @@ -41,12 +43,6 @@ test.describe('onboarding', () => {
await page.bringToFront()
await page.goto('https://duckduckgo.com/?q=hello')

// we wait that the onboarding content script is injected
await page.waitForFunction(() => {
const scripts = document.querySelectorAll('script:not([src])')
return Array.from(scripts).some((s) => s.textContent.includes('window.onFirstSearchPostExtensionInstall'))
}, { polling: 'mutation' })

const data = await page.evaluate(() => {
return new Promise((resolve) => {
globalThis.addEventListener('message', (e) => {
Expand All @@ -70,12 +66,6 @@ test.describe('onboarding', () => {

await page.goto('https://duckduckgo.com/?q=hello')

// we wait that the onboarding content script is injected
await page.waitForFunction(() => {
const scripts = document.querySelectorAll('script:not([src])')
return Array.from(scripts).some((s) => s.textContent.includes('window.onFirstSearchPostExtensionInstall'))
}, { polling: 'mutation' })

await page.evaluate(() => {
globalThis.postMessage({ type: 'rescheduleCounterMessagingRequest' }, globalThis.location.origin)
})
Expand Down
17 changes: 16 additions & 1 deletion shared/js/background/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ async function onboardingMessaging ({ transitionQualifiers, tabId }) {
})
}

if (manifestVersion === 3) {
browserWrapper.executeScript({
target: { tabId },
func: onboarding.onDocumentEndMainWorld,
args: [{
isAddressBarQuery,
showWelcomeBanner,
showCounterMessaging
}],
injectImmediately: false,
world: 'MAIN'
})
}

browserWrapper.executeScript({
target: { tabId },
func: onboarding.onDocumentEnd,
Expand All @@ -151,7 +165,8 @@ async function onboardingMessaging ({ transitionQualifiers, tabId }) {
showCounterMessaging,
browserName,
duckDuckGoSerpHostname: constants.duckDuckGoSerpHostname,
extensionId: browserWrapper.getExtensionId()
extensionId: browserWrapper.getExtensionId(),
manifestVersion
}],
injectImmediately: false
})
Expand Down
38 changes: 35 additions & 3 deletions shared/js/background/onboarding.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ function onDocumentEnd ({
showCounterMessaging,
extensionId,
duckDuckGoSerpHostname,
browserName
browserName,
manifestVersion
}) {
const origin = `https://${duckDuckGoSerpHostname}`

Expand Down Expand Up @@ -76,12 +77,22 @@ function onDocumentEnd ({
})
}

// The content script do not share the same `window` as the page
// so we inject a `<script>` to be able to access the page `window`
// This content script in the "isolated world" does not share the
// same `window` Object as the website itself (in the "main world").
// Call onFirstSearchPostExtensionInstall in the "main world"
// instead.
//
// Note that this is not done through messaging in order to prevent
// setting up an event listner on the SERP (this would be wasteful)
// as this is only needed on the _first_ search post extension install

// For MV3 builds, send a message to the "main world" content script.
if (manifestVersion === 3) {
window.postMessage({ type: 'onFirstSearch', documentStartData })
return
}

// For MV2 builds, inject a `<script>` element.
const script = document.createElement('script')
script.textContent = `
if (window.onFirstSearchPostExtensionInstall) {
Expand All @@ -99,6 +110,26 @@ function onDocumentEnd ({
}
}

function onDocumentEndMainWorld ({
isAddressBarQuery,
showWelcomeBanner,
showCounterMessaging
}) {
window.addEventListener('message', function handleFirstSearchMessage (e) {
if (e.origin === origin && e.data.type === 'onFirstSearch') {
window.removeEventListener('message', handleFirstSearchMessage)

if (window.onFirstSearchPostExtensionInstall) {
const { documentStartData } = e.data
window.onFirstSearchPostExtensionInstall(
{ isAddressBarQuery, showWelcomeBanner, showCounterMessaging },
documentStartData
)
}
}
})
}

function onDocumentStart ({ duckDuckGoSerpHostname }) {
const hadFocusOnStart = document.hasFocus()

Expand All @@ -112,5 +143,6 @@ function onDocumentStart ({ duckDuckGoSerpHostname }) {

module.exports = {
onDocumentEnd,
onDocumentEndMainWorld,
onDocumentStart
}

0 comments on commit 2e4441d

Please sign in to comment.