diff --git a/apps/remix-ide-e2e/src/tests/matomo.test.ts b/apps/remix-ide-e2e/src/tests/matomo.test.ts new file mode 100644 index 0000000000..85369366bc --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/matomo.test.ts @@ -0,0 +1,492 @@ +'use strict' +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' + +import examples from '../examples/example-contracts' + +const sources = [ + { 'Untitled.sol': { content: examples.ballot.content } } +] + +module.exports = { + '@disabled': true, + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done, 'http://127.0.0.1:8080', false) + }, + 'confirm Matomo #group1': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser + .execute(function () { + localStorage.removeItem('config-v0.8:.remix.config') + localStorage.setItem('showMatomo', 'true') + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .execute(function () { + return (window as any)._paq + }, [], (res) => { + console.log('_paq', res) + }) + .waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .pause(1000) + .click('[data-id="matomoModal-modal-footer-ok-react"]') // submitted + .execute(function () { + return (window as any)._paq + }, [], (res) => { + console.log('_paq', res) + }) + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .waitForElementVisible('*[data-id="beginnerbtn"]', 10000) + .pause(1000) + .click('[data-id="beginnerbtn"]') + .waitForElementNotPresent('*[data-id="beginnerbtn"]') + .waitForElementVisible({ + selector: `//*[contains(text(), 'Welcome to Remix IDE')]`, + locateStrategy: 'xpath' + }) + .refreshPage() + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]') + .clickLaunchIcon('settings') + .verify.elementPresent('[id="settingsMatomoAnalytics"]:checked') + .execute(function () { + return JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics'] == true + }, [], (res) => { + console.log('res', res) + browser.assert.ok((res as any).value, 'matomo analytics is enabled') + }) + }, + 'decline Matomo #group1': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + localStorage.removeItem('config-v0.8:.remix.config') + localStorage.setItem('showMatomo', 'true') + localStorage.removeItem('matomo-analytics-consent') + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .execute(function () { + return (window as any)._paq + }, [], (res) => { + console.log('_paq', res) + }) + .waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel + .execute(function () { + return (window as any)._paq + }, [], (res) => { + console.log('_paq', res) + }) + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .pause(2000) + .waitForElementNotPresent('*[data-id="beginnerbtn"]', 10000) + .clickLaunchIcon('settings') + .waitForElementNotPresent('[id="settingsMatomoAnalytics"]:checked') + .execute(function () { + return JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics'] == false + }, [], (res) => { + console.log('res', res) + browser.assert.ok((res as any).value, 'matomo analytics is disabled') + }) + }, + 'blur matomo #group2': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + localStorage.removeItem('config-v0.8:.remix.config') + localStorage.setItem('showMatomo', 'true') + localStorage.removeItem('matomo-analytics-consent') + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .waitForElementVisible({ + selector: '*[data-id="matomoModalModalDialogModalBody-react"]', + abortOnFailure: true + }) + .waitForElementVisible('*[data-id="matomoModal-modal-close"]') + .click('[data-id="matomoModal-modal-close"]') + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .pause(2000) + .waitForElementNotPresent('*[data-id="beginnerbtn"]', 10000) + .clickLaunchIcon('settings') + .waitForElementNotPresent('[id="settingsMatomoAnalytics"]:checked') + .execute(function () { + return JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics'] == undefined + }, [], (res) => { + console.log('res', res) + browser.assert.ok((res as any).value, 'matomo analytics is undefined') + }) + }, + 'matomo should reappear #group2': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .waitForElementVisible({ + selector: '*[data-id="matomoModalModalDialogModalBody-react"]', + abortOnFailure: true + }) + .waitForElementVisible('*[data-id="matomoModal-modal-close"]') + .click('[data-id="matomoModal-modal-close"]') + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + }, + 'change settings #group2': function (browser: NightwatchBrowser) { + browser + .clickLaunchIcon('settings') + .waitForElementVisible('*[data-id="label-matomo-settings"]') + .pause(1000) + .click('*[data-id="label-matomo-settings"]') + .refreshPage() + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]') + }, + 'should get enter dialog again #group2': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="beginnerbtn"]', 10000) + .pause(1000) + .click('[data-id="beginnerbtn"]') + .waitForElementNotPresent('*[data-id="beginnerbtn"]') + .waitForElementVisible({ + selector: `//*[contains(text(), 'Welcome to Remix IDE')]`, + locateStrategy: 'xpath' + }) + .waitForElementVisible('*[id="remixTourSkipbtn"]') + .click('*[id="remixTourSkipbtn"]') + .clickLaunchIcon('settings') + .waitForElementPresent('[id="settingsMatomoAnalytics"]:checked') + .execute(function () { + return JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics'] == true + }, [], (res) => { + console.log('res', res) + browser.assert.ok((res as any).value, 'matomo analytics is enabled') + }) + }, + 'decline Matomo and check timestamp #group3': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + localStorage.removeItem('config-v0.8:.remix.config') + localStorage.setItem('showMatomo', 'true') + localStorage.removeItem('matomo-analytics-consent') + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + // output the contents of the storage + .execute(function () { + return { + consent: window.localStorage.getItem('matomo-analytics-consent'), + config: window.localStorage.getItem('config-v0.8:.remix.config'), + showMatomo: window.localStorage.getItem('showMatomo') + } + }, [], (res) => { + console.log('res', res) + }) + .waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .pause(2000) + .execute(function () { + + const timestamp = window.localStorage.getItem('matomo-analytics-consent'); + if (timestamp) { + + const consentDate = new Date(Number(timestamp)); + // validate it is actually a date + if (isNaN(consentDate.getTime())) { + return false; + } + const now = new Date(); + console.log('timestamp', timestamp, consentDate, now.getTime()) + const diffInMinutes = (now.getTime() - consentDate.getTime()) / (1000 * 60); + console.log('diffInMinutes', diffInMinutes) + return diffInMinutes < 2; + } + return false; + + }, [], (res) => { + console.log('res', res) + browser.assert.ok((res as any).value, 'matomo analytics consent timestamp is set') + }) + }, + 'check old timestamp and reappear Matomo #group3': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + const oldTimestamp = new Date() + oldTimestamp.setMonth(oldTimestamp.getMonth() - 7) + localStorage.setItem('matomo-analytics-consent', oldTimestamp.getTime().toString()) + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .execute(function () { + + const timestamp = window.localStorage.getItem('matomo-analytics-consent'); + if (timestamp) { + + const consentDate = new Date(Number(timestamp)); + // validate it is actually a date + if (isNaN(consentDate.getTime())) { + return false; + } + // validate it's older than 6 months + const now = new Date(); + const diffInMonths = (now.getFullYear() - consentDate.getFullYear()) * 12 + now.getMonth() - consentDate.getMonth(); + console.log('timestamp', timestamp, consentDate, now.getTime()) + console.log('diffInMonths', diffInMonths) + return diffInMonths > 6; + } + return false; + + }, [], (res) => { + console.log('res', res) + browser.assert.ok((res as any).value, 'matomo analytics consent timestamp is set') + }) + .waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + }, + 'check recent timestamp and do not reappear Matomo #group3': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + const recentTimestamp = new Date() + recentTimestamp.setMonth(recentTimestamp.getMonth() - 1) + localStorage.setItem('matomo-analytics-consent', recentTimestamp.getTime().toString()) + }, []) + .refreshPage() + .perform(done()) + }) + // check if timestamp is younger than 6 months + .execute(function () { + + const timestamp = window.localStorage.getItem('matomo-analytics-consent'); + if (timestamp) { + + const consentDate = new Date(Number(timestamp)); + // validate it is actually a date + if (isNaN(consentDate.getTime())) { + return false; + } + // validate it's younger than 2 months + const now = new Date(); + const diffInMonths = (now.getFullYear() - consentDate.getFullYear()) * 12 + now.getMonth() - consentDate.getMonth(); + console.log('timestamp', timestamp, consentDate, now.getTime()) + console.log('diffInMonths', diffInMonths) + return diffInMonths < 2; + } + return false; + + }, [], (res) => { + console.log('res', res) + browser.assert.ok((res as any).value, 'matomo analytics consent timestamp is set') + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .pause(2000) + .waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]') + }, + 'accept Matomo and check timestamp #group3': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + localStorage.removeItem('config-v0.8:.remix.config') + localStorage.setItem('showMatomo', 'true') + localStorage.removeItem('matomo-analytics-consent') + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .click('[data-id="matomoModal-modal-footer-ok-react"]') // accept + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .pause(2000) + .execute(function () { + + const timestamp = window.localStorage.getItem('matomo-analytics-consent'); + if (timestamp) { + + const consentDate = new Date(Number(timestamp)); + // validate it is actually a date + if (isNaN(consentDate.getTime())) { + return false; + } + const now = new Date(); + console.log('timestamp', timestamp, consentDate, now.getTime()) + const diffInMinutes = (now.getTime() - consentDate.getTime()) / (1000 * 60); + console.log('diffInMinutes', diffInMinutes) + return diffInMinutes < 1; + } + return false; + + }, [], (res) => { + console.log('res', res) + browser.assert.ok((res as any).value, 'matomo analytics consent timestamp is to a recent date') + }) + }, + 'check old timestamp and do not reappear Matomo after accept #group3': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + const oldTimestamp = new Date() + oldTimestamp.setMonth(oldTimestamp.getMonth() - 7) + localStorage.setItem('matomo-analytics-consent', oldTimestamp.getTime().toString()) + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .pause(2000) + .waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]') + }, + 'check recent timestamp and do not reappear Matomo after accept #group3': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + const recentTimestamp = new Date() + recentTimestamp.setMonth(recentTimestamp.getMonth() - 1) + localStorage.setItem('matomo-analytics-consent', recentTimestamp.getTime().toString()) + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementPresent({ + selector: `//*[@data-id='compilerloaded']`, + locateStrategy: 'xpath', + timeout: 120000 + }) + .pause(2000) + .waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]') + }, + 'when there is a recent timestamp but no config the dialog should reappear #group3': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + localStorage.removeItem('config-v0.8:.remix.config') + const recentTimestamp = new Date() + recentTimestamp.setMonth(recentTimestamp.getMonth() - 1) + localStorage.setItem('matomo-analytics-consent', recentTimestamp.getTime().toString()) + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + }, + 'when there is a old timestamp but no config the dialog should reappear #group3': function (browser: NightwatchBrowser) { + browser.perform((done) => { + browser.execute(function () { + localStorage.removeItem('config-v0.8:.remix.config') + const oldTimestamp = new Date() + oldTimestamp.setMonth(oldTimestamp.getMonth() - 7) + localStorage.setItem('matomo-analytics-consent', oldTimestamp.getTime().toString()) + }, []) + .refreshPage() + .perform(done()) + }) + .waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + .click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel + .waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]') + }, + 'verify Matomo events are tracked on app start #group4 #lfaky': function (browser: NightwatchBrowser) { + browser + .execute(function () { + return (window as any)._paq + }, [], (res) => { + const expectedEvents = [ + ["trackEvent", "Preload", "start"], + ["trackEvent", "Storage", "activate", "indexedDB"], + ["trackEvent", "App", "load"], + ]; + + const actualEvents = (res as any).value; + + const areEventsPresent = expectedEvents.every(expectedEvent => + actualEvents.some(actualEvent => + JSON.stringify(actualEvent) === JSON.stringify(expectedEvent) + ) + ); + + browser.assert.ok(areEventsPresent, 'Matomo events are tracked correctly'); + }) + }, + + '@sources': function () { + return sources + }, + 'Add Ballot #group4': function (browser: NightwatchBrowser) { + browser + .addFile('Untitled.sol', sources[0]['Untitled.sol']) + }, + 'Deploy Ballot #group4': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) + .clickLaunchIcon('solidity') + .waitForElementVisible('*[data-id="compilerContainerCompileBtn"]') + .click('*[data-id="compilerContainerCompileBtn"]') + .testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot']) + }, + 'verify Matomo compiler events are tracked #group4': function (browser: NightwatchBrowser) { + browser + .execute(function () { + return (window as any)._paq + }, [], (res) => { + const expectedEvent = ["trackEvent", "compiler", "compiled"]; + const actualEvents = (res as any).value; + + const isEventPresent = actualEvents.some(actualEvent => + actualEvent[0] === expectedEvent[0] && + actualEvent[1] === expectedEvent[1] && + actualEvent[2] === expectedEvent[2] && + actualEvent[3].startsWith("with_version_") + ); + + browser.assert.ok(isEventPresent, 'Matomo compiler events are tracked correctly'); + }) + }, +} \ No newline at end of file diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 4c490aaf34..76dcbd5cd9 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -178,16 +178,30 @@ class AppComponent { '6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod': 35 // remix desktop } + _paq.push(['trackEvent', 'App', 'load']); this.matomoConfAlreadySet = Registry.getInstance().get('config').api.exists('settings/matomo-analytics') this.matomoCurrentSetting = Registry.getInstance().get('config').api.get('settings/matomo-analytics') - let electronTracking = false - - if (window.electronAPI) { - electronTracking = await window.electronAPI.canTrackMatomo() - } - - this.showMatamo = (matomoDomains[window.location.hostname] || electronTracking) && !this.matomoConfAlreadySet + let electronTracking = window.electronAPI ? await window.electronAPI.canTrackMatomo() : false + + const lastMatomoCheck = window.localStorage.getItem('matomo-analytics-consent') + const sixMonthsAgo = new Date(); + sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); + + this.showMatomo = + (matomoDomains[window.location.hostname] || electronTracking + || (window.localStorage.getItem('showMatomo') + && window.localStorage.getItem('showMatomo') === 'true')) + && (!this.matomoConfAlreadySet + || (this.matomoCurrentSetting === false + && (!lastMatomoCheck || new Date(Number(lastMatomoCheck)) < sixMonthsAgo) + )); + + if(this.matomoCurrentSetting === false + && (!lastMatomoCheck || new Date(Number(lastMatomoCheck)) < sixMonthsAgo)) { + _paq.push(['trackEvent', 'Matomo', 'refreshMatomoPermissions']); + } + this.walkthroughService = new WalkthroughService(appManager) diff --git a/apps/remix-ide/src/app/components/preload.tsx b/apps/remix-ide/src/app/components/preload.tsx index 091d4686ef..18e7665e0a 100644 --- a/apps/remix-ide/src/app/components/preload.tsx +++ b/apps/remix-ide/src/app/components/preload.tsx @@ -10,6 +10,8 @@ import './styles/preload.css' import isElectron from 'is-electron' const _paq = (window._paq = window._paq || []) +_paq.push(['trackEvent', 'Preload', 'start']) + export const Preload = (props: any) => { const [tip, setTip] = useState('') const [supported, setSupported] = useState(true) diff --git a/apps/remix-ide/src/app/tabs/settings-tab.tsx b/apps/remix-ide/src/app/tabs/settings-tab.tsx index 3878b29e89..08557e3a31 100644 --- a/apps/remix-ide/src/app/tabs/settings-tab.tsx +++ b/apps/remix-ide/src/app/tabs/settings-tab.tsx @@ -109,6 +109,8 @@ module.exports = class SettingsTab extends ViewPlugin { updateMatomoAnalyticsChoice(isChecked) { this.config.set('settings/matomo-analytics', isChecked) + // set timestamp to local storage to track when the user has given consent + localStorage.setItem('matomo-analytics-consent', Date.now().toString()) this.useMatomoAnalytics = isChecked if (!isChecked) { // revoke tracking consent diff --git a/apps/remix-ide/src/assets/js/loader.js b/apps/remix-ide/src/assets/js/loader.js index 44a7687830..92146c7204 100644 --- a/apps/remix-ide/src/assets/js/loader.js +++ b/apps/remix-ide/src/assets/js/loader.js @@ -18,14 +18,26 @@ function trackDomain(domainToTrack) { _paq.push(['trackPageView']); _paq.push(['enableLinkTracking']); _paq.push(['enableHeartBeatTimer']); - if (!window.localStorage.getItem('config-v0.8:.remix.config') || - (window.localStorage.getItem('config-v0.8:.remix.config') && !window.localStorage.getItem('config-v0.8:.remix.config').includes('settings/matomo-analytics'))) { + const remixConfig = window.localStorage.getItem('config-v0.8:.remix.config'); + if (!remixConfig || (remixConfig && !remixConfig.includes('settings/matomo-analytics'))) { // require user tracking consent before processing data _paq.push(['requireConsent']); } else { - // user has given consent to process their data - _paq.push(['setConsentGiven']) + try { + const config = JSON.parse(remixConfig); + if (config['settings/matomo-analytics'] === true) { + // user has given consent to process their data + _paq.push(['setConsentGiven']); + } else { + // user has not given consent to process their data + _paq.push(['requireConsent']); + } + } catch (e) { + console.error('Error parsing remix config:', e); + _paq.push(['requireConsent']); + } } + _paq.push(['trackEvent', 'loader', 'load']); (function () { var u = "https://ethereumfoundation.matomo.cloud/"; _paq.push(['setTrackerUrl', u + 'matomo.php?debug=1']); diff --git a/libs/remix-ui/app/src/index.ts b/libs/remix-ui/app/src/index.ts index b18c36cb9a..31dea89385 100644 --- a/libs/remix-ui/app/src/index.ts +++ b/libs/remix-ui/app/src/index.ts @@ -3,5 +3,5 @@ export { dispatchModalContext, dispatchModalInterface, AppContext, appProviderCo export { ModalProvider, useDialogDispatchers } from './lib/remix-app/context/provider' export { AppModal } from './lib/remix-app/interface/index' export { AlertModal } from './lib/remix-app/interface/index' -export { ModalTypes } from './lib/remix-app/types/index' +export { ModalTypes, AppModalCancelTypes } from './lib/remix-app/types/index' export { AppAction, appActionTypes } from './lib/remix-app/actions/app' diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx index bdd57d8b11..32e130507c 100644 --- a/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx @@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react' import { FormattedMessage } from 'react-intl' import { AppContext } from '../../context/context' import { useDialogDispatchers } from '../../context/provider' +import { AppModalCancelTypes } from '../../types' declare global { interface Window { _paq: any @@ -15,7 +16,7 @@ interface MatomoDialogProps { } const MatomoDialog = (props: MatomoDialogProps) => { - const { settings, showMatamo, appManager } = useContext(AppContext) + const { settings, showMatomo, appManager } = useContext(AppContext) const { modal } = useDialogDispatchers() const [visible, setVisible] = useState(props.hide) @@ -63,7 +64,7 @@ const MatomoDialog = (props: MatomoDialogProps) => { } useEffect(() => { - if (visible && showMatamo) { + if (visible && showMatomo) { modal({ id: 'matomoModal', title: , @@ -72,18 +73,22 @@ const MatomoDialog = (props: MatomoDialogProps) => { okFn: handleModalOkClick, cancelLabel: , cancelFn: declineModal, + preventBlur: true }) } }, [visible]) - const declineModal = async () => { - settings.updateMatomoAnalyticsChoice(false) - // revoke tracking consent - _paq.push(['forgetConsentGiven']) - setVisible(false) + const declineModal = async (reason: AppModalCancelTypes) => { + if (reason === AppModalCancelTypes.click || reason === AppModalCancelTypes.enter) { + settings.updateMatomoAnalyticsChoice(false) + // revoke tracking consent + _paq.push(['forgetConsentGiven']) + setVisible(false) + } } const handleModalOkClick = async () => { + // user has given consent to process their data _paq.push(['setConsentGiven']) settings.updateMatomoAnalyticsChoice(true) diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx index d0ae6026b2..5349eb2eed 100644 --- a/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react' import { ModalDialog, ModalDialogProps, ValidationResult } from '@remix-ui/modal-dialog' -import { ModalTypes } from '../../types' +import { AppModalCancelTypes, ModalTypes } from '../../types' interface ModalWrapperProps extends ModalDialogProps { modalType?: ModalTypes @@ -42,8 +42,8 @@ const ModalWrapper = (props: ModalWrapperProps) => { props.okFn ? props.okFn(data.current) : props.resolve(data.current || true) } - const onCancelFn = async () => { - props.cancelFn ? props.cancelFn() : props.resolve(false) + const onCancelFn = async (reason?: AppModalCancelTypes) => { + props.cancelFn ? props.cancelFn(reason) : props.resolve(false) } const onInputChanged = (event) => { @@ -142,6 +142,8 @@ const ModalWrapper = (props: ModalWrapperProps) => { props.handleHide() } + if (!props.id || props.id === '') return null + return } export default ModalWrapper diff --git a/libs/remix-ui/app/src/lib/remix-app/context/context.tsx b/libs/remix-ui/app/src/lib/remix-app/context/context.tsx index 8f3fd119ed..48aa1b391e 100644 --- a/libs/remix-ui/app/src/lib/remix-app/context/context.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/context/context.tsx @@ -5,7 +5,7 @@ import { AppAction } from '../actions/app' export type appProviderContextType = { settings: any, - showMatamo: boolean, + showMatomo: boolean, showEnter: boolean, appManager: any modal: any diff --git a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx index c6dc3d057b..23e5add367 100644 --- a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx @@ -23,7 +23,8 @@ export const ModalProvider = ({ children = [], reducer = modalReducer, initialSt } const modal = (modalData: AppModal) => { - const { id, title, message, validationFn, okLabel, okFn, cancelLabel, cancelFn, modalType, modalParentClass, defaultValue, hideFn, data } = modalData + const { id, title, message, validationFn, okLabel, okFn, cancelLabel, cancelFn, modalType, modalParentClass, defaultValue, hideFn, data, preventBlur } = modalData + console.log('modalData', modalData) return new Promise((resolve, reject) => { dispatch({ type: modalActionTypes.setModal, @@ -42,7 +43,8 @@ export const ModalProvider = ({ children = [], reducer = modalReducer, initialSt hideFn, resolve, next: onNextFn, - data + data, + preventBlur } }) }) diff --git a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts index b1cee75257..adefcf2a2b 100644 --- a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts +++ b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts @@ -1,5 +1,5 @@ import { GitHubUser } from '@remix-api' -import { ModalTypes } from '../types' +import { AppModalCancelTypes, ModalTypes } from '../types' export type ValidationResult = { valid: boolean, @@ -17,14 +17,15 @@ export interface AppModal { okLabel: string | JSX.Element okFn?: (value?:any) => void cancelLabel?: string | JSX.Element - cancelFn?: () => void, + cancelFn?: (reason?: AppModalCancelTypes) => void, modalType?: ModalTypes, modalParentClass?: string defaultValue?: string hideFn?: () => void, resolve?: (value?:any) => void, next?: () => void, - data?: any + data?: any, + preventBlur?: boolean } export interface AlertModal { diff --git a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts index a24c8af6aa..2d20142deb 100644 --- a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts +++ b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts @@ -23,7 +23,8 @@ export const modalReducer = (state: ModalState = ModalInitialState, action: Moda hideFn: action.payload.hideFn, resolve: action.payload.resolve, next: action.payload.next, - data: action.payload.data + data: action.payload.data, + preventBlur: action.payload.preventBlur } const modalList: AppModal[] = state.modals.slice() diff --git a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx index 805f8f3407..2e4101eef6 100644 --- a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx @@ -60,7 +60,7 @@ const RemixApp = (props: IRemixAppUi) => { activateApp() } const hadUsageTypeAsked = localStorage.getItem('hadUsageTypeAsked') - if (props.app.showMatamo) { + if (props.app.showMatomo) { // if matomo dialog is displayed, it will take care of calling "setShowEnterDialog", // if the user approves matomo tracking. // so "showEnterDialog" stays false @@ -149,7 +149,7 @@ const RemixApp = (props: IRemixAppUi) => { const value: appProviderContextType = { settings: props.app.settings, - showMatamo: props.app.showMatamo, + showMatomo: props.app.showMatomo, appManager: props.app.appManager, showEnter: props.app.showEnter, modal: props.app.notification, diff --git a/libs/remix-ui/app/src/lib/remix-app/types/index.ts b/libs/remix-ui/app/src/lib/remix-app/types/index.ts index 217d6ea5c0..dce9885100 100644 --- a/libs/remix-ui/app/src/lib/remix-app/types/index.ts +++ b/libs/remix-ui/app/src/lib/remix-app/types/index.ts @@ -8,6 +8,15 @@ export const enum ModalTypes { forceChoice = 'forceChoice' } +export const enum AppModalCancelTypes { + close = 'close', + cancel = 'cancel', + blur = 'blur', + escape = 'escape', + enter = 'enter', + click = 'click' +} + export const enum UsageTypes { Beginner = 1, Prototyper, diff --git a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx index fbeaafe439..ee891c3abd 100644 --- a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx +++ b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx @@ -2,6 +2,7 @@ import React, {useRef, useState, useEffect} from 'react' // eslint-disable-line import {ModalDialogProps} from './types' // eslint-disable-line import './remix-ui-modal-dialog.css' +import { AppModalCancelTypes } from '@remix-ui/app' declare global { // eslint-disable-next-line no-unused-vars @@ -24,12 +25,14 @@ export const ModalDialog = (props: ModalDialogProps) => { } useEffect(() => { + if (!props.id) return calledHideFunctionOnce.current = props.hide - modal.current.focus() - - if (modal.current) { + if (!props.hide) { + modal.current.focus() modal.current.removeEventListener('blur', handleBlur) - modal.current.addEventListener('blur', handleBlur) + if (modal.current && !props.preventBlur) { + modal.current.addEventListener('blur', handleBlur) + } } return () => { modal.current && modal.current.removeEventListener('blur', handleBlur) @@ -37,11 +40,11 @@ export const ModalDialog = (props: ModalDialogProps) => { }, [props.hide]) function handleBlur(e) { - if (!e.currentTarget.contains(e.relatedTarget)) { + if (e.currentTarget && !e.currentTarget.contains(e.relatedTarget)) { e.stopPropagation() if (document.activeElement !== this) { !window.testmode && handleHide() - !window.testmode && props.cancelFn && props.cancelFn() + !window.testmode && props.cancelFn && props.cancelFn(AppModalCancelTypes.blur) } } } @@ -49,7 +52,7 @@ export const ModalDialog = (props: ModalDialogProps) => { const modalKeyEvent = (keyCode) => { if (keyCode === 27) { // Esc - if (props.cancelFn) props.cancelFn() + if (props.cancelFn) props.cancelFn(AppModalCancelTypes.escape) handleHide() } else if (keyCode === 13) { // Enter @@ -71,7 +74,7 @@ export const ModalDialog = (props: ModalDialogProps) => { if (state.toggleBtn) { if (props.okFn) props.okFn() } else { - if (props.cancelFn) props.cancelFn() + if (props.cancelFn) props.cancelFn(AppModalCancelTypes.enter) } handleHide() } @@ -99,7 +102,7 @@ export const ModalDialog = (props: ModalDialogProps) => { {props.title && props.title} {!props.showCancelIcon && ( - handleHide()}> + handleHide()}> )} @@ -130,7 +133,7 @@ export const ModalDialog = (props: ModalDialogProps) => { className={'modal-cancel btn btn-sm ' + (props.cancelBtnClass ? props.cancelBtnClass : state.toggleBtn ? 'border-secondary' : 'border-primary')} data-dismiss="modal" onClick={() => { - if (props.cancelFn) props.cancelFn() + if (props.cancelFn) props.cancelFn(AppModalCancelTypes.click) handleHide() }} > diff --git a/libs/remix-ui/modal-dialog/src/lib/types/index.ts b/libs/remix-ui/modal-dialog/src/lib/types/index.ts index 8c7442a3fc..90c18ad2fe 100644 --- a/libs/remix-ui/modal-dialog/src/lib/types/index.ts +++ b/libs/remix-ui/modal-dialog/src/lib/types/index.ts @@ -1,3 +1,5 @@ +import { AppModalCancelTypes } from "@remix-ui/app" + export type ValidationResult = { valid: boolean, message?: string @@ -15,7 +17,7 @@ export interface ModalDialogProps { okFn?: (value?:any) => void, donotHideOnOkClick?: boolean, cancelLabel?: string | JSX.Element, - cancelFn?: () => void, + cancelFn?: (reason?: AppModalCancelTypes) => void, modalClass?: string, modalParentClass?: string showCancelIcon?: boolean, @@ -26,5 +28,6 @@ export interface ModalDialogProps { next?: () => void, data?: any, okBtnClass?: string, - cancelBtnClass?: string + cancelBtnClass?: string, + preventBlur?: boolean } diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx index f77700b94e..6f0fe3f3eb 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx @@ -315,7 +315,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
-