From 195525b95fbe526fa8a41a20b9f713167c74bc0a Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 17 Jan 2022 09:34:10 +0100 Subject: [PATCH 01/44] Onboarding tour first iteraction - Added new Welcome Modal - Added TourComponent - Added new composable useTour - Updated some DOM/translations in other components --- package.json | 1 + src/App.vue | 20 +- src/components/BtcTransactionList.vue | 2 +- src/components/Tour.vue | 411 ++++++++++++++++++ src/components/TransactionList.vue | 16 +- .../icons/GreenNimiqLogoOutlineWithStars.vue | 8 + src/components/modals/Modal.vue | 8 +- .../modals/StartOnBoardingTourModal.vue | 96 ++++ src/composables/useTour.ts | 384 ++++++++++++++++ src/router.ts | 13 + yarn.lock | 22 +- 11 files changed, 970 insertions(+), 11 deletions(-) create mode 100644 src/components/Tour.vue create mode 100644 src/components/icons/GreenNimiqLogoOutlineWithStars.vue create mode 100644 src/components/modals/StartOnBoardingTourModal.vue create mode 100644 src/composables/useTour.ts diff --git a/package.json b/package.json index 3e3522536..c5164907f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "vue-responsive-components": "^0.2.3", "vue-router": "^3.1.6", "vue-simple-markdown": "^1.1.4", + "vue-tour": "^2.0.0", "vue-virtual-scroller": "https://github.com/sisou/vue-virtual-scroller#nimiq/build", "webpack-i18n-tools": "https://github.com/nimiq/webpack-i18n-tools#master" }, diff --git a/src/App.vue b/src/App.vue index e2b0b81cf..9a3466d71 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,11 @@ + + +
+ + + {{ currentStep + 1 }} / {{ nSteps }} + +
+ + +
+
+
+ + + + + + + + + diff --git a/src/components/TransactionList.vue b/src/components/TransactionList.vue index 44603bc22..99da8b1aa 100644 --- a/src/components/TransactionList.vue +++ b/src/components/TransactionList.vue @@ -61,15 +61,17 @@ > {{ $t('Cashlink') }} - - {{ $t('Buy NIM') }} + + - +
@@ -64,8 +64,14 @@ export default defineComponent({ type: Boolean, default: true, }, + showCloseIcon: { + type: Boolean, + default: true, + }, }, setup(props, context) { + const { showCloseIcon } = props; + function close() { if (props.showOverlay) { context.emit('close-overlay'); diff --git a/src/components/modals/StartOnBoardingTourModal.vue b/src/components/modals/StartOnBoardingTourModal.vue new file mode 100644 index 000000000..addb67f37 --- /dev/null +++ b/src/components/modals/StartOnBoardingTourModal.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/composables/useTour.ts b/src/composables/useTour.ts new file mode 100644 index 000000000..95acf6a8f --- /dev/null +++ b/src/composables/useTour.ts @@ -0,0 +1,384 @@ +import { Transaction, useTransactionsStore } from '@/stores/Transactions'; +import { SetupContext } from '@vue/composition-api'; +import { useAddressStore } from '../stores/Address'; + +type OnboardingTourPages = '/' | '/transactions' | '/?sidebar=true' + +enum MobileOnboardingTourStep { + FIRST_ADDRESS, + TRANSACTIONS_LIST, + FIRST_TRANSACTION, + BITCOIN_ADDRESS, + WALLET_BALANCE, + RECOVERY_WORDS_ALERT, + MENU_ICON, + ACCOUNT_OPTIONS, + ONBOARDING_COMPLETED +} + +type NetworkTourPages = '/network' +enum NetworkTourStep { + TODO, +} + +export type TourStepIndex = MobileOnboardingTourStep | NetworkTourStep; + +// eslint-disable-next-line max-len +// https://github.com/floating-ui/floating-ui/blob/59d15d4f81a2d71d9b42d836e47d7114bc32f7f2/packages/core/src/types.ts#L4 +type Alignment = 'start' | 'end'; +type BasePlacement = 'top' | 'right' | 'bottom' | 'left'; +type AlignedPlacement = `${BasePlacement}-${Alignment}`; +export type Placement = BasePlacement | AlignedPlacement; + +export interface TourStep { + page: OnboardingTourPages | NetworkTourPages; + + // data for the steps of v-tour + tooltip: { + target: string, + content: string[], + params: Readonly<{ + placement: BasePlacement | AlignedPlacement, + }>, + }; + + lifecycle?: { + prepareDOMPrevPage?: () => Promise, + prepareDOMNextPage?: () => Promise, + onBeforeMountedNextStep?: () => Promise, + onBeforeMountedPrevStep?: () => Promise, + onMountedStep?: (cbu: () => void) => void, + }; + + ui: { + // Elements that must have opacity to focus attention in other elements in screen + elementsWithOpacity?: string[], // array of selectors + + // Elements that shouldn't allow interactivity + elementsWithoutInteractivity?: string[], // array of selectors + + disabledNextStep?: boolean, + }; +} + +export type TourSteps = { + [x in TourStepIndex]: TourStep; +}; + +// TODO Remove me +export function useFakeTx(): Transaction { + return { + transactionHash: '0x123', + format: 'nim', + timestamp: 1532739000, + sender: useAddressStore().activeAddress.value || '', + recipient: '0x123', + senderType: 'nim', + recipientType: 'nim', + blockHeight: 1, + blockHash: '0x123456789ABCDEF', + value: 100_000, + fee: 1, + feePerByte: 1, + validityStartHeight: 1, + network: 'testnet', + flags: 0, + data: { + raw: 'My awesome data', + }, + proof: { + raw: 'ES256K', + }, + size: 1, + valid: true, + state: 'confirmed', + }; +} +export function useOnboardingTourSteps({ root }: SetupContext): TourSteps { + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + const closeAccountOptionsModal = async () => { + const sidebar = (document.querySelector('.column-sidebar') as HTMLDivElement); + sidebar!.removeAttribute('data-non-interactable'); + await root.$nextTick(); + + const closeBtn = (document.querySelector('.modal .close-button') as HTMLDivElement); + closeBtn.click(); + + await sleep(400); + }; + + const openAccountOptionsModal = async () => { + const account = document.querySelector('.sidebar .account-menu') as HTMLButtonElement; + account.click(); + await sleep(500); // TODO Check this random value + }; + + const steps: TourSteps = { + [MobileOnboardingTourStep.FIRST_ADDRESS]: { + page: '/', + tooltip: { + target: '.address-list > .address-button .identicon img', + content: [ + 'This is your first address, represented by your avatar.', + 'Tap it to open the address details.', + ], + params: { + placement: 'bottom-start', + }, + }, + lifecycle: { + onMountedStep: (cbu: () => void) => { + const addressButton = document.querySelector('.address-list > .address-button'); + + // eslint-disable-next-line no-unused-expressions + addressButton?.addEventListener('click', () => cbu(), { once: true }); + // TODO Remove listener somehow if user clicks on the '>' button instead on the address item + }, + }, + ui: { + elementsWithOpacity: [ + '.account-overview .backup-warning', + '.account-overview .mobile-menu-bar', + '.account-overview .bitcoin-account', + '.account-overview .account-balance-container', + '.account-overview .mobile-action-bar', + ], + }, + }, + [MobileOnboardingTourStep.TRANSACTIONS_LIST]: { + page: '/transactions', + tooltip: { + target: '.transaction-list > .empty-state h2', + content: [ + 'This is where all your transactions will appear.', + 'Click the green button to receive a free NIM from Team Nimiq.', + ], + params: { + placement: 'top', + }, + }, + ui: { + elementsWithOpacity: [ + '.address-overview .mobile-action-bar', + ], + elementsWithoutInteractivity: [ + '.address-overview .actions-mobile', + '.address-overview .active-address', + ], + disabledNextStep: true, + }, + }, + [MobileOnboardingTourStep.FIRST_TRANSACTION]: { + page: '/transactions', + tooltip: { + target: '.transaction-list .list-element > .transaction > .identicon', + content: [ + "Here's your first transaction with your first NIM.", + 'Every NIM address comes with an avatar. They help to make sure you got the right one.', + ], + params: { + placement: 'bottom-start', + }, + }, + lifecycle: { + onMountedStep: () => { + const buyNimBtn = document + .querySelector('.address-overview .transaction-list a button') as HTMLButtonElement; + if (!buyNimBtn) return; + buyNimBtn.disabled = true; + }, + }, + ui: { + elementsWithOpacity: [ + '.address-overview .mobile-action-bar', + ], + elementsWithoutInteractivity: [ + '.address-overview', + ], + }, + }, + [MobileOnboardingTourStep.BITCOIN_ADDRESS]: { + page: '/', + tooltip: { + target: '.account-overview .bitcoin-account > .bitcoin-account-item > svg', + content: [ + 'This is your Bitcoin wallet. You get one with every Nimiq account.', + ], + params: { + placement: 'top-start', + }, + }, + ui: { + elementsWithOpacity: [ + '.account-overview .backup-warning', + '.account-overview .mobile-menu-bar', + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .mobile-action-bar', + ], + elementsWithoutInteractivity: [ + '.account-overview .bitcoin-account', + ], + }, + }, + [MobileOnboardingTourStep.WALLET_BALANCE]: { + page: '/', + tooltip: { + target: '.account-overview .account-balance-container .amount', + content: [ + 'Check the bar-chart to see how your addresses compose your total balance.', + 'Currently you have 100% NIM, and no BTC.', + ], + params: { + placement: 'bottom', + }, + }, + ui: { + elementsWithOpacity: [ + '.account-overview .backup-warning', + '.account-overview .mobile-action-bar', + ], + elementsWithoutInteractivity: [ + '.account-overview .mobile-menu-bar', + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + }, + }, + [MobileOnboardingTourStep.RECOVERY_WORDS_ALERT]: { + page: '/', + tooltip: { + target: '.account-overview .backup-warning button', + content: [ + 'There is no \'forgot password\'.', + 'Create a backup to make sure you stay in control.', + ], + params: { + placement: 'bottom', + }, + }, + ui: { + elementsWithOpacity: [ + '.account-overview .mobile-action-bar', + ], + elementsWithoutInteractivity: [ + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + }, + }, + [MobileOnboardingTourStep.MENU_ICON]: { + page: '/', + tooltip: { + target: '.account-overview .mobile-menu-bar > button.reset', + content: [ + 'Tap on the menu icon to access your account and wallet settings.', + ], + params: { + placement: 'bottom-start', + }, + }, + lifecycle: { + onBeforeMountedNextStep: async () => { + await openAccountOptionsModal(); + }, + onMountedStep: (cbu: () => void) => { + const addressButton = document + .querySelector('.account-overview .mobile-menu-bar > button.reset'); + + // eslint-disable-next-line no-unused-expressions + addressButton?.addEventListener('click', () => cbu(), { once: true }); + }, + }, + ui: { + elementsWithOpacity: [ + '.account-overview .mobile-action-bar', + ], + elementsWithoutInteractivity: [ + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + disabledNextStep: true, + }, + }, + [MobileOnboardingTourStep.ACCOUNT_OPTIONS]: { + page: '/?sidebar=true', + tooltip: { + target: '.modal .small-page', + content: [ + 'Create, switch and log out of accounts.', + 'All security relevant actions can be found here too.', + ], + params: { + placement: 'top', + }, + }, + ui: { + elementsWithoutInteractivity: [ + '.column-sidebar', + ], + }, + lifecycle: { + prepareDOMPrevPage: async () => { + await closeAccountOptionsModal(); + + root.$router.push('/'); + await sleep(500); // TODO Check this random value + }, + prepareDOMNextPage: async () => { + await closeAccountOptionsModal(); + }, + }, + }, + [MobileOnboardingTourStep.ONBOARDING_COMPLETED]: { + page: '/?sidebar=true', + tooltip: { + target: '.column-sidebar .network .consensus-icon', + content: [ + 'Wallet tour completed!', + 'Nimiq is not just any crypto - Click on {WORLD} Network and discover true decentralization.', + ], + params: { + placement: 'top-start', + }, + }, + ui: { + elementsWithoutInteractivity: [ + '.column-sidebar', + ], + }, + lifecycle: { + onBeforeMountedPrevStep: async () => { + await openAccountOptionsModal(); + }, + }, + }, + }; + steps[MobileOnboardingTourStep.TRANSACTIONS_LIST].lifecycle = { + ...steps[MobileOnboardingTourStep.TRANSACTIONS_LIST].lifecycle, + + // TODO Maybe it could be possible without async/await + // eslint-disable-next-line no-async-promise-executor + onMountedStep: async () => new Promise(async (resolve) => { + root.$watch(() => useTransactionsStore().state.transactions, (txs) => { + if (Object.values(txs).length > 0) { + // Once the user has at least one transaction, tooltip in step TRANSACTIONS_LIST is modified + steps[MobileOnboardingTourStep.TRANSACTIONS_LIST].tooltip = { + target: '.vue-recycle-scroller__item-wrapper', + content: ['This is where all your transactions will appear.'], + params: { + placement: 'bottom', + }, + }; + steps[MobileOnboardingTourStep.TRANSACTIONS_LIST].ui.disabledNextStep = false; + } + resolve(); + }); + }), + }; + return steps; +} diff --git a/src/router.ts b/src/router.ts index c76359ace..14b28aa58 100644 --- a/src/router.ts +++ b/src/router.ts @@ -57,6 +57,10 @@ const MoonpayModal = () => const SimplexModal = () => import(/* webpackChunkName: "simplex-modal" */ './components/modals/SimplexModal.vue'); +// Tour modals +const StartOnBoardingTourModal = () => + import(/* webpackChunkName: "start-onboarding-tour-modal" */ './components/modals/StartOnBoardingTourModal.vue'); + Vue.use(VueRouter); export enum Columns { @@ -281,6 +285,15 @@ const routes: RouteConfig[] = [{ name: 'release-notes', props: { modal: true }, meta: { column: Columns.DYNAMIC }, + }, { + // TODO change this route + path: '/welcome-2', + components: { + modal: StartOnBoardingTourModal, + }, + name: 'welcome', + props: { modal: true }, + meta: { column: Columns.ACCOUNT }, }], }], }, { diff --git a/yarn.lock b/yarn.lock index e50c34a4b..0a6a90fcd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1253,6 +1253,11 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" +"@popperjs/core@^2.9.1": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" + integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== + "@sentry/browser@6.14.3": version "6.14.3" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.14.3.tgz#4e3b67a48b12a70c381cab326d053ee5dfc087d6" @@ -6868,6 +6873,11 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jump.js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jump.js/-/jump.js-1.0.2.tgz#e0641b47f40a38f2139c25fda0500bf28e43015a" + integrity sha1-4GQbR/QKOPITnCX9oFAL8o5DAVo= + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -10842,6 +10852,16 @@ vue-template-es2015-compiler@^1.9.0: resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== +vue-tour@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/vue-tour/-/vue-tour-2.0.0.tgz#aa4489cfc9f9090ca57d3208a074010f3be8ec05" + integrity sha512-vhKzqdhunQ3EoO1733UxhOB389u3EKv2X8JqYhX4tIq4ilqlZtnY3azPFBYPFmnAqHn5RyZBrP2CpqSaxTs8og== + dependencies: + "@popperjs/core" "^2.9.1" + hash-sum "^2.0.0" + jump.js "^1.0.2" + vue "^2.6.12" + "vue-virtual-scroller@https://github.com/sisou/vue-virtual-scroller#nimiq/build": version "1.0.10" resolved "https://github.com/sisou/vue-virtual-scroller#8e03a1432c611856e92a3f61f496b93337338701" @@ -10850,7 +10870,7 @@ vue-template-es2015-compiler@^1.9.0: vue-observe-visibility "^0.4.4" vue-resize "^0.4.5" -vue@^2.6.11: +vue@^2.6.11, vue@^2.6.12: version "2.6.14" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235" integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ== From ebde766e9ffd3bc6da334ede0bff4d3841f0b4a2 Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 20 Jan 2022 00:10:30 +0100 Subject: [PATCH 02/44] Removed useless async/await function --- src/composables/useTour.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/composables/useTour.ts b/src/composables/useTour.ts index 95acf6a8f..8f3a12349 100644 --- a/src/composables/useTour.ts +++ b/src/composables/useTour.ts @@ -362,8 +362,7 @@ export function useOnboardingTourSteps({ root }: SetupContext): TourSteps { ...steps[MobileOnboardingTourStep.TRANSACTIONS_LIST].lifecycle, // TODO Maybe it could be possible without async/await - // eslint-disable-next-line no-async-promise-executor - onMountedStep: async () => new Promise(async (resolve) => { + onMountedStep: async () => new Promise((resolve) => { root.$watch(() => useTransactionsStore().state.transactions, (txs) => { if (Object.values(txs).length > 0) { // Once the user has at least one transaction, tooltip in step TRANSACTIONS_LIST is modified From de3dfb193c81c9642c885863c8024096d643ad3f Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 20 Jan 2022 15:23:48 +0100 Subject: [PATCH 03/44] removed onBeforeMountedToFutureStep phase and adjusted timings --- src/components/Tour.vue | 12 +++--------- src/composables/useTour.ts | 8 +++----- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/components/Tour.vue b/src/components/Tour.vue index 241bff8d8..6bcde27e1 100644 --- a/src/components/Tour.vue +++ b/src/components/Tour.vue @@ -119,7 +119,7 @@ export default defineComponent({ // TODO This will be a prop const steps = useOnboardingTourSteps(context); - const currentStep: Ref = ref(0); + const currentStep: Ref = ref(6); const nSteps = Object.keys(steps).length; // TODO Might be a ref/computed instead const disableNextStep = ref(currentStep.value >= nSteps - 1 || !!steps[currentStep.value].ui.disabledNextStep); @@ -164,9 +164,9 @@ export default defineComponent({ // changePage if (!goingForward && currentLifecycle && currentLifecycle.prepareDOMPrevPage) { - currentLifecycle.prepareDOMPrevPage(); + await currentLifecycle.prepareDOMPrevPage(); } else if (goingForward && currentLifecycle && currentLifecycle.prepareDOMNextPage) { - currentLifecycle.prepareDOMNextPage(); + await currentLifecycle.prepareDOMNextPage(); } else if (futurePage !== currentPage && currentPage.startsWith(context.root.$route.path)) { // Default prepare DOM context.root.$router.push(futurePage); @@ -179,12 +179,6 @@ export default defineComponent({ await sleep(500); } - // onBeforeMountedToFutureStep - if (goingForward && currentLifecycle?.onBeforeMountedNextStep) { - await currentLifecycle.onBeforeMountedNextStep(); - } else if (!goingForward && currentLifecycle?.onBeforeMountedPrevStep) { - await currentLifecycle.onBeforeMountedPrevStep(); - } _removeClasses(currentStepIndex); tour!.start(futureStepIndex.toString()); diff --git a/src/composables/useTour.ts b/src/composables/useTour.ts index 8f3a12349..e5ca6d930 100644 --- a/src/composables/useTour.ts +++ b/src/composables/useTour.ts @@ -45,8 +45,6 @@ export interface TourStep { lifecycle?: { prepareDOMPrevPage?: () => Promise, prepareDOMNextPage?: () => Promise, - onBeforeMountedNextStep?: () => Promise, - onBeforeMountedPrevStep?: () => Promise, onMountedStep?: (cbu: () => void) => void, }; @@ -105,7 +103,7 @@ export function useOnboardingTourSteps({ root }: SetupContext): TourSteps { const closeBtn = (document.querySelector('.modal .close-button') as HTMLDivElement); closeBtn.click(); - await sleep(400); + await sleep(500); // TODO Check this random value }; const openAccountOptionsModal = async () => { @@ -282,7 +280,7 @@ export function useOnboardingTourSteps({ root }: SetupContext): TourSteps { }, }, lifecycle: { - onBeforeMountedNextStep: async () => { + prepareDOMNextPage: async () => { await openAccountOptionsModal(); }, onMountedStep: (cbu: () => void) => { @@ -352,7 +350,7 @@ export function useOnboardingTourSteps({ root }: SetupContext): TourSteps { ], }, lifecycle: { - onBeforeMountedPrevStep: async () => { + prepareDOMPrevPage: async () => { await openAccountOptionsModal(); }, }, From 60b70fde4304f96dfd8214ecb4b02106c9de80d1 Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 20 Jan 2022 15:26:59 +0100 Subject: [PATCH 04/44] renamed classes to attributes in functions names --- src/components/Tour.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Tour.vue b/src/components/Tour.vue index 6bcde27e1..8acc142fb 100644 --- a/src/components/Tour.vue +++ b/src/components/Tour.vue @@ -126,7 +126,7 @@ export default defineComponent({ onMounted(() => { tour = context.root.$tours['onboarding-tour']; - _addClasses(steps[currentStep.value].ui, currentStep.value); + _addAttributes(steps[currentStep.value].ui, currentStep.value); // eslint-disable-next-line no-unused-expressions steps[currentStep.value].lifecycle?.onMountedStep?.(goToNextStep); @@ -173,13 +173,13 @@ export default defineComponent({ await context.root.$nextTick(); } - _addClasses(futureUI, futureStepIndex); + _addAttributes(futureUI, futureStepIndex); if (futurePage !== currentPage) { await sleep(500); } - _removeClasses(currentStepIndex); + _removeAttributes(currentStepIndex); tour!.start(futureStepIndex.toString()); @@ -199,7 +199,7 @@ export default defineComponent({ currentStep.value = futureStepIndex; } - function _addClasses(uiConfig: TourStep['ui'], stepIndex: TourStepIndex) { + function _addAttributes(uiConfig: TourStep['ui'], stepIndex: TourStepIndex) { const elementsWithOpacity = uiConfig.elementsWithOpacity || []; const elementsWithoutInteractivity = uiConfig.elementsWithoutInteractivity || []; @@ -217,7 +217,7 @@ export default defineComponent({ }); } - function _removeClasses(stepIndex: TourStepIndex) { + function _removeAttributes(stepIndex: TourStepIndex) { document.querySelectorAll(`[data-non-interactable="${stepIndex}"]`).forEach((el) => { el.removeAttribute('data-non-interactable'); }); @@ -262,7 +262,7 @@ export default defineComponent({ diff --git a/src/components/icons/TourPreviousLeftArrowIcon.vue b/src/components/icons/TourPreviousLeftArrowIcon.vue new file mode 100644 index 000000000..7e812839e --- /dev/null +++ b/src/components/icons/TourPreviousLeftArrowIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/composables/useTour.ts b/src/composables/useTour.ts deleted file mode 100644 index 119848835..000000000 --- a/src/composables/useTour.ts +++ /dev/null @@ -1,473 +0,0 @@ -import { useAccountStore } from '@/stores/Account'; -import { Transaction, useTransactionsStore } from '@/stores/Transactions'; -import { SetupContext } from '@vue/composition-api'; -import { useAddressStore } from '../stores/Address'; -import { CryptoCurrency } from '../lib/Constants'; - -export type TourName = 'onboarding' | 'network' - -enum MobileOnboardingTourStep { - FIRST_ADDRESS, - TRANSACTIONS_LIST, - FIRST_TRANSACTION, - BITCOIN_ADDRESS, - WALLET_BALANCE, - RECOVERY_WORDS_ALERT, - MENU_ICON, - ACCOUNT_OPTIONS, - ONBOARDING_COMPLETED -} - -enum NetworkTourStep { - TODO, -} - -export type TourStepIndex = MobileOnboardingTourStep | NetworkTourStep; - -// eslint-disable-next-line max-len -// https://github.com/floating-ui/floating-ui/blob/59d15d4f81a2d71d9b42d836e47d7114bc32f7f2/packages/core/src/types.ts#L4 -type Alignment = 'start' | 'end'; -type BasePlacement = 'top' | 'right' | 'bottom' | 'left'; -type AlignedPlacement = `${BasePlacement}-${Alignment}`; -export type Placement = BasePlacement | AlignedPlacement; - -export interface LifecycleArgs { - goToNextStep: () => void; - goingForward: boolean; -} - -export type MountedReturnFn = ((args?: { goingForward: boolean, ending?: boolean }) => Promise | void); - -export interface TourStep { - path: '/' | '/transactions' | '/?sidebar=true' | '/network'; - - // data for the steps of v-tour - tooltip: { - target: string, - content: string[], - params: { - placement: BasePlacement | AlignedPlacement, - }, - button?: { - text: string, - fn: () => void, - }, - }; - - lifecycle?: { - created?: (args: LifecycleArgs) => Promise | void, - mounted?: (args: LifecycleArgs) => MountedReturnFn | Promise | void, - }; - - ui: { - // Elements that must have opacity to focus attention in other elements in screen - fadedElements?: string[], // array of selectors - - // Elements that shouldn't allow interactivity - disabledElements?: string[], // array of selectors - - isNextStepDisabled?: boolean, - }; -} - -export type TourSteps = { - [x in T]: TourStep; -}; - -// TODO Remove me -export function useFakeTx(): Transaction { - return { - transactionHash: '0x123', - format: 'basic', - timestamp: 1532739000, - sender: 'NQ02 YP68 BA76 0KR3 QY9C SF0K LP8Q THB6 LTKU', - recipient: useAddressStore().activeAddress.value || 'NQ07 0000 0000 0000 0000 0000 0000 0000 0000', - senderType: 'basic', - recipientType: 'basic', - blockHeight: 1, - blockHash: '0x123456789ABCDEF', - value: 100_000, - fee: 1, - feePerByte: 1, - validityStartHeight: 1, - network: 'testnet', - flags: 0, - data: { - raw: 'My awesome data', - }, - proof: { - raw: 'ES256K', - }, - size: 1, - valid: true, - state: 'confirmed', - }; -} -function getOnboardingTourSteps({ root }: SetupContext): TourSteps { - const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - - const toggleDisabledAttribute = async (selector: string, disabled: boolean) => { - const el = document.querySelector(selector) as HTMLButtonElement; - if (el) { - el.disabled = disabled; - await root.$nextTick(); - } - }; - - const steps: TourSteps = { - [MobileOnboardingTourStep.FIRST_ADDRESS]: { - path: '/', - tooltip: { - target: '.address-list > .address-button .identicon img', - content: [ - 'This is your first address, represented by your avatar.', - 'Tap it to open the address details.', - ], - params: { - placement: 'bottom-start', - }, - }, - lifecycle: { - created: () => { - const { setActiveCurrency } = useAccountStore(); - const { addressInfos, selectAddress } = useAddressStore(); - setActiveCurrency(CryptoCurrency.NIM); - selectAddress(addressInfos.value[0].address); - }, - mounted: ({ goToNextStep }) => { - const addressButton = document - .querySelector('.address-list > .address-button') as HTMLButtonElement; - - let addressClicked = false; - const onClick = (e: MouseEvent) => { - addressClicked = true; - goToNextStep(); - e.preventDefault(); - e.stopPropagation(); - }; - - addressButton!.addEventListener('click', onClick, { once: true, capture: true }); - - return async (args) => { - if (!args?.ending && !addressClicked - && root.$route.path === steps[MobileOnboardingTourStep.FIRST_ADDRESS].path) { - addressButton!.click(); - await root.$nextTick(); - } - addressButton!.removeEventListener('click', onClick, true); - }; - }, - }, - ui: { - fadedElements: [ - '.account-overview .backup-warning', - '.account-overview .mobile-menu-bar', - '.account-overview .bitcoin-account', - '.account-overview .account-balance-container', - '.account-overview .mobile-action-bar', - ], - }, - }, - [MobileOnboardingTourStep.TRANSACTIONS_LIST]: { - path: '/transactions', - tooltip: { - target: '.transaction-list > .empty-state h2', - content: [ - 'This is where all your transactions will appear.', - 'Click the green button to receive a free NIM from Team Nimiq.', - ], - params: { - placement: 'top', - }, - }, - lifecycle: { - mounted: () => { - const { transactions } = useTransactionsStore().state; - - if (Object.values(transactions.value || []).length === 0) { - const unwatch = root.$watch(() => useTransactionsStore().state.transactions, (txs) => { - if (!Object.values(txs).length) { - unwatch(); - return; - } - - // Once the user has at least one transaction, tooltip in step TRANSACTIONS_LIST - // is modified - steps[MobileOnboardingTourStep.TRANSACTIONS_LIST].tooltip = { - target: '.vue-recycle-scroller__item-wrapper', - content: ['This is where all your transactions will appear.'], - params: { - placement: 'bottom', - }, - }; - steps[MobileOnboardingTourStep.TRANSACTIONS_LIST].ui.isNextStepDisabled = false; - toggleDisabledAttribute('.address-overview .transaction-list a button', true); - steps[MobileOnboardingTourStep.TRANSACTIONS_LIST].lifecycle = { - created: async () => { - await toggleDisabledAttribute( - '.address-overview .transaction-list a button', true); - }, - mounted() { - return (args) => { - if (args?.ending || !args?.goingForward) { - setTimeout(() => { - toggleDisabledAttribute( - '.address-overview .transaction-list a button', false); - }, args?.ending ? 0 : 1000); - } - }; - }, - }; - unwatch(); - }); - } - }, - }, - ui: { - fadedElements: [ - '.address-overview .mobile-action-bar', - ], - disabledElements: [ - '.address-overview .actions-mobile', - '.address-overview .active-address', - ], - isNextStepDisabled: true, - }, - }, - [MobileOnboardingTourStep.FIRST_TRANSACTION]: { - path: '/transactions', - tooltip: { - target: '.transaction-list .list-element > .transaction > .identicon', - content: [ - "Here's your first transaction with your first NIM.", - 'Every NIM address comes with an avatar. They help to make sure you got the right one.', - ], - params: { - placement: 'bottom-start', - }, - }, - lifecycle: { - created: async () => { - await toggleDisabledAttribute('.address-overview .transaction-list a button', true); - }, - mounted() { - return (args) => { - if (args?.ending || args?.goingForward) { - setTimeout(() => { - toggleDisabledAttribute('.address-overview .transaction-list a button', false); - }, args?.ending ? 0 : 1000); - } - }; - }, - }, - ui: { - fadedElements: [ - '.address-overview .mobile-action-bar', - ], - disabledElements: [ - '.address-overview', - ], - }, - }, - [MobileOnboardingTourStep.BITCOIN_ADDRESS]: { - path: '/', - tooltip: { - target: '.account-overview .bitcoin-account > .bitcoin-account-item > svg', - content: [ - 'This is your Bitcoin wallet. You get one with every Nimiq account.', - ], - params: { - placement: 'top-start', - }, - }, - ui: { - fadedElements: [ - '.account-overview .backup-warning', - '.account-overview .mobile-menu-bar', - '.account-overview .account-balance-container', - '.account-overview .address-list', - '.account-overview .mobile-action-bar', - ], - disabledElements: [ - '.account-overview .bitcoin-account', - ], - }, - }, - [MobileOnboardingTourStep.WALLET_BALANCE]: { - path: '/', - tooltip: { - target: '.account-overview .account-balance-container .amount', - content: [ - 'Check the bar-chart to see how your addresses compose your total balance.', - 'Currently you have 100% NIM, and no BTC.', - ], - params: { - placement: 'bottom', - }, - }, - ui: { - fadedElements: [ - '.account-overview .backup-warning', - '.account-overview .mobile-action-bar', - ], - disabledElements: [ - '.account-overview .mobile-menu-bar', - '.account-overview .account-balance-container', - '.account-overview .address-list', - '.account-overview .bitcoin-account', - ], - }, - }, - [MobileOnboardingTourStep.RECOVERY_WORDS_ALERT]: { - path: '/', - tooltip: { - target: '.account-overview .backup-warning button', - content: [ - 'There is no \'forgot password\'.', - 'Create a backup to make sure you stay in control.', - ], - params: { - placement: 'bottom', - }, - }, - ui: { - fadedElements: [ - '.account-overview .mobile-action-bar', - ], - disabledElements: [ - '.account-overview .account-balance-container', - '.account-overview .address-list', - '.account-overview .bitcoin-account', - ], - }, - }, - [MobileOnboardingTourStep.MENU_ICON]: { - path: '/', - tooltip: { - target: '.account-overview .mobile-menu-bar > button.reset', - content: [ - 'Tap on the menu icon to access your account and wallet settings.', - ], - params: { - placement: 'bottom-start', - }, - }, - lifecycle: { - mounted: async ({ goToNextStep }) => { - const hamburguerIcon = document - .querySelector('.account-overview .mobile-menu-bar > button.reset') as HTMLButtonElement; - - hamburguerIcon!.addEventListener('click', () => goToNextStep(), { once: true, capture: true }); - }, - }, - ui: { - fadedElements: [ - '.account-overview .mobile-action-bar', - ], - disabledElements: [ - '.account-overview .account-balance-container', - '.account-overview .address-list', - '.account-overview .bitcoin-account', - ], - isNextStepDisabled: true, - }, - }, - [MobileOnboardingTourStep.ACCOUNT_OPTIONS]: { - path: '/?sidebar=true', - tooltip: { - target: '.modal .small-page', - content: [ - 'Create, switch and log out of accounts.', - 'All security relevant actions can be found here too.', - ], - params: { - placement: 'top', - }, - }, - ui: { - disabledElements: [ - '.column-sidebar', - ], - }, - lifecycle: { - created: async () => { - await sleep(500); - const account = document.querySelector('.sidebar .account-menu') as HTMLButtonElement; - account.click(); - await sleep(500); // TODO Check this random value - }, - mounted: async () => { - const sidebar = (document.querySelector('.column-sidebar') as HTMLDivElement); - sidebar!.removeAttribute('data-non-interactable'); - await root.$nextTick(); - - return async () => { - const closeBtn = (document.querySelector('.modal .close-button') as HTMLDivElement); - closeBtn.click(); - - await sleep(500); // TODO Check this random value - }; - }, - }, - }, - [MobileOnboardingTourStep.ONBOARDING_COMPLETED]: { - path: '/?sidebar=true', - tooltip: { - target: '.column-sidebar .network .consensus-icon', - content: [ - 'Wallet tour completed!', - 'Nimiq is not just any crypto - Click on {WORLD} Network and discover true decentralization.', - ], - params: { - placement: 'top-start', - }, - button: { - text: 'Go to Network', - fn: () => { - const { setTour } = useAccountStore(); - setTour(null); - root.$router.push('/network'); - }, - }, - }, - ui: { - disabledElements: [ - '.column-sidebar', - ], - }, - }, - }; - return steps; -} - -function getNetworkTourSteps({ root }: SetupContext): TourSteps { - return { - [NetworkTourStep.TODO]: { - path: '/network', - tooltip: { - target: '.network-overview .network-name', - content: [ - 'Welcome to the {WORLD} Network!', - 'This is the main network where all Nimiq transactions take place.', - 'You can switch between networks by clicking on the {WORLD} Network icon in the top right corner.', - ], - params: { - placement: 'bottom', - }, - }, - ui: {}, - }, - }; -} - -export function useTour(tour: TourName | null, context: SetupContext) - : TourSteps | TourSteps | undefined { - switch (tour) { - case 'onboarding': - return getOnboardingTourSteps(context); - case 'network': - return getNetworkTourSteps(context); - default: - return undefined; - } -} diff --git a/src/i18n/en.po b/src/i18n/en.po index bdd8a6572..a26656d3b 100644 --- a/src/i18n/en.po +++ b/src/i18n/en.po @@ -115,7 +115,7 @@ msgstr "" msgid "72 hours after your first buy, your limit will be increased automatically." msgstr "" -#: src/components/layouts/Settings.vue:339 +#: src/components/layouts/Settings.vue:353 msgid "" "A contact with the address \"{address}\", but a different name already exists.\n" " Do you want to replace it?" @@ -139,7 +139,7 @@ msgstr "" msgid "Activate Bitcoin" msgstr "" -#: src/components/layouts/Settings.vue:198 +#: src/components/layouts/Settings.vue:211 msgid "Add" msgstr "" @@ -163,7 +163,7 @@ msgstr "" msgid "Add address" msgstr "" -#: src/components/layouts/Settings.vue:194 +#: src/components/layouts/Settings.vue:207 msgid "Add an existing vesting contract to your wallet." msgstr "" @@ -186,7 +186,7 @@ msgstr "" msgid "Addresses" msgstr "" -#: src/components/layouts/Settings.vue:152 +#: src/components/layouts/Settings.vue:165 msgid "Advanced" msgstr "" @@ -194,7 +194,7 @@ msgstr "" msgid "After 72 hours" msgstr "" -#: src/components/layouts/Settings.vue:132 +#: src/components/layouts/Settings.vue:145 #: src/components/layouts/Settings.vue:72 msgid "all" msgstr "" @@ -264,11 +264,11 @@ msgstr "" msgid "Atomic swaps require two BTC transactions." msgstr "" -#: src/components/layouts/Settings.vue:184 +#: src/components/layouts/Settings.vue:197 msgid "Auto" msgstr "" -#: src/components/layouts/Settings.vue:177 +#: src/components/layouts/Settings.vue:190 msgid "Automatic mode uses {behavior}." msgstr "" @@ -307,7 +307,7 @@ msgstr "" #: src/components/BalanceDistribution.vue:77 #: src/components/layouts/AccountOverview.vue:64 #: src/components/layouts/AddressOverview.vue:68 -#: src/components/layouts/Settings.vue:119 +#: src/components/layouts/Settings.vue:132 #: src/components/LegacyAccountNotice.vue:21 #: src/components/modals/BtcTransactionModal.vue:119 #: src/components/modals/BtcTransactionModal.vue:135 @@ -320,7 +320,7 @@ msgstr "" msgid "Bitcoin addresses are used only once, so there are no contacts. Use labels instead to find transactions in your history easily." msgstr "" -#: src/components/layouts/Settings.vue:138 +#: src/components/layouts/Settings.vue:151 msgid "Bitcoin Unit" msgstr "" @@ -379,7 +379,7 @@ msgstr "" msgid "Buy more here in the wallet." msgstr "" -#: src/components/TransactionList.vue:65 +#: src/components/TransactionList.vue:66 msgid "Buy NIM" msgstr "" @@ -436,7 +436,7 @@ msgstr "" msgid "Cancelled Swap" msgstr "" -#: src/components/layouts/Settings.vue:319 +#: src/components/layouts/Settings.vue:333 msgid "Cannot import contacts, wrong file format." msgstr "" @@ -483,11 +483,11 @@ msgstr "" msgid "Claiming Cashlink" msgstr "" -#: src/components/layouts/Settings.vue:210 +#: src/components/layouts/Settings.vue:223 msgid "Clear" msgstr "" -#: src/components/layouts/Settings.vue:204 +#: src/components/layouts/Settings.vue:217 msgid "Clear Cache" msgstr "" @@ -651,11 +651,15 @@ msgstr "" msgid "Developer resources" msgstr "" -#: src/components/layouts/Settings.vue:248 +#: src/components/layouts/Settings.vue:261 #: src/components/modals/DisclaimerModal.vue:5 msgid "Disclaimer" msgstr "" +#: src/components/modals/StartOnBoardingTourModal.vue:14 +msgid "Discover the Nimiq Wallet!" +msgstr "" + #: src/components/BtcAddressInput.vue:85 #: src/components/modals/SendModal.vue:355 msgid "Domain does not resolve to a valid address" @@ -690,7 +694,7 @@ msgstr "" msgid "Edit contacts" msgstr "" -#: src/components/layouts/Settings.vue:125 +#: src/components/layouts/Settings.vue:138 msgid "Edit the amount of decimals visible for BTC values." msgstr "" @@ -703,10 +707,14 @@ msgstr "" msgid "Edit transaction" msgstr "" -#: src/components/layouts/Settings.vue:158 +#: src/components/layouts/Settings.vue:171 msgid "Enable experimental support for swiping gestures on mobile." msgstr "" +#: src/components/Tour.vue:71 +msgid "End Tour" +msgstr "" + #: src/components/modals/overlays/SellCryptoBankCheckOverlay.vue:35 msgid "Enter account holder name" msgstr "" @@ -730,7 +738,7 @@ msgstr "" msgid "Enter recipient address..." msgstr "" -#: src/components/layouts/Settings.vue:218 +#: src/components/layouts/Settings.vue:231 msgid "Enter the password of the trial you want to enable, then press enter." msgstr "" @@ -813,7 +821,7 @@ msgstr "" msgid "Fiat value unavailable" msgstr "" -#: src/components/layouts/Settings.vue:325 +#: src/components/layouts/Settings.vue:339 msgid "File contains no contacts." msgstr "" @@ -854,6 +862,10 @@ msgstr "" msgid "Get NIM" msgstr "" +#: src/components/layouts/Settings.vue:94 +msgid "Go through the product again" +msgstr "" + #: src/components/BankCheckInput.vue:92 #: src/components/modals/BtcActivationModal.vue:21 #: src/components/swap/OasisLaunchModal.vue:26 @@ -988,11 +1000,15 @@ msgstr "" msgid "It is fast, safe and makes you truly independent." msgstr "" +#: src/components/modals/StartOnBoardingTourModal.vue:16 +msgid "It's free, does not collect data and is controlled by no one but you." +msgstr "" + #: src/components/swap/SwapNotification.vue:39 msgid "It's safe to close your wallet now" msgstr "" -#: src/components/layouts/Settings.vue:171 +#: src/components/layouts/Settings.vue:184 msgid "Keyguard Mode" msgstr "" @@ -1138,6 +1154,10 @@ msgstr "" msgid "Network fee: {sats} sat/vByte" msgstr "" +#: src/components/Tour.vue:58 +msgid "Next" +msgstr "" + #: src/components/swap/SwapFeesTooltip.vue:39 msgid "NIM network fee" msgstr "" @@ -1173,11 +1193,11 @@ msgid "No registration – not even email." msgstr "" #: src/components/BtcTransactionList.vue:80 -#: src/components/TransactionList.vue:83 +#: src/components/TransactionList.vue:85 msgid "No transactions found" msgstr "" -#: src/components/layouts/Settings.vue:130 +#: src/components/layouts/Settings.vue:143 #: src/components/layouts/Settings.vue:70 msgid "None" msgstr "" @@ -1213,11 +1233,11 @@ msgstr "" msgid "OASIS service fee" msgstr "" -#: src/components/layouts/Settings.vue:165 +#: src/components/layouts/Settings.vue:178 msgid "Off" msgstr "" -#: src/components/layouts/Settings.vue:348 +#: src/components/layouts/Settings.vue:362 msgid "OK! Contacts imported successfully." msgstr "" @@ -1225,7 +1245,7 @@ msgstr "" msgid "Old and new Accounts" msgstr "" -#: src/components/layouts/Settings.vue:164 +#: src/components/layouts/Settings.vue:177 msgid "On" msgstr "" @@ -1241,7 +1261,7 @@ msgstr "" msgid "Or buy BTC directly in the wallet." msgstr "" -#: src/components/layouts/Settings.vue:173 +#: src/components/layouts/Settings.vue:186 msgid "Overwrite how the Keyguard is opened." msgstr "" @@ -1292,14 +1312,18 @@ msgstr "" msgid "Please transfer" msgstr "" -#: src/components/layouts/Settings.vue:178 +#: src/components/layouts/Settings.vue:191 msgid "popups" msgstr "" -#: src/components/layouts/Settings.vue:185 +#: src/components/layouts/Settings.vue:198 msgid "Popups" msgstr "" +#: src/components/Tour.vue:44 +msgid "Previous" +msgstr "" + #: src/components/TransactionDetailOasisPayoutStatus.vue:30 msgid "Proceed to the troubleshooting page to find out what to do next." msgstr "" @@ -1308,6 +1332,10 @@ msgstr "" msgid "Processing payout" msgstr "" +#: src/components/layouts/Settings.vue:92 +msgid "Product Tour" +msgstr "" + #: src/components/swap/SwapSepaFundingInstructions.vue:53 msgid "Purpose" msgstr "" @@ -1326,7 +1354,7 @@ msgid "Receive fiat to your bank account." msgstr "" #: src/components/TestnetFaucet.vue:11 -#: src/components/TransactionList.vue:79 +#: src/components/TransactionList.vue:81 msgid "Receive free NIM" msgstr "" @@ -1334,7 +1362,7 @@ msgstr "" msgid "Receive NIM" msgstr "" -#: src/components/TransactionList.vue:73 +#: src/components/TransactionList.vue:75 msgid "Receive some free NIM to get started." msgstr "" @@ -1359,19 +1387,19 @@ msgstr "" msgid "Recovery Words" msgstr "" -#: src/components/layouts/Settings.vue:174 +#: src/components/layouts/Settings.vue:187 msgid "Redirect mode does not yet support swaps." msgstr "" -#: src/components/layouts/Settings.vue:178 +#: src/components/layouts/Settings.vue:191 msgid "redirects" msgstr "" -#: src/components/layouts/Settings.vue:186 +#: src/components/layouts/Settings.vue:199 msgid "Redirects" msgstr "" -#: src/components/layouts/Settings.vue:227 +#: src/components/layouts/Settings.vue:240 msgid "Reference currency" msgstr "" @@ -1388,7 +1416,7 @@ msgstr "" msgid "Regular transactions are too slow and will not process. Your bank might use another name, like ‘real-time transactions’ i.a." msgstr "" -#: src/components/layouts/Settings.vue:246 +#: src/components/layouts/Settings.vue:259 #: src/components/UpdateNotification.vue:8 msgid "Release Notes" msgstr "" @@ -1413,7 +1441,7 @@ msgstr "" msgid "Rescan" msgstr "" -#: src/components/layouts/Settings.vue:206 +#: src/components/layouts/Settings.vue:219 msgid "Reset your wallet settings and reload data from the blockchain." msgstr "" @@ -1459,7 +1487,7 @@ msgstr "" msgid "Select a currency and an amount." msgstr "" -#: src/components/layouts/Settings.vue:140 +#: src/components/layouts/Settings.vue:153 msgid "Select which unit to show Bitcoin amounts in." msgstr "" @@ -1618,7 +1646,7 @@ msgstr "" msgid "Show all my addresses" msgstr "" -#: src/components/layouts/Settings.vue:123 +#: src/components/layouts/Settings.vue:136 #: src/components/layouts/Settings.vue:63 msgid "Show Decimals" msgstr "" @@ -1668,6 +1696,14 @@ msgstr "" msgid "standard" msgstr "" +#: src/components/layouts/Settings.vue:99 +msgid "Start Tour" +msgstr "" + +#: src/components/modals/StartOnBoardingTourModal.vue:21 +msgid "Start Wallet tour" +msgstr "" + #: src/components/modals/WelcomeModal.vue:7 msgid "Store, send and receive NIM." msgstr "" @@ -1712,7 +1748,7 @@ msgstr "" msgid "Swap to {address}" msgstr "" -#: src/components/layouts/Settings.vue:156 +#: src/components/layouts/Settings.vue:169 msgid "Swiping Gestures" msgstr "" @@ -1830,8 +1866,8 @@ msgstr "" #: src/components/BtcTransactionList.vue:238 #: src/components/BtcTransactionList.vue:260 -#: src/components/TransactionList.vue:249 -#: src/components/TransactionList.vue:271 +#: src/components/TransactionList.vue:251 +#: src/components/TransactionList.vue:273 msgid "This month" msgstr "" @@ -1899,7 +1935,7 @@ msgstr "" msgid "Transfer funds" msgstr "" -#: src/components/layouts/Settings.vue:216 +#: src/components/layouts/Settings.vue:229 msgid "Trials" msgstr "" @@ -1957,7 +1993,7 @@ msgstr "" msgid "Use the slider or edit values to set up a swap." msgstr "" -#: src/components/layouts/Settings.vue:192 +#: src/components/layouts/Settings.vue:205 msgid "Vesting Contract" msgstr "" @@ -1985,7 +2021,7 @@ msgstr "" msgid "What is your bank called?" msgstr "" -#: src/components/layouts/Settings.vue:163 +#: src/components/layouts/Settings.vue:176 msgid "When ready" msgstr "" @@ -2117,6 +2153,6 @@ msgid "Your transaction must be SEPA Instant" msgstr "" #: src/components/BtcTransactionList.vue:65 -#: src/components/TransactionList.vue:72 +#: src/components/TransactionList.vue:74 msgid "Your transactions will appear here" msgstr "" diff --git a/src/lib/tour/index.ts b/src/lib/tour/index.ts new file mode 100644 index 000000000..b1ef311f7 --- /dev/null +++ b/src/lib/tour/index.ts @@ -0,0 +1,50 @@ +import { useAddressStore } from '@/stores/Address'; +import { SetupContext } from '@vue/composition-api'; +import { Transaction } from '@/stores/Transactions'; +import { getNetworkTourSteps } from './network'; +import { getOnboardingTourSteps } from './onboarding'; +import { NetworkTourStep, OnboardingTourStep, TourName, TourSteps } from './types'; + +export function getTour(tour: TourName | null, context: SetupContext) + : TourSteps | TourSteps | undefined { + switch (tour) { + case 'onboarding': + return getOnboardingTourSteps(context); + case 'network': + return getNetworkTourSteps(); + default: + return undefined; + } +} + +// TODO Remove me +export function getFakeTx(): Transaction { + return { + transactionHash: '0x123', + format: 'basic', + timestamp: 1532739000, + sender: 'NQ02 YP68 BA76 0KR3 QY9C SF0K LP8Q THB6 LTKU', + recipient: useAddressStore().activeAddress.value || 'NQ07 0000 0000 0000 0000 0000 0000 0000 0000', + senderType: 'basic', + recipientType: 'basic', + blockHeight: 1, + blockHash: '0x123456789ABCDEF', + value: 100_000, + fee: 1, + feePerByte: 1, + validityStartHeight: 1, + network: 'testnet', + flags: 0, + data: { + raw: 'My awesome data', + }, + proof: { + raw: 'ES256K', + }, + size: 1, + valid: true, + state: 'confirmed', + }; +} + +export * from './types'; diff --git a/src/lib/tour/network/index.ts b/src/lib/tour/network/index.ts new file mode 100644 index 000000000..d16869195 --- /dev/null +++ b/src/lib/tour/network/index.ts @@ -0,0 +1,21 @@ +import { NetworkTourStep, TourSteps } from '../types'; + +export function getNetworkTourSteps(): TourSteps { + return { + [NetworkTourStep.TODO]: { + path: '/network', + tooltip: { + target: '.network-overview .network-name', + content: [ + 'Welcome to the {WORLD} Network!', + 'This is the main network where all Nimiq transactions take place.', + 'You can switch between networks by clicking on the {WORLD} Network icon in the top right corner.', + ], + params: { + placement: 'bottom', + }, + }, + ui: {}, + }, + }; +} diff --git a/src/lib/tour/onboarding/01_FirstAddressStep.ts b/src/lib/tour/onboarding/01_FirstAddressStep.ts new file mode 100644 index 000000000..a05aaecdd --- /dev/null +++ b/src/lib/tour/onboarding/01_FirstAddressStep.ts @@ -0,0 +1,69 @@ +import { CryptoCurrency } from '@/lib/Constants'; +import { useAccountStore } from '@/stores/Account'; +import { useAddressStore } from '@/stores/Address'; +import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { onboardingTexts } from './OnboardingTourTexts'; + +export function getFirstAddressStep({ isMobile, root }: GetStepFnArgs): TourStep { + const firstAddressLifecycle: TourStep['lifecycle'] = { + created: () => { + const { setActiveCurrency } = useAccountStore(); + const { addressInfos, selectAddress } = useAddressStore(); + setActiveCurrency(CryptoCurrency.NIM); + selectAddress(addressInfos.value[0].address); + }, + }; + + const firstAddressMobile: TourStep = { + path: '/', + tooltip: { + target: '.address-list > .address-button .identicon img', + content: onboardingTexts[OnboardingTourStep.FIRST_ADDRESS].default, + params: { + placement: 'bottom-start', + }, + }, + lifecycle: { + ...firstAddressLifecycle, + mounted: ({ goToNextStep }) => { + if (!isMobile) { + return undefined; + } + + // Listener for the first address button only for mobile + + const addressButton = document + .querySelector('.address-list > .address-button') as HTMLButtonElement; + + let addressClicked = false; + const onClick = (e: MouseEvent) => { + addressClicked = true; + goToNextStep(); + e.preventDefault(); + e.stopPropagation(); + }; + + addressButton!.addEventListener('click', onClick, { once: true, capture: true }); + + return async (args) => { + if (!args?.ending && !addressClicked && root.$route.path === firstAddressMobile.path) { + addressButton!.click(); + await root.$nextTick(); + } + addressButton!.removeEventListener('click', onClick, true); + }; + }, + }, + ui: { + fadedElements: [ + '.account-overview .backup-warning', + '.account-overview .mobile-menu-bar', + '.account-overview .bitcoin-account', + '.account-overview .account-balance-container', + '.account-overview .mobile-action-bar', + ], + }, + }; + + return firstAddressMobile; +} diff --git a/src/lib/tour/onboarding/02_TransactionListStep.ts b/src/lib/tour/onboarding/02_TransactionListStep.ts new file mode 100644 index 000000000..821e3cbc8 --- /dev/null +++ b/src/lib/tour/onboarding/02_TransactionListStep.ts @@ -0,0 +1,71 @@ +import { useTransactionsStore } from '@/stores/Transactions'; +import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; + +export function getTransactionListStep( + { root, steps, toggleDisabledAttribute }: GetStepFnArgs): TourStep { + return { + path: '/transactions', + tooltip: { + target: '.transaction-list > .empty-state h2', + content: [ + 'This is where all your transactions will appear.', + 'Click the green button to receive a free NIM from Team Nimiq.', + ], + params: { + placement: 'top', + }, + }, + lifecycle: { + mounted: () => { + const { transactions } = useTransactionsStore().state; + + if (Object.values(transactions.value || []).length === 0) { + const unwatch = root.$watch(() => useTransactionsStore().state.transactions, (txs) => { + if (!Object.values(txs).length) { + unwatch(); + return; + } + + // Once the user has at least one transaction, step TRANSACTIONS_LIST is modified + steps[OnboardingTourStep.TRANSACTIONS_LIST]!.tooltip = { + target: '.vue-recycle-scroller__item-wrapper', + content: ['This is where all your transactions will appear.'], + params: { + placement: 'bottom', + }, + }; + steps[OnboardingTourStep.TRANSACTIONS_LIST]!.ui.isNextStepDisabled = false; + toggleDisabledAttribute('.address-overview .transaction-list a button', true); + steps[OnboardingTourStep.TRANSACTIONS_LIST]!.lifecycle = { + created: async () => { + await toggleDisabledAttribute( + '.address-overview .transaction-list a button', true); + }, + mounted() { + return (args) => { + if (args?.ending || !args?.goingForward) { + setTimeout(() => { + toggleDisabledAttribute( + '.address-overview .transaction-list a button', false); + }, args?.ending ? 0 : 1000); + } + }; + }, + }; + unwatch(); + }); + } + }, + }, + ui: { + fadedElements: [ + '.address-overview .mobile-action-bar', + ], + disabledElements: [ + '.address-overview .actions-mobile', + '.address-overview .active-address', + ], + isNextStepDisabled: true, + }, + }; +} diff --git a/src/lib/tour/onboarding/03_FirstTransactionStep.ts b/src/lib/tour/onboarding/03_FirstTransactionStep.ts new file mode 100644 index 000000000..5c6a3e6fd --- /dev/null +++ b/src/lib/tour/onboarding/03_FirstTransactionStep.ts @@ -0,0 +1,38 @@ +import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { onboardingTexts } from './OnboardingTourTexts'; + +export function getFirstTransactionStep( + { toggleDisabledAttribute }: GetStepFnArgs): TourStep { + return { + path: '/transactions', + tooltip: { + target: '.transaction-list .list-element > .transaction > .identicon', + content: onboardingTexts[OnboardingTourStep.FIRST_TRANSACTION].default, + params: { + placement: 'bottom-start', + }, + }, + lifecycle: { + created: async () => { + await toggleDisabledAttribute('.address-overview .transaction-list a button', true); + }, + mounted() { + return (args) => { + if (args?.ending || args?.goingForward) { + setTimeout(() => { + toggleDisabledAttribute('.address-overview .transaction-list a button', false); + }, args?.ending ? 0 : 1000); + } + }; + }, + }, + ui: { + fadedElements: [ + '.address-overview .mobile-action-bar', + ], + disabledElements: [ + '.address-overview', + ], + }, + } as TourStep; +} diff --git a/src/lib/tour/onboarding/04_BitcoinAddressStep.ts b/src/lib/tour/onboarding/04_BitcoinAddressStep.ts new file mode 100644 index 000000000..ba2c9e22f --- /dev/null +++ b/src/lib/tour/onboarding/04_BitcoinAddressStep.ts @@ -0,0 +1,27 @@ +import { OnboardingTourStep, TourStep } from '../types'; +import { onboardingTexts } from './OnboardingTourTexts'; + +export function getBitcoinAddressStep() { + return { + path: '/', + tooltip: { + target: '.account-overview .bitcoin-account > .bitcoin-account-item > svg', + content: onboardingTexts[OnboardingTourStep.BITCOIN_ADDRESS].default, + params: { + placement: 'top-start', + }, + }, + ui: { + fadedElements: [ + '.account-overview .backup-warning', + '.account-overview .mobile-menu-bar', + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .mobile-action-bar', + ], + disabledElements: [ + '.account-overview .bitcoin-account', + ], + }, + } as TourStep; +} diff --git a/src/lib/tour/onboarding/05_WalletBalanceStep.ts b/src/lib/tour/onboarding/05_WalletBalanceStep.ts new file mode 100644 index 000000000..15fb80d1b --- /dev/null +++ b/src/lib/tour/onboarding/05_WalletBalanceStep.ts @@ -0,0 +1,27 @@ +import { OnboardingTourStep, TourStep } from '../types'; +import { onboardingTexts } from './OnboardingTourTexts'; + +export function getWalletBalanceStep(): TourStep { + return { + path: '/', + tooltip: { + target: '.account-overview .account-balance-container .amount', + content: onboardingTexts[OnboardingTourStep.WALLET_BALANCE].default, + params: { + placement: 'bottom', + }, + }, + ui: { + fadedElements: [ + '.account-overview .backup-warning', + '.account-overview .mobile-action-bar', + ], + disabledElements: [ + '.account-overview .mobile-menu-bar', + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + }, + } as TourStep; +} diff --git a/src/lib/tour/onboarding/06_0_BackupAlertStep.ts b/src/lib/tour/onboarding/06_0_BackupAlertStep.ts new file mode 100644 index 000000000..e92123250 --- /dev/null +++ b/src/lib/tour/onboarding/06_0_BackupAlertStep.ts @@ -0,0 +1,25 @@ +import { OnboardingTourStep, TourStep } from '../types'; +import { onboardingTexts } from './OnboardingTourTexts'; + +export function getBackupAlertStep(): TourStep { + return { + path: '/', + tooltip: { + target: '.account-overview .backup-warning button', + content: onboardingTexts[OnboardingTourStep.BACKUP_ALERT].default, + params: { + placement: 'bottom', + }, + }, + ui: { + fadedElements: [ + '.account-overview .mobile-action-bar', + ], + disabledElements: [ + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + }, + } as TourStep; +} diff --git a/src/lib/tour/onboarding/06_1_MenuIconStep.ts b/src/lib/tour/onboarding/06_1_MenuIconStep.ts new file mode 100644 index 000000000..454136a05 --- /dev/null +++ b/src/lib/tour/onboarding/06_1_MenuIconStep.ts @@ -0,0 +1,34 @@ +import { OnboardingTourStep, TourStep } from '../types'; +import { onboardingTexts } from './OnboardingTourTexts'; + +export function getMenuIconStep(): TourStep { + return { + path: '/', + tooltip: { + target: '.account-overview .mobile-menu-bar > button.reset', + content: onboardingTexts[OnboardingTourStep.MENU_ICON].default, + params: { + placement: 'bottom-start', + }, + }, + lifecycle: { + mounted: async ({ goToNextStep }) => { + const hamburguerIcon = document + .querySelector('.account-overview .mobile-menu-bar > button.reset') as HTMLButtonElement; + + hamburguerIcon!.addEventListener('click', () => goToNextStep(), { once: true, capture: true }); + }, + }, + ui: { + fadedElements: [ + '.account-overview .mobile-action-bar', + ], + disabledElements: [ + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + isNextStepDisabled: true, + }, + } as TourStep; +} diff --git a/src/lib/tour/onboarding/07_AccountOptionsStep.ts b/src/lib/tour/onboarding/07_AccountOptionsStep.ts new file mode 100644 index 000000000..14098f06a --- /dev/null +++ b/src/lib/tour/onboarding/07_AccountOptionsStep.ts @@ -0,0 +1,40 @@ +import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { onboardingTexts } from './OnboardingTourTexts'; + +export function getAccountOptionsStep({ sleep, root }: GetStepFnArgs): TourStep { + return { + path: '/?sidebar=true', + tooltip: { + target: '.modal .small-page', + content: onboardingTexts[OnboardingTourStep.ACCOUNT_OPTIONS].default, + params: { + placement: 'top', + }, + }, + ui: { + disabledElements: [ + '.column-sidebar', + ], + }, + lifecycle: { + created: async () => { + await sleep(500); // TODO Check this random value + const account = document.querySelector('.sidebar .account-menu') as HTMLButtonElement; + account.click(); + await sleep(500); // TODO Check this random value + }, + mounted: async () => { + const sidebar = (document.querySelector('.column-sidebar') as HTMLDivElement); + sidebar!.removeAttribute('data-non-interactable'); + await root.$nextTick(); + + return async () => { + const closeBtn = (document.querySelector('.modal .close-button') as HTMLDivElement); + closeBtn.click(); + + await sleep(500); // TODO Check this random value + }; + }, + }, + } as TourStep; +} diff --git a/src/lib/tour/onboarding/08_OnboardingCompleted.ts b/src/lib/tour/onboarding/08_OnboardingCompleted.ts new file mode 100644 index 000000000..518f3d9e1 --- /dev/null +++ b/src/lib/tour/onboarding/08_OnboardingCompleted.ts @@ -0,0 +1,29 @@ +import { useAccountStore } from '@/stores/Account'; +import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { onboardingTexts } from './OnboardingTourTexts'; + +export function getOnboardingCompletedStep({ root }: GetStepFnArgs): TourStep { + return { + path: '/?sidebar=true', + tooltip: { + target: '.column-sidebar .network .consensus-icon', + content: onboardingTexts[OnboardingTourStep.ONBOARDING_COMPLETED].default, + params: { + placement: 'top-start', + }, + button: { + text: 'Go to Network', + fn: () => { + const { setTour } = useAccountStore(); + setTour(null); + root.$router.push('/network'); + }, + }, + }, + ui: { + disabledElements: [ + '.column-sidebar', + ], + }, + } as TourStep; +} diff --git a/src/lib/tour/onboarding/OnboardingTourTexts.ts b/src/lib/tour/onboarding/OnboardingTourTexts.ts new file mode 100644 index 000000000..44d9d7657 --- /dev/null +++ b/src/lib/tour/onboarding/OnboardingTourTexts.ts @@ -0,0 +1,64 @@ +import { OnboardingTourStep } from '../types'; + +type TourStepTexts = { + [x in T]: { + default: string[], alternative?: string[], + } +} + +export const onboardingTexts: TourStepTexts = { + [OnboardingTourStep.FIRST_ADDRESS]: { + default: [ + 'This is your first address, represented by your avatar.', + 'You can click on the address to copy and share it.', + ], + }, + [OnboardingTourStep.TRANSACTIONS_LIST]: { + default: [ + 'This is where all your transactions will appear.', + 'Click the green button to receive a free NIM from Team Nimiq.', + ], + alternative: ['This is where all your transactions will appear.'], + }, + [OnboardingTourStep.FIRST_TRANSACTION]: { + default: [ + "Here's your first transaction with your first NIM.", + 'Every NIM address comes with an avatar. They help to make sure you got the right one.', + ], + }, + [OnboardingTourStep.BITCOIN_ADDRESS]: { + default: [ + 'Check the bar-chart to see how your addresses compose your total balance.', + 'Currently you have 100% NIM, and no BTC.', + ], + }, + [OnboardingTourStep.WALLET_BALANCE]: { + default: [ + 'Check the bar-chart to see how your addresses compose your total balance.', + 'Currently you have 100% NIM, and no BTC.', + ], + }, + [OnboardingTourStep.BACKUP_ALERT]: { + default: [ + 'There is no \'forgot password\'.', + 'Create a backup to make sure you stay in control.', + ], + }, + [OnboardingTourStep.MENU_ICON]: { + default: [ + 'Tap on the menu icon to access your account and wallet settings.', + ], + }, + [OnboardingTourStep.ACCOUNT_OPTIONS]: { + default: [ + 'Create, switch and log out of accounts.', + 'All security relevant actions can be found here too.', + ], + }, + [OnboardingTourStep.ONBOARDING_COMPLETED]: { + default: [ + 'Wallet tour completed!', + 'Nimiq is not just any crypto - Click on {WORLD} Network and discover true decentralization.', + ], + }, +}; diff --git a/src/lib/tour/onboarding/index.ts b/src/lib/tour/onboarding/index.ts new file mode 100644 index 000000000..a464dc306 --- /dev/null +++ b/src/lib/tour/onboarding/index.ts @@ -0,0 +1,51 @@ +import { useWindowSize } from '@/composables/useWindowSize'; +import { SetupContext } from '@vue/composition-api'; +import { GetStepFnArgs, OnboardingTourStep, TourSteps } from '../types'; +import { getFirstAddressStep } from './01_FirstAddressStep'; +import { getTransactionListStep } from './02_TransactionListStep'; +import { getFirstTransactionStep } from './03_FirstTransactionStep'; +import { getBitcoinAddressStep } from './04_BitcoinAddressStep'; +import { getWalletBalanceStep } from './05_WalletBalanceStep'; +import { getBackupAlertStep } from './06_0_BackupAlertStep'; +import { getMenuIconStep } from './06_1_MenuIconStep'; +import { getAccountOptionsStep } from './07_AccountOptionsStep'; +import { getOnboardingCompletedStep } from './08_OnboardingCompleted'; + +export function getOnboardingTourSteps({ root }: SetupContext): TourSteps { + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + const toggleDisabledAttribute = async (selector: string, disabled: boolean) => { + const el = document.querySelector(selector) as HTMLButtonElement; + if (el) { + el.disabled = disabled; + await root.$nextTick(); + } + }; + + const { isMobile, isTablet, isFullDesktop } = useWindowSize(); + + // Declaring steps here so we can reference it in the lifecycle functions + const steps: TourSteps = {}; + + const args: GetStepFnArgs = { + sleep, + steps, + toggleDisabledAttribute, + root, + isMobile, + isTablet, + isFullDesktop, + }; + + steps[OnboardingTourStep.FIRST_ADDRESS] = getFirstAddressStep(args); + steps[OnboardingTourStep.TRANSACTIONS_LIST] = getTransactionListStep(args); + steps[OnboardingTourStep.FIRST_TRANSACTION] = getFirstTransactionStep(args); + steps[OnboardingTourStep.BITCOIN_ADDRESS] = getBitcoinAddressStep(); + steps[OnboardingTourStep.WALLET_BALANCE] = getWalletBalanceStep(); + steps[OnboardingTourStep.BACKUP_ALERT] = getBackupAlertStep(); + steps[OnboardingTourStep.MENU_ICON] = getMenuIconStep(); + steps[OnboardingTourStep.ACCOUNT_OPTIONS] = getAccountOptionsStep(args); + steps[OnboardingTourStep.ONBOARDING_COMPLETED] = getOnboardingCompletedStep(args); + + return steps; +} diff --git a/src/lib/tour/types.ts b/src/lib/tour/types.ts new file mode 100644 index 000000000..2f403da55 --- /dev/null +++ b/src/lib/tour/types.ts @@ -0,0 +1,82 @@ +import { useWindowSize } from '@/composables/useWindowSize'; +import { SetupContext } from '@vue/composition-api'; + +export type TourName = 'onboarding' | 'network' + +export enum OnboardingTourStep { + FIRST_ADDRESS, + TRANSACTIONS_LIST, + FIRST_TRANSACTION, + BITCOIN_ADDRESS, + WALLET_BALANCE, + BACKUP_ALERT, + MENU_ICON, + ACCOUNT_OPTIONS, + ONBOARDING_COMPLETED, + BI +} + +export enum NetworkTourStep { + TODO, +} + +export type TourStepIndex = OnboardingTourStep | NetworkTourStep; + +// eslint-disable-next-line max-len +// https://github.com/floating-ui/floating-ui/blob/59d15d4f81a2d71d9b42d836e47d7114bc32f7f2/packages/core/src/types.ts#L4 +type Alignment = 'start' | 'end'; +type BasePlacement = 'top' | 'right' | 'bottom' | 'left'; +type AlignedPlacement = `${BasePlacement}-${Alignment}`; +export type Placement = BasePlacement | AlignedPlacement; + +export interface LifecycleArgs { + goToNextStep: () => void; + goingForward: boolean; +} + +export type MountedReturnFn = ((args?: { goingForward: boolean, ending?: boolean }) => Promise | void); + +export interface TourStep { + path: '/' | '/transactions' | '/?sidebar=true' | '/network'; + + // data for the steps of v-tour + tooltip: { + target: string, + content: string[], + params: { + placement: BasePlacement | AlignedPlacement, + }, + button?: { + text: string, + fn: () => void, + }, + }; + + lifecycle?: { + created?: (args: LifecycleArgs) => Promise | void, + mounted?: (args: LifecycleArgs) => MountedReturnFn | Promise | void, + }; + + ui: { + // Elements that must have opacity to focus attention in other elements in screen + fadedElements?: string[], // array of selectors + + // Elements that shouldn't allow interactivity + disabledElements?: string[], // array of selectors + + isNextStepDisabled?: boolean, + }; +} + +export type TourSteps = { + [x in T]?: TourStep; +}; + +export type GetStepFnArgs = + Pick, 'isMobile' | 'isTablet' | 'isFullDesktop'> & + { + root: SetupContext['root'], + steps: TourSteps, + toggleDisabledAttribute: (selector: string, disabled: boolean) => Promise, + sleep: (ms: number) => Promise, + }; From 4fb289f1d4a4072357065fbf58f2439c6ab5c936 Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 26 Jan 2022 11:33:56 +0100 Subject: [PATCH 10/44] reestructure steps and added tour in bigger screens --- src/App.vue | 3 + src/components/Tour.vue | 418 ++++++++++-------- src/components/TourLargeScreenManager.vue | 133 ++++++ src/components/_Template.vue | 15 - src/components/layouts/Sidebar.vue | 3 + src/composables/useWindowSize.ts | 4 +- src/i18n/en.po | 6 +- src/lib/tour/network/index.ts | 1 + .../tour/onboarding/01_FirstAddressStep.ts | 130 +++--- .../tour/onboarding/02_TransactionListStep.ts | 113 +++-- .../onboarding/03_FirstTransactionStep.ts | 46 +- .../tour/onboarding/04_BitcoinAddressStep.ts | 26 +- .../tour/onboarding/05_WalletBalanceStep.ts | 27 +- .../tour/onboarding/06_0_BackupAlertStep.ts | 24 +- src/lib/tour/onboarding/06_1_MenuIconStep.ts | 4 + .../tour/onboarding/07_AccountOptionsStep.ts | 64 ++- .../tour/onboarding/08_OnboardingCompleted.ts | 15 +- .../tour/onboarding/OnboardingTourTexts.ts | 6 +- src/lib/tour/onboarding/index.ts | 10 +- src/lib/tour/types.ts | 22 +- src/stores/Account.ts | 2 +- 21 files changed, 716 insertions(+), 356 deletions(-) create mode 100644 src/components/TourLargeScreenManager.vue delete mode 100644 src/components/_Template.vue diff --git a/src/App.vue b/src/App.vue index faab8b53e..eedb4ccbd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -53,6 +53,9 @@ export default defineComponent({ if (!['root', 'transactions'].includes(context.root.$route.name as string) && accountState.tour === 'onboarding') { setTour(null); + } else if (!['network'].includes(context.root.$route.name as string) + && accountState.tour === 'network') { + setTour(null); } const showTour = computed(() => !!accountState.tour); diff --git a/src/components/Tour.vue b/src/components/Tour.vue index 10b9ebb9a..11735aa46 100644 --- a/src/components/Tour.vue +++ b/src/components/Tour.vue @@ -2,12 +2,13 @@
- -
- + {{ currentStep + 1 }} / {{ nSteps }} +
+ + - {{ currentStep + 1 }} / {{ nSteps }} -
- - -
- +
@@ -110,7 +109,6 @@ import { getTour, MountedReturnFn, TourStep, TourStepIndex, - TourSteps, } from '../lib/tour'; import CaretRightIcon from './icons/CaretRightIcon.vue'; import TourPreviousLeftArrowIcon from './icons/TourPreviousLeftArrowIcon.vue'; @@ -130,11 +128,29 @@ export default defineComponent({ const { state: tourStore, setTour } = useAccountStore(); let tour: VueTour.Tour | null = null; - const steps: TourSteps = getTour(tourStore.tour, context) || {}; + const tourOptions: any = { + // eslint-disable-next-line max-len + // see example: https://github.com/pulsardev/vue-tour/blob/6ee85afdae3a4cb8689959b3b0c2035e165072fa/src/shared/constants.js + enabledButtons: { + buttonSkip: false, + buttonPrevious: false, + buttonNext: false, + buttonStop: false, + }, + labels: { + buttonSkip: 'Skip tour', + buttonPrevious: 'Previous', + buttonNext: 'Next', + buttonStop: 'Finish', + }, + useKeyboardNavigation: false, // handled by us + }; + // TODO Go back to index + const steps: TourStep[] = Object.values(getTour(tourStore.tour, context) ?? []); // Initial state const isLoading = ref(true); - const currentStep: Ref = ref(0); + const currentStep: Ref = tourStore.tour === 'onboarding' ? ref(5) : ref(0); // TODO Remove const nSteps: Ref = ref(0); const disableNextStep = ref(true); @@ -157,13 +173,16 @@ export default defineComponent({ nSteps.value = Object.keys(steps).length; disableNextStep.value = currentStep.value >= nSteps.value - 1 || !!step.ui.isNextStepDisabled; - await step.lifecycle?.created?.({ goToNextStep, goingForward: true }); + if (step.lifecycle?.created) { + await step.lifecycle.created({ goToNextStep, goingForward: true }); + } _addAttributes(step.ui, currentStep.value); unmounted = await step.lifecycle?.mounted?.({ goToNextStep, goingForward: true, + ending: false, }); if (context.root.$route.path !== step.path) { @@ -175,6 +194,11 @@ export default defineComponent({ tour = context.root.$tours['nimiq-tour']; tour.start(`${currentStep.value}`); isLoading.value = false; + + _broadcastTourData(); + context.root.$on('tour-end', () => { + endTour(); + }); } // Dont allow user to interact with the page while it is loading @@ -205,29 +229,31 @@ export default defineComponent({ currentStepIndex: TourStepIndex, futureStepIndex: TourStepIndex, ) { + // TODO https://stackoverflow.com/questions/42990308/vue-js-how-to-call-method-from-another-component const goingForward = futureStepIndex > currentStepIndex; - const { path: currentPath } = steps[currentStepIndex]!; + const { path: currentPath, ui: currentUI } = steps[currentStepIndex]!; const { path: futurePath, ui: futureUI, lifecycle: futureLifecycle } = steps[futureStepIndex]!; isLoading.value = true; tour!.stop(); + await sleep(500); if (unmounted) { - await unmounted({ goingForward }); + await unmounted({ goingForward, ending: false }); } + // created await futureLifecycle?.created?.({ goToNextStep, goingForward }); - if ( - futurePath !== currentPath - && context.root.$route.fullPath !== futurePath - ) { + if (futurePath !== currentPath && context.root.$route.fullPath !== futurePath) { context.root.$router.push(futurePath); await context.root.$nextTick(); } + _toggleDisabledButtons(currentUI.disabledButtons, false); + _toggleDisabledButtons(futureUI.disabledButtons, true); _addAttributes(futureUI, futureStepIndex); if (futurePath !== currentPath) { @@ -243,16 +269,34 @@ export default defineComponent({ // investigation // goingForward ? tour!.nextStep() : tour!.previousStep(); - // onMountedStep + // mounted isLoading.value = false; disableNextStep.value = futureStepIndex >= nSteps.value - 1 || !!futureUI.isNextStepDisabled; unmounted = await futureLifecycle?.mounted?.({ goToNextStep, goingForward, + ending: false, }); currentStep.value = futureStepIndex; + _broadcastTourData(); + } + + function _broadcastTourData() { + context.root.$emit('tour-data', { + nSteps: nSteps.value, + currentStep: currentStep.value, + }); + } + + function _toggleDisabledButtons(disabledButtons: TourStep['ui']['disabledButtons'], disabled:boolean) { + if (!disabledButtons) return; + disabledButtons.forEach((element) => { + const el = document.querySelector(element) as HTMLButtonElement; + if (!el) return; + el.disabled = disabled; + }); } function _addAttributes( @@ -292,6 +336,7 @@ export default defineComponent({ async function endTour() { _removeAttributes(currentStep.value); + _toggleDisabledButtons(steps[currentStep.value]?.ui.disabledButtons, false); if (unmounted) { await unmounted({ ending: true, goingForward: false }); @@ -302,6 +347,7 @@ export default defineComponent({ app.removeAttribute('data-non-interactable'); setTour(null); + context.root.$off('tour-end'); } // TODO REMOVE ME - Simulate tx @@ -312,11 +358,14 @@ export default defineComponent({ } return { - steps, isMobile, isTablet, isFullDesktop, + // tour + tourOptions, + steps, + // control bar currentStep, nSteps, @@ -362,170 +411,171 @@ export default defineComponent({ diff --git a/src/components/TourLargeScreenManager.vue b/src/components/TourLargeScreenManager.vue new file mode 100644 index 000000000..d9d707958 --- /dev/null +++ b/src/components/TourLargeScreenManager.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/components/_Template.vue b/src/components/_Template.vue deleted file mode 100644 index 41683b640..000000000 --- a/src/components/_Template.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/src/components/layouts/Sidebar.vue b/src/components/layouts/Sidebar.vue index 55454218c..2cef60ccd 100644 --- a/src/components/layouts/Sidebar.vue +++ b/src/components/layouts/Sidebar.vue @@ -17,6 +17,7 @@ +
@@ -68,6 +69,7 @@ import { GearIcon, Tooltip, InfoCircleIcon } from '@nimiq/vue-components'; import Config from 'config'; import AnnouncementBox from '../AnnouncementBox.vue'; +import TourLargeScreenManager from '../TourLargeScreenManager.vue'; import AccountMenu from '../AccountMenu.vue'; import PriceChart, { TimeRange } from '../PriceChart.vue'; import ConsensusIcon from '../ConsensusIcon.vue'; @@ -140,6 +142,7 @@ export default defineComponent({ }, components: { AnnouncementBox, + TourLargeScreenManager, GearIcon, PriceChart, AccountMenu, diff --git a/src/composables/useWindowSize.ts b/src/composables/useWindowSize.ts index b8f1e9d68..9f204bc39 100644 --- a/src/composables/useWindowSize.ts +++ b/src/composables/useWindowSize.ts @@ -23,8 +23,8 @@ export function useWindowSize() { width = ref(0); height = ref(0); listener(); - isMobile = computed(() => width!.value <= 700); // Full mobile breakpoint - isTablet = computed(() => width!.value <= 960); // Tablet breakpoint + isMobile = computed(() => width!.value <= 960); // Full mobile breakpoint + isTablet = computed(() => width!.value <= 1160); // Tablet breakpoint isFullDesktop = computed(() => width!.value > 1160); // Desktop breakpoint } diff --git a/src/i18n/en.po b/src/i18n/en.po index a26656d3b..5b97e3d5d 100644 --- a/src/i18n/en.po +++ b/src/i18n/en.po @@ -711,7 +711,7 @@ msgstr "" msgid "Enable experimental support for swiping gestures on mobile." msgstr "" -#: src/components/Tour.vue:71 +#: src/components/Tour.vue:64 msgid "End Tour" msgstr "" @@ -1154,7 +1154,7 @@ msgstr "" msgid "Network fee: {sats} sat/vByte" msgstr "" -#: src/components/Tour.vue:58 +#: src/components/Tour.vue:51 msgid "Next" msgstr "" @@ -1320,7 +1320,7 @@ msgstr "" msgid "Popups" msgstr "" -#: src/components/Tour.vue:44 +#: src/components/Tour.vue:38 msgid "Previous" msgstr "" diff --git a/src/lib/tour/network/index.ts b/src/lib/tour/network/index.ts index d16869195..f9e463887 100644 --- a/src/lib/tour/network/index.ts +++ b/src/lib/tour/network/index.ts @@ -16,6 +16,7 @@ export function getNetworkTourSteps(): TourSteps { }, }, ui: {}, + lifecycle: {}, }, }; } diff --git a/src/lib/tour/onboarding/01_FirstAddressStep.ts b/src/lib/tour/onboarding/01_FirstAddressStep.ts index a05aaecdd..5baca4918 100644 --- a/src/lib/tour/onboarding/01_FirstAddressStep.ts +++ b/src/lib/tour/onboarding/01_FirstAddressStep.ts @@ -5,65 +5,93 @@ import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { onboardingTexts } from './OnboardingTourTexts'; export function getFirstAddressStep({ isMobile, root }: GetStepFnArgs): TourStep { - const firstAddressLifecycle: TourStep['lifecycle'] = { - created: () => { - const { setActiveCurrency } = useAccountStore(); - const { addressInfos, selectAddress } = useAddressStore(); - setActiveCurrency(CryptoCurrency.NIM); - selectAddress(addressInfos.value[0].address); - }, + const created = () => { + const { setActiveCurrency } = useAccountStore(); + const { addressInfos, selectAddress } = useAddressStore(); + setActiveCurrency(CryptoCurrency.NIM); + selectAddress(addressInfos.value[0].address); }; + const path = '/'; - const firstAddressMobile: TourStep = { - path: '/', - tooltip: { - target: '.address-list > .address-button .identicon img', - content: onboardingTexts[OnboardingTourStep.FIRST_ADDRESS].default, - params: { - placement: 'bottom-start', + return isMobile.value + ? { + path, + tooltip: { + target: '.address-list > .address-button .identicon img', + content: onboardingTexts[OnboardingTourStep.FIRST_ADDRESS].default, + params: { + placement: 'bottom-start', + }, }, - }, - lifecycle: { - ...firstAddressLifecycle, - mounted: ({ goToNextStep }) => { - if (!isMobile) { - return undefined; - } + lifecycle: { + created, + mounted: ({ goToNextStep }) => { + if (!isMobile) { + return undefined; + } - // Listener for the first address button only for mobile + // Listener for the first address button only for mobile - const addressButton = document - .querySelector('.address-list > .address-button') as HTMLButtonElement; + const addressButton = document + .querySelector('.address-list > .address-button') as HTMLButtonElement; - let addressClicked = false; - const onClick = (e: MouseEvent) => { - addressClicked = true; - goToNextStep(); - e.preventDefault(); - e.stopPropagation(); - }; + let addressClicked = false; + const onClick = (e: MouseEvent) => { + addressClicked = true; + goToNextStep(); + e.preventDefault(); + e.stopPropagation(); + }; - addressButton!.addEventListener('click', onClick, { once: true, capture: true }); + addressButton!.addEventListener('click', onClick, { once: true, capture: true }); - return async (args) => { - if (!args?.ending && !addressClicked && root.$route.path === firstAddressMobile.path) { - addressButton!.click(); - await root.$nextTick(); - } - addressButton!.removeEventListener('click', onClick, true); - }; + return async (args) => { + if (!args?.ending && !addressClicked && root.$route.path === path) { + addressButton!.click(); + await root.$nextTick(); + } + addressButton!.removeEventListener('click', onClick, true); + }; + }, }, - }, - ui: { - fadedElements: [ - '.account-overview .backup-warning', - '.account-overview .mobile-menu-bar', - '.account-overview .bitcoin-account', - '.account-overview .account-balance-container', - '.account-overview .mobile-action-bar', - ], - }, - }; + ui: { + fadedElements: [ + '.account-overview .backup-warning', + '.account-overview .mobile-menu-bar', + '.account-overview .bitcoin-account', + '.account-overview .account-balance-container', + '.account-overview .mobile-action-bar', + ], + }, + } as TourStep - return firstAddressMobile; + // Not mobile + : { + path, + tooltip: { + target: '.address-overview .active-address', + content: onboardingTexts[OnboardingTourStep.FIRST_ADDRESS].default, + params: { + placement: 'left-start', + }, + }, + lifecycle: { + created, + }, + ui: { + disabledElements: [ + '.address-overview', + ], + fadedElements: [ + '.sidebar', + '.account-overview .backup-warning', + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + disabledButtons: [ + '.address-overview .transaction-list a button', + ], + }, + } as TourStep; } diff --git a/src/lib/tour/onboarding/02_TransactionListStep.ts b/src/lib/tour/onboarding/02_TransactionListStep.ts index 821e3cbc8..63d4832c5 100644 --- a/src/lib/tour/onboarding/02_TransactionListStep.ts +++ b/src/lib/tour/onboarding/02_TransactionListStep.ts @@ -2,8 +2,42 @@ import { useTransactionsStore } from '@/stores/Transactions'; import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; export function getTransactionListStep( - { root, steps, toggleDisabledAttribute }: GetStepFnArgs): TourStep { - return { + { root, steps, isMobile }: GetStepFnArgs): TourStep { + const mounted = () => { + const { transactions } = useTransactionsStore().state; + + if (Object.values(transactions.value || []).length === 0) { + const unwatch = root.$watch(() => useTransactionsStore().state.transactions, (txs) => { + if (!Object.values(txs).length) { + unwatch(); + return; + } + + const buyNimBtn = document + .querySelector('.address-overview .transaction-list a button') as HTMLButtonElement; + buyNimBtn.disabled = true; + + // Once the user has at least one transaction, step TRANSACTIONS_LIST is modified + steps[OnboardingTourStep.TRANSACTIONS_LIST] = { + path: steps[OnboardingTourStep.TRANSACTIONS_LIST]?.path, + tooltip: { + target: '.vue-recycle-scroller__item-wrapper', + content: ['This is where all your transactions will appear.'], + params: { + placement: 'bottom', + }, + }, + ui: { + ...steps[OnboardingTourStep.TRANSACTIONS_LIST]!.ui, + isNextStepDisabled: false, + }, + } as TourStep; + unwatch(); + }); + } + }; + + const stepForMobile: TourStep = { path: '/transactions', tooltip: { target: '.transaction-list > .empty-state h2', @@ -15,48 +49,7 @@ export function getTransactionListStep( placement: 'top', }, }, - lifecycle: { - mounted: () => { - const { transactions } = useTransactionsStore().state; - - if (Object.values(transactions.value || []).length === 0) { - const unwatch = root.$watch(() => useTransactionsStore().state.transactions, (txs) => { - if (!Object.values(txs).length) { - unwatch(); - return; - } - - // Once the user has at least one transaction, step TRANSACTIONS_LIST is modified - steps[OnboardingTourStep.TRANSACTIONS_LIST]!.tooltip = { - target: '.vue-recycle-scroller__item-wrapper', - content: ['This is where all your transactions will appear.'], - params: { - placement: 'bottom', - }, - }; - steps[OnboardingTourStep.TRANSACTIONS_LIST]!.ui.isNextStepDisabled = false; - toggleDisabledAttribute('.address-overview .transaction-list a button', true); - steps[OnboardingTourStep.TRANSACTIONS_LIST]!.lifecycle = { - created: async () => { - await toggleDisabledAttribute( - '.address-overview .transaction-list a button', true); - }, - mounted() { - return (args) => { - if (args?.ending || !args?.goingForward) { - setTimeout(() => { - toggleDisabledAttribute( - '.address-overview .transaction-list a button', false); - }, args?.ending ? 0 : 1000); - } - }; - }, - }; - unwatch(); - }); - } - }, - }, + lifecycle: { mounted }, ui: { fadedElements: [ '.address-overview .mobile-action-bar', @@ -66,6 +59,38 @@ export function getTransactionListStep( '.address-overview .active-address', ], isNextStepDisabled: true, + disabledButtons: ['.address-overview .transaction-list a button'], }, }; + + const stepForNotMobile: TourStep = { + path: '/', + tooltip: { + target: '.address-overview', + content: [ + 'This is where all your transactions will appear.', + 'Click the green button to receive a free NIM from Team Nimiq.', + ], + params: { + placement: 'left', + }, + }, + lifecycle: { mounted }, + ui: { + disabledElements: [ + '.address-overview', + ], + fadedElements: [ + '.sidebar', + '.account-overview .backup-warning', + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + isNextStepDisabled: true, + disabledButtons: ['.address-overview .transaction-list a button'], + }, + }; + + return isMobile.value ? stepForMobile : stepForNotMobile; } diff --git a/src/lib/tour/onboarding/03_FirstTransactionStep.ts b/src/lib/tour/onboarding/03_FirstTransactionStep.ts index 5c6a3e6fd..856c0cf8b 100644 --- a/src/lib/tour/onboarding/03_FirstTransactionStep.ts +++ b/src/lib/tour/onboarding/03_FirstTransactionStep.ts @@ -1,9 +1,8 @@ import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { onboardingTexts } from './OnboardingTourTexts'; -export function getFirstTransactionStep( - { toggleDisabledAttribute }: GetStepFnArgs): TourStep { - return { +export function getFirstTransactionStep({ isMobile }: GetStepFnArgs): TourStep { + const stepForMobile = { path: '/transactions', tooltip: { target: '.transaction-list .list-element > .transaction > .identicon', @@ -12,20 +11,6 @@ export function getFirstTransactionStep( placement: 'bottom-start', }, }, - lifecycle: { - created: async () => { - await toggleDisabledAttribute('.address-overview .transaction-list a button', true); - }, - mounted() { - return (args) => { - if (args?.ending || args?.goingForward) { - setTimeout(() => { - toggleDisabledAttribute('.address-overview .transaction-list a button', false); - }, args?.ending ? 0 : 1000); - } - }; - }, - }, ui: { fadedElements: [ '.address-overview .mobile-action-bar', @@ -33,6 +18,33 @@ export function getFirstTransactionStep( disabledElements: [ '.address-overview', ], + disabledButtons: ['.address-overview .transaction-list a button'], }, } as TourStep; + + const stepForNotMobile = { + path: '/transactions', + tooltip: { + target: '.address-overview .transaction-list .vue-recycle-scroller__item-view:nth-child(2)', + content: onboardingTexts[OnboardingTourStep.FIRST_TRANSACTION].default, + params: { + placement: 'left', + }, + }, + ui: { + disabledElements: [ + '.address-overview', + ], + fadedElements: [ + '.sidebar', + '.account-overview .backup-warning', + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + ], + disabledButtons: ['.address-overview .transaction-list a button'], + }, + } as TourStep; + + return isMobile.value ? stepForMobile : stepForNotMobile; } diff --git a/src/lib/tour/onboarding/04_BitcoinAddressStep.ts b/src/lib/tour/onboarding/04_BitcoinAddressStep.ts index ba2c9e22f..ec32c9062 100644 --- a/src/lib/tour/onboarding/04_BitcoinAddressStep.ts +++ b/src/lib/tour/onboarding/04_BitcoinAddressStep.ts @@ -1,17 +1,17 @@ -import { OnboardingTourStep, TourStep } from '../types'; +import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { onboardingTexts } from './OnboardingTourTexts'; -export function getBitcoinAddressStep() { +export function getBitcoinAddressStep({ isMobile }: GetStepFnArgs): TourStep { return { path: '/', tooltip: { - target: '.account-overview .bitcoin-account > .bitcoin-account-item > svg', + target: `.account-overview .bitcoin-account ${isMobile.value ? '> .bitcoin-account-item > svg' : ''}`, content: onboardingTexts[OnboardingTourStep.BITCOIN_ADDRESS].default, params: { - placement: 'top-start', + placement: isMobile.value ? 'top-start' : 'right-end', }, }, - ui: { + ui: isMobile.value ? { fadedElements: [ '.account-overview .backup-warning', '.account-overview .mobile-menu-bar', @@ -22,6 +22,22 @@ export function getBitcoinAddressStep() { disabledElements: [ '.account-overview .bitcoin-account', ], + } : { + fadedElements: [ + '.sidebar', + '.account-overview .backup-warning', + '.account-overview .mobile-menu-bar', + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .mobile-action-bar', + ], + disabledButtons: [ + '.address-overview .transaction-list a button', + ], + disabledElements: [ + '.account-overview .bitcoin-account', + '.address-overview', + ], }, } as TourStep; } diff --git a/src/lib/tour/onboarding/05_WalletBalanceStep.ts b/src/lib/tour/onboarding/05_WalletBalanceStep.ts index 15fb80d1b..543ee6459 100644 --- a/src/lib/tour/onboarding/05_WalletBalanceStep.ts +++ b/src/lib/tour/onboarding/05_WalletBalanceStep.ts @@ -1,20 +1,23 @@ -import { OnboardingTourStep, TourStep } from '../types'; +import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { onboardingTexts } from './OnboardingTourTexts'; -export function getWalletBalanceStep(): TourStep { +export function getWalletBalanceStep({ isMobile }: GetStepFnArgs): TourStep { return { path: '/', tooltip: { - target: '.account-overview .account-balance-container .amount', + target: `.account-overview .account-balance-container + ${isMobile.value ? '.amount' : '.balance-distribution'}`, content: onboardingTexts[OnboardingTourStep.WALLET_BALANCE].default, params: { - placement: 'bottom', + placement: isMobile.value ? 'bottom' : 'right', }, }, - ui: { + ui: isMobile.value ? { fadedElements: [ + '.sidebar', '.account-overview .backup-warning', '.account-overview .mobile-action-bar', + '.address-overview', ], disabledElements: [ '.account-overview .mobile-menu-bar', @@ -22,6 +25,20 @@ export function getWalletBalanceStep(): TourStep { '.account-overview .address-list', '.account-overview .bitcoin-account', ], + } : { + fadedElements: [ + '.sidebar', + '.account-overview .backup-warning', + ], + disabledButtons: [ + '.address-overview .transaction-list a button', + ], + disabledElements: [ + '.account-overview .account-balance-container', + '.account-overview .address-list', + '.account-overview .bitcoin-account', + '.address-overview', + ], }, } as TourStep; } diff --git a/src/lib/tour/onboarding/06_0_BackupAlertStep.ts b/src/lib/tour/onboarding/06_0_BackupAlertStep.ts index e92123250..af06b330f 100644 --- a/src/lib/tour/onboarding/06_0_BackupAlertStep.ts +++ b/src/lib/tour/onboarding/06_0_BackupAlertStep.ts @@ -1,8 +1,8 @@ -import { OnboardingTourStep, TourStep } from '../types'; +import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { onboardingTexts } from './OnboardingTourTexts'; -export function getBackupAlertStep(): TourStep { - return { +export function getBackupAlertStep({ isMobile }: GetStepFnArgs): TourStep { + return isMobile.value ? { path: '/', tooltip: { target: '.account-overview .backup-warning button', @@ -21,5 +21,21 @@ export function getBackupAlertStep(): TourStep { '.account-overview .bitcoin-account', ], }, - } as TourStep; + } : { + path: '/', + tooltip: { + target: '.account-overview .backup-warning', + content: onboardingTexts[OnboardingTourStep.BACKUP_ALERT].default, + params: { + placement: 'right', + }, + }, + ui: { + fadedElements: ['.sidebar'], + disabledElements: [ + '.account-overview', + ], + disabledButtons: ['.address-overview .transaction-list a button'], + }, + }; } diff --git a/src/lib/tour/onboarding/06_1_MenuIconStep.ts b/src/lib/tour/onboarding/06_1_MenuIconStep.ts index 454136a05..1c78e2196 100644 --- a/src/lib/tour/onboarding/06_1_MenuIconStep.ts +++ b/src/lib/tour/onboarding/06_1_MenuIconStep.ts @@ -27,6 +27,10 @@ export function getMenuIconStep(): TourStep { '.account-overview .account-balance-container', '.account-overview .address-list', '.account-overview .bitcoin-account', + '.address-overview', + ], + disabledButtons: [ + '.address-overview .transaction-list a button', ], isNextStepDisabled: true, }, diff --git a/src/lib/tour/onboarding/07_AccountOptionsStep.ts b/src/lib/tour/onboarding/07_AccountOptionsStep.ts index 14098f06a..ca9291e6d 100644 --- a/src/lib/tour/onboarding/07_AccountOptionsStep.ts +++ b/src/lib/tour/onboarding/07_AccountOptionsStep.ts @@ -1,8 +1,20 @@ import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { onboardingTexts } from './OnboardingTourTexts'; -export function getAccountOptionsStep({ sleep, root }: GetStepFnArgs): TourStep { - return { +export function getAccountOptionsStep({ isTablet, sleep, root }: GetStepFnArgs): TourStep { + const onClickBackdrop = (e: Event) => { + e.stopPropagation(); + e.preventDefault(); + }; + const toggleBackdropInteractivity = async (allow: boolean) => { + const backdrop = document.querySelector('.modal.backdrop') as HTMLDivElement; + const action = allow ? 'addEventListener' : 'removeEventListener'; + + backdrop[action]('click', onClickBackdrop, true); + await root.$nextTick(); + }; + + const stepForSmallScreens = { path: '/?sidebar=true', tooltip: { target: '.modal .small-page', @@ -13,7 +25,7 @@ export function getAccountOptionsStep({ sleep, root }: GetStepFnArgs { + e.stopPropagation(); + e.preventDefault(); + }, { capture: true }); + return async () => { const closeBtn = (document.querySelector('.modal .close-button') as HTMLDivElement); closeBtn.click(); + await sleep(200); // TODO Check this random value + }; + }, + }, + } as TourStep; - await sleep(500); // TODO Check this random value + const stepForLargeScreens = { + path: '/', + tooltip: { + target: '.modal .small-page', + content: onboardingTexts[OnboardingTourStep.ACCOUNT_OPTIONS].default, + params: { + placement: 'right', + }, + }, + ui: { + disabledElements: [ + '.modal .wrapper', // TODO Show cursor not-allowed + ], + disabledButtons: [ + '.address-overview .transaction-list a button', + ], + }, + lifecycle: { + created: async () => { + await sleep(200); // TODO Check this random value + const account = document.querySelector('.sidebar .account-menu') as HTMLButtonElement; + account.click(); + }, + mounted: async () => { + await toggleBackdropInteractivity(true); + + return async () => { + // TODO + // await toggleBackdropInteractivity(false); + // const backdrop = document.querySelector('.modal.backdrop') as HTMLDivElement; + // backdrop.click(); + // await sleep(200); // TODO Check this random value }; }, }, } as TourStep; + + return isTablet.value ? stepForSmallScreens : stepForLargeScreens; } diff --git a/src/lib/tour/onboarding/08_OnboardingCompleted.ts b/src/lib/tour/onboarding/08_OnboardingCompleted.ts index 518f3d9e1..ff1849ecd 100644 --- a/src/lib/tour/onboarding/08_OnboardingCompleted.ts +++ b/src/lib/tour/onboarding/08_OnboardingCompleted.ts @@ -2,20 +2,23 @@ import { useAccountStore } from '@/stores/Account'; import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { onboardingTexts } from './OnboardingTourTexts'; -export function getOnboardingCompletedStep({ root }: GetStepFnArgs): TourStep { +export function getOnboardingCompletedStep({ root, isTablet }: GetStepFnArgs): TourStep { return { - path: '/?sidebar=true', + path: isTablet.value ? '/?sidebar=true' : '/', tooltip: { - target: '.column-sidebar .network .consensus-icon', + target: `.sidebar .network ${isTablet.value ? '.consensus-icon' : 'span'}`, content: onboardingTexts[OnboardingTourStep.ONBOARDING_COMPLETED].default, params: { - placement: 'top-start', + placement: isTablet.value ? 'top-start' : 'right', }, button: { text: 'Go to Network', - fn: () => { + fn: async (endTour) => { + if (endTour) { + await endTour(); + } const { setTour } = useAccountStore(); - setTour(null); + setTour('network'); root.$router.push('/network'); }, }, diff --git a/src/lib/tour/onboarding/OnboardingTourTexts.ts b/src/lib/tour/onboarding/OnboardingTourTexts.ts index 44d9d7657..fa78f44bd 100644 --- a/src/lib/tour/onboarding/OnboardingTourTexts.ts +++ b/src/lib/tour/onboarding/OnboardingTourTexts.ts @@ -28,8 +28,7 @@ export const onboardingTexts: TourStepTexts = { }, [OnboardingTourStep.BITCOIN_ADDRESS]: { default: [ - 'Check the bar-chart to see how your addresses compose your total balance.', - 'Currently you have 100% NIM, and no BTC.', + 'This is your Bitcoin wallet. You get one with every Nimiq account.', ], }, [OnboardingTourStep.WALLET_BALANCE]: { @@ -40,8 +39,7 @@ export const onboardingTexts: TourStepTexts = { }, [OnboardingTourStep.BACKUP_ALERT]: { default: [ - 'There is no \'forgot password\'.', - 'Create a backup to make sure you stay in control.', + 'There is no \'forgot password\'. Create a backup to make sure you stay in control.', ], }, [OnboardingTourStep.MENU_ICON]: { diff --git a/src/lib/tour/onboarding/index.ts b/src/lib/tour/onboarding/index.ts index a464dc306..0b211524c 100644 --- a/src/lib/tour/onboarding/index.ts +++ b/src/lib/tour/onboarding/index.ts @@ -40,10 +40,12 @@ export function getOnboardingTourSteps({ root }: SetupContext): TourSteps void; goingForward: boolean; + ending: boolean; } -export type MountedReturnFn = ((args?: { goingForward: boolean, ending?: boolean }) => Promise | void); +export type MountedReturnFn = ((args: Omit) => Promise | void); export interface TourStep { path: '/' | '/transactions' | '/?sidebar=true' | '/network'; @@ -48,13 +48,14 @@ export interface TourStep { }, button?: { text: string, - fn: () => void, + fn: (callback?: () => Promise) => void, }, }; lifecycle?: { - created?: (args: LifecycleArgs) => Promise | void, - mounted?: (args: LifecycleArgs) => MountedReturnFn | Promise | void, + created?: (args: Omit) => Promise | void, + mounted?: (args: LifecycleArgs) => + MountedReturnFn | Promise | void, }; ui: { @@ -65,6 +66,8 @@ export interface TourStep { disabledElements?: string[], // array of selectors isNextStepDisabled?: boolean, + + disabledButtons?: string[], }; } @@ -80,3 +83,8 @@ export type GetStepFnArgs = toggleDisabledAttribute: (selector: string, disabled: boolean) => Promise, sleep: (ms: number) => Promise, }; + +export interface TourDataBroadcast { + currentStep: TourStepIndex; + nSteps: number; +} diff --git a/src/stores/Account.ts b/src/stores/Account.ts index 11f4c081e..85a4b0dfe 100644 --- a/src/stores/Account.ts +++ b/src/stores/Account.ts @@ -1,6 +1,6 @@ import { createStore } from 'pinia'; import { Account } from '@nimiq/hub-api'; -import { TourName } from '@/composables/useTour'; +import { TourName } from '@/lib/tour'; import { useAddressStore } from './Address'; import { CryptoCurrency } from '../lib/Constants'; From 3c40a1e18982d3b1d274fb42aa71814ad15c4b01 Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 27 Jan 2022 14:58:04 +0100 Subject: [PATCH 11/44] onboarding tour draft finished --- src/App.vue | 10 +- src/components/AccountBalance.vue | 4 +- src/components/AccountMenu.vue | 1 + src/components/BtcTransactionList.vue | 4 +- src/components/MobileActionBar.vue | 6 +- src/components/Tour.vue | 149 ++++++++++++++---- src/components/TourLargeScreenManager.vue | 48 ++++-- src/components/TransactionList.vue | 4 +- src/components/icons/PartyConfettiIcon.vue | 5 + src/components/layouts/AccountOverview.vue | 10 +- src/components/layouts/AddressOverview.vue | 6 +- src/components/layouts/Sidebar.vue | 4 +- src/components/modals/BtcActivationModal.vue | 4 +- src/components/modals/BtcSendModal.vue | 4 +- src/components/modals/BuyCryptoModal.vue | 4 +- src/components/modals/Modal.vue | 14 +- src/components/modals/SellCryptoModal.vue | 4 +- src/components/modals/SendModal.vue | 4 +- src/components/modals/WelcomeModal.vue | 6 +- src/composables/useWindowSize.ts | 20 +-- src/lib/tour/index.ts | 13 ++ .../tour/onboarding/01_FirstAddressStep.ts | 97 ++++++------ .../tour/onboarding/02_TransactionListStep.ts | 129 ++++++++------- .../onboarding/03_FirstTransactionStep.ts | 74 ++++----- .../tour/onboarding/04_BitcoinAddressStep.ts | 67 ++++---- .../tour/onboarding/05_WalletBalanceStep.ts | 66 ++++---- .../tour/onboarding/06_0_BackupAlertStep.ts | 64 ++++---- src/lib/tour/onboarding/06_1_MenuIconStep.ts | 19 +-- .../tour/onboarding/07_AccountOptionsStep.ts | 136 ++++++++-------- .../tour/onboarding/08_OnboardingCompleted.ts | 43 +++-- src/lib/tour/onboarding/index.ts | 11 +- src/lib/tour/types.ts | 40 ++++- 32 files changed, 635 insertions(+), 435 deletions(-) create mode 100644 src/components/icons/PartyConfettiIcon.vue diff --git a/src/App.vue b/src/App.vue index eedb4ccbd..5cf13015b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -67,7 +67,7 @@ export default defineComponent({ // Swiping const $main = ref(null); let $mobileTapArea: HTMLDivElement | null = null; - const { width, isMobile } = useWindowSize(); + const { width, isSmallScreen } = useWindowSize(); async function updateSwipeRestPosition( velocityDistance: number, @@ -136,18 +136,18 @@ export default defineComponent({ excludeSelector: '.scroller, .scroller *', }); - watch([isMobile, swipingEnabled], ([isMobileNow, newSwiping], [wasMobile, oldSwiping]) => { + watch([isSmallScreen, swipingEnabled], ([isSmallScreenNow, newSwiping], [wasMobile, oldSwiping]) => { if (!$main.value) return; - if ((isMobileNow && !wasMobile) || (newSwiping === 1 && oldSwiping !== 1)) { + if ((isSmallScreenNow && !wasMobile) || (newSwiping === 1 && oldSwiping !== 1)) { attachSwipe(); - } else if (!isMobileNow || newSwiping !== 1) { + } else if (!isSmallScreenNow || newSwiping !== 1) { detachSwipe(); } }, { lazy: true }); onMounted(() => { - if (isMobile.value && swipingEnabled.value === 1) attachSwipe(); + if (isSmallScreen.value && swipingEnabled.value === 1) attachSwipe(); }); return { diff --git a/src/components/AccountBalance.vue b/src/components/AccountBalance.vue index e4dfc9923..1ad5ed1e4 100644 --- a/src/components/AccountBalance.vue +++ b/src/components/AccountBalance.vue @@ -63,8 +63,8 @@ export default defineComponent({ const $fiatAmountContainer = ref(null); const $fiatAmount = ref(null); - const { isFullDesktop } = useWindowSize(); - const fiatAmountMaxSize = computed(() => isFullDesktop.value ? 7 : 5.5); // rem + const { isLargeScreen } = useWindowSize(); + const fiatAmountMaxSize = computed(() => isLargeScreen.value ? 7 : 5.5); // rem const fiatAmountFontSize = ref(fiatAmountMaxSize.value); async function updateFontSize() { diff --git a/src/components/AccountMenu.vue b/src/components/AccountMenu.vue index 0bcca8b03..2c7c63315 100644 --- a/src/components/AccountMenu.vue +++ b/src/components/AccountMenu.vue @@ -37,6 +37,7 @@ import { useAccountStore, AccountType } from '../stores/Account'; import { useAddressStore } from '../stores/Address'; export default defineComponent({ + name: 'account-menu', setup(props, context) { const { activeAccountInfo } = useAccountStore(); const { state: addressState } = useAddressStore(); diff --git a/src/components/BtcTransactionList.vue b/src/components/BtcTransactionList.vue index b6e3bd9e0..1935d5dc6 100644 --- a/src/components/BtcTransactionList.vue +++ b/src/components/BtcTransactionList.vue @@ -150,8 +150,8 @@ export default defineComponent({ const scrollerBuffer = 300; // Height of items in pixel - const { isMobile } = useWindowSize(); - const itemSize = computed(() => isMobile.value ? 68 : 72); // mobile: 64px + 4px margin between items + const { isSmallScreen } = useWindowSize(); + const itemSize = computed(() => isSmallScreen.value ? 68 : 72); // mobile: 64px + 4px margin between items // Get all transactions for the active addresses const txsForActiveAddress = computed(() => Object.values(btcTransactions$.transactions) diff --git a/src/components/MobileActionBar.vue b/src/components/MobileActionBar.vue index 7f3af67d9..de0a81c81 100644 --- a/src/components/MobileActionBar.vue +++ b/src/components/MobileActionBar.vue @@ -30,7 +30,7 @@ export default defineComponent({ const { activeAddressInfo, addressInfos } = useAddressStore(); const { activeCurrency, activeAccountInfo, hasBitcoinAddresses } = useAccountStore(); const { accountBalance } = useBtcAddressStore(); - const { isMobile } = useWindowSize(); + const { isSmallScreen } = useWindowSize(); const { activeMobileColumn } = useActiveMobileColumn(); function nimOrBtc(nim: T, btc: T): T { @@ -45,7 +45,7 @@ export default defineComponent({ addressInfos.value.filter(({ type }) => type === AddressType.BASIC).length > 1)); function receive() { - if (isMobile.value + if (isSmallScreen.value && activeMobileColumn.value !== ColumnType.ADDRESS && (hasMultipleReceivableAddresses.value || hasBitcoinAddresses.value) ) { @@ -60,7 +60,7 @@ export default defineComponent({ activeAccountInfo.value && activeAccountInfo.value.addresses.length > 1); function send() { - if (isMobile.value + if (isSmallScreen.value && activeMobileColumn.value !== ColumnType.ADDRESS && (hasMultipleSendableAddresses.value || hasBitcoinAddresses.value) ) { diff --git a/src/components/Tour.vue b/src/components/Tour.vue index 11735aa46..4c0ec652a 100644 --- a/src/components/Tour.vue +++ b/src/components/Tour.vue @@ -21,19 +21,21 @@ :labels="tour.labels" >
-

-

+
+ +

{{ $t(content) }}

+
-
+
Simulate Receive NIM
- @@ -44,13 +46,13 @@ {{ $t(tour.steps[tour.currentStep].button.text) }} -
@@ -59,7 +61,7 @@ -
+
@@ -112,15 +114,16 @@ import { } from '../lib/tour'; import CaretRightIcon from './icons/CaretRightIcon.vue'; import TourPreviousLeftArrowIcon from './icons/TourPreviousLeftArrowIcon.vue'; +import PartyConfettiIcon from './icons/PartyConfettiIcon.vue'; Vue.use(VueTour); require('vue-tour/dist/vue-tour.css'); export default defineComponent({ - name: 'tour', + name: 'nimiq-tour', setup(props, context) { - const { isMobile, isTablet, isFullDesktop } = useWindowSize(); + const { isSmallScreen, isMediumScreen, isLargeScreen } = useWindowSize(); const { state: $network } = useNetworkStore(); const disconnected = computed(() => $network.consensus !== 'established'); @@ -150,12 +153,14 @@ export default defineComponent({ // Initial state const isLoading = ref(true); - const currentStep: Ref = tourStore.tour === 'onboarding' ? ref(5) : ref(0); // TODO Remove + const currentStep: Ref = ref(0); const nSteps: Ref = ref(0); const disableNextStep = ref(true); let unmounted: MountedReturnFn | void; + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + onMounted(async () => { await tourSetup(); @@ -177,6 +182,7 @@ export default defineComponent({ await step.lifecycle.created({ goToNextStep, goingForward: true }); } + _toggleDisabledButtons(step.ui.disabledButtons, true); _addAttributes(step.ui, currentStep.value); unmounted = await step.lifecycle?.mounted?.({ @@ -189,12 +195,16 @@ export default defineComponent({ context.root.$router.push(step.path); } - await sleep(500); + // ensures animation ends + await sleep(1000); tour = context.root.$tours['nimiq-tour']; tour.start(`${currentStep.value}`); isLoading.value = false; + window.addEventListener('keyup', onKeyDown); + + // This is to communicate with TourLargeScreenManager _broadcastTourData(); context.root.$on('tour-end', () => { endTour(); @@ -203,7 +213,7 @@ export default defineComponent({ // Dont allow user to interact with the page while it is loading // But allow to end it - watch([isLoading, disconnected], () => { + watch([isLoading, disconnected], async () => { const app = document.querySelector('#app main') as HTMLDivElement; if (isLoading.value || disconnected.value) { @@ -211,25 +221,30 @@ export default defineComponent({ } else { app.removeAttribute('data-non-interactable'); } + + // FIXME we should wait until the button is rendered and the we could + // execute _toggleDisabledButtons but it is kind of random the amount of time + // it takes to render the button. I don't know how to fix it. + + // Ensure that we disabled 'Receive Free NIM' button + await sleep(500); + _toggleDisabledButtons(steps[currentStep.value]?.ui.disabledButtons, true); }); function goToPrevStep() { - if (currentStep.value <= 0) return; + if (currentStep.value <= 0 || disconnected.value || isLoading.value) return; _moveToFutureStep(currentStep.value, currentStep.value - 1); } function goToNextStep() { - if (currentStep.value + 1 >= nSteps.value) return; + if (currentStep.value + 1 >= nSteps.value || disconnected.value || isLoading.value) return; _moveToFutureStep(currentStep.value, currentStep.value + 1); } - const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - async function _moveToFutureStep( currentStepIndex: TourStepIndex, futureStepIndex: TourStepIndex, ) { - // TODO https://stackoverflow.com/questions/42990308/vue-js-how-to-call-method-from-another-component const goingForward = futureStepIndex > currentStepIndex; const { path: currentPath, ui: currentUI } = steps[currentStepIndex]!; @@ -284,19 +299,40 @@ export default defineComponent({ } function _broadcastTourData() { + // Send data to TourLargeScreenManager context.root.$emit('tour-data', { nSteps: nSteps.value, currentStep: currentStep.value, }); } + // TODO In tablets 'buy nim' in sidebar does not get its original state + let _buttonNimClasses: {[x:string]: string} = {}; function _toggleDisabledButtons(disabledButtons: TourStep['ui']['disabledButtons'], disabled:boolean) { if (!disabledButtons) return; + + // Classes that have to be removed while the tour is shown + const btnNimiqClasses = ['light-blue', 'green', 'orange', 'red', 'gold']; disabledButtons.forEach((element) => { const el = document.querySelector(element) as HTMLButtonElement; if (!el) return; el.disabled = disabled; + + for (const className of el.classList.values() ?? []) { + if (disabled && btnNimiqClasses.includes(className)) { + el.classList.remove(className); + _buttonNimClasses[element] = className; + } + } }); + if (!disabled) { + Object.keys(_buttonNimClasses).forEach((el) => { + const btn = document.querySelector(el) as HTMLButtonElement; + if (!btn) return; + btn.classList.add(_buttonNimClasses[el]); + }); + _buttonNimClasses = {}; + } } function _addAttributes( @@ -337,6 +373,7 @@ export default defineComponent({ async function endTour() { _removeAttributes(currentStep.value); _toggleDisabledButtons(steps[currentStep.value]?.ui.disabledButtons, false); + window.removeEventListener('keyup', onKeyDown); if (unmounted) { await unmounted({ ending: true, goingForward: false }); @@ -350,6 +387,24 @@ export default defineComponent({ context.root.$off('tour-end'); } + function onKeyDown(event: KeyboardEvent) { + switch (event.key) { + case 'ArrowRight': + if (!disableNextStep.value) { + goToNextStep(); + } + break; + case 'ArrowLeft': + goToPrevStep(); + break; + case 'Escape': + endTour(); + break; + default: + break; + } + } + // TODO REMOVE ME - Simulate tx function simulate() { const { addTransactions } = useTransactionsStore(); @@ -358,9 +413,9 @@ export default defineComponent({ } return { - isMobile, - isTablet, - isFullDesktop, + isSmallScreen, + isMediumScreen, + isLargeScreen, // tour tourOptions, @@ -379,11 +434,13 @@ export default defineComponent({ // TODO REMOVE ME simulate, + _buttonNimClasses, }; }, components: { CaretRightIcon, TourPreviousLeftArrowIcon, + PartyConfettiIcon, CircleSpinner, }, }); @@ -404,9 +461,17 @@ export default defineComponent({ pointer-events: none !important; } -#app > *:not(.tour) { +#app > *:not(.tour):not(.tour-manager) { cursor: not-allowed; } + +button.highlighted { + background: linear-gradient( + 274.28deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.2) 27.6%, rgba(255, 255, 255, 0) 53.12%, + rgba(255, 255, 255, 0.2) 81.25%, rgba(255, 255, 255, 0) 100% + ), + radial-gradient(100% 100% at 100% 100%, #41A38E 0%, #21BCA5 100%) !important; +} diff --git a/src/components/modals/StartOnBoardingTourModal.vue b/src/components/modals/StartOnBoardingTourModal.vue deleted file mode 100644 index 104aff83c..000000000 --- a/src/components/modals/StartOnBoardingTourModal.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - diff --git a/src/components/modals/WelcomeModal.vue b/src/components/modals/WelcomeModal.vue deleted file mode 100644 index de69e22ec..000000000 --- a/src/components/modals/WelcomeModal.vue +++ /dev/null @@ -1,251 +0,0 @@ - - - - - diff --git a/src/i18n/en.po b/src/i18n/en.po index 5b97e3d5d..5f5f29e39 100644 --- a/src/i18n/en.po +++ b/src/i18n/en.po @@ -203,10 +203,6 @@ msgstr "" msgid "All countries" msgstr "" -#: src/components/modals/WelcomeModal.vue:35 -msgid "All in one easy place." -msgstr "" - #: src/components/layouts/AccountOverview.vue:91 #: src/components/LegacyAccountNotice.vue:37 msgid "All new features are exclusive to new accounts. Upgrade now, it only takes seconds." @@ -232,10 +228,6 @@ msgstr "" msgid "An update to the Wallet is available." msgstr "" -#: src/components/modals/WelcomeModal.vue:27 -msgid "And {BTC}, the gold standard of crypto and a sound store of value." -msgstr "" - #: src/components/modals/TradeModal.vue:30 msgid "Android and iOS App" msgstr "" @@ -348,7 +340,7 @@ msgstr "" msgid "BTC network fee" msgstr "" -#: src/components/layouts/Sidebar.vue:30 +#: src/components/layouts/Sidebar.vue:31 msgid "Buy" msgstr "" @@ -524,12 +516,6 @@ msgstr "" msgid "Congrats" msgstr "" -#: src/components/modals/WelcomeModal.vue:15 -msgid "" -"Connect directly to the Nimiq blockchain.\n" -"Be independent from any middleman." -msgstr "" - #: src/components/NetworkStats.vue:8 msgid "Connected to" msgstr "" @@ -568,7 +554,6 @@ msgstr "" #: src/components/modals/MigrationWelcomeModal.vue:87 #: src/components/modals/SendModal.vue:87 -#: src/components/modals/WelcomeModal.vue:63 msgid "Continue" msgstr "" @@ -711,7 +696,7 @@ msgstr "" msgid "Enable experimental support for swiping gestures on mobile." msgstr "" -#: src/components/Tour.vue:64 +#: src/components/Tour.vue:67 msgid "End Tour" msgstr "" @@ -876,10 +861,6 @@ msgstr "" msgid "Got it!" msgstr "" -#: src/components/modals/WelcomeModal.vue:5 -msgid "Great, you’re here!" -msgstr "" - #: src/components/modals/MigrationWelcomeModal.vue:29 msgid "Handling multiple addresses is now convenient and easy – with one password and shared login information." msgstr "" @@ -996,10 +977,6 @@ msgstr "" msgid "Is your bank eligible?" msgstr "" -#: src/components/modals/WelcomeModal.vue:53 -msgid "It is fast, safe and makes you truly independent." -msgstr "" - #: src/components/modals/StartOnBoardingTourModal.vue:16 msgid "It's free, does not collect data and is controlled by no one but you." msgstr "" @@ -1037,10 +1014,6 @@ msgstr "" msgid "Let's go" msgstr "" -#: src/components/modals/WelcomeModal.vue:64 -msgid "Let's go!" -msgstr "" - #: src/components/NetworkMap.vue:30 msgid "Light Node" msgstr "" @@ -1146,7 +1119,7 @@ msgstr "" msgid "Name your contact" msgstr "" -#: src/components/layouts/Sidebar.vue:50 +#: src/components/layouts/Sidebar.vue:51 msgid "Network" msgstr "" @@ -1154,7 +1127,7 @@ msgstr "" msgid "Network fee: {sats} sat/vByte" msgstr "" -#: src/components/Tour.vue:51 +#: src/components/Tour.vue:54 msgid "Next" msgstr "" @@ -1320,7 +1293,7 @@ msgstr "" msgid "Popups" msgstr "" -#: src/components/Tour.vue:38 +#: src/components/Tour.vue:41 msgid "Previous" msgstr "" @@ -1491,7 +1464,7 @@ msgstr "" msgid "Select which unit to show Bitcoin amounts in." msgstr "" -#: src/components/layouts/Sidebar.vue:35 +#: src/components/layouts/Sidebar.vue:36 msgid "Sell" msgstr "" @@ -1531,10 +1504,6 @@ msgstr "" msgid "Send all" msgstr "" -#: src/components/modals/WelcomeModal.vue:50 -msgid "Send and receive NIM directly on the blockchain." -msgstr "" - #: src/components/modals/NetworkInfoModal.vue:16 msgid "Send and receive NIM without a middleman and enjoy true decentralization." msgstr "" @@ -1620,7 +1589,7 @@ msgstr "" msgid "Setting up the {currency} side of the swap." msgstr "" -#: src/components/layouts/Sidebar.vue:58 +#: src/components/layouts/Sidebar.vue:59 msgid "Settings" msgstr "" @@ -1704,10 +1673,6 @@ msgstr "" msgid "Start Wallet tour" msgstr "" -#: src/components/modals/WelcomeModal.vue:7 -msgid "Store, send and receive NIM." -msgstr "" - #. avoid displaying the proxy address until we know related peer address #: src/components/BtcTransactionListItem.vue:252 #: src/components/modals/BtcTransactionModal.vue:493 @@ -1797,10 +1762,6 @@ msgstr "" msgid "The new Login Files are an easy and convenient way to gain access to your account and its addresses." msgstr "" -#: src/components/modals/WelcomeModal.vue:47 -msgid "The Nimiq wallet is more than a web-wallet, it is a network node." -msgstr "" - #: src/components/modals/BuyCryptoModal.vue:44 #: src/components/modals/SellCryptoModal.vue:43 #: src/components/swap/SwapModal.vue:19 @@ -1973,8 +1934,8 @@ msgstr "" msgid "Update now" msgstr "" -#: src/components/modals/WelcomeModal.vue:22 -msgid "Use {NIM}, the super performant and browser- based payment coin." +#: src/components/modals/BtcReceiveModal.vue:55 +msgid "Use a new Bitcoin address for every transaction to improve privacy." msgstr "" #: src/components/TransactionDetailOasisPayoutStatus.vue:11 @@ -1993,6 +1954,10 @@ msgstr "" msgid "Use the slider or edit values to set up a swap." msgstr "" +#: src/components/TourLargeScreenManager.vue:4 +msgid "Use the tooltips to navigate your tour." +msgstr "" + #: src/components/layouts/Settings.vue:205 msgid "Vesting Contract" msgstr "" @@ -2117,10 +2082,6 @@ msgstr "" msgid "Your browser does not support Keyguard popups, or they are disabled in the Settings." msgstr "" -#: src/components/modals/WelcomeModal.vue:13 -msgid "Your browser is a Node" -msgstr "" - #: src/components/swap/SwapAnimation.vue:98 #: src/components/TransactionDetailOasisPayoutStatus.vue:20 msgid "Your EUR will be transferred to your bank account as soon as new limit is available." diff --git a/src/lib/tour/types.ts b/src/lib/tour/types.ts index f60f12dd7..0a4e25cff 100644 --- a/src/lib/tour/types.ts +++ b/src/lib/tour/types.ts @@ -89,12 +89,16 @@ export type GetStepFnArgs = closeAccountOptions: () => Promise, }; -export type TourBroadcast = TourBroadcastEnd | TourBroadcastStepChanged +export type TourBroadcast = TourBroadcastEnd | TourBroadcastStepChanged | TourBroadcastClickedOutsideTour interface TourBroadcastEnd { type: 'end-tour'; } +interface TourBroadcastClickedOutsideTour { + type: 'clicked-outside-tour'; +} + export interface TourBroadcastStepChanged { type: 'tour-step-changed'; payload: { diff --git a/src/router.ts b/src/router.ts index 14b28aa58..768280b28 100644 --- a/src/router.ts +++ b/src/router.ts @@ -16,6 +16,9 @@ const Network = () => // Modals const AccountMenuModal = () => import(/* webpackChunkName: "account-menu-modal" */ './components/modals/AccountMenuModal.vue'); +const DiscoverTheNimiqWalletModal = () => + import(/* webpackChunkName: "discover-the-nimiq-wallet-modal" */ + './components/modals/DiscoverTheNimiqWalletModal.vue'); const SendModal = () => import(/* webpackChunkName: "send-modal" */ './components/modals/SendModal.vue'); const ReceiveModal = () => import(/* webpackChunkName: "receive-modal" */ './components/modals/ReceiveModal.vue'); const AddressSelectorModal = () => @@ -26,8 +29,6 @@ const TradeModal = () => import(/* webpackChunkName: "trade-modal" */ './compone const BuyOptionsModal = () => import(/* webpackChunkName: "buy-options-modal" */ './components/modals/BuyOptionsModal.vue'); const ScanQrModal = () => import(/* webpackChunkName: "scan-qr-modal" */ './components/modals/ScanQrModal.vue'); -const WelcomeModal = () => - import(/* webpackChunkName: "welcome-modal" */ './components/modals/WelcomeModal.vue'); const MigrationWelcomeModal = () => import(/* webpackChunkName: "migration-welcome-modal" */ './components/modals/MigrationWelcomeModal.vue'); const DisclaimerModal = () => @@ -57,10 +58,6 @@ const MoonpayModal = () => const SimplexModal = () => import(/* webpackChunkName: "simplex-modal" */ './components/modals/SimplexModal.vue'); -// Tour modals -const StartOnBoardingTourModal = () => - import(/* webpackChunkName: "start-onboarding-tour-modal" */ './components/modals/StartOnBoardingTourModal.vue'); - Vue.use(VueRouter); export enum Columns { @@ -193,7 +190,7 @@ const routes: RouteConfig[] = [{ }, { path: '/welcome', components: { - modal: WelcomeModal, + modal: DiscoverTheNimiqWalletModal, }, name: 'welcome', meta: { column: Columns.ACCOUNT }, @@ -285,15 +282,6 @@ const routes: RouteConfig[] = [{ name: 'release-notes', props: { modal: true }, meta: { column: Columns.DYNAMIC }, - }, { - // TODO change this route - path: '/welcome-2', - components: { - modal: StartOnBoardingTourModal, - }, - name: 'welcome', - props: { modal: true }, - meta: { column: Columns.ACCOUNT }, }], }], }, { From ca41a3a93c62a71dfab220a125322b6f4021de16 Mon Sep 17 00:00:00 2001 From: onmax Date: Fri, 28 Jan 2022 21:46:52 +0100 Subject: [PATCH 15/44] add flash animation if user clicks outside tour --- src/components/Tour.vue | 54 +++++++++++++++++++++-- src/components/TourLargeScreenManager.vue | 29 ++++++++++-- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/components/Tour.vue b/src/components/Tour.vue index 0aba8598b..2dacf2b06 100644 --- a/src/components/Tour.vue +++ b/src/components/Tour.vue @@ -9,6 +9,7 @@ = ref(4); + const currentStep: Ref = ref(0); const nSteps: Ref = ref(0); const disableNextStep = ref(true); @@ -206,7 +207,8 @@ export default defineComponent({ tour.start(`${currentStep.value}`); isLoading.value = false; - window.addEventListener('keyup', onKeyDown); + window.addEventListener('keyup', _onKeyDown); + window.addEventListener('click', _userClicked()); _receiveEvents(); _broadcast({ @@ -323,6 +325,16 @@ export default defineComponent({ }); } + function _userClicked() { + const userCanClick = ['.tour', '.tour-manager'].map((s) => document.querySelector(s) as HTMLElement); + return ({ target }: MouseEvent) => { + if (!target) return; + if (!userCanClick.some((el) => el.contains(target as Node))) { + _broadcast({ type: 'clicked-outside-tour' }); + } + }; + } + // TODO In tablets 'buy nim' in sidebar does not get its original state let _buttonNimClasses: {[x:string]: string} = {}; function _toggleDisabledButtons(disabledButtons: TourStep['ui']['disabledButtons'], disabled:boolean) { @@ -391,7 +403,9 @@ export default defineComponent({ _removeAttributes(currentStep.value); _toggleDisabledButtons(steps[currentStep.value]?.ui.disabledButtons, false); - window.removeEventListener('keyup', onKeyDown); + window.removeEventListener('keyup', _onKeyDown); + window.addEventListener('click', _userClicked()); + context.root.$off('nimiq-tour-event'); if (unmounted) { @@ -405,7 +419,7 @@ export default defineComponent({ setTour(null); } - function onKeyDown(event: KeyboardEvent) { + function _onKeyDown(event: KeyboardEvent) { switch (event.key) { case 'ArrowRight': if (!disableNextStep.value) { @@ -590,6 +604,38 @@ button.highlighted { } } } + ::v-deep .v-step__arrow { + background: var(--nimiq-light-blue); + } + + &.bottom-start ::v-deep .v-step__arrow, &.right-start ::v-deep .v-step__arrow, + &.bottom ::v-deep .v-step__arrow, &.right ::v-deep .v-step__arrow, &.right-end ::v-deep .v-step__arrow { + background: #0582ca !important; + } + + &.bottom-end ::v-deep .v-step__arrow { + background: #0681ca !important; + } + + &.top-start ::v-deep .v-step__arrow { + background: #087ecb !important; + } + + &.left-start ::v-deep .v-step__arrow { + background: #0a7ccc !important; + } + + &.left ::v-deep .v-step__arrow { + background: #1570d0 !important; + } + + &.top ::v-deep .v-step__arrow { + background: #1570d0 !important; + } + + &.left-end ::v-deep .v-step__arrow, &.top-end ::v-deep .v-step__arrow { + background: #2163d5 !important; + } } } diff --git a/src/components/TourLargeScreenManager.vue b/src/components/TourLargeScreenManager.vue index 310cca0f6..123037d2e 100644 --- a/src/components/TourLargeScreenManager.vue +++ b/src/components/TourLargeScreenManager.vue @@ -32,11 +32,12 @@ export default defineComponent({ const $originalManager = ref(null); onMounted(() => { context.root.$on('nimiq-tour-event', (data: TourBroadcast) => { - if (data.type === 'tour-step-changed') stepChanged(data.payload); + if (data.type === 'tour-step-changed') _stepChanged(data.payload); + if (data.type === 'clicked-outside-tour') _flash(); }); }); - function stepChanged({ nSteps: newNSteps, currentStep: newCurrentStep }:TourBroadcastStepChanged['payload']) { + function _stepChanged({ nSteps: newNSteps, currentStep: newCurrentStep }:TourBroadcastStepChanged['payload']) { nSteps.value = newNSteps; currentStep.value = newCurrentStep; if (!isLargeScreen.value) return; @@ -90,6 +91,16 @@ export default defineComponent({ context.root.$emit('nimiq-tour-event', { type: 'end-tour' } as TourBroadcast); } + function _flash() { + const tourManager = document.querySelector('body .tour-manager'); + if (tourManager) { + tourManager.classList.add('flash'); + setTimeout(() => { + tourManager.classList.remove('flash'); + }, 500); + } + } + return { isTourActive, isLargeScreen, @@ -110,7 +121,9 @@ export default defineComponent({ flex-direction: column; padding: 12px; gap: 2rem; - background-color: rgba(255, 255, 255, 0.12); // TODO Move to a variable?? + background-color: rgba(255, 255, 255, 0.12); // TODO This should be var(--text-12) + transition-property: background-color; + transition-timing-function: ease-in-out; border-radius: 4px; cursor: initial !important; @@ -148,5 +161,15 @@ export default defineComponent({ font-weight: 700; } } + + &.flash { + animation: flash 0.4s; + } + + @keyframes flash { + from { background-color: rgba(255, 255, 255, 0.12); } // TODO This should be var(--text-12) + 50% { background-color: rgba(255, 255, 255, 0.30); } // TODO This should be var(--text-30) + to { background-color: rgba(255, 255, 255, 0.12); } // TODO This should be var(--text-12) + } } From 04cd662b30fa9163f89b24282ec512faee2c88c0 Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 31 Jan 2022 16:05:04 +0100 Subject: [PATCH 16/44] finished network tour --- src/components/NetworkMap.vue | 1 + src/components/Tour.vue | 120 +++++++++++++----- src/components/TourLargeScreenManager.vue | 19 +-- src/components/layouts/Network.vue | 6 +- src/components/layouts/Settings.vue | 4 +- src/components/layouts/Sidebar.vue | 10 +- .../modals/DiscoverTheNimiqWalletModal.vue | 3 +- src/components/modals/NetworkInfoModal.vue | 57 ++++++--- src/composables/useWindowSize.ts | 6 +- src/lib/tour/index.ts | 4 +- src/lib/tour/network/01_YourLocationStep.ts | 41 ++++++ src/lib/tour/network/02_BackboneNodeStep.ts | 56 ++++++++ src/lib/tour/network/03_NetworkMetricsStep.ts | 31 +++++ .../tour/network/04_NetworkCompletedStep.ts | 45 +++++++ src/lib/tour/network/NetworkTourTexts.ts | 34 +++++ src/lib/tour/network/index.ts | 57 ++++++--- .../tour/onboarding/OnboardingTourTexts.ts | 12 +- src/lib/tour/onboarding/index.ts | 28 ++-- src/lib/tour/types.ts | 30 ++++- src/stores/Account.ts | 2 +- 20 files changed, 447 insertions(+), 119 deletions(-) create mode 100644 src/lib/tour/network/01_YourLocationStep.ts create mode 100644 src/lib/tour/network/02_BackboneNodeStep.ts create mode 100644 src/lib/tour/network/03_NetworkMetricsStep.ts create mode 100644 src/lib/tour/network/04_NetworkCompletedStep.ts create mode 100644 src/lib/tour/network/NetworkTourTexts.ts diff --git a/src/components/NetworkMap.vue b/src/components/NetworkMap.vue index 0f92b1601..f2a954100 100644 --- a/src/components/NetworkMap.vue +++ b/src/components/NetworkMap.vue @@ -60,6 +60,7 @@ import NetworkMap, { } from '../lib/NetworkMap'; export default defineComponent({ + name: 'network-map', setup(props, context) { const $container = ref(null); const $network = ref(null); diff --git a/src/components/Tour.vue b/src/components/Tour.vue index 2dacf2b06..b061a252b 100644 --- a/src/components/Tour.vue +++ b/src/components/Tour.vue @@ -24,7 +24,14 @@
-

{{ $t(content) }}

+
+

+
    +
  • + - + +
  • +
@@ -100,6 +107,7 @@ import { computed, defineComponent, onMounted, + onUnmounted, Ref, ref, watch, @@ -142,16 +150,10 @@ export default defineComponent({ buttonNext: false, buttonStop: false, }, - labels: { - buttonSkip: 'Skip tour', - buttonPrevious: 'Previous', - buttonNext: 'Next', - buttonStop: 'Finish', - }, + // TODO Add padding to arrow useKeyboardNavigation: false, // handled by us }; - // TODO Go back to index - const steps = Object.values(getTour(tourStore.tour?.name, context)); + let steps = Object.values(getTour(tourStore.tour?.name, context)); // Initial state const isLoading = ref(true); @@ -168,10 +170,12 @@ export default defineComponent({ // REMOVE ME const { removeTransactions, addTransactions } = useTransactionsStore(); - // removeTransactions([getFakeTx()]); - addTransactions([getFakeTx()]); + removeTransactions([getFakeTx()]); + // addTransactions([getFakeTx()]); }); + onUnmounted(() => endTour()); + async function tourSetup() { await context.root.$nextTick(); // to ensure the DOM is ready @@ -186,6 +190,11 @@ export default defineComponent({ await step.lifecycle.created({ goToNextStep, goingForward: true }); } + if (context.root.$route.fullPath !== step.path) { + context.root.$router.push(step.path); + await context.root.$nextTick(); + } + _toggleDisabledButtons(step.ui.disabledButtons, true); _addAttributes(step.ui, currentStep.value); @@ -209,6 +218,10 @@ export default defineComponent({ window.addEventListener('keyup', _onKeyDown); window.addEventListener('click', _userClicked()); + // window.addEventListener('resize', _OnResize(_OnResizeEnd)); TODO + + const app = document.querySelector('#app'); + app!.setAttribute('data-tour-active', ''); _receiveEvents(); _broadcast({ @@ -236,7 +249,9 @@ export default defineComponent({ // it takes to render the button. I don't know how to fix it. // Ensure that we disabled 'Receive Free NIM' button - await sleep(500); + await sleep(500); // TODO + // TODO Remove this code for the network, find other way + // steps = Object.values(getTour(tourStore.tour?.name, context)); _toggleDisabledButtons(steps[currentStep.value]?.ui.disabledButtons, true); }); @@ -326,7 +341,10 @@ export default defineComponent({ } function _userClicked() { - const userCanClick = ['.tour', '.tour-manager'].map((s) => document.querySelector(s) as HTMLElement); + const userCanClick = ['.tour', '.tour-manager'] + .map((s) => document.querySelector(s) as HTMLElement) + .filter((e) => !!e); + return ({ target }: MouseEvent) => { if (!target) return; if (!userCanClick.some((el) => el.contains(target as Node))) { @@ -399,26 +417,47 @@ export default defineComponent({ }); } - async function endTour() { - _removeAttributes(currentStep.value); - _toggleDisabledButtons(steps[currentStep.value]?.ui.disabledButtons, false); - + async function endTour(soft = false) { window.removeEventListener('keyup', _onKeyDown); - window.addEventListener('click', _userClicked()); - - context.root.$off('nimiq-tour-event'); + window.removeEventListener('click', () => _userClicked()); if (unmounted) { await unmounted({ ending: true, goingForward: false }); } + if (soft) { + return; + } + + window.removeEventListener('resize', () => _OnResize(_OnResizeEnd)); + + _removeAttributes(currentStep.value); + _toggleDisabledButtons(steps[currentStep.value]?.ui.disabledButtons, false); + + context.root.$off('nimiq-tour-event'); // If user finalizes tour while it is loading, allow interaction again - const app = document.querySelector('#app main') as HTMLDivElement; - app.removeAttribute('data-non-interactable'); + const app = document.querySelector('#app') as HTMLDivElement; + app.removeAttribute('data-tour-active'); + app.querySelector('main')!.removeAttribute('data-non-interactable'); setTour(null); } + function _OnResize(func: () => void) { + endTour(true); + tour!.stop(); + let timer: ReturnType | null = null; + return () => { + if (timer) clearTimeout(timer); + timer = setTimeout(func, 100); + }; + } + + function _OnResizeEnd() { + steps = Object.values(getTour(tourStore.tour?.name, context)); + tourSetup(); + } + function _onKeyDown(event: KeyboardEvent) { switch (event.key) { case 'ArrowRight': @@ -483,21 +522,21 @@ export default defineComponent({ // updated with opacity and non-interactivity properties as data attributes allow to use a value like // [data-opaified="1"] although the CSS selector that we can use is [data-opacified]. @see _removeAttributes -[data-opacified] { +[data-tour-active] [data-opacified] { filter: opacity(0.3); } -[data-non-interactable], -[data-non-interactable] * { +[data-tour-active] [data-non-interactable], +[data-tour-active] [data-non-interactable] * { user-select: none !important; pointer-events: none !important; } -#app > *:not(.tour):not(.tour-manager) { +[data-tour-active]#app > *:not(.tour):not(.tour-manager) { cursor: not-allowed; } -button.highlighted { +[data-tour-active] button.highlighted { background: linear-gradient( 274.28deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.2) 27.6%, rgba(255, 255, 255, 0) 53.12%, rgba(255, 255, 255, 0.2) 81.25%, rgba(255, 255, 255, 0) 100% @@ -542,7 +581,23 @@ button.highlighted { display: flex; align-items: center; - p { + ul { + list-style-type: none; + margin: 0; + padding-left: 0; + + li { + display: flex; + gap: 1rem; + margin-top: 1rem; + + .dash { + user-select: none; + } + } + } + + p, li { margin: 0; font-size: 15px; line-height: 21px; @@ -553,11 +608,17 @@ button.highlighted { } } - p::selection { + p::selection, li::selection { color: var(--nimiq-light-blue); background: var(--nimiq-light-gray); } + hr { + width: 100%; + opacity: 0.2; + height: 1.5px; + } + ::v-deep svg { float: left; margin-right: 2rem; @@ -569,6 +630,7 @@ button.highlighted { .actions { margin-top: 2rem; display: flex; + gap: 1rem; button { font-weight: 700; diff --git a/src/components/TourLargeScreenManager.vue b/src/components/TourLargeScreenManager.vue index 123037d2e..28d3667eb 100644 --- a/src/components/TourLargeScreenManager.vue +++ b/src/components/TourLargeScreenManager.vue @@ -1,5 +1,5 @@ @@ -134,19 +142,30 @@ export default defineComponent({ max-height: 38rem; } } -} -.nq-h1 { - margin-top: 0; - margin-bottom: 2rem; -} + .content { + .nq-h1 { + margin-top: 0; + margin-bottom: 2rem; + margin-right: 5rem; + } -p, a { - font-size: var(--body-size); -} + p, button { + font-size: var(--body-size); + } -a { - display: inline-flex; + button { + margin-top: 1.75rem; + display: inline-flex; + align-items: center; + gap: 1.5rem; + transition: margin 0.2s ease-in-out; + } + + button:hover svg { + left: 10px; + } + } } @media (min-width: 700px) { // Full mobile breakpoint @@ -166,6 +185,10 @@ a { .page-body { --padding: 3rem; + + button { + margin-top: 1.5rem; + } } } diff --git a/src/composables/useWindowSize.ts b/src/composables/useWindowSize.ts index 572a4b1ef..8e04205eb 100644 --- a/src/composables/useWindowSize.ts +++ b/src/composables/useWindowSize.ts @@ -23,9 +23,9 @@ export function useWindowSize() { width = ref(0); height = ref(0); listener(); - isSmallScreen = computed(() => width!.value <= 700); // Small screen breakpoint - isMediumScreen = computed(() => width!.value > 700 && width!.value <= 1160); // Small screen breakpoint - isLargeScreen = computed(() => width!.value > 1160); // Small screen breakpoint + isSmallScreen = computed(() => width!.value <= 700); + isMediumScreen = computed(() => width!.value > 700 && width!.value <= 1160); + isLargeScreen = computed(() => width!.value > 1160); } onMounted(() => { diff --git a/src/lib/tour/index.ts b/src/lib/tour/index.ts index 02c39d1fe..0fc8f77e6 100644 --- a/src/lib/tour/index.ts +++ b/src/lib/tour/index.ts @@ -1,6 +1,6 @@ import { useAddressStore } from '@/stores/Address'; -import { SetupContext } from '@vue/composition-api'; import { Transaction } from '@/stores/Transactions'; +import { SetupContext } from '@vue/composition-api'; import { getNetworkTourSteps } from './network'; import { getOnboardingTourSteps } from './onboarding'; import { NetworkTourStep, OnboardingTourStep, TourName, TourSteps } from './types'; @@ -11,7 +11,7 @@ export function getTour(tour: TourName | undefined, context: SetupContext) case 'onboarding': return getOnboardingTourSteps(context); case 'network': - return getNetworkTourSteps(); + return getNetworkTourSteps(context); default: return {}; } diff --git a/src/lib/tour/network/01_YourLocationStep.ts b/src/lib/tour/network/01_YourLocationStep.ts new file mode 100644 index 000000000..0caf15b10 --- /dev/null +++ b/src/lib/tour/network/01_YourLocationStep.ts @@ -0,0 +1,41 @@ +import { SCALING_FACTOR } from '@/lib/NetworkMap'; +import { NetworkGetStepFnArgs, NetworkTourStep, TourStep, WalletHTMLElements } from '..'; +import { getNetworkTexts } from './NetworkTourTexts'; + +export function getYourLocationStep({ nodes, scrollIntoView, sleep, selfNodeIndex }: NetworkGetStepFnArgs): TourStep { + return { + path: '/network', + tooltip: { + target: `${WalletHTMLElements.NETWORK_NODES} span:nth-child(${selfNodeIndex + 1})`, + content: getNetworkTexts(NetworkTourStep.YOUR_LOCATION), + params: { + // TODO On mobile phones if the node is in the south, the tooltip might break the web + placement: 'bottom', + }, + }, + ui: { + fadedElements: [ + WalletHTMLElements.SIDEBAR_TESTNET, + WalletHTMLElements.SIDEBAR_LOGO, + WalletHTMLElements.SIDEBAR_PRICE_CHARTS, + WalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + WalletHTMLElements.SIDEBAR_NETWORK, + WalletHTMLElements.SIDEBAR_SETTINGS, + WalletHTMLElements.NETWORK_STATS, + ], + disabledElements: [ + WalletHTMLElements.NETWORK_TABLET_MENU_BAR, + WalletHTMLElements.NETWORK_MAP, + ], + disabledButtons: [WalletHTMLElements.BUTTON_SIDEBAR_BUY, WalletHTMLElements.BUTTON_SIDEBAR_SELL], + }, + lifecycle: { + created: (async ({ goingForward }) => { + scrollIntoView((nodes()[selfNodeIndex].x / 2) * SCALING_FACTOR); + if (!goingForward) { + await sleep(500); + } + }), + }, + } as TourStep; +} diff --git a/src/lib/tour/network/02_BackboneNodeStep.ts b/src/lib/tour/network/02_BackboneNodeStep.ts new file mode 100644 index 000000000..93c8ad0f0 --- /dev/null +++ b/src/lib/tour/network/02_BackboneNodeStep.ts @@ -0,0 +1,56 @@ +import { SCALING_FACTOR } from '@/lib/NetworkMap'; +import { ref } from '@vue/composition-api'; +import { NetworkGetStepFnArgs, NetworkTourStep, TourStep, WalletHTMLElements } from '..'; +import { getNetworkTexts } from './NetworkTourTexts'; + +export function getBackboneNodeStep( + { nodes, selfNodeIndex, isLargeScreen, scrollIntoView, sleep }: NetworkGetStepFnArgs): TourStep { + const selectedNode = ref(-1); + return { + path: '/network', + tooltip: { + get target() { + return `${WalletHTMLElements.NETWORK_NODES} span:nth-child(${selectedNode.value + 1})`; + }, + content: getNetworkTexts(NetworkTourStep.BACKBONE_NODE), + params: { + // TODO On mobile phones if the node is in the south, the tooltip might break the web + placement: isLargeScreen.value ? 'right' : 'bottom', + }, + }, + ui: { + fadedElements: [ + WalletHTMLElements.SIDEBAR_TESTNET, + WalletHTMLElements.SIDEBAR_LOGO, + WalletHTMLElements.SIDEBAR_PRICE_CHARTS, + WalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + WalletHTMLElements.SIDEBAR_NETWORK, + WalletHTMLElements.SIDEBAR_SETTINGS, + WalletHTMLElements.NETWORK_STATS, + ], + disabledElements: [ + WalletHTMLElements.NETWORK_TABLET_MENU_BAR, + WalletHTMLElements.NETWORK_MAP, + ], + disabledButtons: [WalletHTMLElements.BUTTON_SIDEBAR_BUY, WalletHTMLElements.BUTTON_SIDEBAR_SELL], + }, + lifecycle: { + created: (async () => { + const distance = ([x1, y1]: number[], [x2, y2]: number[]) => (((x1 - x2) ** 2) + (y1 - y2) ** 2) ** 0.5; + const _nodes = nodes(); + const { position: pSelf } = _nodes[selfNodeIndex]; + + // get closest west node minimum distance of 5 + const node = _nodes + .map((n, i) => ({ ...n, i, x: n.x })) // add index + .filter((_, i) => i !== selfNodeIndex) + .map((n) => ({ ...n, d: distance([n.position.x, n.position.y], [pSelf.x, pSelf.y]) })) + .filter(({ d }) => d > 5) + .sort((a, b) => a.d - b.d)[0]; + selectedNode.value = node.i; + scrollIntoView((node.x / 2) * SCALING_FACTOR); + await sleep(500); + }), + }, + } as TourStep; +} diff --git a/src/lib/tour/network/03_NetworkMetricsStep.ts b/src/lib/tour/network/03_NetworkMetricsStep.ts new file mode 100644 index 000000000..4a87fab29 --- /dev/null +++ b/src/lib/tour/network/03_NetworkMetricsStep.ts @@ -0,0 +1,31 @@ +import { NetworkTourStep, TourStep, WalletHTMLElements } from '..'; +import { getNetworkTexts } from './NetworkTourTexts'; + +export function getNetworkMetricsStep(): TourStep { + return { + path: '/network', + tooltip: { + target: WalletHTMLElements.NETWORK_STATS, + content: getNetworkTexts(NetworkTourStep.METRICS), + params: { + placement: 'top', + }, + }, + ui: { + fadedElements: [ + WalletHTMLElements.SIDEBAR_TESTNET, + WalletHTMLElements.SIDEBAR_LOGO, + WalletHTMLElements.SIDEBAR_PRICE_CHARTS, + WalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + WalletHTMLElements.SIDEBAR_NETWORK, + WalletHTMLElements.SIDEBAR_SETTINGS, + ], + disabledElements: [ + WalletHTMLElements.NETWORK_TABLET_MENU_BAR, + WalletHTMLElements.NETWORK_MAP, + WalletHTMLElements.NETWORK_STATS, + ], + disabledButtons: [WalletHTMLElements.BUTTON_SIDEBAR_BUY, WalletHTMLElements.BUTTON_SIDEBAR_SELL], + }, + } as TourStep; +} diff --git a/src/lib/tour/network/04_NetworkCompletedStep.ts b/src/lib/tour/network/04_NetworkCompletedStep.ts new file mode 100644 index 000000000..f7a95ecff --- /dev/null +++ b/src/lib/tour/network/04_NetworkCompletedStep.ts @@ -0,0 +1,45 @@ +import { NetworkGetStepFnArgs, NetworkTourStep, TourStep, WalletHTMLElements } from '..'; +import { getNetworkTexts } from './NetworkTourTexts'; + +export function getNetworkCompletedStep({ isLargeScreen }: NetworkGetStepFnArgs): TourStep { + return { + path: '/network', + tooltip: { + get target() { + return isLargeScreen.value + ? WalletHTMLElements.SIDEBAR_ACCOUNT_MENU + : `${WalletHTMLElements.NETWORK_TABLET_MENU_BAR} .account-button`; + }, + content: getNetworkTexts(NetworkTourStep.NETWORK_COMPLETED, isLargeScreen.value), + params: { + get placement() { + return isLargeScreen.value ? 'right' : 'top'; + }, + }, + button: { + text: 'End Tour', + fn: async (endTour) => { + if (endTour) { + await endTour(); + } + }, + }, + }, + ui: { + fadedElements: [ + WalletHTMLElements.SIDEBAR_TESTNET, + WalletHTMLElements.SIDEBAR_LOGO, + WalletHTMLElements.SIDEBAR_PRICE_CHARTS, + WalletHTMLElements.SIDEBAR_NETWORK, + WalletHTMLElements.SIDEBAR_SETTINGS, + ], + disabledElements: [ + WalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + WalletHTMLElements.NETWORK_TABLET_MENU_BAR, + WalletHTMLElements.NETWORK_MAP, + WalletHTMLElements.NETWORK_STATS, + ], + disabledButtons: [WalletHTMLElements.BUTTON_SIDEBAR_BUY, WalletHTMLElements.BUTTON_SIDEBAR_SELL], + }, + } as TourStep; +} diff --git a/src/lib/tour/network/NetworkTourTexts.ts b/src/lib/tour/network/NetworkTourTexts.ts new file mode 100644 index 000000000..5624a2280 --- /dev/null +++ b/src/lib/tour/network/NetworkTourTexts.ts @@ -0,0 +1,34 @@ +import { NetworkTourStep } from '../types'; + +type TourStepTexts = { + [x in T]: string[] +} + +export function getNetworkTexts(i: NetworkTourStep, isLargeScreen?: boolean) { + return ({ + [NetworkTourStep.YOUR_LOCATION]: [ + 'This is you. Your location is determined by your IP address.', + 'Nimiq doesn’t collect or store such data.', + ], + [NetworkTourStep.BACKBONE_NODE]: [ + 'This is a peer or a backbone node that you are connected to.', + 'These connections enable you to establish consensus with a sub set of participants directly.', + [ + '‘Available browsers’ are other user’s browsers, just like yours.', + '‘Backbone nodes’ provide a fallback to connect to.', + ], + ], + [NetworkTourStep.METRICS]: [ + 'Find the network’s key performance metrics below.', + 'The {{NETWORK}}-icon indicates that you are connected to the network.', + ], + [NetworkTourStep.NETWORK_COMPLETED]: [ + 'You made it!', + 'HR', + 'Enjoy the decentralized future, and don’t forget to invite your friends and family.', + isLargeScreen + ? 'Click {{ ACCOUNT }} to get back to your wallet.' + : 'Click ‘Back to Addresses’ to get back to your wallet.', + ], + } as TourStepTexts)[i]; +} diff --git a/src/lib/tour/network/index.ts b/src/lib/tour/network/index.ts index f9e463887..545b70e97 100644 --- a/src/lib/tour/network/index.ts +++ b/src/lib/tour/network/index.ts @@ -1,22 +1,43 @@ -import { NetworkTourStep, TourSteps } from '../types'; +import { useWindowSize } from '@/composables/useWindowSize'; +import { NodeHexagon, NodeType, WIDTH } from '@/lib/NetworkMap'; +import { SetupContext } from '@vue/composition-api'; +import { searchComponentByName } from '..'; +import { NetworkGetStepFnArgs, NetworkTourStep, TourSteps, WalletHTMLElements } from '../types'; +import { getYourLocationStep } from './01_YourLocationStep'; +import { getBackboneNodeStep } from './02_BackboneNodeStep'; +import { getNetworkMetricsStep } from './03_NetworkMetricsStep'; +import { getNetworkCompletedStep } from './04_NetworkCompletedStep'; + +export function getNetworkTourSteps({ root }: SetupContext): TourSteps { + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + const { isSmallScreen, isMediumScreen, isLargeScreen } = useWindowSize(); + + const networkMapInstance = searchComponentByName(root, 'network-map') as any; + const nodes = () => networkMapInstance?.nodes as NodeHexagon[] || []; + const selfNodeIndex = nodes().findIndex((node) => [...node.peers].find((p) => p.type === NodeType.SELF)); + + const scrollIntoView = async (x: number) => { + const map = document.querySelector(WalletHTMLElements.NETWORK_SCROLLER) as HTMLElement; + const mapWidth = map.children[0]!.clientWidth; + const adjustedX = x * (mapWidth / WIDTH); + const scrollTarget = adjustedX - (window.innerWidth / 2); + map.scrollTo(scrollTarget, 0); + }; + + const args: NetworkGetStepFnArgs = { + nodes, + selfNodeIndex, + isSmallScreen, + isMediumScreen, + isLargeScreen, + scrollIntoView, + sleep, + }; -export function getNetworkTourSteps(): TourSteps { return { - [NetworkTourStep.TODO]: { - path: '/network', - tooltip: { - target: '.network-overview .network-name', - content: [ - 'Welcome to the {WORLD} Network!', - 'This is the main network where all Nimiq transactions take place.', - 'You can switch between networks by clicking on the {WORLD} Network icon in the top right corner.', - ], - params: { - placement: 'bottom', - }, - }, - ui: {}, - lifecycle: {}, - }, + [NetworkTourStep.YOUR_LOCATION]: getYourLocationStep(args), + [NetworkTourStep.BACKBONE_NODE]: getBackboneNodeStep(args), + [NetworkTourStep.METRICS]: getNetworkMetricsStep(), + [NetworkTourStep.NETWORK_COMPLETED]: getNetworkCompletedStep(args), }; } diff --git a/src/lib/tour/onboarding/OnboardingTourTexts.ts b/src/lib/tour/onboarding/OnboardingTourTexts.ts index 7cd5d404d..831a797cd 100644 --- a/src/lib/tour/onboarding/OnboardingTourTexts.ts +++ b/src/lib/tour/onboarding/OnboardingTourTexts.ts @@ -14,7 +14,7 @@ export function getOnboardingTexts(i: OnboardingTourStep, isANewUser: boolean) { 'You can click on the address to copy and share it.', ], }, - [OnboardingTourStep.TRANSACTIONS_LIST]: { + [OnboardingTourStep.TRANSACTION_LIST]: { default: [ 'This is where all your transactions will appear.', 'Click the green button to receive a free NIM from Team Nimiq.', @@ -23,7 +23,7 @@ export function getOnboardingTexts(i: OnboardingTourStep, isANewUser: boolean) { }, [OnboardingTourStep.FIRST_TRANSACTION]: { default: [ - "Here's your first transaction with your first NIM.", + 'Here’s your first transaction with your first NIM.', 'Every NIM address comes with an avatar. They help to make sure you got the right one.', ], }, @@ -40,9 +40,9 @@ export function getOnboardingTexts(i: OnboardingTourStep, isANewUser: boolean) { }, [OnboardingTourStep.BACKUP_ALERT]: { default: isANewUser ? [ - 'There is no \'forgot password\'. Create a backup to make sure you stay in control.', + 'There is no ‘forgot password’. Create a backup to make sure you stay in control.', ] : [ - 'Seriously! There is no \'forgot password\'! Create a backup to make sure you stay in control.', + 'Seriously! There is no ‘forgot password’! Create a backup to make sure you stay in control.', ], }, [OnboardingTourStep.MENU_ICON]: { @@ -52,7 +52,7 @@ export function getOnboardingTexts(i: OnboardingTourStep, isANewUser: boolean) { }, [OnboardingTourStep.BACKUP_OPTION_LARGE_SCREENS]: { default: [ - 'You can always create a new backup. Simply click your account and select \'Create backup\'.', + 'You can always create a new backup. Simply click your account and select ‘Create backup’.', ], }, [OnboardingTourStep.ACCOUNT_OPTIONS]: { @@ -64,7 +64,7 @@ export function getOnboardingTexts(i: OnboardingTourStep, isANewUser: boolean) { [OnboardingTourStep.BACKUP_OPTION_NOT_LARGE_SCREENS]: { default: [ 'You can always create a new backup.', - 'Simply click your account and select \'Create backup\'.', + 'Simply click your account and select ‘Create backup’.', ], }, [OnboardingTourStep.ONBOARDING_COMPLETED]: { diff --git a/src/lib/tour/onboarding/index.ts b/src/lib/tour/onboarding/index.ts index 96d672234..0ae2a219f 100644 --- a/src/lib/tour/onboarding/index.ts +++ b/src/lib/tour/onboarding/index.ts @@ -1,7 +1,7 @@ import { useWindowSize } from '@/composables/useWindowSize'; import { AccountType, useAccountStore } from '@/stores/Account'; import { SetupContext } from '@vue/composition-api'; -import { searchComponentByName } from '..'; +import { searchComponentByName, TourName } from '..'; import { GetStepFnArgs, OnboardingTourStep, TourSteps } from '../types'; import { getFirstAddressStep } from './01_FirstAddressStep'; import { getTransactionListStep } from './02_TransactionListStep'; @@ -27,14 +27,12 @@ export function getOnboardingTourSteps({ root }: SetupContext): TourSteps = {}; - const openAccountOptions = async () => { const accountMenu = searchComponentByName(root, 'account-menu') as any; if (!accountMenu || !('closeMenu' in accountMenu) @@ -55,7 +53,6 @@ export function getOnboardingTourSteps({ root }: SetupContext): TourSteps = { sleep, - steps, toggleDisabledAttribute, root, isSmallScreen, @@ -66,11 +63,16 @@ export function getOnboardingTourSteps({ root }: SetupContext): TourSteps = { + [OnboardingTourStep.FIRST_ADDRESS]: getFirstAddressStep(args), + [OnboardingTourStep.TRANSACTION_LIST]: getTransactionListStep(args), + [OnboardingTourStep.FIRST_TRANSACTION]: getFirstTransactionStep(args), + [OnboardingTourStep.BITCOIN_ADDRESS]: getBitcoinAddressStep(args), + [OnboardingTourStep.WALLET_BALANCE]: getWalletBalanceStep(args), + [OnboardingTourStep.ACCOUNT_OPTIONS]: getAccountOptionsStep( + { ...args, keepMenuOpenOnForward: fileExported && !isLargeScreen.value }), + [OnboardingTourStep.ONBOARDING_COMPLETED]: getOnboardingCompletedStep(args), + }; if (!fileExported) { steps[OnboardingTourStep.BACKUP_ALERT] = getBackupAlertStep(args); } @@ -80,12 +82,8 @@ export function getOnboardingTourSteps({ root }: SetupContext): TourSteps = { [x in T]?: TourStep; }; +// TODO Rename or mix with NetworkGetStepFnArgs export type GetStepFnArgs = Pick, 'isSmallScreen' | 'isMediumScreen' | 'isLargeScreen'> & { root: SetupContext['root'], - steps: TourSteps, toggleDisabledAttribute: (selector: string, disabled: boolean) => Promise, sleep: (ms: number) => Promise, isANewUser: boolean, @@ -89,6 +93,14 @@ export type GetStepFnArgs = closeAccountOptions: () => Promise, }; +export type NetworkGetStepFnArgs = + Pick, 'isSmallScreen' | 'isMediumScreen' | 'isLargeScreen'> & { + nodes: () => NodeHexagon[], + selfNodeIndex: number, + scrollIntoView: (x: number) => void, + sleep: (ms: number) => Promise, + } + export type TourBroadcast = TourBroadcastEnd | TourBroadcastStepChanged | TourBroadcastClickedOutsideTour interface TourBroadcastEnd { @@ -139,7 +151,11 @@ export enum WalletHTMLElements { MODAL_CONTAINER = '.modal.backdrop', MODAL_WRAPPER = '.modal .wrapper', MODAL_PAGE = '.modal .small-page', - MODAL_CLOSE_BUTTON = '.modal .close-button' + MODAL_CLOSE_BUTTON = '.modal .close-button', - // TODO: NETWORK + NETWORK_STATS = '.network .network-stats', + NETWORK_MAP = '.network .network-map', + NETWORK_NODES = '.network .network-map .nodes', + NETWORK_SCROLLER = '.network .scroller', + NETWORK_TABLET_MENU_BAR = '.network .menu-bar', } diff --git a/src/stores/Account.ts b/src/stores/Account.ts index 494448cbf..20d68c284 100644 --- a/src/stores/Account.ts +++ b/src/stores/Account.ts @@ -8,7 +8,7 @@ export type AccountState = { accountInfos: {[id: string]: AccountInfo}, activeAccountId: string | null, activeCurrency: CryptoCurrency, - tour: { name: TourName, isANewUser: boolean } | null, + tour: { name: TourName.NETWORK } | { name: TourName.ONBOARDING, isANewUser: boolean } | null, } // Mirror of Hub WalletType, which is not exported From 2113ebf88e240b0b89a83e653cb63e0c0845a248 Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 31 Jan 2022 16:05:19 +0100 Subject: [PATCH 17/44] steps are reactive --- src/components/TourLargeScreenManager.vue | 6 +- ...tars.vue => NimiqLogoOutlineWithStars.vue} | 0 src/components/layouts/Network.vue | 4 +- .../modals/DiscoverTheNimiqWalletModal.vue | 6 +- .../tour/onboarding/01_FirstAddressStep.ts | 88 +++++++-------- .../tour/onboarding/02_TransactionListStep.ts | 102 ++++++++---------- .../onboarding/03_FirstTransactionStep.ts | 20 ++-- .../tour/onboarding/04_BitcoinAddressStep.ts | 16 +-- .../tour/onboarding/05_WalletBalanceStep.ts | 14 ++- .../tour/onboarding/06_0_BackupAlertStep.ts | 4 +- src/lib/tour/onboarding/06_1_MenuIconStep.ts | 8 +- .../07_1_BackupOptionNotLargeScreenStep.ts | 8 +- .../07_2_BackupOptionLargeScreenStep.ts | 4 +- .../tour/onboarding/07_AccountOptionsStep.ts | 12 ++- .../tour/onboarding/08_OnboardingCompleted.ts | 26 +++-- src/lib/tour/onboarding/index.ts | 4 +- src/lib/tour/types.ts | 3 +- 17 files changed, 164 insertions(+), 161 deletions(-) rename src/components/icons/{GreenNimiqLogoOutlineWithStars.vue => NimiqLogoOutlineWithStars.vue} (100%) diff --git a/src/components/TourLargeScreenManager.vue b/src/components/TourLargeScreenManager.vue index 28d3667eb..dbeae3803 100644 --- a/src/components/TourLargeScreenManager.vue +++ b/src/components/TourLargeScreenManager.vue @@ -55,6 +55,9 @@ export default defineComponent({ original.style.visibility = 'initial'; } + // at some steps, a modal will be openened in the tour and we still need to show the tour + // manager to the user, therefore, we need to duplicate the manager and set it to the body + // positionated over the modal function _duplicateManager() { _removeClonedManager(); const original = $originalManager.value!; @@ -69,8 +72,7 @@ export default defineComponent({ } manager.style.position = 'absolute'; - // manager.style.top = `${original.offsetTop}px`; // FIXME: it is getting 114px instead of 98px - manager.style.top = '98px'; + manager.style.top = `${original.offsetTop - 16}px`; // TODO Test this with other announcements manager.style.left = `${original.offsetLeft}px`; manager.style.width = `${original.offsetWidth}px`; manager.style.height = `${original.offsetHeight}px`; diff --git a/src/components/icons/GreenNimiqLogoOutlineWithStars.vue b/src/components/icons/NimiqLogoOutlineWithStars.vue similarity index 100% rename from src/components/icons/GreenNimiqLogoOutlineWithStars.vue rename to src/components/icons/NimiqLogoOutlineWithStars.vue diff --git a/src/components/layouts/Network.vue b/src/components/layouts/Network.vue index 786b28622..b3f06520a 100644 --- a/src/components/layouts/Network.vue +++ b/src/components/layouts/Network.vue @@ -57,8 +57,8 @@ const LOCALSTORAGE_KEY = 'network-info-dismissed'; export default defineComponent({ setup(props, context) { - const showNetworkInfo = ref(true // TOOD Remove me - || !window.localStorage.getItem(LOCALSTORAGE_KEY) || !!context.root.$route.params.showNetworkInfo); + const showNetworkInfo = ref( + !window.localStorage.getItem(LOCALSTORAGE_KEY) || !!context.root.$route.params.showNetworkInfo); function onNetworkInfoClosed() { window.localStorage.setItem(LOCALSTORAGE_KEY, '1'); diff --git a/src/components/modals/DiscoverTheNimiqWalletModal.vue b/src/components/modals/DiscoverTheNimiqWalletModal.vue index 88e2fb82e..8bc34e1fb 100644 --- a/src/components/modals/DiscoverTheNimiqWalletModal.vue +++ b/src/components/modals/DiscoverTheNimiqWalletModal.vue @@ -15,7 +15,7 @@
- +

{{ $t('Discover the Nimiq Wallet!') }}

{{ $t('It\'s free, does not collect data and is controlled by no one but you.') }} @@ -43,7 +43,7 @@ import { PageHeader } from '@nimiq/vue-components'; import { defineComponent } from '@vue/composition-api'; import { TourName } from '@/lib/tour'; import { Languages } from '../../i18n/i18n-setup'; -import GreenNimiqLogoOutlineWithStars from '../icons/GreenNimiqLogoOutlineWithStars.vue'; +import NimiqLogoOutlineWithStars from '../icons/NimiqLogoOutlineWithStars.vue'; import CaretRightIcon from '../icons/CaretRightIcon.vue'; import Modal from './Modal.vue'; @@ -73,7 +73,7 @@ export default defineComponent({ components: { Modal, PageHeader, - GreenNimiqLogoOutlineWithStars, + NimiqLogoOutlineWithStars, CaretRightIcon, }, }); diff --git a/src/lib/tour/onboarding/01_FirstAddressStep.ts b/src/lib/tour/onboarding/01_FirstAddressStep.ts index a9afbc1ea..e1a745308 100644 --- a/src/lib/tour/onboarding/01_FirstAddressStep.ts +++ b/src/lib/tour/onboarding/01_FirstAddressStep.ts @@ -1,11 +1,11 @@ import { CryptoCurrency } from '@/lib/Constants'; import { useAccountStore } from '@/stores/Account'; import { useAddressStore } from '@/stores/Address'; -import { GetStepFnArgs, OnboardingTourStep, TourStep, WalletHTMLElements } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep, WalletHTMLElements } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; export function getFirstAddressStep( - { isSmallScreen, isANewUser, root }: GetStepFnArgs): TourStep { + { isSmallScreen, isANewUser, root }: OnboardingGetStepFnArgs): TourStep { const created = () => { const { setActiveCurrency } = useAccountStore(); const { addressInfos, selectAddress } = useAddressStore(); @@ -43,63 +43,51 @@ export function getFirstAddressStep( ], }; - if (isSmallScreen.value) { - return { - path, - tooltip: { - target: `${WalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST} .address-button .identicon img`, - content: getOnboardingTexts(OnboardingTourStep.FIRST_ADDRESS, isANewUser).default, - params: { - placement: 'bottom-start', - }, - }, - lifecycle: { - created, - mounted: ({ goToNextStep }) => { - if (!isSmallScreen.value) { - return undefined; - } - - // Listener for the first address button only for mobile - - const addressButton = document - .querySelector('.address-list > .address-button') as HTMLButtonElement; - - let addressClicked = false; - const onClick = (e: MouseEvent) => { - addressClicked = true; - goToNextStep(); - e.preventDefault(); - e.stopPropagation(); - }; - - addressButton!.addEventListener('click', onClick, { once: true, capture: true }); - - return async (args) => { - if (!args?.ending && !addressClicked && root.$route.path === path) { - addressButton!.click(); - await root.$nextTick(); - } - addressButton!.removeEventListener('click', onClick, true); - }; - }, - }, - ui, - } as TourStep; - } - - // Not mobile return { path, tooltip: { - target: `${WalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS} .identicon`, + get target() { + return isSmallScreen.value + ? `${WalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST} .address-button .identicon img` + : `${WalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS} .identicon`; + }, content: getOnboardingTexts(OnboardingTourStep.FIRST_ADDRESS, isANewUser).default, params: { - placement: 'left-start', + get placement() { + return isSmallScreen.value ? 'bottom-start' : 'left-start'; + }, }, }, lifecycle: { created, + mounted: ({ goToNextStep }) => { + if (!isSmallScreen.value) { + return undefined; + } + + // Listener for the first address button only for mobile + + const addressButton = document + .querySelector('.address-list > .address-button') as HTMLButtonElement; + + let addressClicked = false; + const onClick = (e: MouseEvent) => { + addressClicked = true; + goToNextStep(); + e.preventDefault(); + e.stopPropagation(); + }; + + addressButton!.addEventListener('click', onClick, { once: true, capture: true }); + + return async (args) => { + if (!args?.ending && !addressClicked && root.$route.path === path) { + addressButton!.click(); + await root.$nextTick(); + } + addressButton!.removeEventListener('click', onClick, true); + }; + }, }, ui, } as TourStep; diff --git a/src/lib/tour/onboarding/02_TransactionListStep.ts b/src/lib/tour/onboarding/02_TransactionListStep.ts index 25c044436..72d323338 100644 --- a/src/lib/tour/onboarding/02_TransactionListStep.ts +++ b/src/lib/tour/onboarding/02_TransactionListStep.ts @@ -1,68 +1,18 @@ import { useTransactionsStore } from '@/stores/Transactions'; import { WalletHTMLElements } from '..'; -import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; export function getTransactionListStep( - { root, steps, isSmallScreen, isANewUser }: GetStepFnArgs): TourStep { - const txs = useTransactionsStore().state.transactions; - const startsWithNoTransactions = Object.values(txs).length === 0; + { isSmallScreen, isANewUser }: OnboardingGetStepFnArgs): TourStep { + const txsLen = () => Object.values(useTransactionsStore().state.transactions).length; - const tooltipWhenNoTx: TourStep['tooltip'] = { - target: isSmallScreen.value - ? `${WalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} > .empty-state h2` - : WalletHTMLElements.ADDRESS_OVERVIEW, - content: getOnboardingTexts(OnboardingTourStep.TRANSACTIONS_LIST, isANewUser).default, - params: { - placement: isSmallScreen.value ? 'top' : 'left', - }, - }; - const tooltipWhenAtLeastOneTx: TourStep['tooltip'] = { - target: isSmallScreen.value - ? '.vue-recycle-scroller__item-wrapper' - : WalletHTMLElements.ADDRESS_OVERVIEW, - content: getOnboardingTexts(OnboardingTourStep.TRANSACTIONS_LIST, isANewUser).alternative || [], - params: { - placement: isSmallScreen.value ? 'bottom' : 'left', - }, - }; - - let userHasClicked = false; const highlightButton = (highlight: boolean) => { - if (userHasClicked) return; - const receiveNim = document .querySelector(WalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_RECEIVE_FREE_NIM) as HTMLButtonElement; if (!receiveNim) return; receiveNim.classList[highlight ? 'add' : 'remove']('highlighted'); }; - const mounted = () => { - const { transactions } = useTransactionsStore().state; - - if (Object.values(transactions.value || []).length === 0) { - const unwatch = root.$watch(() => txs, () => { - if (!Object.values(txs).length) { - unwatch(); - return; - } - - userHasClicked = true; - - const buyNimBtn = document - .querySelector(`${WalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} a button`) as HTMLButtonElement; - buyNimBtn.disabled = true; - - // Once the user has at least one transaction, step TRANSACTIONS_LIST is modified - steps[OnboardingTourStep.TRANSACTIONS_LIST]!.tooltip = tooltipWhenAtLeastOneTx; - steps[OnboardingTourStep.TRANSACTIONS_LIST]!.ui.isNextStepDisabled = false; - steps[OnboardingTourStep.TRANSACTIONS_LIST]!.lifecycle = {}; - - unwatch(); - }); - } - highlightButton(true); - return () => highlightButton(false); - }; const ui: TourStep['ui'] = { fadedElements: [ @@ -92,13 +42,49 @@ export function getTransactionListStep( WalletHTMLElements.BUTTON_SIDEBAR_SELL, WalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, ], - isNextStepDisabled: startsWithNoTransactions, }; return { - path: isSmallScreen.value ? '/transactions' : '/', - tooltip: startsWithNoTransactions ? tooltipWhenNoTx : tooltipWhenAtLeastOneTx, - lifecycle: startsWithNoTransactions ? { mounted } : {}, - ui, + get path() { + return isSmallScreen.value ? '/transactions' : '/'; + }, + tooltip: { + get target() { + if (txsLen() > 0) { + return isSmallScreen.value + ? `${WalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller__item-wrapper` + : WalletHTMLElements.ADDRESS_OVERVIEW; + } + return isSmallScreen.value + ? `${WalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} > .empty-state h2` + : WalletHTMLElements.ADDRESS_OVERVIEW; + }, + get content() { + return txsLen() > 0 + ? getOnboardingTexts(OnboardingTourStep.TRANSACTION_LIST, isANewUser).alternative || [] + : getOnboardingTexts(OnboardingTourStep.TRANSACTION_LIST, isANewUser).default || []; + }, + params: { + get placement() { + if (txsLen() > 0) { + return isSmallScreen.value ? 'bottom' : 'left'; + } + return isSmallScreen.value ? 'top' : 'left'; + }, + }, + }, + lifecycle: { + mounted: () => { + if (txsLen() > 0) return undefined; + highlightButton(true); + return () => highlightButton(false); + }, + }, + get ui() { + return { + ...ui, + isNextStepDisabled: txsLen() === 0, + }; + }, }; } diff --git a/src/lib/tour/onboarding/03_FirstTransactionStep.ts b/src/lib/tour/onboarding/03_FirstTransactionStep.ts index 4b047e0ad..a14218697 100644 --- a/src/lib/tour/onboarding/03_FirstTransactionStep.ts +++ b/src/lib/tour/onboarding/03_FirstTransactionStep.ts @@ -1,8 +1,8 @@ import { WalletHTMLElements } from '..'; -import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; -export function getFirstTransactionStep({ isSmallScreen, isANewUser }: GetStepFnArgs): TourStep { +export function getFirstTransactionStep({ isSmallScreen, isANewUser }: OnboardingGetStepFnArgs): TourStep { const ui: TourStep['ui'] = { fadedElements: [ WalletHTMLElements.SIDEBAR_TESTNET, @@ -31,14 +31,20 @@ export function getFirstTransactionStep({ isSmallScreen, isANewUser }: GetStepFn }; return { - path: isSmallScreen.value ? '/transactions' : '/', + get path() { + return isSmallScreen.value ? '/transactions' : '/'; + }, tooltip: { - target: isSmallScreen.value - ? `${WalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .transaction > .identicon` - : `${WalletHTMLElements.ADDRESS_OVERVIEW} .vue-recycle-scroller__item-view:nth-child(2)`, + get target() { + return isSmallScreen.value + ? `${WalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .transaction > .identicon` + : `${WalletHTMLElements.ADDRESS_OVERVIEW} .vue-recycle-scroller__item-view:nth-child(2)`; + }, content: getOnboardingTexts(OnboardingTourStep.FIRST_TRANSACTION, isANewUser).default, params: { - placement: isSmallScreen.value ? 'bottom-start' : 'left', + get placement() { + return isSmallScreen.value ? 'bottom-start' : 'left'; + }, }, }, ui, diff --git a/src/lib/tour/onboarding/04_BitcoinAddressStep.ts b/src/lib/tour/onboarding/04_BitcoinAddressStep.ts index 6e2a83fea..cb252158d 100644 --- a/src/lib/tour/onboarding/04_BitcoinAddressStep.ts +++ b/src/lib/tour/onboarding/04_BitcoinAddressStep.ts @@ -1,8 +1,8 @@ -import { GetStepFnArgs, OnboardingTourStep, TourStep, WalletHTMLElements } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep, WalletHTMLElements } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; export function getBitcoinAddressStep( - { isSmallScreen, isMediumScreen, isANewUser }: GetStepFnArgs): TourStep { + { isLargeScreen, isANewUser }: OnboardingGetStepFnArgs): TourStep { const ui: TourStep['ui'] = { fadedElements: [ WalletHTMLElements.SIDEBAR_TESTNET, @@ -33,12 +33,16 @@ export function getBitcoinAddressStep( return { path: '/', tooltip: { - target: `.account-overview .bitcoin-account ${isSmallScreen.value || isMediumScreen.value - ? '> .bitcoin-account-item > svg' : ''}`, + get target() { + return `.account-overview .bitcoin-account ${!isLargeScreen.value + ? '> .bitcoin-account-item > svg' : ''}`; + }, content: getOnboardingTexts(OnboardingTourStep.BITCOIN_ADDRESS, isANewUser).default, params: { - placement: isSmallScreen.value || isMediumScreen.value ? 'top-start' : 'right-end', - // TODO Add margin in large screens + get placement() { + // TODO Add margin in large screens + return !isLargeScreen.value ? 'bottom-start' : 'left'; + }, }, }, ui, diff --git a/src/lib/tour/onboarding/05_WalletBalanceStep.ts b/src/lib/tour/onboarding/05_WalletBalanceStep.ts index 2e1aab6cb..a07d8a7eb 100644 --- a/src/lib/tour/onboarding/05_WalletBalanceStep.ts +++ b/src/lib/tour/onboarding/05_WalletBalanceStep.ts @@ -1,7 +1,7 @@ -import { GetStepFnArgs, OnboardingTourStep, TourStep, WalletHTMLElements } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep, WalletHTMLElements } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; -export function getWalletBalanceStep({ isSmallScreen, isANewUser }: GetStepFnArgs): TourStep { +export function getWalletBalanceStep({ isSmallScreen, isANewUser }: OnboardingGetStepFnArgs): TourStep { const ui: TourStep['ui'] = { fadedElements: [ WalletHTMLElements.SIDEBAR_TESTNET, @@ -32,11 +32,15 @@ export function getWalletBalanceStep({ isSmallScreen, isANewUser }: GetStepFnArg return { path: '/', tooltip: { - target: `${WalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE} - ${isSmallScreen.value ? '.amount' : '.balance-distribution'}`, + get target() { + return `${WalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE} + ${isSmallScreen.value ? '.amount' : '.balance-distribution'}`; + }, content: getOnboardingTexts(OnboardingTourStep.WALLET_BALANCE, isANewUser).default, params: { - placement: isSmallScreen.value ? 'bottom' : 'right', + get placement() { + return isSmallScreen.value ? 'bottom' : 'right'; + }, }, }, ui, diff --git a/src/lib/tour/onboarding/06_0_BackupAlertStep.ts b/src/lib/tour/onboarding/06_0_BackupAlertStep.ts index 27fae670a..e4e7dbf69 100644 --- a/src/lib/tour/onboarding/06_0_BackupAlertStep.ts +++ b/src/lib/tour/onboarding/06_0_BackupAlertStep.ts @@ -1,9 +1,9 @@ import { WalletHTMLElements } from '..'; -import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; export function getBackupAlertStep( - { isSmallScreen, isANewUser }: GetStepFnArgs): TourStep { + { isSmallScreen, isANewUser }: OnboardingGetStepFnArgs): TourStep { const ui: TourStep['ui'] = { fadedElements: [ WalletHTMLElements.SIDEBAR_TESTNET, diff --git a/src/lib/tour/onboarding/06_1_MenuIconStep.ts b/src/lib/tour/onboarding/06_1_MenuIconStep.ts index 086afe317..a9045e3a9 100644 --- a/src/lib/tour/onboarding/06_1_MenuIconStep.ts +++ b/src/lib/tour/onboarding/06_1_MenuIconStep.ts @@ -1,12 +1,12 @@ import { WalletHTMLElements } from '..'; -import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; -export function getMenuIconStep({ isANewUser }: GetStepFnArgs): TourStep { +export function getMenuIconStep({ isANewUser }: OnboardingGetStepFnArgs): TourStep { return { path: '/', tooltip: { - target: `${WalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR} > button.reset`, + target: `${WalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR} > button.reset`, content: getOnboardingTexts(OnboardingTourStep.MENU_ICON, isANewUser).default, params: { placement: 'bottom-start', @@ -15,7 +15,7 @@ export function getMenuIconStep({ isANewUser }: GetStepFnArgs { const hamburguerIcon = document.querySelector( - `${WalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR} > button.reset`) as HTMLButtonElement; + `${WalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR} > button.reset`) as HTMLButtonElement; hamburguerIcon!.addEventListener('click', () => goToNextStep(), { once: true, capture: true }); }, diff --git a/src/lib/tour/onboarding/07_1_BackupOptionNotLargeScreenStep.ts b/src/lib/tour/onboarding/07_1_BackupOptionNotLargeScreenStep.ts index 8e1e9bec0..63791fbc0 100644 --- a/src/lib/tour/onboarding/07_1_BackupOptionNotLargeScreenStep.ts +++ b/src/lib/tour/onboarding/07_1_BackupOptionNotLargeScreenStep.ts @@ -1,10 +1,10 @@ import { WalletHTMLElements } from '..'; -import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; export function getBackupOptionNotLargeScreenStep( { closeAccountOptions, openAccountOptions, isANewUser, isSmallScreen } - : GetStepFnArgs): TourStep { + : OnboardingGetStepFnArgs): TourStep { const ui: TourStep['ui'] = { fadedElements: [ WalletHTMLElements.SIDEBAR_TESTNET, @@ -41,7 +41,9 @@ export function getBackupOptionNotLargeScreenStep( content: getOnboardingTexts( OnboardingTourStep.BACKUP_OPTION_NOT_LARGE_SCREENS, isANewUser).default, params: { - placement: isSmallScreen.value ? 'top-start' : 'right', + get placement() { + return isSmallScreen.value ? 'top-start' : 'right'; + }, }, }, ui, diff --git a/src/lib/tour/onboarding/07_2_BackupOptionLargeScreenStep.ts b/src/lib/tour/onboarding/07_2_BackupOptionLargeScreenStep.ts index 1c0e5319c..6253c1a64 100644 --- a/src/lib/tour/onboarding/07_2_BackupOptionLargeScreenStep.ts +++ b/src/lib/tour/onboarding/07_2_BackupOptionLargeScreenStep.ts @@ -1,9 +1,9 @@ import { WalletHTMLElements } from '..'; -import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; export function getBackupOptionLargeScreenStep( - { isANewUser }: GetStepFnArgs): TourStep { + { isANewUser }: OnboardingGetStepFnArgs): TourStep { const ui: TourStep['ui'] = { fadedElements: [ WalletHTMLElements.SIDEBAR_TESTNET, diff --git a/src/lib/tour/onboarding/07_AccountOptionsStep.ts b/src/lib/tour/onboarding/07_AccountOptionsStep.ts index eed381c82..1c6f56942 100644 --- a/src/lib/tour/onboarding/07_AccountOptionsStep.ts +++ b/src/lib/tour/onboarding/07_AccountOptionsStep.ts @@ -1,8 +1,8 @@ import { WalletHTMLElements } from '..'; -import { GetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; -type AccountStep = GetStepFnArgs & { keepMenuOpenOnForward: boolean }; +type AccountStep = OnboardingGetStepFnArgs & { keepMenuOpenOnForward: boolean }; export function getAccountOptionsStep( { isSmallScreen, isLargeScreen, isANewUser, openAccountOptions, closeAccountOptions, keepMenuOpenOnForward } : AccountStep): TourStep { @@ -35,12 +35,16 @@ export function getAccountOptionsStep( }; return { - path: isLargeScreen.value ? '/' : '/?sidebar=true', + get path() { + return isLargeScreen.value ? '/' : '/?sidebar=true'; + }, tooltip: { target: WalletHTMLElements.MODAL_PAGE, content: getOnboardingTexts(OnboardingTourStep.ACCOUNT_OPTIONS, isANewUser).default, params: { - placement: isSmallScreen.value ? 'top' : 'right', + get placement() { + return isSmallScreen.value ? 'top' : 'right'; + }, }, }, ui, diff --git a/src/lib/tour/onboarding/08_OnboardingCompleted.ts b/src/lib/tour/onboarding/08_OnboardingCompleted.ts index 7d8e43e4a..314e846f3 100644 --- a/src/lib/tour/onboarding/08_OnboardingCompleted.ts +++ b/src/lib/tour/onboarding/08_OnboardingCompleted.ts @@ -1,9 +1,8 @@ -import { useAccountStore } from '@/stores/Account'; -import { GetStepFnArgs, OnboardingTourStep, TourStep, WalletHTMLElements } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourStep, WalletHTMLElements } from '../types'; import { getOnboardingTexts } from './OnboardingTourTexts'; export function getOnboardingCompletedStep( - { root, isLargeScreen, isANewUser }: GetStepFnArgs): TourStep { + { root, isLargeScreen, isANewUser }: OnboardingGetStepFnArgs): TourStep { const ui: TourStep['ui'] = { fadedElements: [ WalletHTMLElements.SIDEBAR_TESTNET, @@ -32,12 +31,18 @@ export function getOnboardingCompletedStep( ], }; return { - path: isLargeScreen.value ? '/' : '/?sidebar=true', + get path() { + return isLargeScreen.value ? '/' : '/?sidebar=true'; + }, tooltip: { - target: `${WalletHTMLElements.SIDEBAR_NETWORK} ${isLargeScreen.value ? 'span' : '.consensus-icon'}`, + get target() { + return `${WalletHTMLElements.SIDEBAR_NETWORK} ${isLargeScreen.value ? 'span' : '.consensus-icon'}`; + }, content: getOnboardingTexts(OnboardingTourStep.ONBOARDING_COMPLETED, isANewUser).default, params: { - placement: isLargeScreen.value ? 'right' : 'top-start', + get placement() { + return isLargeScreen.value ? 'right' : 'top-start'; + }, }, button: { text: 'Go to Network', @@ -45,9 +50,12 @@ export function getOnboardingCompletedStep( if (endTour) { await endTour(); } - const { setTour } = useAccountStore(); - setTour({ name: 'network', isANewUser: false }); - root.$router.push('/network'); + root.$router.push({ + name: 'network', + params: { + showNetworkInfo: 'true', + }, + }); }, }, }, diff --git a/src/lib/tour/onboarding/index.ts b/src/lib/tour/onboarding/index.ts index 0ae2a219f..b551368a4 100644 --- a/src/lib/tour/onboarding/index.ts +++ b/src/lib/tour/onboarding/index.ts @@ -2,7 +2,7 @@ import { useWindowSize } from '@/composables/useWindowSize'; import { AccountType, useAccountStore } from '@/stores/Account'; import { SetupContext } from '@vue/composition-api'; import { searchComponentByName, TourName } from '..'; -import { GetStepFnArgs, OnboardingTourStep, TourSteps } from '../types'; +import { OnboardingGetStepFnArgs, OnboardingTourStep, TourSteps } from '../types'; import { getFirstAddressStep } from './01_FirstAddressStep'; import { getTransactionListStep } from './02_TransactionListStep'; import { getFirstTransactionStep } from './03_FirstTransactionStep'; @@ -51,7 +51,7 @@ export function getOnboardingTourSteps({ root }: SetupContext): TourSteps = { + const args: OnboardingGetStepFnArgs = { sleep, toggleDisabledAttribute, root, diff --git a/src/lib/tour/types.ts b/src/lib/tour/types.ts index f3d7d00b7..52c016a65 100644 --- a/src/lib/tour/types.ts +++ b/src/lib/tour/types.ts @@ -81,8 +81,7 @@ export type TourSteps = { [x in T]?: TourStep; }; -// TODO Rename or mix with NetworkGetStepFnArgs -export type GetStepFnArgs = +export type OnboardingGetStepFnArgs = Pick, 'isSmallScreen' | 'isMediumScreen' | 'isLargeScreen'> & { root: SetupContext['root'], From 8a8fd0896094c296a71ff555e93914dcd330ab12 Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 2 Feb 2022 22:42:23 +0100 Subject: [PATCH 18/44] small tweaks and ui improvments --- src/App.vue | 3 +- src/components/Tour.vue | 150 +++++++++++++----- src/components/TourLargeScreenManager.vue | 55 ++++--- src/components/layouts/AccountOverview.vue | 6 +- src/components/layouts/Settings.vue | 4 +- src/components/layouts/Sidebar.vue | 16 +- .../modals/DiscoverTheNimiqWalletModal.vue | 14 +- src/lib/tour/network/01_YourLocationStep.ts | 1 + src/lib/tour/network/02_BackboneNodeStep.ts | 1 + src/lib/tour/network/03_NetworkMetricsStep.ts | 1 + .../tour/network/04_NetworkCompletedStep.ts | 1 + .../tour/onboarding/01_FirstAddressStep.ts | 33 +++- .../tour/onboarding/02_TransactionListStep.ts | 28 ++-- .../onboarding/03_FirstTransactionStep.ts | 23 ++- .../tour/onboarding/04_BitcoinAddressStep.ts | 21 ++- .../tour/onboarding/05_WalletBalanceStep.ts | 29 +++- .../tour/onboarding/06_0_BackupAlertStep.ts | 14 +- src/lib/tour/onboarding/06_1_MenuIconStep.ts | 12 +- .../07_1_BackupOptionNotLargeScreenStep.ts | 11 +- .../07_2_BackupOptionLargeScreenStep.ts | 14 +- .../tour/onboarding/07_AccountOptionsStep.ts | 11 +- .../tour/onboarding/08_OnboardingCompleted.ts | 11 +- .../tour/onboarding/OnboardingTourTexts.ts | 17 +- src/lib/tour/onboarding/index.ts | 33 ++-- src/lib/tour/types.ts | 14 +- src/stores/Account.ts | 4 +- 26 files changed, 364 insertions(+), 163 deletions(-) diff --git a/src/App.vue b/src/App.vue index f80f5da2a..e7fa3b54d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -176,8 +176,7 @@ export default defineComponent({ @include flex-full-height; @include ios-flex; - // TODO: Find another alternative. - // overflow: hidden; // To prevent horizontal scrollbars during panel sliding + overflow: hidden; // To prevent horizontal scrollbars during panel sliding touch-action: pan-y; /* Default: >= 1500px */ diff --git a/src/components/Tour.vue b/src/components/Tour.vue index b061a252b..441c71b85 100644 --- a/src/components/Tour.vue +++ b/src/components/Tour.vue @@ -82,7 +82,7 @@ @click="goToPrevStep()" style="transform: rotate(180deg)" > - +

@@ -102,7 +102,7 @@ import { useAccountStore } from '@/stores/Account'; import { useNetworkStore } from '@/stores/Network'; import { useTransactionsStore } from '@/stores/Transactions'; -import { CircleSpinner } from '@nimiq/vue-components'; +import { CircleSpinner, CaretRightSmallIcon } from '@nimiq/vue-components'; import { computed, defineComponent, @@ -121,7 +121,6 @@ import { MountedReturnFn, TourBroadcast, TourStep, TourStepIndex, } from '../lib/tour'; -import CaretRightIcon from './icons/CaretRightIcon.vue'; import PartyConfettiIcon from './icons/PartyConfettiIcon.vue'; import TourPreviousLeftArrowIcon from './icons/TourPreviousLeftArrowIcon.vue'; @@ -137,8 +136,7 @@ export default defineComponent({ const { state: $network } = useNetworkStore(); const disconnected = computed(() => $network.consensus !== 'established'); - const { state: tourStore, setTour, activeAccountInfo } = useAccountStore(); - activeAccountInfo.value!.fileExported = false; + const { state: tourStore, setTour } = useAccountStore(); let tour: VueTour.Tour | null = null; const tourOptions: any = { @@ -217,7 +215,10 @@ export default defineComponent({ isLoading.value = false; window.addEventListener('keyup', _onKeyDown); - window.addEventListener('click', _userClicked()); + setTimeout(() => { + window.addEventListener('click', _userClicked()); + }, 100); // avoid click event to be triggered by the setting button + // window.addEventListener('resize', _OnResize(_OnResizeEnd)); TODO const app = document.querySelector('#app'); @@ -236,22 +237,26 @@ export default defineComponent({ // Dont allow user to interact with the page while it is loading // But allow to end it watch([isLoading, disconnected], async () => { - const app = document.querySelector('#app main') as HTMLDivElement; - - if (isLoading.value || disconnected.value) { - app.setAttribute('data-non-interactable', ''); - } else { - app.removeAttribute('data-non-interactable'); - } - - // FIXME we should wait until the button is rendered and the we could + // TODO Avoid interaction with any of the elements when loading except tour elements (bar, manager and tooltip) + // const elements = Object.values(WalletHTMLElements).filter((e) => e); + // if (isLoading.value || disconnected.value) { + // elements.forEach((element) => { + // const el = document.querySelector(element); + // if (!el) return; + // el.setAttribute('data-non-interactable', 'loading'); + // }); + // } else { + // elements.forEach((element) => { + // const el = document.querySelector(element); + // if (!el) return; + // el.removeAttribute('data-non-interactable'); + // }); + // } + + // FIXME we should wait until the buttons are rendered and the we could // execute _toggleDisabledButtons but it is kind of random the amount of time - // it takes to render the button. I don't know how to fix it. - - // Ensure that we disabled 'Receive Free NIM' button - await sleep(500); // TODO - // TODO Remove this code for the network, find other way - // steps = Object.values(getTour(tourStore.tour?.name, context)); + // it takes to render the button. I don't know how to fix it. Waiting 500ms works. + await sleep(500); _toggleDisabledButtons(steps[currentStep.value]?.ui.disabledButtons, true); }); @@ -274,7 +279,6 @@ export default defineComponent({ const { path: currentPath, ui: currentUI } = steps[currentStepIndex]!; const { path: futurePath, ui: futureUI, lifecycle: futureLifecycle } = steps[futureStepIndex]!; - isLoading.value = true; tour!.stop(); await sleep(500); @@ -294,6 +298,7 @@ export default defineComponent({ _toggleDisabledButtons(currentUI.disabledButtons, false); _toggleDisabledButtons(futureUI.disabledButtons, true); _addAttributes(futureUI, futureStepIndex); + await context.root.$nextTick(); if (futurePath !== currentPath) { await sleep(500); @@ -302,6 +307,7 @@ export default defineComponent({ _removeAttributes(currentStepIndex); tour!.start(futureStepIndex.toString()); + await context.root.$nextTick(); // FIXME Instead of doing tour!.end and tour!.start, we could also use .nextStep() or previsousStep() // The problem with this solution is that some animations glitch the UI so it needs further @@ -309,7 +315,6 @@ export default defineComponent({ // goingForward ? tour!.nextStep() : tour!.previousStep(); // mounted - isLoading.value = false; disableNextStep.value = futureStepIndex >= nSteps.value - 1 || !!futureUI.isNextStepDisabled; unmounted = await futureLifecycle?.mounted?.({ @@ -338,6 +343,17 @@ export default defineComponent({ context.root.$on('nimiq-tour-event', (data: TourBroadcast) => { if (data.type === 'end-tour') endTour(); }); + context.root.$on('nimiq-tour-event', (data: TourBroadcast) => { + if (data.type === 'clicked-outside-tour') { + const tourManager = document.querySelector('.tour-control-bar'); + if (tourManager) { + tourManager.classList.add('flash'); + setTimeout(() => { + tourManager.classList.remove('flash'); + }, 400); + } + } + }); } function _userClicked() { @@ -382,12 +398,17 @@ export default defineComponent({ } } + function _onScrollLockedElement(e: Event, el: Element) { + e.preventDefault(); + el.scrollTop = 0; + } function _addAttributes( uiConfig: TourStep['ui'], stepIndex: TourStepIndex, ) { const fadedElements = uiConfig.fadedElements || []; const disabledElements = uiConfig.disabledElements || []; + const scrollLockedElements = uiConfig.scrollLockedElements || []; disabledElements.filter((e) => e).forEach((element) => { const el = document.querySelector(element); @@ -401,20 +422,33 @@ export default defineComponent({ el.setAttribute('data-opacified', stepIndex.toString()); el.setAttribute('data-non-interactable', stepIndex.toString()); }); + + scrollLockedElements.filter((e) => e).forEach((element) => { + const el = document.querySelector(element); + if (!el) return; + el.setAttribute('data-scroll-locked', stepIndex.toString()); + // Avoid scrolling when tooltip is instantiated + el.addEventListener('scroll', (e) => _onScrollLockedElement(e, el)); + el.scrollTop = 0; + }); } function _removeAttributes(stepIndex: TourStepIndex) { - document - .querySelectorAll(`[data-non-interactable="${stepIndex}"]`) + document.querySelectorAll(`[data-non-interactable="${stepIndex}"]`) .forEach((el) => { el.removeAttribute('data-non-interactable'); }); - document - .querySelectorAll(`[data-opacified="${stepIndex}"]`) + document.querySelectorAll(`[data-opacified="${stepIndex}"]`) .forEach((el) => { el.removeAttribute('data-opacified'); }); + + document.querySelectorAll(`[data-scroll-locked="${stepIndex}"]`) + .forEach((el) => { + el.removeAttribute('data-scroll-locked'); + el.addEventListener('scroll', (e) => _onScrollLockedElement(e, el)); + }); } async function endTour(soft = false) { @@ -495,7 +529,7 @@ export default defineComponent({ // control bar currentStep, nSteps, - isLoading: disconnected || isLoading, + isLoading: computed(() => disconnected.value || isLoading.value), disableNextStep, // actions @@ -509,7 +543,7 @@ export default defineComponent({ }; }, components: { - CaretRightIcon, + CaretRightSmallIcon, TourPreviousLeftArrowIcon, PartyConfettiIcon, CircleSpinner, @@ -530,24 +564,43 @@ export default defineComponent({ [data-tour-active] [data-non-interactable] * { user-select: none !important; pointer-events: none !important; + cursor: not-allowed; } -[data-tour-active]#app > *:not(.tour):not(.tour-manager) { - cursor: not-allowed; +[data-tour-active] [data-scroll-locked], +[data-tour-active] [data-scroll-locked] * { + overflow: hidden; } -[data-tour-active] button.highlighted { +[data-tour-active] button.green-highlight { background: linear-gradient( 274.28deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.2) 27.6%, rgba(255, 255, 255, 0) 53.12%, rgba(255, 255, 255, 0.2) 81.25%, rgba(255, 255, 255, 0) 100% - ), - radial-gradient(100% 100% at 100% 100%, #41A38E 0%, #21BCA5 100%) !important; + ), var(--nimiq-green-bg) !important; + background-blend-mode: hard-light, normal !important; +} + +[data-tour-active] button.gray-highlight { + background: linear-gradient( + 274.28deg, rgba(31, 35, 72, 0) 0%, rgba(31, 35, 72, 0.07) 27.6%, rgba(31, 35, 72, 0) 53.12%, + rgba(31, 35, 72, 0.07) 81.25%, rgba(31, 35, 72, 0) 100%) !important; + background-blend-mode: hard-light, normal !important; } diff --git a/src/components/layouts/AccountOverview.vue b/src/components/layouts/AccountOverview.vue index a9bb01647..3a742402b 100644 --- a/src/components/layouts/AccountOverview.vue +++ b/src/components/layouts/AccountOverview.vue @@ -155,7 +155,7 @@ export default defineComponent({ const canHaveMultipleAddresses = computed(() => (activeAccountInfo.value || false) && activeAccountInfo.value.type !== AccountType.LEGACY); - const { isSmallScreen, isMediumScreen } = useWindowSize(); + const { isSmallScreen, isLargeScreen } = useWindowSize(); function onAddressSelected() { setActiveCurrency(CryptoCurrency.NIM); @@ -176,12 +176,12 @@ export default defineComponent({ const showFullLegacyAccountNotice = computed(() => isLegacyAccount.value && activeAccountInfo.value!.addresses.length === 1 - && !isMediumScreen.value); + && isLargeScreen.value); const showModalLegacyAccountNotice = ref(false); function determineIfShowModalLegacyAccountNotice() { - showModalLegacyAccountNotice.value = isLegacyAccount.value && isMediumScreen.value; + showModalLegacyAccountNotice.value = isLegacyAccount.value && !isLargeScreen.value; } function determineModalToShow() { diff --git a/src/components/layouts/Settings.vue b/src/components/layouts/Settings.vue index 08908f248..85479d92a 100644 --- a/src/components/layouts/Settings.vue +++ b/src/components/layouts/Settings.vue @@ -276,7 +276,7 @@ import { CircleSpinner } from '@nimiq/vue-components'; import { Portal } from '@linusborg/vue-simple-portal'; import { useAccountStore } from '@/stores/Account'; -import { TourName } from '@/lib/tour'; +import { TourName, TourOrigin } from '@/lib/tour'; import MenuIcon from '../icons/MenuIcon.vue'; import CrossCloseButton from '../CrossCloseButton.vue'; import CountryFlag from '../CountryFlag.vue'; @@ -375,7 +375,7 @@ export default defineComponent({ } function goToOnboardingTour() { - useAccountStore().setTour({ name: TourName.ONBOARDING, isANewUser: false }); + useAccountStore().setTour({ name: TourName.ONBOARDING, startedFrom: TourOrigin.SETTINGS }); context.root.$router.push('/'); } diff --git a/src/components/layouts/Sidebar.vue b/src/components/layouts/Sidebar.vue index a3c885b1b..a4820134c 100644 --- a/src/components/layouts/Sidebar.vue +++ b/src/components/layouts/Sidebar.vue @@ -16,8 +16,10 @@ Nimiq - - +
+ + +
@@ -230,10 +232,16 @@ export default defineComponent({ cursor: pointer; } -.announcement-box { +.panels { + display: flex; + flex-direction: column; + gap: 2rem; margin-bottom: 2.5rem; margin-top: 2rem; - align-self: stretch; + + ::v-deep .announcement-box, ::v-deep .tour-manager { + align-self: stretch; + } } .price-chart-wrapper { diff --git a/src/components/modals/DiscoverTheNimiqWalletModal.vue b/src/components/modals/DiscoverTheNimiqWalletModal.vue index 8bc34e1fb..3dc9ad8b0 100644 --- a/src/components/modals/DiscoverTheNimiqWalletModal.vue +++ b/src/components/modals/DiscoverTheNimiqWalletModal.vue @@ -2,7 +2,6 @@ -