diff --git a/guides/toggle-report.md b/guides/toggle-report.md index 3a98d010..15c9308e 100644 --- a/guides/toggle-report.md +++ b/guides/toggle-report.md @@ -26,6 +26,8 @@ This is called immediately to retrieve the data needed to render the list. - WebKit: {@link "macOS integration".privacyDashboardGetToggleReportOptions} - Other platforms will be added +See also: [Data disclosure item ids and their meanings](#appendix-data-disclosure-item-ids-and-their-meanings) + ## Step 4: Implement new handlers The following are all sent in response to user interactions @@ -42,3 +44,29 @@ The following are all sent in response to user interactions ### 👆Tapping anywhere on the success screen (macos only) - Webkit: {@link "macOS integration".privacyDashboardClose} + +## Appendix: Data disclosure item ids and their meanings + +| ID | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------------------ | +| appVersion | App version number | +| atb | Anonymous experiment group for feature testing | +| description | Your selected category and optional comments | +| device | Device make, model, and manufacturer | +| didOpenReportInfo | Whether or not you opted to show this report info | +| errorDescriptions | Browser-reported errors | +| extensionVersion | Extension version number | +| features | List of which browser features were active | +| httpErrorCodes | Website response status (HTTP) codes | +| jsPerformance | How quickly parts of the page loaded | +| lastSentDay | Date of last report sent for this site | +| listVersions | Information about which versions of our protections were active | +| locale | Primary language and country of your device | +| openerContext | How you got to this page, either: "SERP" (DuckDuckGo search), "Navigation" (link/URL), or "External" (other means) | +| os | Operating system version number | +| reportFlow | Which reporting form you used ("menu", "dashboard", etc.) | +| requests | Hostnames of trackers blocked, surrogate requests, ignored requests, and requests not in tracker blocking list | +| siteUrl | Page URL (without identifiable info) | +| toggleReportCounter | Number of times protections were toggled off | +| userRefreshCount | Number of refreshes since page load | +| wvVersion | Web browser engine version number | diff --git a/integration-tests/DashboardPage.js b/integration-tests/DashboardPage.js index 5da9795a..d6811f85 100644 --- a/integration-tests/DashboardPage.js +++ b/integration-tests/DashboardPage.js @@ -248,15 +248,17 @@ export class DashboardPage { * @param {import("@playwright/test").Page} page * @param {object} [opts] * @param {import('../schema/__generated__/schema.types').EventOrigin['screen']} [opts.screen] + * @param {'menu' | 'dashboard'} [opts.opener] * @return {Promise} */ static async windows(page, opts = {}) { /** @type {import('../schema/__generated__/schema.types').EventOrigin['screen']} */ const screen = opts?.screen || 'primaryScreen'; + const opener = opts?.opener || 'dashboard'; const dash = new DashboardPage(page, { name: 'windows' }); await dash.withMarker(); await dash.mocks.install(); - await dash.loadPage({ screen }); + await dash.loadPage({ screen, opener }); await page.waitForFunction(() => typeof window.__playwright !== 'undefined'); return dash; } @@ -266,7 +268,7 @@ export class DashboardPage { * @param {import("../shared/js/ui/views/tests/generate-data.mjs").MockData} initial * @returns {Promise} */ - static async browser(page, initial = testDataStates.empty) { + static async browser(page, initial = testDataStates.empty, opts = {}) { const dash = new DashboardPage(page, { name: 'browser' }); await dash.withMarker(); @@ -672,6 +674,15 @@ export class DashboardPage { await this.mocks.calledForClose({ screen: 'toggleReport' }); } + /** + * @param {import('../schema/__generated__/schema.types').EventOrigin['screen']} screen + */ + async backButtonRejectsToggleReport(screen = 'toggleReport') { + const selector = this.parent(screen); + await this.page.locator(selector).getByLabel('Back', { exact: true }).click(); + await this.mocks.calledForRejectToggleReport(); + } + async rejectToggleReport() { const { page } = this; await page.getByRole('button', { name: `Don't send` }).click(); diff --git a/integration-tests/Mocks.js b/integration-tests/Mocks.js index 0c463434..a78b5370 100644 --- a/integration-tests/Mocks.js +++ b/integration-tests/Mocks.js @@ -80,7 +80,11 @@ export class Mocks { } async calledForSendToggleReport() { - if (!['browser', 'ios', 'macos'].includes(this.platform.name)) return; + if (this.platform.name === 'android') { + const calls = await this.outgoing({ names: ['sendToggleReport'] }); + expect(calls).toMatchObject([['sendToggleReport', undefined]]); + return; + } if (this.platform.name === 'browser') { const calls = await this.outgoing({ names: ['sendToggleReport'] }); expect(calls).toMatchObject([ @@ -91,16 +95,44 @@ export class Mocks { }, ], ]); - } else { + return; + } + + if (this.platform.name === 'windows') { + const calls = await this.outgoing({ + names: ['SendToggleBreakageReport'], + }); + expect(calls).toMatchObject([ + [ + 'SendToggleBreakageReport', + { + Feature: 'PrivacyDashboard', + Name: 'SendToggleBreakageReport', + Data: {}, + }, + ], + ]); + return; + } + + if (this.platform.name === 'macos' || this.platform.name === 'ios') { const out = await this.outgoing({ names: ['privacyDashboardSendToggleReport'], }); expect(out).toMatchObject([['privacyDashboardSendToggleReport', {}]]); + return; } + + throw new Error('unreachable. mockCalledForSendToggleReport must be handled'); } async calledForRejectToggleReport() { - if (!['browser', 'ios', 'macos'].includes(this.platform.name)) return; + if (this.platform.name === 'android') { + const calls = await this.outgoing({ names: ['rejectToggleReport'] }); + expect(calls).toMatchObject([['rejectToggleReport', undefined]]); + return; + } + if (this.platform.name === 'browser') { const calls = await this.outgoing({ names: ['rejectToggleReport'] }); expect(calls).toMatchObject([ @@ -111,20 +143,84 @@ export class Mocks { }, ], ]); - } else { - // ios/macos + return; + } + + if (this.platform.name === 'windows') { + const calls = await this.outgoing({ + names: ['RejectToggleBreakageReport'], + }); + expect(calls).toMatchObject([ + [ + 'RejectToggleBreakageReport', + { + Feature: 'PrivacyDashboard', + Name: 'RejectToggleBreakageReport', + Data: {}, + }, + ], + ]); + return; + } + + if (this.platform.name === 'macos' || this.platform.name === 'ios') { const out = await this.outgoing({ names: ['privacyDashboardRejectToggleReport'], }); expect(out).toMatchObject([['privacyDashboardRejectToggleReport', {}]]); + + return; } + + throw new Error('unreachable. mockCalledForRejectToggleReport must be handled'); } async calledForSeeWhatsSent() { - const out = await this.outgoing({ - names: ['privacyDashboardSeeWhatIsSent'], - }); - expect(out).toMatchObject([['privacyDashboardSeeWhatIsSent', {}]]); + if (this.platform.name === 'android') { + const calls = await this.outgoing({ names: ['seeWhatIsSent'] }); + expect(calls).toMatchObject([['seeWhatIsSent', undefined]]); + return; + } + + if (this.platform.name === 'browser') { + const calls = await this.outgoing({ names: ['seeWhatIsSent'] }); + expect(calls).toMatchObject([ + [ + 'seeWhatIsSent', + { + messageType: 'seeWhatIsSent', + }, + ], + ]); + return; + } + + if (this.platform.name === 'windows') { + const calls = await this.outgoing({ + names: ['SeeWhatIsSent'], + }); + expect(calls).toMatchObject([ + [ + 'SeeWhatIsSent', + { + Feature: 'PrivacyDashboard', + Name: 'SeeWhatIsSent', + Data: {}, + }, + ], + ]); + return; + } + + if (this.platform.name === 'macos' || this.platform.name === 'ios') { + const out = await this.outgoing({ + names: ['privacyDashboardSeeWhatIsSent'], + }); + expect(out).toMatchObject([['privacyDashboardSeeWhatIsSent', {}]]); + return; + } + + throw new Error('unreachable. mockCalledForSeeWhatsSent must be handled'); } /** @@ -167,39 +263,41 @@ export class Mocks { } } async calledForNativeFeedback() { - const calls = await this.outgoing({ - names: ['privacyDashboardShowNativeFeedback'], - }); - expect(calls).toMatchObject([['privacyDashboardShowNativeFeedback', {}]]); - } - - async calledForSubmitBreakageForm({ category = '', description = '' }) { if (this.platform.name === 'windows') { const calls = await this.outgoing({ - names: ['SubmitBrokenSiteReport'], + names: ['ShowNativeFeedback'], }); expect(calls).toMatchObject([ [ - 'SubmitBrokenSiteReport', + 'ShowNativeFeedback', { Feature: 'PrivacyDashboard', - Name: 'SubmitBrokenSiteReport', - Data: { - category, - description, - }, + Name: 'ShowNativeFeedback', + Data: {}, }, ], ]); return; } + if (this.platform.name === 'macos' || this.platform.name === 'ios') { - const out = await this.outgoing({ - names: ['privacyDashboardSubmitBrokenSiteReport'], + const calls = await this.outgoing({ + names: ['privacyDashboardShowNativeFeedback'], }); - expect(out).toMatchObject([['privacyDashboardSubmitBrokenSiteReport', { category, description }]]); + expect(calls).toMatchObject([['privacyDashboardShowNativeFeedback', {}]]); return; } + + throw new Error('unreachable. mockCalledForNativeFeedback must be handled'); + } + + async calledForSubmitBreakageForm({ category = '', description = '' }) { + if (this.platform.name === 'android') { + const out = await this.outgoing({ names: ['submitBrokenSiteReport'] }); + expect(out).toMatchObject([['submitBrokenSiteReport', JSON.stringify({ category, description })]]); + return; + } + if (this.platform.name === 'browser') { const out = await this.outgoing({ names: ['submitBrokenSiteReport'] }); expect(out).toMatchObject([ @@ -216,11 +314,35 @@ export class Mocks { ]); return; } - if (this.platform.name === 'android') { - const out = await this.outgoing({ names: ['submitBrokenSiteReport'] }); - expect(out).toMatchObject([['submitBrokenSiteReport', JSON.stringify({ category, description })]]); + + if (this.platform.name === 'windows') { + const calls = await this.outgoing({ + names: ['SubmitBrokenSiteReport'], + }); + expect(calls).toMatchObject([ + [ + 'SubmitBrokenSiteReport', + { + Feature: 'PrivacyDashboard', + Name: 'SubmitBrokenSiteReport', + Data: { + category, + description, + }, + }, + ], + ]); return; } + + if (this.platform.name === 'macos' || this.platform.name === 'ios') { + const out = await this.outgoing({ + names: ['privacyDashboardSubmitBrokenSiteReport'], + }); + expect(out).toMatchObject([['privacyDashboardSubmitBrokenSiteReport', { category, description }]]); + return; + } + throw new Error('unreachable. mockCalledForSubmitBreakageForm must be handled'); } @@ -298,6 +420,22 @@ export class Mocks { expect(calls).toMatchObject([['close', undefined]]); return; } + if (this.platform.name === 'windows') { + const calls = await this.outgoing({ names: ['CloseCommand'] }); + expect(calls).toMatchObject([ + [ + 'CloseCommand', + { + Data: { + eventOrigin, + }, + Feature: 'PrivacyDashboard', + Name: 'CloseCommand', + }, + ], + ]); + return; + } if (this.platform.name === 'ios' || this.platform.name === 'macos') { const calls = await this.outgoing({ names: ['privacyDashboardClose'] }); expect(calls).toMatchObject([ diff --git a/integration-tests/android.spec-int.js b/integration-tests/android.spec-int.js index a537f820..7a97baae 100644 --- a/integration-tests/android.spec-int.js +++ b/integration-tests/android.spec-int.js @@ -109,6 +109,41 @@ test.describe('Breakage form', () => { }); }); +test.describe('opens toggle report', () => { + test('sends toggle report', { tag: '@screenshots' }, async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.android(page, { screen: 'toggleReport', opener: 'menu' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.screenshot('screen-toggle-report.png'); + await dash.showsOnlyBackButton('toggleReport'); + await dash.sendToggleReport(); + }); + test('back button rejects toggle report', async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.android(page, { screen: 'toggleReport', opener: 'menu' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.backButtonRejectsToggleReport(); + }); + test('rejects toggle report', async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.android(page, { screen: 'toggleReport', opener: 'dashboard' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.rejectToggleReport(); + }); + test('shows information once', { tag: '@screenshots' }, async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.android(page, { screen: 'toggleReport', opener: 'dashboard' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.showsInformation(); + await dash.cannotHideInformation(); + await dash.screenshot('screen-toggle-report-show.png'); + }); +}); + test.describe('Close', () => { test('pressing close should call native API', async ({ page }) => { const dash = await DashboardPage.android(page); diff --git a/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-non-trackers-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-non-trackers-android-darwin.png index e133fbb0..62597140 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-non-trackers-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-non-trackers-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-primary-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-primary-android-darwin.png index ff222ba7..1586c89d 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-primary-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-primary-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-trackers-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-trackers-android-darwin.png index a9eb5f74..eff9b49a 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-trackers-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/ad-attribution-state-trackers-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/alternative-layout-exp-1-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/alternative-layout-exp-1-android-darwin.png index d996159b..f7b09bf3 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/alternative-layout-exp-1-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/alternative-layout-exp-1-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/alternative-layout-exp-1-protections-off-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/alternative-layout-exp-1-protections-off-android-darwin.png index ac335f88..00b8eecc 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/alternative-layout-exp-1-protections-off-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/alternative-layout-exp-1-protections-off-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/breakage-form-only-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/breakage-form-only-android-darwin.png index 29d4b42b..aade0d48 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/breakage-form-only-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/breakage-form-only-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/breakage-form-prompt-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/breakage-form-prompt-android-darwin.png index bea94da9..75fff017 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/breakage-form-prompt-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/breakage-form-prompt-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/cnn-state-non-trackers-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/cnn-state-non-trackers-android-darwin.png index 454e8f1c..110f7a1d 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/cnn-state-non-trackers-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/cnn-state-non-trackers-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/cnn-state-primary-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/cnn-state-primary-android-darwin.png index 68fbf74d..06f8aa8e 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/cnn-state-primary-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/cnn-state-primary-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/cnn-state-trackers-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/cnn-state-trackers-android-darwin.png index 9d85ce09..3b288962 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/cnn-state-trackers-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/cnn-state-trackers-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/consent-managed-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/consent-managed-android-darwin.png index 09f41ea9..af64cad5 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/consent-managed-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/consent-managed-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/consent-managed-configurable-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/consent-managed-configurable-android-darwin.png index f9f2565f..a8fb3a2b 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/consent-managed-configurable-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/consent-managed-configurable-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/consent-managed-configurable-primary-cosmetic-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/consent-managed-configurable-primary-cosmetic-android-darwin.png index 1ccdff0b..0fdad572 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/consent-managed-configurable-primary-cosmetic-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/consent-managed-configurable-primary-cosmetic-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/google-off-state-non-trackers-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/google-off-state-non-trackers-android-darwin.png index 96964735..7090759a 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/google-off-state-non-trackers-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/google-off-state-non-trackers-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/material-dialog-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/material-dialog-android-darwin.png index 5ba6c34f..9653ebb8 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/material-dialog-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/material-dialog-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/new-entities-state-primary-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/new-entities-state-primary-android-darwin.png index bf7cf80e..34397e33 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/new-entities-state-primary-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/new-entities-state-primary-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/new-entities-state-trackers-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/new-entities-state-trackers-android-darwin.png index ba235ee6..9e6166dd 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/new-entities-state-trackers-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/new-entities-state-trackers-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/primary-cnn-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/primary-cnn-android-darwin.png index 68fbf74d..06f8aa8e 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/primary-cnn-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/primary-cnn-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/screen-breakage-form-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/screen-breakage-form-android-darwin.png index 29d4b42b..aade0d48 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/screen-breakage-form-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/screen-breakage-form-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/screen-toggle-report-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/screen-toggle-report-android-darwin.png new file mode 100644 index 00000000..c9c25217 Binary files /dev/null and b/integration-tests/android.spec-int.js-snapshots/screen-toggle-report-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/screen-toggle-report-show-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/screen-toggle-report-show-android-darwin.png index 75836422..bfb86fbe 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/screen-toggle-report-show-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/screen-toggle-report-show-android-darwin.png differ diff --git a/integration-tests/android.spec-int.js-snapshots/upgraded-secure-state-primary-android-darwin.png b/integration-tests/android.spec-int.js-snapshots/upgraded-secure-state-primary-android-darwin.png index bac9fed7..00ce2fb3 100644 Binary files a/integration-tests/android.spec-int.js-snapshots/upgraded-secure-state-primary-android-darwin.png and b/integration-tests/android.spec-int.js-snapshots/upgraded-secure-state-primary-android-darwin.png differ diff --git a/integration-tests/helpers.js b/integration-tests/helpers.js index 908ec6c7..cd3e85f7 100644 --- a/integration-tests/helpers.js +++ b/integration-tests/helpers.js @@ -26,6 +26,10 @@ export async function playTimeline(page, state, platform) { } if (platform.name === 'windows') { messages.windowsViewModel = state.toWindowsViewModel(); + messages.GetToggleReportOptions = state.toWindowsToggleReportOptions(); + } + if (platform.name === 'ios' || platform.name === 'macos') { + messages.privacyDashboardGetToggleReportOptions = toggleReportScreen; } await page.evaluate(mockDataProvider, { state, platform, messages }); return messages; @@ -33,7 +37,11 @@ export async function playTimeline(page, state, platform) { export async function installAndroidMocks(page) { await page.waitForFunction(() => typeof window.onChangeRequestData === 'function'); - return page.evaluate(mockAndroidApis); + return page.evaluate(mockAndroidApis, { + messages: { + getToggleReportOptions: toggleReportScreen, + }, + }); } /** @@ -52,7 +60,7 @@ export function installWindowsMocks(page) { export async function installWebkitMocks(page, _args) { await page.waitForFunction(() => typeof window.onChangeRequestData === 'function'); return page.evaluate(webkitMockApis, { - responses: { + messages: { privacyDashboardGetToggleReportOptions: toggleReportScreen, }, }); diff --git a/integration-tests/ios.spec-int.js-snapshots/screen-toggle-report-show-ios-darwin.png b/integration-tests/ios.spec-int.js-snapshots/screen-toggle-report-show-ios-darwin.png index 2076f2e3..1c85ae95 100644 Binary files a/integration-tests/ios.spec-int.js-snapshots/screen-toggle-report-show-ios-darwin.png and b/integration-tests/ios.spec-int.js-snapshots/screen-toggle-report-show-ios-darwin.png differ diff --git a/integration-tests/macos.spec-int.js b/integration-tests/macos.spec-int.js index f03a6b1e..e8bec2e9 100644 --- a/integration-tests/macos.spec-int.js +++ b/integration-tests/macos.spec-int.js @@ -34,7 +34,7 @@ test('invalid/missing certificate', { tag: '@screenshots' }, async ({ page }) => await dash.showsInvalidCertDetail(); }); -test('phishing warning', async ({ page }) => { +test('phishing warning', { tag: '@screenshots' }, async ({ page }) => { /** @type {DashboardPage} */ const dash = await DashboardPage.webkit(page, { platform: 'macos' }); await dash.addState([testDataStates.phishing]); @@ -119,7 +119,6 @@ test.describe('opening breakage form', () => { await dash.showsInformation(); await dash.mocks.calledForSeeWhatsSent(); await dash.screenshot('screen-toggle-report-show.png'); - await dash.hidesInformation(); }); }); diff --git a/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-dashboard-macos-darwin.png b/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-dashboard-macos-darwin.png index 3f2393de..d771f79b 100644 Binary files a/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-dashboard-macos-darwin.png and b/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-dashboard-macos-darwin.png differ diff --git a/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-menu-macos-darwin.png b/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-menu-macos-darwin.png index 42786272..7577710d 100644 Binary files a/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-menu-macos-darwin.png and b/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-menu-macos-darwin.png differ diff --git a/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-sent-macos-darwin.png b/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-sent-macos-darwin.png index 7961b301..7e69a2a7 100644 Binary files a/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-sent-macos-darwin.png and b/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-sent-macos-darwin.png differ diff --git a/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-show-macos-darwin.png b/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-show-macos-darwin.png index 7a9fcfb7..759c901d 100644 Binary files a/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-show-macos-darwin.png and b/integration-tests/macos.spec-int.js-snapshots/screen-toggle-report-show-macos-darwin.png differ diff --git a/integration-tests/windows.spec-int.js b/integration-tests/windows.spec-int.js index 722de5d8..fc6f6551 100644 --- a/integration-tests/windows.spec-int.js +++ b/integration-tests/windows.spec-int.js @@ -26,6 +26,51 @@ test.describe('opening breakage form', () => { }); }); +test.describe('toggle report', () => { + test('shows toggle report when opened from menu', { tag: '@screenshots' }, async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.windows(page, { screen: 'toggleReport', opener: 'menu' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.screenshot('screen-toggle-report-menu.png'); + }); + test('shows toggle report when opened from dashboard', { tag: '@screenshots' }, async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.windows(page, { screen: 'toggleReport', opener: 'dashboard' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.screenshot('screen-toggle-report-dashboard.png'); + }); + test('sends toggle report', { tag: '@screenshots' }, async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.windows(page, { screen: 'toggleReport', opener: 'dashboard' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.sendToggleReport(); + + // this is macOS specific: + await dash.showsSuccessScreen(); + await dash.screenshot('screen-toggle-report-sent.png'); + await dash.clickingSuccessScreenClosesDashboard(); + }); + test('rejects toggle report', async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.windows(page, { screen: 'toggleReport', opener: 'dashboard' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.rejectToggleReport(); + }); + test('shows information', { tag: '@screenshots' }, async ({ page }) => { + /** @type {DashboardPage} */ + const dash = await DashboardPage.windows(page, { screen: 'toggleReport', opener: 'dashboard' }); + await dash.addState([testDataStates.google]); + await dash.toggleReportIsVisible(); + await dash.showsInformation(); + await dash.mocks.calledForSeeWhatsSent(); + await dash.screenshot('screen-toggle-report-show.png'); + }); +}); + test.describe('Protections toggle', () => { toggleFlows((page) => DashboardPage.windows(page)); }); diff --git a/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-dashboard-windows-darwin.png b/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-dashboard-windows-darwin.png new file mode 100644 index 00000000..91c6d88a Binary files /dev/null and b/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-dashboard-windows-darwin.png differ diff --git a/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-menu-windows-darwin.png b/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-menu-windows-darwin.png new file mode 100644 index 00000000..91c6d88a Binary files /dev/null and b/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-menu-windows-darwin.png differ diff --git a/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-sent-windows-darwin.png b/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-sent-windows-darwin.png new file mode 100644 index 00000000..d59e1f8a Binary files /dev/null and b/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-sent-windows-darwin.png differ diff --git a/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-show-windows-darwin.png b/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-show-windows-darwin.png new file mode 100644 index 00000000..59383079 Binary files /dev/null and b/integration-tests/windows.spec-int.js-snapshots/screen-toggle-report-show-windows-darwin.png differ diff --git a/schema/__generated__/schema.parsers.mjs b/schema/__generated__/schema.parsers.mjs index 45388a5d..51b9f28a 100644 --- a/schema/__generated__/schema.parsers.mjs +++ b/schema/__generated__/schema.parsers.mjs @@ -11,45 +11,49 @@ export const adClickAttributionReasonSchema = z.literal("adClickAttribution"); export const otherThirdPartyRequestReasonSchema = z.literal("otherThirdPartyRequest"); -export const screenKindSchema = z.union([z.literal("primaryScreen"), z.literal("breakageForm"), z.literal("promptBreakageForm"), z.literal("toggleReport"), z.literal("categoryTypeSelection"), z.literal("categorySelection"), z.literal("choiceToggle"), z.literal("choiceBreakageForm"), z.literal("connection"), z.literal("trackers"), z.literal("nonTrackers"), z.literal("consentManaged"), z.literal("cookieHidden")]); +export const wVVersionSchema = z.literal("wvVersion"); + +export const requestsSchema = z.literal("requests"); + +export const featuresSchema = z.literal("features"); -export const wvVersionTitleSchema = z.literal("wvVersion"); +export const appVersionSchema = z.literal("appVersion"); -export const requestsTitleSchema = z.literal("requests"); +export const atbSchema = z.literal("atb"); -export const featuresTitleSchema = z.literal("features"); +export const errorDescriptionsSchema = z.literal("errorDescriptions"); -export const appVersionTitleSchema = z.literal("appVersion"); +export const extensionVersionSchema = z.literal("extensionVersion"); -export const atbTitleSchema = z.literal("atb"); +export const hTTPErrorCodesSchema = z.literal("httpErrorCodes"); -export const errorDescriptionsTitleSchema = z.literal("errorDescriptions"); +export const lastSentDaySchema = z.literal("lastSentDay"); -export const extensionVersionTitleSchema = z.literal("extensionVersion"); +export const deviceSchema = z.literal("device"); -export const httpErrorCodesTitleSchema = z.literal("httpErrorCodes"); +export const osSchema = z.literal("os"); -export const lastSentDayTitleSchema = z.literal("lastSentDay"); +export const listVersionsSchema = z.literal("listVersions"); -export const deviceTitleSchema = z.literal("device"); +export const reportFlowSchema = z.literal("reportFlow"); -export const osTitleSchema = z.literal("os"); +export const siteURLSchema = z.literal("siteUrl"); -export const listVersionsTitleSchema = z.literal("listVersions"); +export const didOpenReportInfoSchema = z.literal("didOpenReportInfo"); -export const reportFlowTitleSchema = z.literal("reportFlow"); +export const toggleReportCounterSchema = z.literal("toggleReportCounter"); -export const siteUrlTitleSchema = z.literal("siteUrl"); +export const openerContextSchema = z.literal("openerContext"); -export const didOpenReportInfoTitleSchema = z.literal("didOpenReportInfo"); +export const userRefreshCountSchema = z.literal("userRefreshCount"); -export const toggleReportCounterTitleSchema = z.literal("toggleReportCounter"); +export const jSPerformanceSchema = z.literal("jsPerformance"); -export const openerContextTitleSchema = z.literal("openerContext"); +export const localeSchema = z.literal("locale"); -export const userRefreshCountTitleSchema = z.literal("userRefreshCount"); +export const descriptionSchema = z.literal("description"); -export const jsPerformanceTitleSchema = z.literal("jsPerformance"); +export const screenKindSchema = z.union([z.literal("primaryScreen"), z.literal("breakageForm"), z.literal("promptBreakageForm"), z.literal("toggleReport"), z.literal("categoryTypeSelection"), z.literal("categorySelection"), z.literal("choiceToggle"), z.literal("choiceBreakageForm"), z.literal("connection"), z.literal("trackers"), z.literal("nonTrackers"), z.literal("consentManaged"), z.literal("cookieHidden")]); export const stateBlockedSchema = z.object({ blocked: z.object({}) @@ -129,6 +133,10 @@ export const cookiePromptManagementStatusSchema = z.object({ configurable: z.boolean().optional() }); +export const siteUrlAdditionalDataSchema = z.object({ + url: z.string() +}); + export const refreshAliasResponseSchema = z.object({ personalAddress: z.string(), privateAddress: z.string() @@ -168,10 +176,6 @@ export const eventOriginSchema = z.object({ screen: screenKindSchema }); -export const siteUrlAdditionalDataSchema = z.object({ - url: z.string() -}); - export const closeMessageParamsSchema = z.object({ eventOrigin: eventOriginSchema }); @@ -218,7 +222,7 @@ export const outgoingExtensionMessageSchema = z.object({ options: z.object({}) }); -export const dataItemIdSchema = z.union([wvVersionTitleSchema, requestsTitleSchema, featuresTitleSchema, appVersionTitleSchema, atbTitleSchema, errorDescriptionsTitleSchema, extensionVersionTitleSchema, httpErrorCodesTitleSchema, lastSentDayTitleSchema, deviceTitleSchema, osTitleSchema, listVersionsTitleSchema, reportFlowTitleSchema, siteUrlTitleSchema, didOpenReportInfoTitleSchema, toggleReportCounterTitleSchema, openerContextTitleSchema, userRefreshCountTitleSchema, jsPerformanceTitleSchema]); +export const dataItemIdSchema = z.union([wVVersionSchema, requestsSchema, featuresSchema, appVersionSchema, atbSchema, errorDescriptionsSchema, extensionVersionSchema, hTTPErrorCodesSchema, lastSentDaySchema, deviceSchema, osSchema, listVersionsSchema, reportFlowSchema, siteURLSchema, didOpenReportInfoSchema, toggleReportCounterSchema, openerContextSchema, userRefreshCountSchema, jSPerformanceSchema, localeSchema, descriptionSchema]); export const incomingExtensionMessageSchema = z.union([incomingResponseSchema, incomingToggleReportSchema, incomingUpdateTabDataSchema, incomingClosePopupSchema, incomingDidResetTrackersDataSchema]); @@ -249,6 +253,11 @@ export const breakageReportSchema = z.object({ response: z.object({}).optional() }); +export const toggleReportScreenDataItemSchema = z.object({ + id: dataItemIdSchema, + additional: siteUrlAdditionalDataSchema.optional() +}); + export const fireButtonDataSchema = z.object({ options: z.array(fireOptionSchema) }); @@ -263,11 +272,6 @@ export const setProtectionParamsSchema = z.object({ eventOrigin: eventOriginSchema }); -export const toggleReportScreenDataItemSchema = z.object({ - id: dataItemIdSchema, - additional: siteUrlAdditionalDataSchema.optional() -}); - export const telemetrySpanSchema = z.object({ attributes: z.union([categoryTypeSelectedSchema, categorySelectedSchema, toggleSkippedSchema]), eventOrigin: eventOriginSchema @@ -308,7 +312,14 @@ export const windowsIncomingViewModelSchema = z.object({ Data: windowsViewModelSchema }); -export const windowsIncomingMessageSchema = z.union([windowsIncomingVisibilitySchema, windowsIncomingViewModelSchema]); +export const windowsIncomingToggleReportOptionsSchema = z.object({ + context: z.literal("PrivacyDashboard"), + featureName: z.literal("GetToggleReportOptions"), + id: z.string(), + result: toggleReportScreenSchema +}); + +export const windowsIncomingMessageSchema = z.union([windowsIncomingVisibilitySchema, windowsIncomingViewModelSchema, windowsIncomingToggleReportOptionsSchema]); export const apiSchema = z.object({ "request-data": requestDataSchema, diff --git a/schema/__generated__/schema.types.ts b/schema/__generated__/schema.types.ts index 63d82113..471dff25 100644 --- a/schema/__generated__/schema.types.ts +++ b/schema/__generated__/schema.types.ts @@ -41,117 +41,130 @@ export type OtherThirdPartyRequestReason = "otherThirdPartyRequest"; /** * A helper list of messages that the Dashboard accepts from Windows */ -export type WindowsIncomingMessage = WindowsIncomingVisibility | WindowsIncomingViewModel; -export type ScreenKind = - | "primaryScreen" - | "breakageForm" - | "promptBreakageForm" - | "toggleReport" - | "categoryTypeSelection" - | "categorySelection" - | "choiceToggle" - | "choiceBreakageForm" - | "connection" - | "trackers" - | "nonTrackers" - | "consentManaged" - | "cookieHidden"; +export type WindowsIncomingMessage = + | WindowsIncomingVisibility + | WindowsIncomingViewModel + | WindowsIncomingToggleReportOptions; export type DataItemId = - | WvVersionTitle - | RequestsTitle - | FeaturesTitle - | AppVersionTitle - | AtbTitle - | ErrorDescriptionsTitle - | ExtensionVersionTitle - | HttpErrorCodesTitle - | LastSentDayTitle - | DeviceTitle - | OsTitle - | ListVersionsTitle - | ReportFlowTitle - | SiteUrlTitle - | DidOpenReportInfoTitle - | ToggleReportCounterTitle - | OpenerContextTitle - | UserRefreshCountTitle - | JsPerformanceTitle; + | WVVersion + | Requests + | Features + | AppVersion + | ATB + | ErrorDescriptions + | ExtensionVersion + | HTTPErrorCodes + | LastSentDay + | Device + | OS + | ListVersions + | ReportFlow + | SiteURL + | DidOpenReportInfo + | ToggleReportCounter + | OpenerContext + | UserRefreshCount + | JSPerformance + | Locale + | Description; +/** + * Web browser engine version number + */ +export type WVVersion = "wvVersion"; +/** + * Hostnames of trackers blocked, surrogate requests, ignored requests, and requests not in tracker blocking list + */ +export type Requests = "requests"; /** - * wvVersion description + * List of which browser features were active */ -export type WvVersionTitle = "wvVersion"; +export type Features = "features"; /** - * requests description + * App version number */ -export type RequestsTitle = "requests"; +export type AppVersion = "appVersion"; /** - * features description + * Anonymous experiment group for feature testing */ -export type FeaturesTitle = "features"; +export type ATB = "atb"; /** - * appVersion description + * Browser-reported errors */ -export type AppVersionTitle = "appVersion"; +export type ErrorDescriptions = "errorDescriptions"; /** - * atb description + * Extension version number */ -export type AtbTitle = "atb"; +export type ExtensionVersion = "extensionVersion"; /** - * errorDescriptions description + * Website response status (HTTP) codes */ -export type ErrorDescriptionsTitle = "errorDescriptions"; +export type HTTPErrorCodes = "httpErrorCodes"; /** - * extensionVersion description + * Date of last report sent for this site */ -export type ExtensionVersionTitle = "extensionVersion"; +export type LastSentDay = "lastSentDay"; /** - * httpErrorCodes description + * Device make, model, and manufacturer */ -export type HttpErrorCodesTitle = "httpErrorCodes"; +export type Device = "device"; /** - * lastSentDay description + * Operating system version number */ -export type LastSentDayTitle = "lastSentDay"; +export type OS = "os"; /** - * device description + * Information about which versions of our protections were active */ -export type DeviceTitle = "device"; +export type ListVersions = "listVersions"; /** - * os description + * Which reporting form you used ('menu', 'dashboard', etc.) */ -export type OsTitle = "os"; +export type ReportFlow = "reportFlow"; /** - * listVersions description + * Page URL (without identifiable info) */ -export type ListVersionsTitle = "listVersions"; +export type SiteURL = "siteUrl"; /** - * reportFlow description + * Whether or not you opted to show this report info */ -export type ReportFlowTitle = "reportFlow"; +export type DidOpenReportInfo = "didOpenReportInfo"; /** - * siteUrl description + * Number of times protections were toggled off */ -export type SiteUrlTitle = "siteUrl"; +export type ToggleReportCounter = "toggleReportCounter"; /** - * didOpenReportInfo description + * How you got to this page, either: 'SERP' (DuckDuckGo search), 'Navigation' (link/URL), or 'External' (other means) */ -export type DidOpenReportInfoTitle = "didOpenReportInfo"; +export type OpenerContext = "openerContext"; /** - * toggleReportCounter description + * Number of refreshes since page load */ -export type ToggleReportCounterTitle = "toggleReportCounter"; +export type UserRefreshCount = "userRefreshCount"; /** - * openerContext description + * How quickly parts of the page loaded */ -export type OpenerContextTitle = "openerContext"; +export type JSPerformance = "jsPerformance"; /** - * userRefreshCount description + * Primary language and country of your device */ -export type UserRefreshCountTitle = "userRefreshCount"; +export type Locale = "locale"; /** - * jsPerformance description + * Your selected category and optional comments */ -export type JsPerformanceTitle = "jsPerformance"; +export type Description = "description"; +export type ScreenKind = + | "primaryScreen" + | "breakageForm" + | "promptBreakageForm" + | "toggleReport" + | "categoryTypeSelection" + | "categorySelection" + | "choiceToggle" + | "choiceBreakageForm" + | "connection" + | "trackers" + | "nonTrackers" + | "consentManaged" + | "cookieHidden"; export type IncomingExtensionMessage = | IncomingResponse | IncomingToggleReport @@ -478,6 +491,31 @@ export interface CookiePromptManagementStatus { */ configurable?: boolean; } +/** + * This message contains user data disclosure options for Toggle Report and Breakage Form + */ +export interface WindowsIncomingToggleReportOptions { + context: "PrivacyDashboard"; + featureName: "GetToggleReportOptions"; + id: string; + result: ToggleReportScreen; +} +/** + * [Sample JSON 📝](../__fixtures__/toggle-report-screen.json) + */ +export interface ToggleReportScreen { + /** + * The line-items to show to the user for indicating what data the report will send to DuckDuckGo + */ + data: ToggleReportScreenDataItem[]; +} +export interface ToggleReportScreenDataItem { + id: DataItemId; + additional?: SiteUrlAdditionalData; +} +export interface SiteUrlAdditionalData { + url: string; +} export interface RefreshAliasResponse { personalAddress: string; privateAddress: string; @@ -531,22 +569,6 @@ export interface SetProtectionParams { export interface EventOrigin { screen: ScreenKind; } -/** - * [Sample JSON 📝](../__fixtures__/toggle-report-screen.json) - */ -export interface ToggleReportScreen { - /** - * The line-items to show to the user for indicating what data the report will send to DuckDuckGo - */ - data: ToggleReportScreenDataItem[]; -} -export interface ToggleReportScreenDataItem { - id: DataItemId; - additional?: SiteUrlAdditionalData; -} -export interface SiteUrlAdditionalData { - url: string; -} export interface CloseMessageParams { eventOrigin: EventOrigin; } diff --git a/schema/toggle-report-screen.json b/schema/toggle-report-screen.json index 4ab669f6..3cdf6bb2 100644 --- a/schema/toggle-report-screen.json +++ b/schema/toggle-report-screen.json @@ -40,7 +40,9 @@ { "$ref": "#/definitions/toggleReportCounter" }, { "$ref": "#/definitions/openerContext" }, { "$ref": "#/definitions/userRefreshCount" }, - { "$ref": "#/definitions/jsPerformance" } + { "$ref": "#/definitions/jsPerformance" }, + { "$ref": "#/definitions/locale" }, + { "$ref": "#/definitions/description" } ] }, "additional": { @@ -61,99 +63,109 @@ } }, "wvVersion": { - "title": "wvVersion title", + "title": "WV Version", "const": "wvVersion", - "description": "wvVersion description" + "description": "Web browser engine version number" }, "requests": { - "title": "requests title", + "title": "Requests", "const": "requests", - "description": "requests description" + "description": "Hostnames of trackers blocked, surrogate requests, ignored requests, and requests not in tracker blocking list" }, "features": { - "title": "features title", + "title": "Features", "const": "features", - "description": "features description" + "description": "List of which browser features were active" }, "appVersion": { - "title": "appVersion title", + "title": "App Version", "const": "appVersion", - "description": "appVersion description" + "description": "App version number" }, "atb": { - "title": "atb title", + "title": "ATB", "const": "atb", - "description": "atb description" + "description": "Anonymous experiment group for feature testing" }, "errorDescriptions": { - "title": "errorDescriptions title", + "title": "Error Descriptions", "const": "errorDescriptions", - "description": "errorDescriptions description" + "description": "Browser-reported errors" }, "extensionVersion": { - "title": "extensionVersion title", + "title": "Extension Version", "const": "extensionVersion", - "description": "extensionVersion description" + "description": "Extension version number" }, "httpErrorCodes": { - "title": "httpErrorCodes title", + "title": "HTTP Error Codes", "const": "httpErrorCodes", - "description": "httpErrorCodes description" + "description": "Website response status (HTTP) codes" }, "lastSentDay": { - "title": "lastSentDay title", + "title": "Last Sent Day", "const": "lastSentDay", - "description": "lastSentDay description" + "description": "Date of last report sent for this site" }, "device": { - "title": "device title", + "title": "Device", "const": "device", - "description": "device description" + "description": "Device make, model, and manufacturer" }, "os": { - "title": "os title", + "title": "OS", "const": "os", - "description": "os description" + "description": "Operating system version number" }, "listVersions": { - "title": "listVersions title", + "title": "List Versions", "const": "listVersions", - "description": "listVersions description" + "description": "Information about which versions of our protections were active" }, "reportFlow": { - "title": "reportFlow title", + "title": "Report Flow", "const": "reportFlow", - "description": "reportFlow description" + "description": "Which reporting form you used ('menu', 'dashboard', etc.)" }, "siteUrl": { - "title": "siteUrl title", + "title": "Site URL", "const": "siteUrl", - "description": "siteUrl description" + "description": "Page URL (without identifiable info)" }, "didOpenReportInfo": { - "title": "didOpenReportInfo title", + "title": "Did Open Report Info", "const": "didOpenReportInfo", - "description": "didOpenReportInfo description" + "description": "Whether or not you opted to show this report info" }, "toggleReportCounter": { - "title": "toggleReportCounter title", + "title": "Toggle Report Counter", "const": "toggleReportCounter", - "description": "toggleReportCounter description" + "description": "Number of times protections were toggled off" }, "openerContext": { - "title": "openerContext title", + "title": "Opener Context", "const": "openerContext", - "description": "openerContext description" + "description": "How you got to this page, either: 'SERP' (DuckDuckGo search), 'Navigation' (link/URL), or 'External' (other means)" }, "userRefreshCount": { - "title": "userRefreshCount title", + "title": "User Refresh Count", "const": "userRefreshCount", - "description": "userRefreshCount description" + "description": "Number of refreshes since page load" }, "jsPerformance": { - "title": "jsPerformance title", + "title": "JS Performance", "const": "jsPerformance", - "description": "jsPerformance description" + "description": "How quickly parts of the page loaded" + }, + "locale": { + "title": "Locale", + "const": "locale", + "description": "Primary language and country of your device" + }, + "description": { + "title": "Description", + "const": "description", + "description": "Your selected category and optional comments" } } } diff --git a/schema/windows-incoming-message.json b/schema/windows-incoming-message.json index b2fd4807..ea516053 100644 --- a/schema/windows-incoming-message.json +++ b/schema/windows-incoming-message.json @@ -8,6 +8,9 @@ }, { "$ref": "./windows-incoming-viewmodel.json" + }, + { + "$ref": "./windows-incoming-toggle-report-options.json" } ] } diff --git a/schema/windows-incoming-toggle-report-options.json b/schema/windows-incoming-toggle-report-options.json new file mode 100644 index 00000000..e2ecc79c --- /dev/null +++ b/schema/windows-incoming-toggle-report-options.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "WindowsIncomingToggleReportOptions", + "type": "object", + "description": "This message contains user data disclosure options for Toggle Report and Breakage Form", + "additionalProperties": false, + "required": ["context", "featureName", "id", "result"], + "properties": { + "context": { + "type": "string", + "const": "PrivacyDashboard" + }, + "featureName": { + "type": "string", + "const": "GetToggleReportOptions" + }, + "id": { + "type": "string" + }, + "result": { + "$ref": "./toggle-report-screen.json" + } + } +} diff --git a/shared/data/text.js b/shared/data/text.js index bf8a7ecb..37acadf8 100644 --- a/shared/data/text.js +++ b/shared/data/text.js @@ -45,5 +45,9 @@ export function namedString(item) { return ns.toggleReport('dynamic_didOpenReportInfo.title'); case 'toggleReportCounter': return ns.toggleReport('dynamic_toggleReportCounter.title'); + case 'locale': + return ns.toggleReport('dynamic_locale.title'); + case 'description': + return ns.toggleReport('dynamic_description.title'); } } diff --git a/shared/img/refresh-assets/breakage-sent.svg b/shared/img/refresh-assets/breakage-sent.svg index 8e33cc8d..8ae55c2b 100644 --- a/shared/img/refresh-assets/breakage-sent.svg +++ b/shared/img/refresh-assets/breakage-sent.svg @@ -1,10 +1,6 @@ - - - - - - - - - + + + + + diff --git a/shared/js/browser/android-communication.js b/shared/js/browser/android-communication.js index e938fbf5..69ecf68b 100644 --- a/shared/js/browser/android-communication.js +++ b/shared/js/browser/android-communication.js @@ -18,10 +18,15 @@ import { setupBlurOnLongPress, setupGlobalOpenerListener } from '../ui/views/uti import { CheckBrokenSiteReportHandledMessage, CloseMessage, + FetchToggleReportOptions, OpenSettingsMessages, + RejectToggleBreakageReport, + SeeWhatIsSent, + SendToggleBreakageReport, SetListsMessage, setupColorScheme, SubmitBrokenSiteReportMessage, + ShowNativeFeedback, } from './common.js'; import { createTabData } from './utils/request-details.mjs'; import invariant from 'tiny-invariant'; @@ -322,6 +327,62 @@ export class PrivacyDashboardJavascriptInterface { invariant(typeof window.PrivacyDashboard?.submitBrokenSiteReport, 'window.PrivacyDashboard.submitBrokenSiteReport required'); window.PrivacyDashboard.submitBrokenSiteReport(JSON.stringify(payload)); } + + /** + * {@inheritDoc common.getToggleReportOptions} + * @type {import("./common.js").getToggleReportOptions} + * @returns {Promise} + */ + getToggleReportOptions() { + invariant(typeof window.PrivacyDashboard?.getToggleReportOptions, 'window.PrivacyDashboard.getToggleReportOptions required'); + window.PrivacyDashboard.getToggleReportOptions(); + return new Promise((resolve) => { + window.onGetToggleReportOptionsResponse = (data) => { + resolve(data); + Reflect.deleteProperty(window, 'onGetToggleReportOptionsResponse'); + }; + }); + } + + /** + * {@inheritDoc common.sendToggleReport} + * @type {import("./common.js").sendToggleReport} + * @example + * ```js + * window.PrivacyDashboard.sendToggleReport() + * ``` + */ + sendToggleReport() { + invariant(window.PrivacyDashboard?.sendToggleReport, 'sendToggleReport missing'); + window.PrivacyDashboard.sendToggleReport(); + } + + /** + * {@inheritDoc common.rejectToggleReport} + * @type {import("./common.js").rejectToggleReport} + */ + rejectToggleReport() { + invariant(window.PrivacyDashboard?.rejectToggleReport, 'rejectToggleReport missing'); + window.PrivacyDashboard.rejectToggleReport(); + } + + /** + * {@inheritDoc common.seeWhatIsSent} + * @type {import("./common.js").seeWhatIsSent} + */ + seeWhatIsSent() { + invariant(window.PrivacyDashboard?.seeWhatIsSent, 'seeWhatIsSent missing'); + window.PrivacyDashboard.seeWhatIsSent(); + } + + /** + * {@inheritDoc common.showNativeFeedback} + * @type {import("./common.js").showNativeFeedback} + */ + showNativeFeedback() { + invariant(window.PrivacyDashboard?.showNativeFeedback, 'showNativeFeedback missing'); + window.PrivacyDashboard.showNativeFeedback(); + } } /** @@ -375,6 +436,26 @@ async function fetchAndroid(message) { }); } + if (message instanceof FetchToggleReportOptions) { + return privacyDashboardApi.getToggleReportOptions(); + } + + if (message instanceof SendToggleBreakageReport) { + return privacyDashboardApi.sendToggleReport(); + } + + if (message instanceof RejectToggleBreakageReport) { + return privacyDashboardApi.rejectToggleReport(); + } + + if (message instanceof SeeWhatIsSent) { + return privacyDashboardApi.seeWhatIsSent(); + } + + if (message instanceof ShowNativeFeedback) { + return privacyDashboardApi.showNativeFeedback(); + } + console.warn('unhandled message', message); } diff --git a/shared/js/browser/common.js b/shared/js/browser/common.js index ed17b9b5..86d2b953 100644 --- a/shared/js/browser/common.js +++ b/shared/js/browser/common.js @@ -285,17 +285,15 @@ export async function getToggleReportOptions() { /** * Send the toggle report - * @returns {Promise} */ -export async function sendToggleReport() { +export function sendToggleReport() { throw new Error('base impl'); } /** * Reject sending the toggle report - * @returns {Promise} */ -export async function rejectToggleReport() { +export function rejectToggleReport() { throw new Error('base impl'); } @@ -306,6 +304,13 @@ export function seeWhatIsSent() { throw new Error('base impl'); } +/** + * Sent when the user chooses to provide general feedback from the breakage form + */ +export function showNativeFeedback() { + throw new Error('base impl'); +} + /** * @param {import('../../../schema/__generated__/schema.types').Search} options */ diff --git a/shared/js/browser/communication.js b/shared/js/browser/communication.js index 4268329b..c6300df4 100644 --- a/shared/js/browser/communication.js +++ b/shared/js/browser/communication.js @@ -4,7 +4,7 @@ import * as iosComms from './ios-communication.js'; import * as androidComms from './android-communication.js'; import * as windowsComms from './windows-communication.js'; import * as macosComms from './macos-communication.js'; -import { installMocks } from './utils/communication-mocks.mjs'; +import { installDebuggerMocks } from './utils/communication-mocks.mjs'; let defaultComms; @@ -38,7 +38,7 @@ let debug = false; // eslint-disable-next-line no-unused-labels, no-labels $TEST: (() => { if (typeof window.__ddg_integration_test === 'undefined') { - installMocks(platform).catch(console.error); + installDebuggerMocks(platform).catch(console.error); debug = true; } })(); diff --git a/shared/js/browser/macos-communication.js b/shared/js/browser/macos-communication.js index 8f7cb22a..50066b6d 100644 --- a/shared/js/browser/macos-communication.js +++ b/shared/js/browser/macos-communication.js @@ -36,6 +36,7 @@ import { setupMutationObserver, SubmitBrokenSiteReportMessage, UpdatePermissionMessage, + ShowNativeFeedback, } from './common.js'; import { createTabData } from './utils/request-details.mjs'; @@ -317,7 +318,7 @@ export function privacyDashboardGetToggleReportOptions() { export function privacyDashboardSendToggleReport() { invariant(window.webkit?.messageHandlers, 'webkit.messageHandlers required'); invariant(window.webkit.messageHandlers.privacyDashboardSendToggleReport, 'privacyDashboardSendToggleReport required'); - return window.webkit.messageHandlers.privacyDashboardSendToggleReport.postMessage({}); + window.webkit.messageHandlers.privacyDashboardSendToggleReport.postMessage({}); } /** @@ -335,7 +336,7 @@ export function privacyDashboardSendToggleReport() { export function privacyDashboardRejectToggleReport() { invariant(window.webkit?.messageHandlers, 'webkit.messageHandlers required'); invariant(window.webkit.messageHandlers.privacyDashboardRejectToggleReport, 'privacyDashboardRejectToggleReport required'); - return window.webkit.messageHandlers.privacyDashboardRejectToggleReport.postMessage({}); + window.webkit.messageHandlers.privacyDashboardRejectToggleReport.postMessage({}); } /** @@ -436,6 +437,11 @@ async function fetch(message) { if (message instanceof SeeWhatIsSent) { return privacyDashboardSeeWhatIsSent(); } + + if (message instanceof ShowNativeFeedback) { + privacyDashboardShowNativeFeedback({}); + return false; // Return true to prevent HTML form from showing + } } /** @@ -502,6 +508,21 @@ export function privacyDashboardSetSize(payload) { } } +/** + * Triggers a native form for general feedback on macOS + * + * @category Webkit Message Handlers + * @param {{}} args - An empty object to keep the `webkit` message handlers happy + * @example + * ```js + * window.webkit.messageHandlers.privacyDashboardShowNativeFeedback.postMessage(args) + * ``` + */ +export function privacyDashboardShowNativeFeedback(args) { + invariant(window.webkit?.messageHandlers, 'webkit.messageHandlers required'); + window.webkit.messageHandlers.privacyDashboardShowNativeFeedback.postMessage(args); +} + /** * These side-effects are used on both ios/macos */ diff --git a/shared/js/browser/utils/communication-mocks.mjs b/shared/js/browser/utils/communication-mocks.mjs index 2a360d67..f33623d4 100644 --- a/shared/js/browser/utils/communication-mocks.mjs +++ b/shared/js/browser/utils/communication-mocks.mjs @@ -73,6 +73,26 @@ export function windowsMockApis() { }; globalThis.windowsInteropPostMessage = (arg) => { window.__playwright.mocks.outgoing.push([arg.Name, arg]); + + let responseData; + switch (arg.Name) { + case 'GetToggleReportOptions': { + if (!window.__playwright.messages.GetToggleReportOptions) + throw new Error('missing `GetToggleReportOptions` on messages'); + responseData = structuredClone(window.__playwright.messages.GetToggleReportOptions); + responseData.id = String(arg.Id); + } + } + + if (responseData) { + setTimeout(() => { + for (const listener of window.__playwright.listeners || []) { + listener({ + data: responseData, + }); + } + }, 0); + } }; } catch (e) { console.error("❌couldn't set up mocks"); @@ -82,16 +102,13 @@ export function windowsMockApis() { /** * @param {object} params - * @param {Partial>} params.responses + * @param {Partial>} params.messages */ -export function webkitMockApis({ responses = {} }) { - const merged = { - ...responses, - }; +export function webkitMockApis({ messages = {} }) { try { window.__playwright = { - messages: {}, - responses: merged, + messages, + responses: {}, mocks: { outgoing: [], incoming: [], @@ -174,7 +191,7 @@ export function webkitMockApis({ responses = {} }) { postMessage: (arg) => { window.__playwright.mocks.outgoing.push(['privacyDashboardGetToggleReportOptions', arg]); setTimeout(() => { - window.onGetToggleReportOptionsResponse?.(window.__playwright.responses.privacyDashboardGetToggleReportOptions); + window.onGetToggleReportOptionsResponse?.(window.__playwright.messages.privacyDashboardGetToggleReportOptions); }, 0); }, }, @@ -186,10 +203,14 @@ export function webkitMockApis({ responses = {} }) { } } -export function mockAndroidApis() { +/** + * @param {object} params + * @param {Partial>} params.messages + */ +export function mockAndroidApis({ messages = {} }) { try { window.__playwright = { - messages: {}, + messages, responses: {}, mocks: { outgoing: [], @@ -216,6 +237,26 @@ export function mockAndroidApis() { submitBrokenSiteReport(arg) { window.__playwright.mocks.outgoing.push(['submitBrokenSiteReport', arg]); }, + getToggleReportOptions() { + const response = window.__playwright.messages.getToggleReportOptions; + if (!response) throw new Error('unreachable, missing mock for getToggleReportOptions'); + + setTimeout(() => { + window.onGetToggleReportOptionsResponse?.(response); + }, 0); + }, + sendToggleReport() { + window.__playwright.mocks.outgoing.push(['sendToggleReport']); + }, + rejectToggleReport() { + window.__playwright.mocks.outgoing.push(['rejectToggleReport']); + }, + seeWhatIsSent() { + window.__playwright.mocks.outgoing.push(['seeWhatIsSent']); + }, + showNativeFeedback() { + window.__playwright.mocks.outgoing.push(['showNativeFeedback']); + }, }; } catch (e) { console.error("❌couldn't set up mocks"); @@ -239,6 +280,7 @@ export function mockBrowserApis(params = { messages: {} }) { sendToggleReport: {}, rejectToggleReport: {}, seeWhatIsSent: {}, + showNativeFeedback: {}, doBurn: {}, getBurnOptions: { clearHistory: true, tabClearEnabled: true, pinnedTabs: 2 }, refreshAlias: { privateAddress: '__mock__', personalAddress: 'dax' }, @@ -340,7 +382,7 @@ export function mockBrowserApis(params = { messages: {} }) { * @param {import("../../ui/platform-features.mjs").Platform} platform * @return {Promise} */ -export async function installMocks(platform) { +export async function installDebuggerMocks(platform) { console.log('instaling...'); if (window.__playwright) { console.log('instaling... NOE'); @@ -350,12 +392,16 @@ export async function installMocks(platform) { windowsMockApis(); } else if (platform.name === 'ios' || platform.name === 'macos') { webkitMockApis({ - responses: { + messages: { privacyDashboardGetToggleReportOptions: toggleReportScreen, }, }); } else if (platform.name === 'android') { - mockAndroidApis(); + mockAndroidApis({ + messages: { + getToggleReportOptions: toggleReportScreen, + }, + }); } else if (platform.name === 'browser') { mockBrowserApis(); } @@ -390,6 +436,7 @@ export async function installMocks(platform) { } if (platform.name === 'windows') { messages.windowsViewModel = mock.toWindowsViewModel(); + messages.GetToggleReportOptions = mock.toWindowsToggleReportOptions(); } await mockDataProvider({ diff --git a/shared/js/browser/windows-communication.js b/shared/js/browser/windows-communication.js index 0e0c3598..8d3960d3 100644 --- a/shared/js/browser/windows-communication.js +++ b/shared/js/browser/windows-communication.js @@ -29,7 +29,11 @@ * @category integrations */ import { z } from 'zod'; -import { windowsIncomingViewModelSchema, windowsIncomingVisibilitySchema } from '../../../schema/__generated__/schema.parsers.mjs'; +import { + windowsIncomingViewModelSchema, + windowsIncomingVisibilitySchema, + windowsIncomingToggleReportOptionsSchema, +} from '../../../schema/__generated__/schema.parsers.mjs'; import { setupGlobalOpenerListener } from '../ui/views/utils/utils'; import { assert, @@ -41,6 +45,11 @@ import { setupMutationObserver, SubmitBrokenSiteReportMessage, UpdatePermissionMessage, + FetchToggleReportOptions, + RejectToggleBreakageReport, + SeeWhatIsSent, + SendToggleBreakageReport, + ShowNativeFeedback, } from './common.js'; import { createTabData } from './utils/request-details.mjs'; @@ -120,19 +129,54 @@ function handleViewModelUpdate(viewModel) { // ----------------------------------------------------------------------------- -function windowsPostMessage(name, data) { +/** + * Posts a message to the Windows Browser + * + * @param {string} name - Message name + * @param {object} data - Message data + * @param {{ Id?: string, SubFeatureName?: string }} [options] - Optional message data + */ +function windowsPostMessage(name, data, options = {}) { assert(typeof globalThis.windowsInteropPostMessage === 'function'); - globalThis.windowsInteropPostMessage({ + + const outgoing = { Feature: 'PrivacyDashboard', Name: name, Data: data, - }); + ...options, + }; + + globalThis.windowsInteropPostMessage(outgoing); } /** * @type {import("./common.js").fetcher} */ async function fetch(message) { + if (message instanceof FetchToggleReportOptions) { + return getToggleReportOptions(); + } + + if (message instanceof RejectToggleBreakageReport) { + rejectToggleBreakageReport(); + return; + } + + if (message instanceof SeeWhatIsSent) { + seeWhatIsSent(); + return; + } + + if (message instanceof ShowNativeFeedback) { + showNativeFeedback(); + return; + } + + if (message instanceof SendToggleBreakageReport) { + sendToggleBreakageReport(); + return; + } + if (message instanceof SubmitBrokenSiteReportMessage) { SubmitBrokenSiteReport({ category: message.category, @@ -352,6 +396,7 @@ export function handleIncomingMessage(message) { } case 'ViewModelUpdated': { handleViewModelUpdate(parsed.data.Data); + break; } } } @@ -364,7 +409,7 @@ export function setup() { setupColorScheme(); assert(typeof globalThis.windowsInteropAddEventListener === 'function', 'globalThis.windowsInteropAddEventListener required'); globalThis.windowsInteropAddEventListener('message', (event) => { - handleIncomingMessage(event.data); + if (event.data.Name) handleIncomingMessage(event.data); }); setupMutationObserver((height) => { SetSize({ height }); @@ -390,4 +435,55 @@ function firstRenderComplete() { } } +/** + * @return {Promise} + */ +function getToggleReportOptions() { + return new Promise((resolve) => { + const requestId = String(Math.random()); + windowsPostMessage('GetToggleReportOptions', {}, { Id: requestId, SubFeatureName: 'GetToggleReportOptions' }); + + /** + * @param event + */ + function handler(event) { + const response = event.data; + + // TODO: How to better filter out non-toggle report data + if (response.featureName !== 'GetToggleReportOptions') return; + + const parsed = windowsIncomingToggleReportOptionsSchema.safeParse(response); + if (!parsed.success) { + console.error('cannot handle incoming message from event data', response); + console.error(parsed.error); + return; + } + + if (parsed.data.id === requestId) { + window.chrome.webview?.removeEventListener?.('message', handler); + resolve(parsed.data.result); + } else { + console.warn('no match', parsed, requestId); + } + } + globalThis.windowsInteropAddEventListener('message', handler); + }); +} + +function sendToggleBreakageReport() { + windowsPostMessage('SendToggleBreakageReport', {}); +} + +function rejectToggleBreakageReport() { + windowsPostMessage('RejectToggleBreakageReport', {}); +} + +function seeWhatIsSent() { + windowsPostMessage('SeeWhatIsSent', {}); +} + +function showNativeFeedback() { + windowsPostMessage('ShowNativeFeedback', {}); +} + export { fetch, backgroundMessage, getBackgroundTabData, firstRenderComplete }; diff --git a/shared/js/ui/components/_button.scss b/shared/js/ui/components/_button.scss index 9ed27352..8a2a61d6 100644 --- a/shared/js/ui/components/_button.scss +++ b/shared/js/ui/components/_button.scss @@ -20,6 +20,44 @@ padding: 0px 12px; } + &[data-variant='desktop-standard'] { + background: rgba(0, 0, 0, 0.06); + border-radius: 6px; + color: rgba(0, 0, 0, 0.84); + + &:hover { + background: rgba(0, 0, 0, 0.09); + } + + &:active { + background: rgba(0, 0, 0, 0.12); + } + + &:disabled { + opacity: 0.8; + } + + .body--theme-dark & { + background: rgba(255, 255, 255, 0.12); + color: rgba(255, 255, 255, 0.84); + + &:hover { + background: rgba(255, 255, 255, 0.18); + } + + &:active { + background: rgba(255, 255, 255, 0.24); + } + } + + &[data-size='desktop-large'] { + font-size: 14px; + font-weight: 400; + height: 40px; + padding: 0 16px; + } + } + &[data-variant='desktop-vibrancy'] { border-radius: 6px; background: rgba(0, 0, 0, 0.1); @@ -87,6 +125,10 @@ > * { width: 100%; } + + .environment--android & { + gap: 8px; + } } } diff --git a/shared/js/ui/components/_stack.scss b/shared/js/ui/components/_stack.scss index 448057dd..5d99d2be 100644 --- a/shared/js/ui/components/_stack.scss +++ b/shared/js/ui/components/_stack.scss @@ -46,13 +46,50 @@ //} } +.data-list__content { + font-size: 14px; + font-weight: 400; + line-height: calc(20 / 14); + + .environment--android & { + font-size: 14px; + font-weight: 400; + line-height: calc(18 / 14); + } + + .environment--ios &, + .environment--macos & { + font-size: 13px; + font-weight: 400; + line-height: calc(18 / 13); + } + + &--breakage { + .environment--android &, + .environment--browser &, + .environment--ios &, + .environment--macos &, + .environment--windows & { + font-size: 13px; + line-height: calc(18 / 13); + } + } +} + +.data-list__heading { + font-weight: 600; + + .environment--android & { + font-weight: 700; + } +} + .data-list { list-style: none; } .data-list__item { word-wrap: anywhere; color: var(--color-text-secondary); - line-height: 18px; position: relative; padding-left: 20px; diff --git a/shared/js/ui/components/button.jsx b/shared/js/ui/components/button.jsx index 3b035c98..b15ddc7c 100644 --- a/shared/js/ui/components/button.jsx +++ b/shared/js/ui/components/button.jsx @@ -3,8 +3,8 @@ import { h } from 'preact'; /** * @typedef {object} ComponentProps - * @property {"desktop-vibrancy" | "ios-secondary"} [variant] - * @property {"big" | "desktop-large"} [btnSize] + * @property {"desktop-vibrancy" | "desktop-standard" | "ios-secondary" | "macos-standard"} [variant] + * @property {"big" | "desktop-large" | "small"} [btnSize] * @param {import("preact").ComponentProps<'button'> & ComponentProps} props */ export function Button({ children, btnSize, variant = 'desktop-vibrancy', ...rest }) { diff --git a/shared/js/ui/components/stack.jsx b/shared/js/ui/components/stack.jsx index 47558e5b..418ffca7 100644 --- a/shared/js/ui/components/stack.jsx +++ b/shared/js/ui/components/stack.jsx @@ -1,13 +1,16 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars import { h } from 'preact'; +import cn from 'classnames'; + /** * @typedef {object} ButtonBarComponentProps * @property {string} [gap] + * @property {string} [className] * @param {import("preact").ComponentProps<'div'> & ButtonBarComponentProps} props */ -export function Stack({ children, gap, ...rest }) { +export function Stack({ children, gap, className, ...rest }) { return ( -
+
{children}
); diff --git a/shared/js/ui/components/toggle-report.jsx b/shared/js/ui/components/toggle-report.jsx index cd8ab42c..11b2f572 100644 --- a/shared/js/ui/components/toggle-report.jsx +++ b/shared/js/ui/components/toggle-report.jsx @@ -1,6 +1,7 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars import { h } from 'preact'; import { ns } from '../base/localize'; +import cn from 'classnames'; import { PlainTextLink } from './text-link'; import { Button, ButtonBar } from './button'; import { platform } from '../../browser/communication'; @@ -15,7 +16,25 @@ import { ToggleReportWrapper } from './toggle-report/toggle-report-wrapper'; import { ToggleReportTitle } from './toggle-report/toggle-report-title'; import { getContentHeight, setupMutationObserverForExtensions } from '../../browser/common'; +/** + * @param {object} props + * @param {'standard'|'sent'} [props.variant='standard'] + * @returns + */ +export function ToggleReportIcon({ variant = 'standard' }) { + const classes = cn({ + 'hero-icon--toggle-report': variant === 'standard', + 'hero-icon--toggle-report-sent': variant === 'sent', + 'large-icon-container': platform.name === 'browser' || platform.name === 'windows', + 'medium-icon-container': platform.name === 'macos' || platform.name === 'ios' || platform.name === 'android', + }); + + return
; +} + export function ToggleReport() { + const mobile = platform.name === 'android' || platform.name === 'ios'; + const innerGap = mobile ? '24px' : '16px'; const desktop = platform.name === 'macos' || platform.name === 'windows'; const extension = platform.name === 'browser'; @@ -42,44 +61,44 @@ export function ToggleReport() { return ( {extension && } -
- -
- {ns.toggleReport('siteNotWorkingTitle.title')} -
-

{ns.toggleReport('siteNotWorkingSubTitle.title')}

- dispatch('toggle')} /> -
+ + + + + {ns.toggleReport('siteNotWorkingTitle.title')} + +

{ns.toggleReport('siteNotWorkingSubTitle.title')}

+ dispatch('toggle')} /> +
+
+ {state.value === 'showing' && ( + + + + )} + dispatch('send')} reject={() => dispatch('reject')} />
-
- {state.value === 'showing' && ( -
- - - -
- )} -
- dispatch('send')} reject={() => dispatch('reject')} /> -
+
); } - if (platform.name === 'ios') { + if (mobile) { return ( - -
+ + {ns.toggleReport('siteNotWorkingTitle.title')}

{ns.toggleReport('siteNotWorkingSubTitle.title')}

dispatch('send')} reject={() => dispatch('reject')} /> - {state.value !== 'showing' && dispatch('toggle-ios')} />} + {state.value !== 'showing' && ( + dispatch(platform.name === 'ios' ? 'toggle-ios' : 'toggle')} /> + )}
{state.value === 'showing' && (
@@ -127,17 +146,34 @@ function SetAutoHeight() { return null; } +/** + * @typedef {object} ButtonStyle + * @property {import('./button').ComponentProps['variant']} variant + * @property {import('./button').ComponentProps['btnSize']} size + * @property {import('./button').ButtonBarComponentProps['layout']} layout + * @returns {ButtonStyle} + */ +function getButtonStyles() { + switch (platform.name) { + case 'ios': + case 'android': + return { variant: 'ios-secondary', size: 'big', layout: 'vertical' }; + case 'windows': + return { variant: 'desktop-standard', size: 'desktop-large', layout: 'horizontal' }; + default: + return { variant: 'desktop-vibrancy', size: 'desktop-large', layout: 'horizontal' }; + } +} + function ToggleReportButtons({ send, reject }) { - const buttonVariant = platform.name === 'ios' ? 'ios-secondary' : 'desktop-vibrancy'; - const buttonLayout = platform.name === 'ios' ? 'vertical' : 'horizontal'; - const buttonSize = platform.name === 'ios' ? 'big' : 'desktop-large'; + const { variant, size, layout } = getButtonStyles(); return ( - - - @@ -155,13 +191,12 @@ function RevealText({ toggle }) { } function DesktopRevealText({ state, toggle }) { + if (state.value === 'showing') return null; + return (

- - {state.value === 'hiding' && ns.toggleReport('siteNotWorkingInfoReveal.title')} - {state.value === 'showing' && ns.toggleReport('siteNotWorkingInfoHide.title')} - + {ns.toggleReport('siteNotWorkingInfoReveal.title')}

); diff --git a/shared/js/ui/components/toggle-report/toggle-report-data-list.jsx b/shared/js/ui/components/toggle-report/toggle-report-data-list.jsx index 7f96e47d..f37c67a6 100644 --- a/shared/js/ui/components/toggle-report/toggle-report-data-list.jsx +++ b/shared/js/ui/components/toggle-report/toggle-report-data-list.jsx @@ -6,14 +6,14 @@ import { namedString } from '../../../../data/text'; export function ToggleReportDataList({ rows }) { return ( - -

{ns.toggleReport('reportsNoInfoSent.title')}

+ +

{ns.toggleReport('reportsNoInfoSent.title')}

    {rows.map((item) => { const string = namedString(item); const additional = item.id === 'siteUrl' ? '[' + item.additional?.url + ']' : null; return ( -
  • +
  • ` }}> {string} {additional && {additional}} diff --git a/shared/js/ui/components/toggle-report/toggle-report-sent.jsx b/shared/js/ui/components/toggle-report/toggle-report-sent.jsx index 999e08a8..162ccff6 100644 --- a/shared/js/ui/components/toggle-report/toggle-report-sent.jsx +++ b/shared/js/ui/components/toggle-report/toggle-report-sent.jsx @@ -2,14 +2,15 @@ import { h } from 'preact'; import { Stack } from '../stack'; import { ns } from '../../base/localize'; +import { ToggleReportIcon } from '../toggle-report'; export function ToggleReportSent({ onClick }) { return (
    -
    +

    {ns.report('thankYou.title')}

    -

    {ns.toggleReport('yourReportWillHelpToggleReport.title')}

    +

    {ns.toggleReport('yourReportWillHelpToggleReport.title')}

    ); diff --git a/shared/js/ui/components/toggle-report/toggle-report-title.jsx b/shared/js/ui/components/toggle-report/toggle-report-title.jsx index 06435a92..9ef392a5 100644 --- a/shared/js/ui/components/toggle-report/toggle-report-title.jsx +++ b/shared/js/ui/components/toggle-report/toggle-report-title.jsx @@ -5,9 +5,11 @@ import { platform } from '../../../browser/communication'; export function ToggleReportTitle({ children }) { switch (platform.name) { case 'android': + return

    {children}

    ; case 'ios': return

    {children}

    ; case 'windows': + return

    {children}

    ; case 'browser': case 'macos': return

    {children}

    ; diff --git a/shared/js/ui/components/toggle-report/toggle-report-wrapper.jsx b/shared/js/ui/components/toggle-report/toggle-report-wrapper.jsx index f0ed7acb..06cc9479 100644 --- a/shared/js/ui/components/toggle-report/toggle-report-wrapper.jsx +++ b/shared/js/ui/components/toggle-report/toggle-report-wrapper.jsx @@ -5,6 +5,7 @@ import { platform } from '../../../browser/communication'; export function ToggleReportWrapper({ children, state }) { switch (platform.name) { case 'android': + return
    {children}
    ; case 'ios': return (
    diff --git a/shared/js/ui/components/toggle-report/toggle-report.scss b/shared/js/ui/components/toggle-report/toggle-report.scss index 9011aa53..e8fc6166 100644 --- a/shared/js/ui/components/toggle-report/toggle-report.scss +++ b/shared/js/ui/components/toggle-report/toggle-report.scss @@ -7,11 +7,13 @@ } } -.environment--ios [data-toggle-report='child'] { +.environment--ios [data-toggle-report='child'], +.environment--android [data-toggle-report='child'] { opacity: 0; visibility: hidden; } -.environment--ios [data-toggle-report='child'][data-ready='true'] { +.environment--ios [data-toggle-report='child'][data-ready='true'], +.environment--android [data-toggle-report='child'][data-ready='true'] { opacity: 1; visibility: visible; } diff --git a/shared/js/ui/components/toggle-report/use-ios-animation.js b/shared/js/ui/components/toggle-report/use-ios-animation.js index 4a40cac9..1e7fb5cb 100644 --- a/shared/js/ui/components/toggle-report/use-ios-animation.js +++ b/shared/js/ui/components/toggle-report/use-ios-animation.js @@ -13,7 +13,7 @@ import { useToggleReportState } from './use-toggle-report-state'; */ export function useIosAnimation(state, dispatch) { useEffect(() => { - if (platform.name !== 'ios') return; + if (platform.name !== 'ios' && platform.name !== 'android') return; if (state.value === 'animating') { const child = /** @type {HTMLDivElement | null} */ (document.querySelector('[data-toggle-report="child"]')); if (!child) return; @@ -25,7 +25,7 @@ export function useIosAnimation(state, dispatch) { }, [state.value]); useEffect(() => { - if (platform.name !== 'ios') return; + if (platform.name !== 'ios' && platform.name !== 'android') return; const child = /** @type {HTMLDivElement | null} */ (document.querySelector('[data-toggle-report="child"]')); const parent = /** @type {HTMLDivElement | null} */ (document.querySelector('[data-toggle-report="parent"]')); diff --git a/shared/js/ui/views/tests/generate-data.mjs b/shared/js/ui/views/tests/generate-data.mjs index 9e0050ae..e9ca1e64 100644 --- a/shared/js/ui/views/tests/generate-data.mjs +++ b/shared/js/ui/views/tests/generate-data.mjs @@ -1,5 +1,6 @@ import { Protections } from '../../../browser/utils/protections.mjs'; import { protectionsOff } from './toggle-protections.mjs'; +import toggleReportScreen from '../../../../../schema/__fixtures__/toggle-report-screen.json'; /** * @typedef {import('../../../browser/utils/request-details.mjs').TabData} TabData @@ -336,6 +337,18 @@ export class MockData { }; } + /** + * @return {import('../../../../../schema/__generated__/schema.types').WindowsIncomingToggleReportOptions} + */ + toWindowsToggleReportOptions() { + return { + context: 'PrivacyDashboard', + featureName: 'GetToggleReportOptions', + id: '0.1', + result: /** @type {import('../../../../../schema/__generated__/schema.types').ToggleReportScreen} */ (toggleReportScreen), + }; + } + /** * @return {import('../../../../../schema/__generated__/schema.types').GetPrivacyDashboardData} */ diff --git a/shared/locales/en/toggle-report.json b/shared/locales/en/toggle-report.json index 77549a67..653cae89 100644 --- a/shared/locales/en/toggle-report.json +++ b/shared/locales/en/toggle-report.json @@ -44,7 +44,7 @@ "note": "This text provides information about various types of requests, including blocked trackers and ignored requests." }, "dynamic_features": { - "title": "List of which protections and browser features were active", + "title": "List of which browser features were active", "note": "This text represents a list of active protections and browser features." }, "dynamic_appVersion": { @@ -110,5 +110,13 @@ "dynamic_jsPerformance": { "title": "How quickly parts of the page loaded", "note": "This text provides information on the performance of the page's JavaScript, indicating how quickly different parts of the page loaded." + }, + "dynamic_locale": { + "title": "Primary language and country of your device", + "note": "This text provides information on the user's locale settings." + }, + "dynamic_description": { + "title": "Your selected category and optional comments", + "note": "This text provides information on the user's selected category and comments." } } diff --git a/shared/scss/base/_tokens.scss b/shared/scss/base/_tokens.scss index 55fa4c0d..c8b7bdcd 100644 --- a/shared/scss/base/_tokens.scss +++ b/shared/scss/base/_tokens.scss @@ -39,6 +39,10 @@ } } +.token-body { + @include token-body; +} + .token-body-em { //styleName: Mac/Body (Emphasis); font-size: 13px; @@ -104,7 +108,7 @@ .token-ios-title-3 { font-size: 20px; font-weight: 600; - line-height: 25px; + line-height: calc(25 / 20); } .token-title-3-em { @@ -118,6 +122,13 @@ line-height: 20px; } + //styleName: Windows/System/Title 3 (Emphasis) + .environment--windows { + font-size: 16px; + font-weight: 600; + line-height: calc(20 / 16); + } + .environment--browser & { font-family: $font__face--browser-alt; font-style: normal; @@ -154,6 +165,13 @@ } } +.token-headline-2 { + //styleName: Android/Headline 2 + font-size: 20px; + font-weight: 500; + line-height: calc(24 / 20); +} + .token-label-em { font-size: 13px; font-weight: 600; diff --git a/shared/scss/base/base.scss b/shared/scss/base/base.scss index ce1fde0a..97487ad7 100644 --- a/shared/scss/base/base.scss +++ b/shared/scss/base/base.scss @@ -90,6 +90,14 @@ textarea { font-weight: bold; } +strong { + font-weight: bold; + + .environment--browser & { + font-weight: 600; + } +} + /* Links */ a { color: var(--color-accent-blue); @@ -200,6 +208,11 @@ a { text-align: left; } +/* Text Wrap */ +.text--balance { + text-wrap: balance; +} + /* Borders */ .border--top { border-top: 1px solid var(--color-lines-light); @@ -236,6 +249,11 @@ a { padding-bottom: 16px; } +.padding-y-third { + padding-top: 24px; + padding-bottom: 24px; +} + .padding-y--reduced { padding-top: 14px; padding-bottom: 14px; diff --git a/shared/scss/views/_hero.scss b/shared/scss/views/_hero.scss index 2a3c28bf..88509f9a 100644 --- a/shared/scss/views/_hero.scss +++ b/shared/scss/views/_hero.scss @@ -14,11 +14,6 @@ background-repeat: no-repeat; margin-bottom: 16px; background-size: contain; - - // todo(Shane): check SVG dimensions to prevent this - &.hero-icon--toggle-report-sent { - height: 62px; - } } .hero-icon--breakage-form { diff --git a/shared/scss/views/_main-nav.scss b/shared/scss/views/_main-nav.scss index 8f12eb1e..8e8025b0 100644 --- a/shared/scss/views/_main-nav.scss +++ b/shared/scss/views/_main-nav.scss @@ -109,6 +109,23 @@ margin-right: 10px; // when text wraps on this buttons (for different languages) this prevents the layout looking broken line-height: 1; + + /** TODO: Move */ + .environment--ios & { + line-height: calc(20 / 15); + } + + .environment--android & { + line-height: calc(20 / 14); + } + + .environment--macos & { + line-height: calc(16 / 13); + } + + .environment--windows & { + line-height: calc(18 / 14); + } } .main-nav__chev { diff --git a/shared/scss/views/_top-nav.scss b/shared/scss/views/_top-nav.scss index c34b5f80..87756566 100644 --- a/shared/scss/views/_top-nav.scss +++ b/shared/scss/views/_top-nav.scss @@ -40,6 +40,7 @@ body { // rounded corners on android because of the ripple effect .environment--android & { border-radius: 100%; + left: 10px; } .environment--ios & { @@ -146,7 +147,8 @@ body { .environment--android & { transform: unset; - left: 60px; + left: 72px; + top: 2px; font-size: 20px; font-weight: 500; &:active { diff --git a/types.ts b/types.ts index 452d00b9..511ef073 100644 --- a/types.ts +++ b/types.ts @@ -60,6 +60,11 @@ interface Window { openInNewTab: (payload: string) => void; openSettings: (payload: string) => void; submitBrokenSiteReport: (payload: string) => void; + getToggleReportOptions: () => void; + sendToggleReport: () => void; + rejectToggleReport: () => void; + seeWhatIsSent: () => void; + showNativeFeedback: () => void; }; /** * This is set in Playwright tests diff --git a/v2/navigation.jsx b/v2/navigation.jsx index ae3d4e57..a3144dcd 100644 --- a/v2/navigation.jsx +++ b/v2/navigation.jsx @@ -343,7 +343,7 @@ export function Navigation(props) { if (item.kind === 'root') { return ( -
    +
    {item.component()}
    diff --git a/v2/screens/toggle-report-screen.jsx b/v2/screens/toggle-report-screen.jsx index 6947eb8a..ad0d7cb6 100644 --- a/v2/screens/toggle-report-screen.jsx +++ b/v2/screens/toggle-report-screen.jsx @@ -1,6 +1,7 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars import { h } from 'preact'; import { useClose, useConnectionCount, useFeatures, useFetcher } from '../data-provider'; +import { useToggleReportState } from '../../shared/js/ui/components/toggle-report/use-toggle-report-state'; import { ToggleReportProvider } from '../../shared/js/ui/components/toggle-report/toggle-report-provider'; import { ToggleReport } from '../../shared/js/ui/components/toggle-report'; import { useEffect } from 'preact/hooks'; @@ -10,7 +11,6 @@ import { platformSwitch } from '../../shared/js/ui/environment-check'; export function ToggleReportScreen() { const fetcher = useFetcher(); const features = useFeatures(); - const onClose = useClose(); const { count } = useConnectionCount(); const connectionId = `connection-${count}`; @@ -21,6 +21,27 @@ export function ToggleReportScreen() { }; }, []); + return ( + + + + + + ); +} + +export function ToggleReportWrapper({ children }) { + const features = useFeatures(); + const onClose = useClose(); + const [_, dispatch] = useToggleReportState(); + + useEffect(() => { + document.body.dataset.screen = 'toggleReport'; + return () => { + document.body.dataset.screen = ''; + }; + }, []); + const done = platformSwitch({ ios: () => , macos: () => , @@ -28,18 +49,14 @@ export function ToggleReportScreen() { }); const back = platformSwitch({ - android: () => , + android: () => dispatch('reject')} />, default: () => null, }); return (
    - {features.opener === 'menu' ? : } -
    - - - -
    + {features.opener === 'menu' ? : } +
    {children}
    ); }