diff --git a/client/cypress/e2e/_2makePendingSnapshot.cy.ts b/client/cypress/e2e/_2makePendingSnapshot.cy.ts index 5a08ab5ac0..9f23bc0da3 100644 --- a/client/cypress/e2e/_2makePendingSnapshot.cy.ts +++ b/client/cypress/e2e/_2makePendingSnapshot.cy.ts @@ -1,6 +1,10 @@ import { mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import { mutateAsyncMakeSnapshot } from 'cypress/utils/moveTime'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; // In E2E snapshotter is disabled. Before the first test can be run, pending snapshot needs to be done. @@ -19,6 +23,7 @@ describe('Make pending snapshot', () => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.playground.absolute); }); diff --git a/client/cypress/e2e/allocationItemWindowClosed.cy.ts b/client/cypress/e2e/allocationItemWindowClosed.cy.ts index d5ed48ace9..4d4cdba844 100644 --- a/client/cypress/e2e/allocationItemWindowClosed.cy.ts +++ b/client/cypress/e2e/allocationItemWindowClosed.cy.ts @@ -10,6 +10,7 @@ import viewports from 'cypress/utils/viewports'; import { QUERY_KEYS } from 'src/api/queryKeys'; import { ALLOCATION_ITEMS_KEY, + HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, } from 'src/constants/localStorageKeys'; @@ -57,6 +58,7 @@ describe('allocation (allocation window closed)', () => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); visitWithLoader(ROOT_ROUTES.projects.absolute); diff --git a/client/cypress/e2e/allocationItemWindowOpen.cy.ts b/client/cypress/e2e/allocationItemWindowOpen.cy.ts index 520565fc10..d736738a88 100644 --- a/client/cypress/e2e/allocationItemWindowOpen.cy.ts +++ b/client/cypress/e2e/allocationItemWindowOpen.cy.ts @@ -13,6 +13,7 @@ import viewports from 'cypress/utils/viewports'; import { QUERY_KEYS } from 'src/api/queryKeys'; import { ALLOCATION_ITEMS_KEY, + HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, } from 'src/constants/localStorageKeys'; @@ -64,6 +65,7 @@ describe('allocation (allocation window open)', () => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); visitWithLoader(ROOT_ROUTES.projects.absolute); connectWallet(true, false); diff --git a/client/cypress/e2e/allocationRewardsBox.cy.ts b/client/cypress/e2e/allocationRewardsBox.cy.ts index 1259263a5e..e23bc56041 100644 --- a/client/cypress/e2e/allocationRewardsBox.cy.ts +++ b/client/cypress/e2e/allocationRewardsBox.cy.ts @@ -3,7 +3,11 @@ import chaiColors from 'chai-colors'; import { visitWithLoader, mockCoinPricesServer, connectWallet } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; chai.use(chaiColors); @@ -16,6 +20,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.allocation.absolute); }); @@ -108,6 +113,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.allocation.absolute); cy.intercept('GET', '/rewards/budget/*/epoch/*', { body: { budget: '10000000000' } }); connectWallet(true, false); diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts index c3bb07dc3a..756cf8cf63 100644 --- a/client/cypress/e2e/earn.cy.ts +++ b/client/cypress/e2e/earn.cy.ts @@ -1,19 +1,13 @@ -import { visitWithLoader, mockCoinPricesServer } from 'cypress/utils/e2e'; +import { visitWithLoader, mockCoinPricesServer, connectWallet } from 'cypress/utils/e2e'; import { moveTime } from 'cypress/utils/moveTime'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; -import Chainable = Cypress.Chainable; - -const connectWallet = (): Chainable => { - cy.intercept('GET', '/user/*/tos', { body: { accepted: true } }); - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); - cy.acceptMetamaskAccess(); - return cy.switchToCypressWindow(); -}; - Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }, idx) => { describe(`earn: ${device}`, { viewportHeight, viewportWidth }, () => { before(() => { @@ -30,6 +24,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.earn.absolute); }); @@ -73,18 +68,18 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes } it('Wallet connected: "Lock GLM" / "Edit Locked GLM" button is active', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').should('not.be.disabled'); }); it('Wallet connected: "Lock GLM" / "Edit Locked GLM" button opens "ModalGlmLock"', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').click(); cy.get('[data-test=ModalGlmLock]').should('be.visible'); }); it('Wallet connected: "ModalGlmLock" has overflow', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').click(); cy.get('[data-test=ModalGlmLock__overflow]').should('exist'); }); @@ -94,7 +89,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes * In EarnGlmLock there are multiple autofocus rules set. * This test checks if user is still able to type without any autofocus disruption. */ - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').click(); cy.get('[data-test=ModalGlmLock]').should('be.visible'); cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.focus'); @@ -106,7 +101,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Wallet connected: "ModalGlmLock" - changing tabs keep focus on first input', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').click(); cy.get('[data-test=ModalGlmLock]').should('be.visible'); cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.focus'); @@ -117,7 +112,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Wallet connected: Lock 1 GLM', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') .invoke('text') @@ -169,7 +164,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Wallet connected: Unlock 1 GLM', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') .invoke('text') @@ -216,7 +211,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') .invoke('text') diff --git a/client/cypress/e2e/layout.cy.ts b/client/cypress/e2e/layout.cy.ts index 9ea9954d24..57f7fbccda 100644 --- a/client/cypress/e2e/layout.cy.ts +++ b/client/cypress/e2e/layout.cy.ts @@ -1,6 +1,10 @@ import { navigateWithCheck, mockCoinPricesServer } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { navigationTabs } from 'src/constants/navigationTabs/navigationTabs'; import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; @@ -16,6 +20,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => cy.disconnectMetamaskWalletFromAllDapps(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); cy.visit(ROOT.absolute); }); diff --git a/client/cypress/e2e/metrics.cy.ts b/client/cypress/e2e/metrics.cy.ts index 829fbfbf1c..411dac359e 100644 --- a/client/cypress/e2e/metrics.cy.ts +++ b/client/cypress/e2e/metrics.cy.ts @@ -1,6 +1,10 @@ import { mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { @@ -18,6 +22,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.metrics.absolute); }); diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts deleted file mode 100644 index 3dec91c7b6..0000000000 --- a/client/cypress/e2e/onboarding.cy.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { visitWithLoader, navigateWithCheck, mockCoinPricesServer } from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { getStepsDecisionWindowClosed } from 'src/hooks/helpers/useOnboardingSteps/steps'; -import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -import Chainable = Cypress.Chainable; - -const connectWallet = ( - isTOSAccepted: boolean, - shouldVisit = true, - shouldReload = false, -): Chainable => { - cy.intercept('GET', '/user/*/tos', { body: { accepted: isTOSAccepted } }); - cy.disconnectMetamaskWalletFromAllDapps(); - if (shouldVisit) { - visitWithLoader(ROOT.absolute, ROOT_ROUTES.projects.absolute); - } - if (shouldReload) { - cy.reload(); - } - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); - cy.switchToMetamaskNotification(); - return cy.acceptMetamaskAccess(); -}; - -const beforeSetup = () => { - mockCoinPricesServer(); - cy.clearLocalStorage(); - cy.setupMetamask(); - window.innerWidth = Cypress.config().viewportWidth; - window.innerHeight = Cypress.config().viewportHeight; -}; - -const checkCurrentElement = (el: number, isCurrent: boolean): Chainable => { - return cy - .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(el) - .invoke('attr', 'data-iscurrent') - .should('eq', `${isCurrent}`); -}; - -const checkProgressStepperSlimIsCurrentAndClickNext = (index, isCurrent = true): Chainable => { - checkCurrentElement(index - 1, isCurrent); - return cy - .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(index) - .click({ force: true }); -}; - -const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { el: 1, key: 'ArrowRight' }, - { el: 2, key: 'ArrowRight' }, - // { el: 3, key: 'ArrowRight' }, - // { el: 3, key: 'ArrowRight' }, - // { el: 2, key: 'ArrowLeft' }, - { el: 1, key: 'ArrowLeft' }, - { el: 0, key: 'ArrowLeft' }, - { el: 0, key: 'ArrowLeft' }, - ].forEach(({ key, el }) => { - cy.get('body').trigger('keydown', { key }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - cy.get('[data-test=ModalOnboarding]').then(element => { - const leftEdgeX = element.offsetParent().offset()?.left as number; - const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; - - [ - { clientX: rightEdgeX - 25, el: 1 }, - { clientX: rightEdgeX - 10, el: 2 }, - // { clientX: rightEdgeX - 5, el: 3 }, - // rightEdgeX === browser right frame - // { clientX: rightEdgeX - 1, el: 3 }, - // { clientX: leftEdgeX + 25, el: 2 }, - { clientX: leftEdgeX + 10, el: 1 }, - { clientX: leftEdgeX + 5, el: 0 }, - { clientX: leftEdgeX, el: 0 }, - ].forEach(({ clientX, el }) => { - cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); - }); -}; - -const checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - cy.get('[data-test=ModalOnboarding]').then(element => { - const leftEdgeX = element.offsetParent().offset()?.left as number; - const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; - - [ - { clientX: rightEdgeX - 25, el: 1 }, - { clientX: rightEdgeX - 26, el: 1 }, - { clientX: leftEdgeX + 26, el: 1 }, - { clientX: leftEdgeX + 25, el: 0 }, - ].forEach(({ clientX, el }) => { - cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); - }); -}; - -const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - // { - // el: 3, - // touchMoveClientX: window.innerWidth / 2 - 5, - // touchStartClientX: window.innerWidth / 2, - // }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { - cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { - touches: [{ clientX: touchStartClientX }], - }); - cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { - touches: [{ clientX: touchMoveClientX }], - }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 4, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 + 4, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { - cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { - touches: [{ clientX: touchStartClientX }], - }); - cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { - touches: [{ clientX: touchMoveClientX }], - }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - connectWallet(true); - }); - - it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < getStepsDecisionWindowClosed('2', '16 Jan').length - 1; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i); - } - - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(getStepsDecisionWindowClosed('2', '16 Jan').length - 1) - .click(); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); - }); - - it('user is able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); - }); - - it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders only once when "Always show Allocate onboarding" option is not checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('not.be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - - it('user can change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(true); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); - }); - - it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - connectWallet(true, false, true); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - }); -}); - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - cy.intercept( - { - method: 'POST', - url: '/user/*/tos', - }, - { body: { accepted: true }, statusCode: 200 }, - ); - connectWallet(false); - }); - - it('onboarding TOS step should be first and active', () => { - checkCurrentElement(0, true); - cy.get('[data-test=ModalOnboardingTOS]').should('be.visible'); - }); - - it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < getStepsDecisionWindowClosed('2', '16 Jan').length; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); - } - }); - - it('user is not able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders every time page is refreshed', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('user cannot change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(false); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); - }); - - it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); - }); - - it('TOS acceptance changes onboarding step to next step', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - }); - - it('TOS acceptance allows the user to close the modal by clicking button in the top-right', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - }); -}); diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts new file mode 100644 index 0000000000..88b42e9916 --- /dev/null +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -0,0 +1,198 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import chaiColors from 'chai-colors'; + +import { navigateWithCheck } from 'cypress/utils/e2e'; +import { moveTime, setupAndMoveToPlayground } from 'cypress/utils/moveTime'; +import { + beforeSetup, + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px, + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px, + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px, + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px, + checkChangeStepsWithArrowKeys, + checkProgressStepperSlimIsCurrentAndClickNext, + connectWallet, +} from 'cypress/utils/onboarding'; +import viewports from 'cypress/utils/viewports'; +import { QUERY_KEYS } from 'src/api/queryKeys'; +import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { getStepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; + +chai.use(chaiColors); + +describe('move time', () => { + before(() => { + /** + * Global Metamask setup done by Synpress is not always done. + * Since Synpress needs to have valid provider to fetch the data from contracts, + * setupMetamask is required in each test suite. + */ + cy.setupMetamask(); + }); + + it('allocation window is open, when it is not, move time', () => { + setupAndMoveToPlayground(); + + cy.window().then(async win => { + moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { + const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( + QUERY_KEYS.isDecisionWindowOpen, + ); + expect(isDecisionWindowOpenAfter).to.be.true; + }); + }); + }); +}); + +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { + describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { + before(() => { + beforeSetup(); + }); + + beforeEach(() => { + cy.clearLocalStorage(); + connectWallet(true); + }); + + it('user is able to click through entire onboarding flow', () => { + const onboardingSteps = getStepsDecisionWindowOpen('2', '16 Jan'); + + for (let i = 1; i < onboardingSteps.length - 1; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i); + } + + cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(onboardingSteps.length - 1) + .click(); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); + }); + + it('user is able to close the modal by clicking button in the top-right', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); + }); + + it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + navigateWithCheck(ROOT_ROUTES.settings.absolute); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('renders only once when "Always show Allocate onboarding" option is not checked', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + navigateWithCheck(ROOT_ROUTES.settings.absolute); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('not.be.checked'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + + it('user can change steps with arrow keys (left, right)', () => { + checkChangeStepsWithArrowKeys(true); + }); + + it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); + }); + + it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); + }); + + it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); + }); + + it('user is able to close the onboarding, and after page reload, onboarding does not show up again', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + + it('Onboarding stepper is visible after closing onboarding modal without going to the last step', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=OnboardingStepper]').should('be.visible'); + }); + + it('Onboarding stepper opens onboarding modal', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=OnboardingStepper]').click(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it(`Onboarding stepper is not visible if "${IS_ONBOARDING_DONE}" is set to "true"`, () => { + localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=OnboardingStepper]').should('not.exist'); + }); + + if (isDesktop) { + it(`Onboarding stepper has tooltip`, () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=OnboardingStepper]').trigger('mouseover'); + cy.get('[data-test=OnboardingStepper__Tooltip__content]').should('be.visible'); + cy.get('[data-test=OnboardingStepper__Tooltip__content]') + .invoke('text') + .should('eq', 'Reopen onboarding'); + }); + } + + it('Onboarding stepper has right amount of steps and highlights correct amount of passed steps', () => { + const onboardingSteps = getStepsDecisionWindowOpen('2', '16 Jan'); + + cy.get('[data-test=ModalOnboarding__Button]').click(); + + cy.get(`[data-test*=OnboardingStepper__circle]`).should( + 'have.length', + onboardingSteps.length, + ); + + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(1); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(2); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(3); + cy.get('[data-test=ModalOnboarding__Button]').click(); + + cy.get('[data-test=OnboardingStepper]').should('not.exist'); + }); + }); +}); diff --git a/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts b/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts new file mode 100644 index 0000000000..87914fc9a5 --- /dev/null +++ b/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts @@ -0,0 +1,112 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import chaiColors from 'chai-colors'; + +import { + beforeSetup, + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px, + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px, + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px, + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px, + checkChangeStepsWithArrowKeys, + checkCurrentElement, + checkProgressStepperSlimIsCurrentAndClickNext, + connectWallet, +} from 'cypress/utils/onboarding'; +import viewports from 'cypress/utils/viewports'; +import { QUERY_KEYS } from 'src/api/queryKeys'; +import { + getStepsDecisionWindowClosed, + getStepsDecisionWindowOpen, +} from 'src/hooks/helpers/useOnboardingSteps/steps'; + +chai.use(chaiColors); + +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { + describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { + before(() => { + beforeSetup(); + }); + + beforeEach(() => { + cy.intercept( + { + method: 'POST', + url: '/user/*/tos', + }, + { body: { accepted: true }, statusCode: 200 }, + ); + connectWallet(false); + }); + + it('onboarding TOS step should be first and active', () => { + checkCurrentElement(0, true); + cy.get('[data-test=ModalOnboardingTOS]').should('be.visible'); + }); + + it('user is not able to click through entire onboarding flow', () => { + cy.window().then(win => { + const isDecisionWindowOpen = win.clientReactQuery.getQueryData( + QUERY_KEYS.isDecisionWindowOpen, + ); + + const onboardingSteps = isDecisionWindowOpen + ? getStepsDecisionWindowOpen('2', '16 Jan') + : getStepsDecisionWindowClosed('2', '16 Jan'); + + for (let i = 1; i < onboardingSteps.length; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); + } + }); + }); + + it('user is not able to close the modal by clicking button in the top-right', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('renders every time page is refreshed', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('user cannot change steps with arrow keys (left, right)', () => { + checkChangeStepsWithArrowKeys(false); + }); + + it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); + }); + + it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); + }); + + it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); + }); + + it('TOS acceptance changes onboarding step to next step', () => { + checkCurrentElement(0, true); + cy.get('[data-test=TOS_InputCheckbox]').check(); + cy.switchToMetamaskNotification(); + cy.confirmMetamaskSignatureRequest(); + checkCurrentElement(1, true); + }); + + it('TOS acceptance allows the user to close the modal by clicking button in the top-right', () => { + checkCurrentElement(0, true); + cy.get('[data-test=TOS_InputCheckbox]').check(); + cy.switchToMetamaskNotification(); + cy.confirmMetamaskSignatureRequest(); + checkCurrentElement(1, true); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + }); +}); diff --git a/client/cypress/e2e/patronMode.cy.ts b/client/cypress/e2e/patronMode.cy.ts index b5f3455c47..ce8445e8ff 100644 --- a/client/cypress/e2e/patronMode.cy.ts +++ b/client/cypress/e2e/patronMode.cy.ts @@ -1,6 +1,10 @@ import { connectWallet, mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { @@ -18,6 +22,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.settings.absolute); connectWallet(true, false); }); @@ -347,6 +352,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes beforeEach(() => { localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.settings.absolute); connectWallet(true, true); }); diff --git a/client/cypress/e2e/project.cy.ts b/client/cypress/e2e/project.cy.ts index 987d29ed04..ac2c8b226b 100644 --- a/client/cypress/e2e/project.cy.ts +++ b/client/cypress/e2e/project.cy.ts @@ -6,7 +6,7 @@ import { } from 'cypress/utils/e2e'; import { getNamesOfProjects } from 'cypress/utils/projects'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -45,6 +45,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); checkProjectsViewLoaded(); @@ -160,7 +161,6 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('entering project view shows Toast with info about IPFS failure when all providers fail', () => { - cy.get('[data-test=Toast--ipfsMessage').should('be.visible'); }); }); @@ -180,6 +180,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); connectWallet(true, true); checkProjectsViewLoaded(); diff --git a/client/cypress/e2e/projects.cy.ts b/client/cypress/e2e/projects.cy.ts index b78e736203..474d90042b 100644 --- a/client/cypress/e2e/projects.cy.ts +++ b/client/cypress/e2e/projects.cy.ts @@ -9,7 +9,7 @@ import { } from 'cypress/utils/e2e'; import { getNamesOfProjects } from 'cypress/utils/projects'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; import getMilestones from 'src/constants/milestones'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; @@ -120,6 +120,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); checkProjectsViewLoaded(); @@ -159,47 +160,48 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => cy.get('[data-test=AllocationItem]') .trigger('pointerdown') .trigger('pointermove', { pageX: x - 20 }) - .trigger('pointerup'); + .trigger('pointerup', { pageX: x - 40 }); + cy.wait(500); cy.get('[data-test=AllocationItem__removeButton]').should('be.visible'); cy.get('[data-test=AllocationItem__removeButton]').click(); cy.get('[data-test=AllocationItem__removeButton]').should('not.exist'); cy.get('[data-test=AllocationItem]').should('not.exist'); cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); }); + }); - it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { - const milestones = getMilestones(); - cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); - cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); - for (let i = 0; i < milestones.length; i++) { - if (milestones[i].href) { - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .within(() => { - cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( - 'be.visible', - ); - }); - - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .then(el => { - const { x } = el[0].getBoundingClientRect(); - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .trigger('mousedown') - .trigger('mouseup', { clientX: x + 10 }); - cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); - - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .trigger('mousedown') - .trigger('mouseup'); - cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); - }); - } + it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { + const milestones = getMilestones(); + cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); + cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); + for (let i = 0; i < milestones.length; i++) { + if (milestones[i].href) { + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .within(() => { + cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( + 'be.visible', + ); + }); + + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .then(el => { + const { x } = el[0].getBoundingClientRect(); + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .trigger('mousedown') + .trigger('mouseup', { clientX: x + 10 }); + cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); + + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .trigger('mousedown') + .trigger('mouseup'); + cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); + }); } - }); + } }); }); @@ -218,6 +220,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); connectWallet(true, true); checkProjectsViewLoaded(); diff --git a/client/cypress/e2e/projectsArchive.cy.ts b/client/cypress/e2e/projectsArchive.cy.ts index 17f5dbb0c4..be0fbb7611 100644 --- a/client/cypress/e2e/projectsArchive.cy.ts +++ b/client/cypress/e2e/projectsArchive.cy.ts @@ -6,7 +6,11 @@ import { import { moveTime } from 'cypress/utils/moveTime'; import viewports from 'cypress/utils/viewports'; import { QUERY_KEYS } from 'src/api/queryKeys'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; let wasEpochMoved = false; @@ -16,6 +20,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); }); diff --git a/client/cypress/e2e/rewardsCalculator.cy.ts b/client/cypress/e2e/rewardsCalculator.cy.ts index c3304b7640..c46cd46984 100644 --- a/client/cypress/e2e/rewardsCalculator.cy.ts +++ b/client/cypress/e2e/rewardsCalculator.cy.ts @@ -3,7 +3,11 @@ import chaiColors from 'chai-colors'; import { ETH_USD, GLM_USD, mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import getValueFiatToDisplay from 'src/utils/getValueFiatToDisplay'; import { parseUnitsBigInt } from 'src/utils/parseUnitsBigInt'; @@ -16,6 +20,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.earn.absolute); }); diff --git a/client/cypress/e2e/settings.cy.ts b/client/cypress/e2e/settings.cy.ts index 087b77329e..1217806db3 100644 --- a/client/cypress/e2e/settings.cy.ts +++ b/client/cypress/e2e/settings.cy.ts @@ -4,6 +4,7 @@ import { FIAT_CURRENCIES_SYMBOLS, DISPLAY_CURRENCIES } from 'src/constants/curre import { ARE_OCTANT_TIPS_ALWAYS_VISIBLE, DISPLAY_CURRENCY, + HAS_ONBOARDING_BEEN_CLOSED, IS_CRYPTO_MAIN_VALUE_DISPLAY, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, @@ -17,6 +18,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.settings.absolute); }); diff --git a/client/cypress/utils/e2e.ts b/client/cypress/utils/e2e.ts index 0e466d0c77..a06e4dac70 100644 --- a/client/cypress/utils/e2e.ts +++ b/client/cypress/utils/e2e.ts @@ -42,7 +42,9 @@ export const connectWallet = ( cy.intercept('GET', '/user/*/patron-mode', { body: { status: isPatronModeEnabled } }); cy.intercept('PATCH', '/user/*/patron-mode', { body: { status: !isPatronModeEnabled } }); cy.disconnectMetamaskWalletFromAllDapps(); + cy.wait(500); cy.get('[data-test=MainLayout__Button--connect]').click(); + cy.wait(500); cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); cy.switchToMetamaskNotification(); return cy.acceptMetamaskAccess(); diff --git a/client/cypress/utils/moveTime.ts b/client/cypress/utils/moveTime.ts index c5e810b3a1..95cca8e938 100644 --- a/client/cypress/utils/moveTime.ts +++ b/client/cypress/utils/moveTime.ts @@ -1,6 +1,7 @@ import { QUERY_KEYS } from 'src/api/queryKeys'; import { ALLOCATION_ITEMS_KEY, + HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, } from 'src/constants/localStorageKeys'; @@ -15,6 +16,7 @@ export const setupAndMoveToPlayground = (): Chainable => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); return visitWithLoader(ROOT_ROUTES.playground.absolute); }; @@ -105,6 +107,7 @@ export const moveTime = ( mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); visitWithLoader(ROOT_ROUTES.playground.absolute); } diff --git a/client/cypress/utils/onboarding.ts b/client/cypress/utils/onboarding.ts new file mode 100644 index 0000000000..dc55ab919d --- /dev/null +++ b/client/cypress/utils/onboarding.ts @@ -0,0 +1,217 @@ +import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; + +import { mockCoinPricesServer, visitWithLoader } from './e2e'; + +import Chainable = Cypress.Chainable; + +export const connectWallet = ( + isTOSAccepted: boolean, + shouldVisit = true, + shouldReload = false, +): Chainable => { + cy.intercept('GET', '/user/*/tos', { body: { accepted: isTOSAccepted } }); + cy.disconnectMetamaskWalletFromAllDapps(); + if (shouldVisit) { + visitWithLoader(ROOT.absolute, ROOT_ROUTES.projects.absolute); + } + if (shouldReload) { + cy.reload(); + } + cy.wait(500); + cy.get('[data-test=MainLayout__Button--connect]').click(); + cy.wait(500); + cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); + cy.switchToMetamaskNotification(); + return cy.acceptMetamaskAccess(); +}; + +export const beforeSetup = (): void => { + mockCoinPricesServer(); + cy.setupMetamask(); + window.innerWidth = Cypress.config().viewportWidth; + window.innerHeight = Cypress.config().viewportHeight; +}; + +export const checkCurrentElement = (el: number, isCurrent: boolean): Chainable => { + return cy + .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(el) + .invoke('attr', 'data-iscurrent') + .should('eq', `${isCurrent}`); +}; + +export const checkProgressStepperSlimIsCurrentAndClickNext = ( + index: number, + isCurrent = true, +): Chainable => { + checkCurrentElement(index - 1, isCurrent); + return cy + .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(index) + .click({ force: true }); +}; + +export const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean): void => { + checkCurrentElement(0, true); + + [ + { el: 1, key: 'ArrowRight' }, + { el: 2, key: 'ArrowRight' }, + { el: 1, key: 'ArrowLeft' }, + { el: 0, key: 'ArrowLeft' }, + { el: 0, key: 'ArrowLeft' }, + ].forEach(({ key, el }) => { + cy.get('body').trigger('keydown', { key }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +export const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolean): void => { + checkCurrentElement(0, true); + + cy.get('[data-test=ModalOnboarding]').then(element => { + const leftEdgeX = element.offsetParent().offset()?.left as number; + const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; + + [ + { clientX: rightEdgeX - 25, el: 1 }, + { clientX: rightEdgeX - 10, el: 2 }, + { clientX: leftEdgeX + 10, el: 1 }, + { clientX: leftEdgeX + 5, el: 0 }, + { clientX: leftEdgeX, el: 0 }, + ].forEach(({ clientX, el }) => { + cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); + }); +}; + +export const checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px = ( + isTOSAccepted: boolean, +): void => { + checkCurrentElement(0, true); + + cy.get('[data-test=ModalOnboarding]').then(element => { + const leftEdgeX = element.offsetParent().offset()?.left as number; + const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; + + [ + { clientX: rightEdgeX - 25, el: 1 }, + { clientX: rightEdgeX - 26, el: 1 }, + { clientX: leftEdgeX + 26, el: 1 }, + { clientX: leftEdgeX + 25, el: 0 }, + ].forEach(({ clientX, el }) => { + cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); + }); +}; + +export const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = ( + isTOSAccepted: boolean, +): void => { + checkCurrentElement(0, true); + + [ + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { + cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { + touches: [{ clientX: touchStartClientX }], + }); + cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { + touches: [{ clientX: touchMoveClientX }], + }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +export const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = ( + isTOSAccepted: boolean, +): void => { + checkCurrentElement(0, true); + + [ + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 4, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 + 4, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { + cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { + touches: [{ clientX: touchStartClientX }], + }); + cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { + touches: [{ clientX: touchMoveClientX }], + }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; diff --git a/client/public/images/slide.webp b/client/public/images/slide.webp new file mode 100644 index 0000000000..01a0c8533b Binary files /dev/null and b/client/public/images/slide.webp differ diff --git a/client/src/App.tsx b/client/src/App.tsx index 957b6dcd73..c9edde6a0e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,10 @@ +import { AnimatePresence } from 'framer-motion'; import React, { ReactElement, useState, Fragment } from 'react'; +import { useAccount } from 'wagmi'; import AppLoader from 'components/shared/AppLoader'; import ModalOnboarding from 'components/shared/ModalOnboarding/ModalOnboarding'; +import OnboardingStepper from 'components/shared/OnboardingStepper'; import useAppConnectManager from 'hooks/helpers/useAppConnectManager'; import useAppIsLoading from 'hooks/helpers/useAppIsLoading'; import useAppPopulateState from 'hooks/helpers/useAppPopulateState'; @@ -9,6 +12,7 @@ import useCypressHelpers from 'hooks/helpers/useCypressHelpers'; import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode'; import useManageTransactionsPending from 'hooks/helpers/useManageTransactionsPending'; import RootRoutes from 'routes/RootRoutes/RootRoutes'; +import useOnboardingStore from 'store/onboarding/store'; import 'react-toastify/dist/ReactToastify.css'; import 'styles/index.scss'; @@ -22,6 +26,11 @@ const App = (): ReactElement => { const { isSyncingInProgress } = useAppConnectManager(isFlushRequired, setIsFlushRequired); const isLoading = useAppIsLoading(isFlushRequired); const isProjectAdminMode = useIsProjectAdminMode(); + const { isConnected } = useAccount(); + const { isOnboardingDone, isOnboardingModalOpen } = useOnboardingStore(state => ({ + isOnboardingDone: state.data.isOnboardingDone, + isOnboardingModalOpen: state.data.isOnboardingModalOpen, + })); // useCypressHelpers needs to be called after all the initial sets done above. const { isFetching: isFetchingCypressHelpers } = useCypressHelpers(); @@ -34,6 +43,9 @@ const App = (): ReactElement => { {!isSyncingInProgress && !isProjectAdminMode && } + + {isConnected && !isOnboardingDone && !isOnboardingModalOpen && } + ); }; diff --git a/client/src/components/Allocation/AllocationItem/AllocationItem.tsx b/client/src/components/Allocation/AllocationItem/AllocationItem.tsx index 0601316df6..ad159b612f 100644 --- a/client/src/components/Allocation/AllocationItem/AllocationItem.tsx +++ b/client/src/components/Allocation/AllocationItem/AllocationItem.tsx @@ -204,12 +204,12 @@ const AllocationItem: FC = ({ animate( ref.current, // @ts-expect-error e is wrongly typed, doesn't see x property. - { x: e.x < startX ? constraints[0] : constraints[1] }, + { x: e.pageX < startX ? constraints[0] : constraints[1] }, { duration: 0.2 }, ); }} // @ts-expect-error e is wrongly typed, doesn't see x property. - onDragStart={e => setStartX(e.x)} + onDragStart={e => setStartX(e.pageX)} style={{ x }} > {(isLoading || isLoadingError) && } diff --git a/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx b/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx index 4630456d05..66f364eb1c 100644 --- a/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx +++ b/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx @@ -12,6 +12,7 @@ import useMediaQuery from 'hooks/helpers/useMediaQuery'; import useOnboardingSteps from 'hooks/helpers/useOnboardingSteps'; import useUserTOS from 'hooks/queries/useUserTOS'; import useOnboardingStore from 'store/onboarding/store'; +import useSettingsStore from 'store/settings/store'; import styles from './ModalOnboarding.module.scss'; @@ -25,33 +26,45 @@ const motionAnimationProps: AnimationProps = { const ModalOnboarding: FC = () => { const { isConnected } = useAccount(); const { data: isUserTOSAccepted } = useUserTOS(); - const { setIsOnboardingDone, isOnboardingDone } = useOnboardingStore(state => ({ + + const { + setIsOnboardingDone, + isOnboardingDone, + hasOnboardingBeenClosed, + lastSeenStep, + setLastSeenStep, + isOnboardingModalOpen, + setIsOnboardingModalOpen, + setHasOnboardingBeenClosed, + } = useOnboardingStore(state => ({ + hasOnboardingBeenClosed: state.data.hasOnboardingBeenClosed, isOnboardingDone: state.data.isOnboardingDone, + isOnboardingModalOpen: state.data.isOnboardingModalOpen, + lastSeenStep: state.data.lastSeenStep, + setHasOnboardingBeenClosed: state.setHasOnboardingBeenClosed, setIsOnboardingDone: state.setIsOnboardingDone, + setIsOnboardingModalOpen: state.setIsOnboardingModalOpen, + setLastSeenStep: state.setLastSeenStep, + })); + const { isAllocateOnboardingAlwaysVisible } = useSettingsStore(state => ({ + isAllocateOnboardingAlwaysVisible: state.data.isAllocateOnboardingAlwaysVisible, })); const { isDesktop } = useMediaQuery(); - const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(lastSeenStep - 1); const [isUserTOSAcceptedInitial] = useState(isUserTOSAccepted); const stepsToUse = useOnboardingSteps(isUserTOSAcceptedInitial); - useEffect(() => { - if (isUserTOSAccepted !== undefined && !isUserTOSAccepted) { - setIsOnboardingDone(false); - } - - if (!isUserTOSAcceptedInitial && isUserTOSAccepted) { - setCurrentStepIndex(prev => prev + 1); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setIsOnboardingDone, isUserTOSAccepted]); - const currentStep = stepsToUse.length > 0 ? stepsToUse[currentStepIndex] : null; const onOnboardingExit = useCallback(() => { if (!isUserTOSAccepted) { return; } - setIsOnboardingDone(true); + setIsOnboardingModalOpen(false); + if (!hasOnboardingBeenClosed) { + setHasOnboardingBeenClosed(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [setIsOnboardingDone, isUserTOSAccepted]); const [touchStart, setTouchStart] = useState(null); @@ -120,7 +133,7 @@ const ModalOnboarding: FC = () => { }; useEffect(() => { - if (isOnboardingDone) { + if (!isConnected || !isUserTOSAccepted) { return; } @@ -134,18 +147,50 @@ const ModalOnboarding: FC = () => { } }; - if (isUserTOSAccepted) { - window.addEventListener('keydown', listener); - } + window.addEventListener('keydown', listener); return () => { window.removeEventListener('keydown', listener); }; - }, [currentStepIndex, stepsToUse, isOnboardingDone, isUserTOSAccepted]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected, currentStepIndex, stepsToUse, isOnboardingDone, isUserTOSAccepted]); + + useEffect(() => { + if ( + isConnected && + (isAllocateOnboardingAlwaysVisible || !isUserTOSAccepted || !hasOnboardingBeenClosed) + ) { + setIsOnboardingModalOpen(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected]); + + useEffect(() => { + if (isOnboardingDone) { + return; + } + const stepNumber = currentStepIndex + 1; + setLastSeenStep(stepNumber); + if (stepNumber === stepsToUse.length) { + setIsOnboardingDone(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentStepIndex, isOnboardingDone]); - const isModalOpen = - (isConnected && !isUserTOSAccepted) || - (isConnected && !!isUserTOSAccepted && !isOnboardingDone); + useEffect(() => { + if (isOnboardingDone) { + return; + } + setCurrentStepIndex(lastSeenStep - 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!isUserTOSAcceptedInitial && isUserTOSAccepted) { + setCurrentStepIndex(prev => prev + 1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isUserTOSAccepted]); return ( { } isCloseButtonDisabled={!isUserTOSAccepted} - isOpen={isModalOpen} + isOpen={isOnboardingModalOpen} isOverflowOnClickDisabled={!isUserTOSAccepted} onClick={handleModalEdgeClick} onClosePanel={onOnboardingExit} diff --git a/client/src/components/shared/OnboardingStepper/OnboardingStepper.module.scss b/client/src/components/shared/OnboardingStepper/OnboardingStepper.module.scss new file mode 100644 index 0000000000..42ef8fd856 --- /dev/null +++ b/client/src/components/shared/OnboardingStepper/OnboardingStepper.module.scss @@ -0,0 +1,76 @@ +$circleDiameter: 5.6rem; + +.root { + height: $circleDiameter; + width: $circleDiameter; + position: fixed; + z-index: $z-index-5; + border-radius: 100%; + cursor: pointer; +} + +.wrapper { + position: relative; + width: $circleDiameter; + height: $circleDiameter; + border-radius: 100%; + overflow: hidden; +} + +.slideImg { + position: absolute; + z-index: $z-index-2; + height: 5rem; + top: 1.65rem; + left: 50%; + transform: translate(-50%, 0); +} + +.backgroundCircleSvg, +.progressLinesSvg { + top: 0; + left: 0; + position: absolute; +} + +.progressLinesSvg { + z-index: $z-index-2; +} + +.stepNumber { + z-index: $z-index-5; + left: 50%; + transform: translate(-50%, 0); + top: 2.4rem; + position: absolute; + + path { + stroke: transparent !important; + } +} + +.backgroundCircle { + fill: $color-octant-grey2; +} + +.progressLine { + stroke-width: 0.2rem; + stroke: $color-white; + stroke-linejoin: round; + fill: transparent; + stroke-linecap: round; + stroke-linejoin: round; + z-index: $z-index-4; + + &.isHighlighted { + stroke: $color-octant-green; + } +} + +.tooltipWrapper { + position: absolute; + width: $circleDiameter; + height: $circleDiameter; + bottom: 4.8rem !important; + right: 4.8rem !important; +} diff --git a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx new file mode 100644 index 0000000000..f48cdfbf25 --- /dev/null +++ b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx @@ -0,0 +1,112 @@ +import cx from 'classnames'; +import { motion } from 'framer-motion'; +import React, { ReactNode, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import Img from 'components/ui/Img'; +import Svg from 'components/ui/Svg'; +import Tooltip from 'components/ui/Tooltip'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; +import useOnboardingSteps from 'hooks/helpers/useOnboardingSteps'; +import useUserTOS from 'hooks/queries/useUserTOS'; +import useOnboardingStore from 'store/onboarding/store'; +import { four, one, three, two } from 'svg/onboardingStepper'; + +import styles from './OnboardingStepper.module.scss'; + +const OnboardingStepper = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'components.shared.onboardingStepper' }); + const { setIsOnboardingModalOpen, lastSeenStep } = useOnboardingStore(state => ({ + isOnboardingModalOpen: state.data.isOnboardingModalOpen, + lastSeenStep: state.data.lastSeenStep, + setIsOnboardingModalOpen: state.setIsOnboardingModalOpen, + })); + + const { isDesktop } = useMediaQuery(); + const { data: isUserTOSAccepted } = useUserTOS(); + const [isUserTOSAcceptedInitial] = useState(isUserTOSAccepted); + const stepsToUse = useOnboardingSteps(isUserTOSAcceptedInitial); + const cxcy = 28; + const viewBox = '0 0 56 56'; + const numberOfSteps = stepsToUse.length; + + const animationProps = isDesktop + ? { + animate: { bottom: 48, opacity: 1, right: 48 }, + exit: { bottom: 24, opacity: 0, right: 48 }, + initial: { bottom: 24, opacity: 0, right: 48 }, + } + : { + animate: { bottom: 116, opacity: 1, right: 24 }, + exit: { bottom: 92, opacity: 0, right: 24 }, + initial: { bottom: 92, opacity: 0, right: 24 }, + }; + + const svgNumber = useMemo(() => { + if (lastSeenStep === 1) { + return one; + } + if (lastSeenStep === 2) { + return two; + } + if (lastSeenStep === 3) { + return three; + } + return four; + }, [lastSeenStep]); + + return ( + setIsOnboardingModalOpen(true)} + whileHover={{ scale: 1.1 }} + {...animationProps} + > + +
+ + + + + + + {[...Array(numberOfSteps).keys()].map(i => ( + + ))} + +
+
+
+ ); +}; + +export default OnboardingStepper; diff --git a/client/src/components/shared/OnboardingStepper/index.tsx b/client/src/components/shared/OnboardingStepper/index.tsx new file mode 100644 index 0000000000..882ff6e981 --- /dev/null +++ b/client/src/components/shared/OnboardingStepper/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './OnboardingStepper'; diff --git a/client/src/constants/localStorageKeys.ts b/client/src/constants/localStorageKeys.ts index 7e9d1071bc..e8a7060b85 100644 --- a/client/src/constants/localStorageKeys.ts +++ b/client/src/constants/localStorageKeys.ts @@ -11,6 +11,11 @@ export const ALLOCATION_REWARDS_FOR_PROJECTS = getLocalStorageKey( const onboardingPrefix = 'onboarding'; export const IS_ONBOARDING_DONE = getLocalStorageKey(onboardingPrefix, 'isOnboardingDone'); +export const HAS_ONBOARDING_BEEN_CLOSED = getLocalStorageKey( + onboardingPrefix, + 'hasOnboardingBeenClosed', +); +export const LAST_SEEN_STEP = getLocalStorageKey(onboardingPrefix, 'lastSeenStep'); const settingsPrefix = 'settings'; export const IS_ONBOARDING_ALWAYS_VISIBLE = getLocalStorageKey( diff --git a/client/src/constants/milestones.ts b/client/src/constants/milestones.ts index 51566e44f6..64330d79a6 100644 --- a/client/src/constants/milestones.ts +++ b/client/src/constants/milestones.ts @@ -44,11 +44,10 @@ export default function getMilestones(): Milestone[] { label: i18n.t('views.projects.projectsTimelineWidget.epochStarts', { epoch: 'Three' }), }, { - from: new Date('2024-02-12T00:00:00+0100'), + from: new Date(), href: 'https://octant.fillout.com/t/u4uT8WFzDvus', id: 'e3-applications-open', label: i18n.t('views.projects.projectsTimelineWidget.applicationsOpen'), - to: new Date('2024-03-14T00:00:00+0100'), }, { from: new Date('2024-03-17T00:00:00+0100'), diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 0e9b6c4a46..a298a5475c 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -56,6 +56,11 @@ "waitingForConfirmation": "Waiting for confirmation" }, "components": { + "shared": { + "onboardingStepper": { + "reopenOnboarding": "Reopen onboarding" + } + }, "settings": { "patronMode": { "enablePatronMode": "Enable patron mode", diff --git a/client/src/services/localStorageService.test.ts b/client/src/services/localStorageService.test.ts index 6f4b52827a..a69aaf6aa0 100644 --- a/client/src/services/localStorageService.test.ts +++ b/client/src/services/localStorageService.test.ts @@ -2,9 +2,11 @@ import { ALLOCATION_ITEMS_KEY, ALLOCATION_REWARDS_FOR_PROJECTS, DISPLAY_CURRENCY, + HAS_ONBOARDING_BEEN_CLOSED, IS_CRYPTO_MAIN_VALUE_DISPLAY, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, + LAST_SEEN_STEP, WAS_ADD_FAVOURITES_ALREADY_CLOSED_TIP, WAS_CONNECT_WALLET_ALREADY_CLOSED_TIP, WAS_LOCK_GLM_ALREADY_CLOSED_TIP, @@ -41,24 +43,21 @@ describe('LocalStorageService', () => { }); it('should validate isOnboardingDone', () => { - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'true'); localStorage.setItem(IS_ONBOARDING_DONE, 'not-a-boolean'); localStorageService.init(); expect(localStorage.getItem(IS_ONBOARDING_DONE)).toBe('false'); }); - it('should validate isOnboardingDone', () => { - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + it('should validate hasOnboardingBeenClosed', () => { + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'not-a-boolean'); localStorageService.init(); - expect(localStorage.getItem(IS_ONBOARDING_DONE)).toBe('true'); + expect(localStorage.getItem(HAS_ONBOARDING_BEEN_CLOSED)).toBe('false'); }); - it('should validate isOnboardingDone', () => { - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'false'); + it('should validate lastSeenStep', () => { + localStorage.setItem(LAST_SEEN_STEP, 'not-a-number'); localStorageService.init(); - expect(localStorage.getItem(IS_ONBOARDING_DONE)).toBe('false'); + expect(localStorage.getItem(LAST_SEEN_STEP)).toBe('0'); }); it('should validate displayCurrency', () => { diff --git a/client/src/services/localStorageService.ts b/client/src/services/localStorageService.ts index 7a5df1383f..d57319df32 100644 --- a/client/src/services/localStorageService.ts +++ b/client/src/services/localStorageService.ts @@ -11,6 +11,8 @@ import { WAS_REWARDS_ALREADY_CLOSED_TIP, WAS_WITHDRAW_ALREADY_CLOSED_TIP, ALLOCATION_REWARDS_FOR_PROJECTS, + HAS_ONBOARDING_BEEN_CLOSED, + LAST_SEEN_STEP, } from 'constants/localStorageKeys'; import { initialState as settingsStoreInitialState } from 'store/settings/store'; import { initialState as tipsStoreInitialState } from 'store/tips/store'; @@ -52,6 +54,19 @@ const LocalStorageService = () => { } }; + const validateNumber = (localStorageKey: string): void => { + let value; + try { + value = parseFloat(JSON.parse(localStorage.getItem(localStorageKey) || '')); + } catch (e) { + value = ''; + } + + if (typeof value !== 'number') { + localStorage.setItem(localStorageKey, '0'); + } + }; + const validateAllocationItems = (): void => { const allocationItems = JSON.parse(localStorage.getItem(ALLOCATION_ITEMS_KEY) || 'null'); if (!Array.isArray(allocationItems)) { @@ -69,16 +84,15 @@ const LocalStorageService = () => { }; const validateIsOnboardingDone = (): void => { - const isOnboardingAlwaysVisible = localStorage.getItem(IS_ONBOARDING_ALWAYS_VISIBLE); + validateBoolean(IS_ONBOARDING_DONE); + }; - if (isOnboardingAlwaysVisible === 'true') { - localStorage.setItem( - IS_ONBOARDING_DONE, - JSON.stringify(settingsStoreInitialState.isAllocateOnboardingAlwaysVisible), - ); - } + const validateHasOnboardingBeenClosed = (): void => { + validateBoolean(HAS_ONBOARDING_BEEN_CLOSED); + }; - validateBoolean(IS_ONBOARDING_DONE); + const validateLastSeenStep = (): void => { + validateNumber(LAST_SEEN_STEP); }; const validateDisplayCurrency = (): void => { @@ -138,6 +152,8 @@ const LocalStorageService = () => { validateWasRewardsAlreadyClosed(); validateWasWithdrawAlreadyClosed(); validateRewardsForProjects(); + validateHasOnboardingBeenClosed(); + validateLastSeenStep(); }; return { diff --git a/client/src/store/onboarding/store.test.ts b/client/src/store/onboarding/store.test.ts index 2c854e7723..8f31b1c017 100644 --- a/client/src/store/onboarding/store.test.ts +++ b/client/src/store/onboarding/store.test.ts @@ -1,4 +1,8 @@ -import { IS_ONBOARDING_DONE } from 'constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_DONE, + LAST_SEEN_STEP, +} from 'constants/localStorageKeys'; import useOnboardingStore from './store'; @@ -10,12 +14,30 @@ describe('useOnboardingStore', () => { }); it('should reset the state', () => { - const { setIsOnboardingDone, reset } = useOnboardingStore.getState(); + const { + setIsOnboardingDone, + setHasOnboardingBeenClosed, + setIsOnboardingModalOpen, + setLastSeenStep, + reset, + } = useOnboardingStore.getState(); setIsOnboardingDone(true); + setHasOnboardingBeenClosed(true); + setLastSeenStep(3); + setIsOnboardingModalOpen(true); + expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(true); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(true); + expect(useOnboardingStore.getState().data.isOnboardingModalOpen).toEqual(true); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(3); + reset(); + expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(false); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(false); + expect(useOnboardingStore.getState().data.isOnboardingModalOpen).toEqual(false); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(1); }); it('should set isOnboardingDone to true in localStorage and state', () => { @@ -26,6 +48,22 @@ describe('useOnboardingStore', () => { expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(true); }); + it('should set hasOnboardingBeenClosed to true in localStorage and state', () => { + const { setHasOnboardingBeenClosed } = useOnboardingStore.getState(); + + setHasOnboardingBeenClosed(true); + expect(localStorage.getItem(HAS_ONBOARDING_BEEN_CLOSED)).toEqual('true'); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(true); + }); + + it('should set lastSeenStep to 3 in localStorage and state', () => { + const { setLastSeenStep } = useOnboardingStore.getState(); + + setLastSeenStep(3); + expect(localStorage.getItem(LAST_SEEN_STEP)).toEqual('3'); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(3); + }); + it('should set isOnboardingDone to false in localStorage and state', () => { const { setIsOnboardingDone } = useOnboardingStore.getState(); @@ -34,6 +72,22 @@ describe('useOnboardingStore', () => { expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(false); }); + it('should set hasOnboardingBeenClosed to false in localStorage and state', () => { + const { setHasOnboardingBeenClosed } = useOnboardingStore.getState(); + + setHasOnboardingBeenClosed(false); + expect(localStorage.getItem(HAS_ONBOARDING_BEEN_CLOSED)).toEqual('false'); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(false); + }); + + it('should set lastSeenStep to 2 in localStorage and state', () => { + const { setLastSeenStep } = useOnboardingStore.getState(); + + setLastSeenStep(2); + expect(localStorage.getItem(LAST_SEEN_STEP)).toEqual('2'); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(2); + }); + it('should get isOnboardingDone from localStorage and set in state', () => { const { setValuesFromLocalStorage } = useOnboardingStore.getState(); @@ -44,10 +98,33 @@ describe('useOnboardingStore', () => { expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(true); }); + it('should get hasOnboardingBeenClosed from localStorage and set in state', () => { + const { setValuesFromLocalStorage } = useOnboardingStore.getState(); + + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); + expect(useOnboardingStore.getState().meta.isInitialized).toEqual(false); + setValuesFromLocalStorage(); + expect(useOnboardingStore.getState().meta.isInitialized).toEqual(true); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(true); + }); + + it('should get lastSeenStep from localStorage and set in state', () => { + const { setValuesFromLocalStorage } = useOnboardingStore.getState(); + + localStorage.setItem(LAST_SEEN_STEP, '3'); + expect(useOnboardingStore.getState().meta.isInitialized).toEqual(false); + setValuesFromLocalStorage(); + expect(useOnboardingStore.getState().meta.isInitialized).toEqual(true); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(3); + }); + it('should return default state when there is no value in localStorage', () => { const { setValuesFromLocalStorage } = useOnboardingStore.getState(); setValuesFromLocalStorage(); expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(false); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(false); + expect(useOnboardingStore.getState().data.isOnboardingModalOpen).toEqual(false); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(1); }); }); diff --git a/client/src/store/onboarding/store.ts b/client/src/store/onboarding/store.ts index 3e46a8fbc2..aee3336c0b 100644 --- a/client/src/store/onboarding/store.ts +++ b/client/src/store/onboarding/store.ts @@ -1,29 +1,55 @@ -import { IS_ONBOARDING_DONE } from 'constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_DONE, + LAST_SEEN_STEP, +} from 'constants/localStorageKeys'; import { getStoreWithMeta } from 'store/utils/getStoreWithMeta'; import { OnboardingMethods, OnboardingData } from './types'; export const initialState: OnboardingData = { + hasOnboardingBeenClosed: false, isOnboardingDone: false, + isOnboardingModalOpen: false, + lastSeenStep: 1, }; export default getStoreWithMeta({ getStoreMethods: set => ({ reset: () => set({ data: initialState }), // eslint-disable-next-line @typescript-eslint/naming-convention + setHasOnboardingBeenClosed: payload => { + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, JSON.stringify(payload)); + set(state => ({ data: { ...state.data, hasOnboardingBeenClosed: payload } })); + }, + // eslint-disable-next-line @typescript-eslint/naming-convention setIsOnboardingDone: payload => { localStorage.setItem(IS_ONBOARDING_DONE, JSON.stringify(payload)); - set({ data: { isOnboardingDone: payload } }); + set(state => ({ data: { ...state.data, isOnboardingDone: payload } })); + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + setIsOnboardingModalOpen: payload => { + set(state => ({ data: { ...state.data, isOnboardingModalOpen: payload } })); + }, + setLastSeenStep: payload => { + localStorage.setItem(LAST_SEEN_STEP, JSON.stringify(payload)); + set(state => ({ data: { ...state.data, lastSeenStep: payload } })); }, setValuesFromLocalStorage: () => - set({ + set(state => ({ data: { + hasOnboardingBeenClosed: localStorage.getItem(HAS_ONBOARDING_BEEN_CLOSED) === 'true', isOnboardingDone: localStorage.getItem(IS_ONBOARDING_DONE) === 'true', + isOnboardingModalOpen: state.data.isOnboardingModalOpen, + lastSeenStep: + localStorage.getItem(IS_ONBOARDING_DONE) === 'true' + ? 1 + : parseInt(localStorage.getItem(LAST_SEEN_STEP) || '1', 10) || 1, }, meta: { isInitialized: true, }, - }), + })), }), initialState, }); diff --git a/client/src/store/onboarding/types.ts b/client/src/store/onboarding/types.ts index 34267ebfa1..5afa5104c9 100644 --- a/client/src/store/onboarding/types.ts +++ b/client/src/store/onboarding/types.ts @@ -1,9 +1,15 @@ export interface OnboardingData { + hasOnboardingBeenClosed: boolean; isOnboardingDone: boolean; + isOnboardingModalOpen: boolean; + lastSeenStep: number; } export interface OnboardingMethods { reset: () => void; + setHasOnboardingBeenClosed: (payload: OnboardingData['hasOnboardingBeenClosed']) => void; setIsOnboardingDone: (payload: OnboardingData['isOnboardingDone']) => void; + setIsOnboardingModalOpen: (payload: OnboardingData['isOnboardingModalOpen']) => void; + setLastSeenStep: (payload: OnboardingData['lastSeenStep']) => void; setValuesFromLocalStorage: () => void; } diff --git a/client/src/store/settings/store.ts b/client/src/store/settings/store.ts index 4af349b745..205bfcd7cf 100644 --- a/client/src/store/settings/store.ts +++ b/client/src/store/settings/store.ts @@ -24,6 +24,7 @@ export default getStoreWithMeta({ set(state => ({ data: { ...state.data, areOctantTipsAlwaysVisible: payload } })); }, + // eslint-disable-next-line @typescript-eslint/naming-convention setDisplayCurrency: payload => { localStorage.setItem(DISPLAY_CURRENCY, JSON.stringify(payload)); set(state => ({ data: { ...state.data, displayCurrency: payload } })); diff --git a/client/src/svg/onboardingStepper.ts b/client/src/svg/onboardingStepper.ts new file mode 100644 index 0000000000..a07f0ed1db --- /dev/null +++ b/client/src/svg/onboardingStepper.ts @@ -0,0 +1,25 @@ +import { SvgImageConfig } from 'components/ui/Svg/types'; + +export const one: SvgImageConfig = { + markup: + '', + viewBox: '0 0 6 12', +}; + +export const two: SvgImageConfig = { + markup: + '', + viewBox: '0 0 9 12', +}; + +export const three: SvgImageConfig = { + markup: + '', + viewBox: '0 0 10 13', +}; + +export const four: SvgImageConfig = { + markup: + '', + viewBox: '0 0 10 12', +}; diff --git a/client/synpress.config.ts b/client/synpress.config.ts index 780da56f26..94248add2b 100644 --- a/client/synpress.config.ts +++ b/client/synpress.config.ts @@ -29,6 +29,7 @@ export default defineConfig({ }, supportFile: 'cypress/support/index.ts', }, + numTestsKeptInMemory: 4, video: true, viewportHeight: 1080, viewportWidth: 1920,