diff --git a/.tx/config b/.tx/config index 5026ff175..1d1fe4c52 100644 --- a/.tx/config +++ b/.tx/config @@ -7,4 +7,3 @@ minimum_perc = 0 source_file = src/i18n/en.po source_lang = en type = PO - 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..a63c5de5b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,9 @@ @@ -143,6 +145,7 @@ function getLocaleMonthStringFromDate( // } export default defineComponent({ + name: 'transactions-list', props: { searchString: { type: String, @@ -163,8 +166,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 address const txsForActiveAddress = computed(() => Object.values(transactions$.transactions) @@ -607,11 +610,11 @@ export default defineComponent({ color: var(--text-60); font-weight: 600; margin-bottom: 1rem; + } - a { - color: inherit; - text-decoration: underline; - } + a { + color: inherit; + text-decoration: none; } } diff --git a/src/components/UpdateNotification.vue b/src/components/UpdateNotification.vue index 3fc73bb21..5043ad622 100644 --- a/src/components/UpdateNotification.vue +++ b/src/components/UpdateNotification.vue @@ -18,6 +18,7 @@ - - diff --git a/src/components/icons/NimiqLogoOutlineWithStars.vue b/src/components/icons/NimiqLogoOutlineWithStars.vue new file mode 100644 index 000000000..ae6fa4af9 --- /dev/null +++ b/src/components/icons/NimiqLogoOutlineWithStars.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/icons/PartyConfettiIcon.vue b/src/components/icons/PartyConfettiIcon.vue new file mode 100644 index 000000000..64662334b --- /dev/null +++ b/src/components/icons/PartyConfettiIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/icons/SpeechBubble.vue b/src/components/icons/SpeechBubble.vue new file mode 100644 index 000000000..0387070fb --- /dev/null +++ b/src/components/icons/SpeechBubble.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/icons/TourPreviousLeftArrowIcon.vue b/src/components/icons/TourPreviousLeftArrowIcon.vue new file mode 100644 index 000000000..d94119f99 --- /dev/null +++ b/src/components/icons/TourPreviousLeftArrowIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/layouts/AccountOverview.vue b/src/components/layouts/AccountOverview.vue index a9a75bcb8..3a742402b 100644 --- a/src/components/layouts/AccountOverview.vue +++ b/src/components/layouts/AccountOverview.vue @@ -155,12 +155,12 @@ export default defineComponent({ const canHaveMultipleAddresses = computed(() => (activeAccountInfo.value || false) && activeAccountInfo.value.type !== AccountType.LEGACY); - const { isMobile, isTablet } = useWindowSize(); + const { isSmallScreen, isLargeScreen } = useWindowSize(); function onAddressSelected() { setActiveCurrency(CryptoCurrency.NIM); - if (isMobile.value) { + if (isSmallScreen.value) { context.root.$router.push('/transactions'); } } @@ -168,7 +168,7 @@ export default defineComponent({ function selectBitcoin() { setActiveCurrency(CryptoCurrency.BTC); - if (isMobile.value) { + if (isSmallScreen.value) { context.root.$router.push('/transactions'); } } @@ -176,12 +176,12 @@ export default defineComponent({ const showFullLegacyAccountNotice = computed(() => isLegacyAccount.value && activeAccountInfo.value!.addresses.length === 1 - && !isTablet.value); + && isLargeScreen.value); const showModalLegacyAccountNotice = ref(false); function determineIfShowModalLegacyAccountNotice() { - showModalLegacyAccountNotice.value = isLegacyAccount.value && isTablet.value; + showModalLegacyAccountNotice.value = isLegacyAccount.value && !isLargeScreen.value; } function determineModalToShow() { diff --git a/src/components/layouts/AddressOverview.vue b/src/components/layouts/AddressOverview.vue index f76f00622..c8ba76bd8 100644 --- a/src/components/layouts/AddressOverview.vue +++ b/src/components/layouts/AddressOverview.vue @@ -250,11 +250,11 @@ export default defineComponent({ } }); - const { isMobile, isFullDesktop } = useWindowSize(); + const { isSmallScreen, isLargeScreen } = useWindowSize(); - const addressMaskedWidth = computed(() => isFullDesktop.value + const addressMaskedWidth = computed(() => isLargeScreen.value ? 396 - : !isMobile.value + : !isSmallScreen.value ? 372 : 322); diff --git a/src/components/layouts/Network.vue b/src/components/layouts/Network.vue index f5752b767..b3f06520a 100644 --- a/src/components/layouts/Network.vue +++ b/src/components/layouts/Network.vue @@ -56,8 +56,9 @@ import { useSettingsStore } from '../../stores/Settings'; const LOCALSTORAGE_KEY = 'network-info-dismissed'; export default defineComponent({ - setup() { - const showNetworkInfo = ref(!window.localStorage.getItem(LOCALSTORAGE_KEY)); + setup(props, context) { + const showNetworkInfo = ref( + !window.localStorage.getItem(LOCALSTORAGE_KEY) || !!context.root.$route.params.showNetworkInfo); function onNetworkInfoClosed() { window.localStorage.setItem(LOCALSTORAGE_KEY, '1'); @@ -119,6 +120,7 @@ export default defineComponent({ .scroller { max-width: 100vw; + scroll-behavior: smooth; &.map { width: 100%; diff --git a/src/components/layouts/Settings.vue b/src/components/layouts/Settings.vue index 94454263c..f7fe7dd45 100644 --- a/src/components/layouts/Settings.vue +++ b/src/components/layouts/Settings.vue @@ -87,6 +87,20 @@ + +
+
+ +

+ {{ $t('Go through the product again') }} +

+
+ + +
+ -

- - - - - - -
- - - - - - + + + + + + +
+ + + {{ option.name }} + + +
+ +
+
+ +

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

- {{ $t('All in one easy place.') }} + {{ $t('It\'s free, does not collect data and is controlled by no one but you.') }}

-
- - -
- - -
-

- {{ $t('The Nimiq wallet is more than a web-wallet, it is a network node.') }} -

-

- {{ $t('Send and receive NIM directly on the blockchain.') }} -

-

- {{ $t('It is fast, safe and makes you truly independent.') }} -

-
-
- -
-
- - - -
- - - {{ lang.name }} - +
+ +
- +
+ - diff --git a/src/composables/useEventListener.ts b/src/composables/useEventListener.ts new file mode 100644 index 000000000..5fbf33b70 --- /dev/null +++ b/src/composables/useEventListener.ts @@ -0,0 +1,11 @@ +import { onBeforeUnmount, onMounted } from '@vue/composition-api'; + +export function useEventListener( + target: EventTarget, + event: string, + handler: (e: any) => any, + options?: AddEventListenerOptions | boolean, +) { + onMounted(() => target.addEventListener(event, handler, options)); + onBeforeUnmount(() => target.removeEventListener(event, handler)); +} diff --git a/src/composables/useKeys.ts b/src/composables/useKeys.ts new file mode 100644 index 000000000..840d3055f --- /dev/null +++ b/src/composables/useKeys.ts @@ -0,0 +1,20 @@ +import { Ref } from '@vue/composition-api'; +import { useEventListener } from './useEventListener'; + +type validKeys = `Arrow${'Right' | 'Left' | 'Up' | 'Down'}` | 'Enter' | 'Escape' + +interface KeyEvent { + key: validKeys; + handler: () => any; + options?: { ignoreIf?: Ref, onlyIf?: Ref }; +} + +export function useKeys(keyEvents: KeyEvent[]) { + useEventListener(window, 'keydown', (e: KeyboardEvent) => { + const keyEvent = keyEvents.find((key) => key.key === e.key); + if (!keyEvent || keyEvent.options?.ignoreIf?.value || keyEvent.options?.onlyIf?.value === false) return; + e.preventDefault(); + e.stopPropagation(); + keyEvent.handler(); + }, true); +} diff --git a/src/composables/useMedia.ts b/src/composables/useMedia.ts new file mode 100644 index 000000000..40f8de08a --- /dev/null +++ b/src/composables/useMedia.ts @@ -0,0 +1,11 @@ +import { Ref, ref } from '@vue/composition-api'; +import { useEventListener } from './useEventListener'; + +export function useMedia(query: string): Readonly> { + const mediaQuery = window.matchMedia(query); + const matches = ref(mediaQuery.matches); + useEventListener(mediaQuery, 'change', (e) => { + matches.value = e.matches; + }); + return matches; +} diff --git a/src/composables/useOutsideClick.ts b/src/composables/useOutsideClick.ts new file mode 100644 index 000000000..93a9b90da --- /dev/null +++ b/src/composables/useOutsideClick.ts @@ -0,0 +1,49 @@ +import { useEventListener } from './useEventListener'; + +// Polyfill +function microTask(cb: () => void) { + if (typeof queueMicrotask === 'function') { + queueMicrotask(cb); + } else { + Promise.resolve() + .then(cb) + .catch((e) => + setTimeout(() => { + throw e; + }), + ); + } +} + +export function useOutsideClick( + selectors: string[], + cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void, +) { + let called = false; + function handle(event: MouseEvent | PointerEvent) { + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + + if (called) return; + called = true; + microTask(() => { + called = false; + }); + + const target = event.target as HTMLElement; + + // Ignore if the target doesn't exist in the DOM anymore + if (!target.ownerDocument.documentElement.contains(target)) return; + + // Ignore if the target exists in one of the containers + for (const container of selectors.map((selector) => document.querySelector(selector))) { + if (container?.contains(target)) return; + } + + cb(event, target); + } + + useEventListener(window, 'pointerdown', handle, true); + useEventListener(window, 'mousedown', handle, true); +} diff --git a/src/composables/useWindowSize.ts b/src/composables/useWindowSize.ts index b8f1e9d68..c1a39f17a 100644 --- a/src/composables/useWindowSize.ts +++ b/src/composables/useWindowSize.ts @@ -1,53 +1,14 @@ -import { ref, onMounted, onUnmounted, Ref, computed } from '@vue/composition-api'; +import { computed, Ref } from '@vue/composition-api'; +import { useMedia } from './useMedia'; -let numberOfListeners = 0; - -// FIXME: In Vue 2, composition-api methods cannot be used before the plugin is activated. -// When switching to Vue 3, the width and height variables can be directly instantiated -// as refs. -let width: Ref | null = null; -let height: Ref | null = null; - -let isMobile: Readonly> | null = null; -let isTablet: Readonly> | null = null; -let isFullDesktop: Readonly> | null = null; - -function listener() { - width!.value = window.innerWidth; - height!.value = window.innerHeight; -} +export type ScreenTypes = Pick, 'isSmallScreen' | 'isMediumScreen' | 'isLargeScreen'> export function useWindowSize() { - // First-time setup - if (!width || !height || !isMobile || !isTablet || !isFullDesktop) { - width = ref(0); - height = ref(0); - listener(); - isMobile = computed(() => width!.value <= 700); // Full mobile breakpoint - isTablet = computed(() => width!.value <= 960); // Tablet breakpoint - isFullDesktop = computed(() => width!.value > 1160); // Desktop breakpoint - } - - onMounted(() => { - if (numberOfListeners === 0) { - window.addEventListener('resize', listener); - } - numberOfListeners += 1; - }); - - onUnmounted(() => { - numberOfListeners -= 1; - - if (numberOfListeners === 0) { - window.removeEventListener('resize', listener); - } - }); - return { - width, - height, - isMobile, - isTablet, - isFullDesktop, + width: computed(() => window.innerWidth) as Readonly>, + height: computed(() => window.innerHeight) as Readonly>, + isSmallScreen: useMedia('(max-width: 700px)'), + isMediumScreen: useMedia('(min-width: 700px) and (max-width: 1160px)'), + isLargeScreen: useMedia('(min-width: 1160px)'), }; } diff --git a/src/i18n/en.po b/src/i18n/en.po index bdd8a6572..824cd1446 100644 --- a/src/i18n/en.po +++ b/src/i18n/en.po @@ -2,6 +2,14 @@ msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" +#: src/lib/tour/network/NetworkTourTexts.ts:18 +msgid "‘Available browsers’ are other user’s browsers, just like yours." +msgstr "" + +#: src/lib/tour/network/NetworkTourTexts.ts:19 +msgid "‘Backbone nodes’ provide a fallback to connect to." +msgstr "" + #: src/components/modals/MigrationWelcomeModal.vue:32 msgid "{accounts} hold, manage and aggregate addresses." msgstr "" @@ -115,7 +123,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:356 msgid "" "A contact with the address \"{address}\", but a different name already exists.\n" " Do you want to replace it?" @@ -139,7 +147,7 @@ msgstr "" msgid "Activate Bitcoin" msgstr "" -#: src/components/layouts/Settings.vue:198 +#: src/components/layouts/Settings.vue:212 msgid "Add" msgstr "" @@ -155,7 +163,7 @@ msgstr "" msgid "Add a public message..." msgstr "" -#: src/components/AccountMenu.vue:70 +#: src/components/modals/AccountMenuModal.vue:43 msgid "Add account" msgstr "" @@ -163,7 +171,7 @@ msgstr "" msgid "Add address" msgstr "" -#: src/components/layouts/Settings.vue:194 +#: src/components/layouts/Settings.vue:208 msgid "Add an existing vesting contract to your wallet." msgstr "" @@ -186,7 +194,7 @@ msgstr "" msgid "Addresses" msgstr "" -#: src/components/layouts/Settings.vue:152 +#: src/components/layouts/Settings.vue:166 msgid "Advanced" msgstr "" @@ -194,7 +202,7 @@ msgstr "" msgid "After 72 hours" msgstr "" -#: src/components/layouts/Settings.vue:132 +#: src/components/layouts/Settings.vue:146 #: src/components/layouts/Settings.vue:72 msgid "all" msgstr "" @@ -203,15 +211,15 @@ 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." msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:67 +msgid "All security relevant actions can be found here too." +msgstr "" + #: src/components/layouts/AddressOverview.vue:149 msgid "All swaps are part of your transaction history and feature a small swap icon." msgstr "" @@ -232,10 +240,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 "" @@ -264,11 +268,11 @@ msgstr "" msgid "Atomic swaps require two BTC transactions." msgstr "" -#: src/components/layouts/Settings.vue:184 +#: src/components/layouts/Settings.vue:198 msgid "Auto" msgstr "" -#: src/components/layouts/Settings.vue:177 +#: src/components/layouts/Settings.vue:191 msgid "Automatic mode uses {behavior}." msgstr "" @@ -277,6 +281,7 @@ msgid "Awaiting swap secret" msgstr "" #: src/components/layouts/Network.vue:15 +#: src/components/Tour.vue:55 msgid "Back to addresses" msgstr "" @@ -307,7 +312,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:133 #: src/components/LegacyAccountNotice.vue:21 #: src/components/modals/BtcTransactionModal.vue:119 #: src/components/modals/BtcTransactionModal.vue:135 @@ -320,7 +325,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:152 msgid "Bitcoin Unit" msgstr "" @@ -348,7 +353,7 @@ msgstr "" msgid "BTC network fee" msgstr "" -#: src/components/layouts/Sidebar.vue:30 +#: src/components/layouts/Sidebar.vue:33 msgid "Buy" msgstr "" @@ -379,7 +384,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 +441,7 @@ msgstr "" msgid "Cancelled Swap" msgstr "" -#: src/components/layouts/Settings.vue:319 +#: src/components/layouts/Settings.vue:336 msgid "Cannot import contacts, wrong file format." msgstr "" @@ -454,7 +459,7 @@ msgstr "" msgid "Cashlink to {address}" msgstr "" -#: src/components/AccountMenu.vue:50 +#: src/components/modals/AccountMenuModal.vue:23 msgid "Change password" msgstr "" @@ -462,6 +467,11 @@ msgstr "" msgid "Change your language setting." msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:43 +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:48 +msgid "Check the bar-chart to see how your addresses compose your total balance." +msgstr "" + #: src/components/modals/AddressSelectorModal.vue:8 #: src/components/modals/SendModal.vue:61 msgid "Choose a Recipient" @@ -483,14 +493,22 @@ msgstr "" msgid "Claiming Cashlink" msgstr "" -#: src/components/layouts/Settings.vue:210 +#: src/components/layouts/Settings.vue:224 msgid "Clear" msgstr "" -#: src/components/layouts/Settings.vue:204 +#: src/components/layouts/Settings.vue:218 msgid "Clear Cache" msgstr "" +#: src/lib/tour/network/NetworkTourTexts.ts:34 +msgid "Click {account_icon} to get back to your wallet." +msgstr "" + +#: src/lib/tour/network/NetworkTourTexts.ts:40 +msgid "Click {back_to_addresses} to get back to your wallet." +msgstr "" + #: src/components/swap/SwapNotification.vue:36 msgid "Click for more information" msgstr "" @@ -499,6 +517,10 @@ msgstr "" msgid "Click on ‘Troubleshooting’ to learn more." msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:20 +msgid "Click the green button to receive a free NIM from Team Nimiq." +msgstr "" + #: src/components/layouts/AddressOverview.vue:151 #: src/components/swap/SwapAnimation.vue:90 msgid "Close" @@ -524,12 +546,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 +584,6 @@ msgstr "" #: src/components/modals/MigrationWelcomeModal.vue:87 #: src/components/modals/SendModal.vue:87 -#: src/components/modals/WelcomeModal.vue:63 msgid "Continue" msgstr "" @@ -613,6 +628,10 @@ msgstr "" msgid "Create request link" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:66 +msgid "Create, switch and log out of accounts." +msgstr "" + #: src/components/modals/BtcReceiveModal.vue:194 msgid "Created {count} day ago | Created {count} days ago" msgstr "" @@ -641,6 +660,10 @@ msgstr "" msgid "Currently under maintenance" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:44 +msgid "Currently you have 100% NIM, and no BTC." +msgstr "" + #: src/components/modals/BuyOptionsModal.vue:7 msgid "" "Depending on your country of residence,\n" @@ -651,11 +674,15 @@ msgstr "" msgid "Developer resources" msgstr "" -#: src/components/layouts/Settings.vue:248 +#: src/components/layouts/Settings.vue:262 #: src/components/modals/DisclaimerModal.vue:5 msgid "Disclaimer" msgstr "" +#: src/components/modals/DiscoverTheNimiqWalletModal.vue:18 +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 +717,7 @@ msgstr "" msgid "Edit contacts" msgstr "" -#: src/components/layouts/Settings.vue:125 +#: src/components/layouts/Settings.vue:139 msgid "Edit the amount of decimals visible for BTC values." msgstr "" @@ -703,10 +730,20 @@ msgstr "" msgid "Edit transaction" msgstr "" -#: src/components/layouts/Settings.vue:158 +#: src/components/layouts/Settings.vue:172 msgid "Enable experimental support for swiping gestures on mobile." msgstr "" +#: src/components/Tour.vue:114 +#: src/lib/tour/network/04_NetworkCompletedStep.ts:32 +msgid "End Tour" +msgstr "" + +#: src/lib/tour/network/NetworkTourTexts.ts:33 +#: src/lib/tour/network/NetworkTourTexts.ts:39 +msgid "Enjoy the decentralized future, and don’t forget to invite your friends and family." +msgstr "" + #: src/components/modals/overlays/SellCryptoBankCheckOverlay.vue:35 msgid "Enter account holder name" msgstr "" @@ -730,7 +767,7 @@ msgstr "" msgid "Enter recipient address..." msgstr "" -#: src/components/layouts/Settings.vue:218 +#: src/components/layouts/Settings.vue:232 msgid "Enter the password of the trial you want to enable, then press enter." msgstr "" @@ -753,6 +790,11 @@ msgstr "" msgid "Euro" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:28 +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:33 +msgid "Every NIM address comes with an avatar. They help to make sure you got the right one." +msgstr "" + #: src/components/BtcTransactionListItem.vue:44 #: src/components/TransactionListItem.vue:38 msgid "expired" @@ -813,7 +855,7 @@ msgstr "" msgid "Fiat value unavailable" msgstr "" -#: src/components/layouts/Settings.vue:325 +#: src/components/layouts/Settings.vue:342 msgid "File contains no contacts." msgstr "" @@ -825,6 +867,10 @@ msgstr "" msgid "Finalizing swap" msgstr "" +#: src/lib/tour/network/NetworkTourTexts.ts:25 +msgid "Find the network’s key performance metrics below." +msgstr "" + #: src/components/modals/TradeModal.vue:49 msgid "For beginners" msgstr "" @@ -854,6 +900,14 @@ msgstr "" msgid "Get NIM" msgstr "" +#: src/components/layouts/Settings.vue:95 +msgid "Go through the product again" +msgstr "" + +#: src/lib/tour/onboarding/08_OnboardingCompleted.ts:77 +msgid "Go to Network" +msgstr "" + #: src/components/BankCheckInput.vue:92 #: src/components/modals/BtcActivationModal.vue:21 #: src/components/swap/OasisLaunchModal.vue:26 @@ -864,14 +918,18 @@ 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 "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:32 +msgid "Here are your transactions." +msgstr "" + +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:27 +msgid "Here’s your first transaction with your first NIM." +msgstr "" + #: src/components/modals/BtcTransactionModal.vue:200 #: src/components/modals/BtcTransactionModal.vue:226 #: src/components/modals/TransactionModal.vue:196 @@ -984,15 +1042,15 @@ msgstr "" msgid "Is your bank eligible?" msgstr "" -#: src/components/modals/WelcomeModal.vue:53 -msgid "It is fast, safe and makes you truly independent." +#: src/components/modals/DiscoverTheNimiqWalletModal.vue:20 +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:185 msgid "Keyguard Mode" msgstr "" @@ -1004,10 +1062,6 @@ msgstr "" msgid "Language" msgstr "" -#: src/components/modals/NetworkInfoModal.vue:19 -msgid "Learn more" -msgstr "" - #: src/components/modals/MigrationWelcomeModal.vue:61 msgid "Learn more here" msgstr "" @@ -1021,10 +1075,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 "" @@ -1072,7 +1122,7 @@ msgstr "" msgid "Login File" msgstr "" -#: src/components/AccountMenu.vue:53 +#: src/components/modals/AccountMenuModal.vue:26 msgid "Logout" msgstr "" @@ -1130,7 +1180,7 @@ msgstr "" msgid "Name your contact" msgstr "" -#: src/components/layouts/Sidebar.vue:50 +#: src/components/layouts/Sidebar.vue:53 msgid "Network" msgstr "" @@ -1138,10 +1188,22 @@ msgstr "" msgid "Network fee: {sats} sat/vByte" msgstr "" +#: src/components/Tour.vue:100 +msgid "Next" +msgstr "" + #: src/components/swap/SwapFeesTooltip.vue:39 msgid "NIM network fee" msgstr "" +#: src/lib/tour/network/NetworkTourTexts.ts:10 +msgid "Nimiq doesn’t collect or store such data." +msgstr "" + +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:80 +msgid "Nimiq is not just any crypto - Click on {network_icon} Network and discover true decentralization." +msgstr "" + #: src/components/modals/DisclaimerModal.vue:17 msgid "Nimiq is not responsible for any loss. Nimiq, wallet.nimiq.com, hub.nimiq.com & keyguard.nimiq.com, and some of the underlying libraries are under active development." msgstr "" @@ -1173,11 +1235,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:144 #: src/components/layouts/Settings.vue:70 msgid "None" msgstr "" @@ -1213,11 +1275,11 @@ msgstr "" msgid "OASIS service fee" msgstr "" -#: src/components/layouts/Settings.vue:165 +#: src/components/layouts/Settings.vue:179 msgid "Off" msgstr "" -#: src/components/layouts/Settings.vue:348 +#: src/components/layouts/Settings.vue:365 msgid "OK! Contacts imported successfully." msgstr "" @@ -1225,7 +1287,7 @@ msgstr "" msgid "Old and new Accounts" msgstr "" -#: src/components/layouts/Settings.vue:164 +#: src/components/layouts/Settings.vue:178 msgid "On" msgstr "" @@ -1241,7 +1303,7 @@ msgstr "" msgid "Or buy BTC directly in the wallet." msgstr "" -#: src/components/layouts/Settings.vue:173 +#: src/components/layouts/Settings.vue:187 msgid "Overwrite how the Keyguard is opened." msgstr "" @@ -1292,14 +1354,18 @@ msgstr "" msgid "Please transfer" msgstr "" -#: src/components/layouts/Settings.vue:178 +#: src/components/layouts/Settings.vue:192 msgid "popups" msgstr "" -#: src/components/layouts/Settings.vue:185 +#: src/components/layouts/Settings.vue:199 msgid "Popups" msgstr "" +#: src/components/Tour.vue:85 +msgid "Previous" +msgstr "" + #: src/components/TransactionDetailOasisPayoutStatus.vue:30 msgid "Proceed to the troubleshooting page to find out what to do next." msgstr "" @@ -1308,6 +1374,10 @@ msgstr "" msgid "Processing payout" msgstr "" +#: src/components/layouts/Settings.vue:93 +msgid "Product Tour" +msgstr "" + #: src/components/swap/SwapSepaFundingInstructions.vue:53 msgid "Purpose" msgstr "" @@ -1326,7 +1396,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 +1404,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 +1429,19 @@ msgstr "" msgid "Recovery Words" msgstr "" -#: src/components/layouts/Settings.vue:174 +#: src/components/layouts/Settings.vue:188 msgid "Redirect mode does not yet support swaps." msgstr "" -#: src/components/layouts/Settings.vue:178 +#: src/components/layouts/Settings.vue:192 msgid "redirects" msgstr "" -#: src/components/layouts/Settings.vue:186 +#: src/components/layouts/Settings.vue:200 msgid "Redirects" msgstr "" -#: src/components/layouts/Settings.vue:227 +#: src/components/layouts/Settings.vue:241 msgid "Reference currency" msgstr "" @@ -1388,14 +1458,14 @@ 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:260 #: src/components/UpdateNotification.vue:8 msgid "Release Notes" msgstr "" -#: src/components/AccountMenu.vue:43 #: src/components/layouts/AddressOverview.vue:30 #: src/components/layouts/AddressOverview.vue:54 +#: src/components/modals/AccountMenuModal.vue:16 msgid "Rename" msgstr "" @@ -1413,7 +1483,7 @@ msgstr "" msgid "Rescan" msgstr "" -#: src/components/layouts/Settings.vue:206 +#: src/components/layouts/Settings.vue:220 msgid "Reset your wallet settings and reload data from the blockchain." msgstr "" @@ -1431,7 +1501,7 @@ msgstr "" msgid "Retrying..." msgstr "" -#: src/components/AccountMenu.vue:34 +#: src/components/modals/AccountMenuModal.vue:7 msgid "Save Login File" msgstr "" @@ -1459,11 +1529,11 @@ msgstr "" msgid "Select a currency and an amount." msgstr "" -#: src/components/layouts/Settings.vue:140 +#: src/components/layouts/Settings.vue:154 msgid "Select which unit to show Bitcoin amounts in." msgstr "" -#: src/components/layouts/Sidebar.vue:35 +#: src/components/layouts/Sidebar.vue:38 msgid "Sell" msgstr "" @@ -1503,10 +1573,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 "" @@ -1575,6 +1641,10 @@ msgstr "" msgid "SEPA Instant transaction" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:56 +msgid "Seriously! There is no ‘forgot password’! Create a backup to make sure you stay in control." +msgstr "" + #: src/components/modals/SendModal.vue:100 #: src/components/modals/SendModal.vue:88 msgid "Set Amount" @@ -1592,7 +1662,7 @@ msgstr "" msgid "Setting up the {currency} side of the swap." msgstr "" -#: src/components/layouts/Sidebar.vue:58 +#: src/components/layouts/Sidebar.vue:61 msgid "Settings" msgstr "" @@ -1618,7 +1688,7 @@ msgstr "" msgid "Show all my addresses" msgstr "" -#: src/components/layouts/Settings.vue:123 +#: src/components/layouts/Settings.vue:137 #: src/components/layouts/Settings.vue:63 msgid "Show Decimals" msgstr "" @@ -1627,7 +1697,7 @@ msgstr "" msgid "Show Link" msgstr "" -#: src/components/AccountMenu.vue:39 +#: src/components/modals/AccountMenuModal.vue:12 msgid "Show Recovery Words" msgstr "" @@ -1635,6 +1705,10 @@ msgstr "" msgid "Signup" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:74 +msgid "Simply click your account and select ‘Create backup’." +msgstr "" + #: src/components/modals/BuyCryptoModal.vue:240 msgid "Simulate EUR payment" msgstr "" @@ -1668,8 +1742,16 @@ msgstr "" msgid "standard" msgstr "" -#: src/components/modals/WelcomeModal.vue:7 -msgid "Store, send and receive NIM." +#: src/components/modals/NetworkInfoModal.vue:19 +msgid "Start Network Tour" +msgstr "" + +#: src/components/layouts/Settings.vue:100 +msgid "Start Tour" +msgstr "" + +#: src/components/modals/DiscoverTheNimiqWalletModal.vue:25 +msgid "Start Wallet tour" msgstr "" #. avoid displaying the proxy address until we know related peer address @@ -1712,7 +1794,7 @@ msgstr "" msgid "Swap to {address}" msgstr "" -#: src/components/layouts/Settings.vue:156 +#: src/components/layouts/Settings.vue:170 msgid "Swiping Gestures" msgstr "" @@ -1720,6 +1802,14 @@ msgstr "" msgid "syncing" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:14 +msgid "Tap it to open the address details." +msgstr "" + +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:61 +msgid "Tap on the menu icon to access your account and wallet settings." +msgstr "" + #: src/components/swap/SwapSepaFundingInstructions.vue:31 msgid "TEN31 Bank provides a bank account to lock your payment." msgstr "" @@ -1732,6 +1822,10 @@ msgstr "" msgid "Testnet" msgstr "" +#: src/lib/tour/network/NetworkTourTexts.ts:26 +msgid "The {network_icon}-icon indicates that you are connected to the network." +msgstr "" + #: src/components/swap/SwapSepaFundingInstructions.vue:105 msgid "The bank is processing your transaction." msgstr "" @@ -1761,10 +1855,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 @@ -1791,10 +1881,18 @@ msgstr "" msgid "There is no ‘forgot password’" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:53 +msgid "There is no ‘forgot password’. Create a backup to make sure you stay in control." +msgstr "" + #: src/components/swap/SwapNotification.vue:23 msgid "There's a problem" msgstr "" +#: src/lib/tour/network/NetworkTourTexts.ts:16 +msgid "These connections enable you to establish consensus with a sub set of participants directly." +msgstr "" + #: src/components/modals/BtcTransactionModal.vue:202 #: src/components/modals/BtcTransactionModal.vue:228 #: src/components/modals/TransactionModal.vue:198 @@ -1810,6 +1908,10 @@ msgstr "" msgid "This is a Legacy Account" msgstr "" +#: src/lib/tour/network/NetworkTourTexts.ts:15 +msgid "This is a peer or a backbone node that you are connected to." +msgstr "" + #: src/components/modals/BtcReceiveModal.vue:49 msgid "This is a single-use address" msgstr "" @@ -1822,6 +1924,24 @@ msgstr "" msgid "This is not a valid IBAN" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:19 +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:22 +msgid "This is where all your transactions will appear." +msgstr "" + +#: src/lib/tour/network/NetworkTourTexts.ts:9 +msgid "This is you. Your location is determined by your IP address." +msgstr "" + +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:38 +msgid "This is your Bitcoin wallet. You get one with every Nimiq account." +msgstr "" + +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:13 +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:9 +msgid "This is your first address, represented by your avatar." +msgstr "" + #: src/components/swap/SwapAnimation.vue:295 #: src/components/swap/SwapAnimation.vue:72 #: src/components/swap/SwapSepaFundingInstructions.vue:106 @@ -1830,8 +1950,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:252 +#: src/components/TransactionList.vue:274 msgid "This month" msgstr "" @@ -1899,7 +2019,7 @@ msgstr "" msgid "Transfer funds" msgstr "" -#: src/components/layouts/Settings.vue:216 +#: src/components/layouts/Settings.vue:230 msgid "Trials" msgstr "" @@ -1937,10 +2057,6 @@ msgstr "" msgid "Update now" msgstr "" -#: src/components/modals/WelcomeModal.vue:22 -msgid "Use {NIM}, the super performant and browser- based payment coin." -msgstr "" - #: src/components/TransactionDetailOasisPayoutStatus.vue:11 msgid "Use a SEPA Instant account to get payouts in minutes." msgstr "" @@ -1957,7 +2073,11 @@ msgstr "" msgid "Use the slider or edit values to set up a swap." msgstr "" -#: src/components/layouts/Settings.vue:192 +#: src/components/TourLargeScreenManager.vue:5 +msgid "Use the tooltips to navigate your tour." +msgstr "" + +#: src/components/layouts/Settings.vue:206 msgid "Vesting Contract" msgstr "" @@ -1973,6 +2093,10 @@ msgstr "" msgid "Waiting for Nimiq network informations" msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:79 +msgid "Wallet tour completed!" +msgstr "" + #: src/components/modals/BuyCryptoModal.vue:14 msgid "Welcome to the first fiat-to-crypto atomic swap. It’s simple, fast and decentralized." msgstr "" @@ -1985,7 +2109,7 @@ msgstr "" msgid "What is your bank called?" msgstr "" -#: src/components/layouts/Settings.vue:163 +#: src/components/layouts/Settings.vue:177 msgid "When ready" msgstr "" @@ -2018,6 +2142,14 @@ msgstr "" msgid "You are using a Legacy Account " msgstr "" +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:73 +msgid "You can always create a new backup." +msgstr "" + +#: src/lib/tour/onboarding/OnboardingTourTexts.ts:10 +msgid "You can click on the address to copy and share it." +msgstr "" + #: src/components/TestnetFaucet.vue:122 msgid "You can receive more free NIM in {waitTime} hours." msgstr "" @@ -2030,6 +2162,11 @@ msgstr "" msgid "You have exceeded your OASIS limit." msgstr "" +#: src/lib/tour/network/NetworkTourTexts.ts:31 +#: src/lib/tour/network/NetworkTourTexts.ts:37 +msgid "You made it!" +msgstr "" + #: src/components/modals/overlays/BuyCryptoBankCheckOverlay.vue:6 msgid "You need a SEPA account that supports instant transactions." msgstr "" @@ -2081,10 +2218,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." @@ -2117,6 +2250,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..52a23b6b9 --- /dev/null +++ b/src/lib/tour/index.ts @@ -0,0 +1,75 @@ +import { ScreenTypes } from '@/composables/useWindowSize'; +import { SetupContext } from '@vue/composition-api'; +import { getNetworkTourSteps } from './network'; +import { getOnboardingTourSteps } from './onboarding'; +import { ITooltipModifier, ITourStep, ITourSteps, NetworkTourStep, OnboardingTourStep, TourName } from './types'; + +// see more about tooltip modifiers at popper official documentation +export const defaultTooltipModifiers: ITooltipModifier[] = [ + { + name: 'arrow', + options: { + element: '.v-step__arrow', + padding: 16, + }, + }, + { + name: 'preventOverflow', + options: { + rootBoundary: 'window', + }, + }, + { + name: 'offset', + options: { + offset: [0, 10], + }, + }, +]; + +export function getTour(tour: TourName | undefined, context: SetupContext, screenTypes: ScreenTypes) + : ITourStep[] { + let steps: ITourSteps = {}; + switch (tour) { + case 'onboarding': + steps = getOnboardingTourSteps(context, screenTypes); + break; + case 'network': + steps = getNetworkTourSteps(context, screenTypes); + break; + default: + } + + // At this moment we have something like this: + // { + // "1": { /** First step object */}, + // "10": { /** This is not the first step object!! */}, + // "2": { /** Second step object */}, + // ... + // } + // where the key is the step index and the value is the step object that we need to sort + // and store it as an array + return Object.entries(steps) + .filter(([, s]) => Boolean(s)) // Remove undefined steps + .sort(([a], [b]) => parseInt(a, 10) - parseInt(b, 10)) // Sort by key + .map(([, s]) => s) as ITourStep[]; // Just return only the step +} + +// Finds the component instance given its name in the Vue tree +// Components in the wallet should not change its logic to fit tour requirements. For example, adding +// listeners for the tour in AddressList or TransactionList components should be done in the tour +// and no in each component. In order to access the component instance, we need to find it in the +// Vue tree. +export function searchComponentByName(c: Vue, name: string): Vue | undefined { + if (c.$options.name === name) { + return c; + } + for (const cc of c.$children) { + const found = searchComponentByName(cc, name); + if (found) return found; + } + + return undefined; +} + +export * from './types'; diff --git a/src/lib/tour/network/01_YourLocationStep.ts b/src/lib/tour/network/01_YourLocationStep.ts new file mode 100644 index 000000000..8a66f9c04 --- /dev/null +++ b/src/lib/tour/network/01_YourLocationStep.ts @@ -0,0 +1,65 @@ +import { defaultTooltipModifiers, INetworkGetStepFnArgs, ITourStep, IWalletHTMLElements, NetworkTourStep } from '..'; +import { getNetworkTexts } from './NetworkTourTexts'; + +export function getYourLocationStep( + { isLargeScreen, nodes, scrollIntoView, sleep, selfNodeIndex }: INetworkGetStepFnArgs): ITourStep { + return { + path: '/network', + tooltip: { + target: `${IWalletHTMLElements.NETWORK_NODES} span:nth-child(${selfNodeIndex + 1})`, + content: getNetworkTexts(NetworkTourStep.YOUR_LOCATION).default, + params: { + get placement() { + const { position } = nodes()[selfNodeIndex] || { position: undefined }; + if (!position) return 'bottom'; + if (isLargeScreen.value) { + // If node is far away in the eastern hemisphere, the tooltip will be on the left + return position.x > 80 ? 'left' : 'right'; + } + // If node is far away in the south hemisphere, the tooltip will be on the top + return position.y > 25 ? 'top' : 'bottom'; + }, + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: isLargeScreen.value ? [0, 12] : [0, 16], + }, + }, + ...defaultTooltipModifiers.filter((d) => d.name !== 'offset'), + ]; + }, + }, + }, + ui: { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.NETWORK_STATS, + ], + disabledElements: [ + IWalletHTMLElements.NETWORK_TABLET_MENU_BAR, + IWalletHTMLElements.NETWORK_MAP, + ], + disabledButtons: [IWalletHTMLElements.BUTTON_SIDEBAR_BUY, IWalletHTMLElements.BUTTON_SIDEBAR_SELL], + scrollLockedElements: [ + IWalletHTMLElements.NETWORK_SCROLLER, + ], + }, + lifecycle: { + created: (async ({ goingForward }) => { + // Scroll to the user's node + scrollIntoView(nodes()[selfNodeIndex].x); + if (!goingForward) { + await sleep(500); + } + }), + }, + } as ITourStep; +} diff --git a/src/lib/tour/network/02_BackboneNodeStep.ts b/src/lib/tour/network/02_BackboneNodeStep.ts new file mode 100644 index 000000000..ef54eb7a2 --- /dev/null +++ b/src/lib/tour/network/02_BackboneNodeStep.ts @@ -0,0 +1,93 @@ +import { ref } from '@vue/composition-api'; +import { defaultTooltipModifiers, INetworkGetStepFnArgs, ITourStep, IWalletHTMLElements, NetworkTourStep } from '..'; +import { getNetworkTexts } from './NetworkTourTexts'; + +export function getBackboneNodeStep( + { nodes, selfNodeIndex, isLargeScreen, scrollIntoView, sleep }: INetworkGetStepFnArgs): ITourStep { + const selectedNode = ref(-1); + return { + path: '/network', + tooltip: { + get target() { + return `${IWalletHTMLElements.NETWORK_NODES} span:nth-child(${selectedNode.value + 1})`; + }, + content: getNetworkTexts(NetworkTourStep.BACKBONE_NODE).default, + params: { + get placement() { + const { position } = nodes()[selectedNode.value] || { position: undefined }; + if (!position) return 'bottom'; + if (isLargeScreen.value) { + // If node is far away in the eastern hemisphere, the tooltip will be on the left + return position.x > 100 ? 'left' : 'right'; + } + // If node is far away in the south hemisphere, the tooltip will be on the top + return position.y > 25 ? 'top' : 'bottom'; + }, + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: [0, 16], + }, + }, + ...defaultTooltipModifiers.filter((d) => d.name !== 'offset'), + ]; + }, + }, + }, + ui: { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.NETWORK_STATS, + ], + disabledElements: [ + IWalletHTMLElements.NETWORK_TABLET_MENU_BAR, + IWalletHTMLElements.NETWORK_MAP, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + ], + scrollLockedElements: [ + IWalletHTMLElements.NETWORK_SCROLLER, + ], + }, + lifecycle: { + created: (async () => { + // euclidean distance + 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. + // we prioritize west nodes so tooltip is shown on the right + const node = _nodes + // add index + .map((n, i) => ({ ...n, i, x: n.x })) + // remove self + .filter((_, i) => i !== selfNodeIndex) + // compute distance + .map((n) => ({ ...n, d: distance([n.position.x, n.position.y], [pSelf.x, pSelf.y]) })) + // at least 5 distance away + .filter(({ d }) => d > 5) + // get closest + .sort((a, b) => a.d - b.d)[0]; + + // get the index so we can select the correct child in the dom to show the tooltip + selectedNode.value = node.i; + + // Scroll to the selected node + scrollIntoView(node.x); + + await sleep(500); + }), + }, + } as ITourStep; +} diff --git a/src/lib/tour/network/03_NetworkMetricsStep.ts b/src/lib/tour/network/03_NetworkMetricsStep.ts new file mode 100644 index 000000000..61c9b85f7 --- /dev/null +++ b/src/lib/tour/network/03_NetworkMetricsStep.ts @@ -0,0 +1,49 @@ +import { NetworkTourStep, ITourStep, IWalletHTMLElements, defaultTooltipModifiers } from '..'; +import { getNetworkTexts } from './NetworkTourTexts'; + +export function getNetworkMetricsStep(): ITourStep { + return { + path: '/network', + tooltip: { + target: IWalletHTMLElements.NETWORK_STATS, + content: getNetworkTexts(NetworkTourStep.METRICS).default, + params: { + placement: 'top', + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: [0, 16], + }, + }, + ...defaultTooltipModifiers.filter((d) => d.name !== 'offset'), + ]; + }, + }, + }, + ui: { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + ], + disabledElements: [ + IWalletHTMLElements.NETWORK_TABLET_MENU_BAR, + IWalletHTMLElements.NETWORK_MAP, + IWalletHTMLElements.NETWORK_STATS, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + ], + scrollLockedElements: [ + IWalletHTMLElements.NETWORK_SCROLLER, + ], + }, + } as ITourStep; +} diff --git a/src/lib/tour/network/04_NetworkCompletedStep.ts b/src/lib/tour/network/04_NetworkCompletedStep.ts new file mode 100644 index 000000000..34ce0db2e --- /dev/null +++ b/src/lib/tour/network/04_NetworkCompletedStep.ts @@ -0,0 +1,64 @@ +import { INetworkGetStepFnArgs, NetworkTourStep, ITourStep, IWalletHTMLElements, defaultTooltipModifiers } from '..'; +import { getNetworkTexts } from './NetworkTourTexts'; + +export function getNetworkCompletedStep({ root, isLargeScreen }: INetworkGetStepFnArgs): ITourStep { + return { + path: '/network', + tooltip: { + get target() { + return isLargeScreen.value + ? IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU + : `${IWalletHTMLElements.NETWORK_TABLET_MENU_BAR} .account-button`; + }, + content: getNetworkTexts(NetworkTourStep.NETWORK_COMPLETED)[ + isLargeScreen.value ? 'default' : 'alternative'], + params: { + get placement() { + return isLargeScreen.value ? 'right' : 'top'; + }, + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: [0, 16], + }, + }, + ...defaultTooltipModifiers.filter((d) => d.name !== 'offset'), + ]; + }, + }, + button: { + text: root.$t('End Tour'), + fn: async (endTour) => { + if (endTour) { + await endTour(); + } + }, + }, + }, + ui: { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + ], + disabledElements: [ + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.NETWORK_TABLET_MENU_BAR, + IWalletHTMLElements.NETWORK_MAP, + IWalletHTMLElements.NETWORK_STATS, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + ], + scrollLockedElements: [ + IWalletHTMLElements.NETWORK_SCROLLER, + ], + }, + } as ITourStep; +} diff --git a/src/lib/tour/network/NetworkTourTexts.ts b/src/lib/tour/network/NetworkTourTexts.ts new file mode 100644 index 000000000..d23451f67 --- /dev/null +++ b/src/lib/tour/network/NetworkTourTexts.ts @@ -0,0 +1,47 @@ +import { IContentSpecialItem, ITourStepTexts, NetworkTourStep } from '../types'; + +// This is used to trick the translation extraction script into extracting those strings +const $t = (s: string) => s; + +const texts: ITourStepTexts = { + [NetworkTourStep.YOUR_LOCATION]: { + default: [ + $t('This is you. Your location is determined by your IP address.'), + $t('Nimiq doesn’t collect or store such data.'), + ], + }, + [NetworkTourStep.BACKBONE_NODE]: { + default: [ + $t('This is a peer or a backbone node that you are connected to.'), + $t('These connections enable you to establish consensus with a sub set of participants directly.'), + [ + $t('‘Available browsers’ are other user’s browsers, just like yours.'), + $t('‘Backbone nodes’ provide a fallback to connect to.'), + ], + ], + }, + [NetworkTourStep.METRICS]: { + default: [ + $t('Find the network’s key performance metrics below.'), + $t('The {network_icon}-icon indicates that you are connected to the network.'), + ], + }, + [NetworkTourStep.NETWORK_COMPLETED]: { + default: [ + $t('You made it!'), + IContentSpecialItem.HR, + $t('Enjoy the decentralized future, and don’t forget to invite your friends and family.'), + $t('Click {account_icon} to get back to your wallet.'), + ], + alternative: [ + $t('You made it!'), + IContentSpecialItem.HR, + $t('Enjoy the decentralized future, and don’t forget to invite your friends and family.'), + $t('Click {back_to_addresses} to get back to your wallet.'), + ], + }, +}; + +export function getNetworkTexts(i: NetworkTourStep) { + return texts[i]; +} diff --git a/src/lib/tour/network/index.ts b/src/lib/tour/network/index.ts new file mode 100644 index 000000000..80e2e1c35 --- /dev/null +++ b/src/lib/tour/network/index.ts @@ -0,0 +1,42 @@ +import { ScreenTypes } from '@/composables/useWindowSize'; +import { NodeHexagon, NodeType, SCALING_FACTOR, WIDTH } from '@/lib/NetworkMap'; +import { SetupContext } from '@vue/composition-api'; +import { searchComponentByName } from '..'; +import { INetworkGetStepFnArgs, ITourSteps, IWalletHTMLElements, NetworkTourStep } 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, screenTypes: ScreenTypes): ITourSteps { + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + const networkMapInstance = searchComponentByName(root, 'network-map') as any; + const nodes = () => networkMapInstance?.nodes as NodeHexagon[] || []; + const selfNodeIndex = nodes().findIndex((node) => [...node.peers].some((peer) => peer.type === NodeType.SELF)); + + // computes how far the x value is from the left border and then scrolls to it + const scrollIntoView = async (x: number) => { + const map = document.querySelector(IWalletHTMLElements.NETWORK_SCROLLER) as HTMLElement; + const mapWidth = map.children[0]!.clientWidth; + const adjustedX = (x / 2) * SCALING_FACTOR * (mapWidth / WIDTH); + const scrollTarget = adjustedX - (window.innerWidth / 2); + map.scrollTo(scrollTarget, 0); + }; + + const args: INetworkGetStepFnArgs = { + nodes, + selfNodeIndex, + ...screenTypes, + scrollIntoView, + sleep, + root, + }; + + return { + [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/01_FirstAddressStep.ts b/src/lib/tour/onboarding/01_FirstAddressStep.ts new file mode 100644 index 000000000..bfd5217d5 --- /dev/null +++ b/src/lib/tour/onboarding/01_FirstAddressStep.ts @@ -0,0 +1,144 @@ +import { CryptoCurrency } from '@/lib/Constants'; +import { useAccountStore } from '@/stores/Account'; +import { useAddressStore } from '@/stores/Address'; +import { defaultTooltipModifiers } from '..'; +import { + ILifecycleArgs, + IOnboardingGetStepFnArgs, ITourStep, + IWalletHTMLElements, OnboardingTourStep, +} from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getFirstAddressStep( + { isSmallScreen, root, toggleHighlightButton }: IOnboardingGetStepFnArgs): ITourStep { + const { setActiveCurrency } = useAccountStore(); + const { addressInfos, selectAddress } = useAddressStore(); + + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + + ...(!isSmallScreen.value + ? [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + ] : [ + ...Array.from({ length: addressInfos.value.length }).map( + (_, i) => `${IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST} button:nth-child(${i + 3})`), + ] + ), + ], + disabledElements: [ + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_RECEIVE_FREE_NIM, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + explicitInteractableElements: [ + `${IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS} .copyable`, + ], + }; + + const path = '/'; + + // User must be able to click the first address to open it in the address overview manually + const mountedFnForSmallScreen = ({ goToNextStep }: ILifecycleArgs) => { + const button = `${IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST} button:nth-child(2)`; + toggleHighlightButton(button, true, 'gray'); + + const addressButton = document.querySelector(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: Omit) => { + if (!args?.ending && !addressClicked && root.$route.path === path) { + // If user clicked the 'next step' button, then we trigger the click on the first address + addressButton!.click(); + await root.$nextTick(); + } + addressButton!.removeEventListener('click', onClick, true); + setTimeout(() => toggleHighlightButton(button, false, 'gray'), 500); + }; + }; + + const mountedFnForNotSmallScreen = () => { + // Not show identicon menu when user hover the active address + const identiconMenu = document.querySelector( + `${IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS} .identicon-wrapper .identicon-menu`) as HTMLElement; + identiconMenu.style.display = 'none'; + return () => { + identiconMenu.style.display = 'flex'; + }; + }; + + return { + path, + tooltip: { + get target() { + return isSmallScreen.value + ? `${IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST} .address-button .identicon img` + : `${IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS} .identicon`; + }, + get content() { + return getOnboardingTexts(OnboardingTourStep.FIRST_ADDRESS)[ + isSmallScreen.value ? 'alternative' : 'default']; + }, + params: { + get placement() { + return isSmallScreen.value ? 'bottom-start' : 'left-start'; + }, + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: isSmallScreen.value ? [0, 12] : [0, 20], + }, + }, + ...defaultTooltipModifiers.filter((d) => d.name !== 'offset'), + ]; + }, + }, + }, + lifecycle: { + created: () => { + // Select first NIM address as active + setActiveCurrency(CryptoCurrency.NIM); + selectAddress(addressInfos.value[0].address); + }, + mounted: (args) => isSmallScreen.value ? mountedFnForSmallScreen(args) : mountedFnForNotSmallScreen(), + }, + ui, + } as ITourStep; +} diff --git a/src/lib/tour/onboarding/02_TransactionListStep.ts b/src/lib/tour/onboarding/02_TransactionListStep.ts new file mode 100644 index 000000000..86527dc9c --- /dev/null +++ b/src/lib/tour/onboarding/02_TransactionListStep.ts @@ -0,0 +1,145 @@ +import { ref, watch } from '@vue/composition-api'; +import { defaultTooltipModifiers, ITooltipModifier, IWalletHTMLElements } from '..'; +import { IOnboardingGetStepFnArgs, ITourStep, OnboardingTourStep } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getTransactionListStep( + { isSmallScreen, toggleHighlightButton, txsLen }: IOnboardingGetStepFnArgs): ITourStep { + const freeNimBtn = ref(() => + document.querySelector(IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_RECEIVE_FREE_NIM) as HTMLDivElement); + + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + ...(txsLen.value > 0 + ? [IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS] + : [ + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} h2`, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} span`, + ] + ), + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + txsLen.value === 0 ? '' : IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + explicitInteractableElements: [ + txsLen.value === 0 ? IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY : '', + ], + }; + + return { + get path() { + return isSmallScreen.value ? '/transactions' : '/'; + }, + tooltip: { + get target() { + if (txsLen.value > 0) { + return isSmallScreen.value + ? `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} + .vue-recycle-scroller__item-view:nth-child(2)` + : '.address-overview'; + } + return isSmallScreen.value + ? `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} > .empty-state h2` + : '.address-overview'; + }, + get content() { + const textType = txsLen.value === 0 && freeNimBtn.value ? 'default' : 'alternative'; + return getOnboardingTexts(OnboardingTourStep.TRANSACTION_LIST)[textType] || []; + }, + params: { + get placement() { + if (!isSmallScreen.value) { + return 'left'; + } + return txsLen.value > 0 ? 'bottom' : 'top'; + }, + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: txsLen.value > 0 && isSmallScreen.value ? [0, 32] : [0, 20], + }, + } as ITooltipModifier, + ...defaultTooltipModifiers.filter((d) => d.name !== 'offset'), + ]; + }, + }, + }, + lifecycle: { + mounted: ({ goToNextStep, isNextStepDisabled }) => { + if (txsLen.value > 0) return undefined; + + function freeNimBtnClickHandler() { + setTimeout(() => { + // User in the mainnet is expected to click on the 'Get Free NIM' button + // This button opens the faucet page and the user exits the wallet. Once the user finishes, he + // is expected to return to the wallet and automatically the tour will go to the next step. + // There might be a possibility that the user gets an error in the faucet, and therefore, the + // tour will not continue. In those cases we will let the user click 'Next step' but it will + // actually skip the third step and go to the fourth step + // FIXME: In the future, if we integrate the faucet into the wallet, we could improve this + // behaviour + if (txsLen.value > 0) return; + isNextStepDisabled.value = false; + }, 2000); + } + + if (freeNimBtn.value() !== null) { + freeNimBtn.value().addEventListener('click', freeNimBtnClickHandler, { once: true }); + } else { + isNextStepDisabled.value = false; + } + + const unwatch = watch(txsLen, (newVal) => { + if (newVal > 0) goToNextStep(); + }); + + // Add hightlight effect to 'Get Free NIM' button. This will be ignored if the user have at least one tx + toggleHighlightButton(IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_RECEIVE_FREE_NIM, true, 'green'); + return () => { + unwatch(); + if (freeNimBtn.value()) { + freeNimBtn.value().removeEventListener('click', freeNimBtnClickHandler); + } + return toggleHighlightButton( + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_RECEIVE_FREE_NIM, false, 'green'); + }; + }, + }, + get ui() { + return { + ...ui, + + // User is expected to click on the 'Get Free NIM' button if they have not txs + isNextStepDisabled: txsLen.value === 0, + }; + }, + }; +} diff --git a/src/lib/tour/onboarding/03_FirstTransactionStep.ts b/src/lib/tour/onboarding/03_FirstTransactionStep.ts new file mode 100644 index 000000000..5f8afb247 --- /dev/null +++ b/src/lib/tour/onboarding/03_FirstTransactionStep.ts @@ -0,0 +1,87 @@ +import { defaultTooltipModifiers, IWalletHTMLElements } from '..'; +import { IOnboardingGetStepFnArgs, ITooltipModifier, ITourStep, OnboardingTourStep } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getFirstTransactionStep({ isSmallScreen, txsLen }: IOnboardingGetStepFnArgs): ITourStep { + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + }; + + return { + get path() { + return isSmallScreen.value ? '/transactions' : '/'; + }, + lifecycle: { + created: ({ goToNextStep, goToPrevStep, goingForward }) => { + // Might be the case that user has no transactions yet and still he reaches this step nonetheless. + // This is because if NIM Faucet fails, we let the user continue to the next step. If that happens, + // we need to skip this step and go to the next/prev one + if (txsLen.value === 0) { + if (goingForward) { + goToNextStep({ withDelay: false }); + } else { + goToPrevStep({ withDelay: false }); + } + } + }, + }, + tooltip: { + get target() { + return `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} ${isSmallScreen.value + ? '.transaction > .identicon' + : '.vue-recycle-scroller__item-view:nth-child(2)'}`; + }, + content: getOnboardingTexts(OnboardingTourStep.FIRST_TRANSACTION)[ + txsLen.value === 1 ? 'default' : 'alternative'], + params: { + get placement() { + return isSmallScreen.value ? 'bottom-start' : 'left'; + }, + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: isSmallScreen.value ? [0, 10] : [0, -4], + }, + } as ITooltipModifier, + ...defaultTooltipModifiers.filter((d) => d.name !== 'offset'), + ]; + }, + }, + }, + ui, + } as ITourStep; +} diff --git a/src/lib/tour/onboarding/04_BitcoinAddressStep.ts b/src/lib/tour/onboarding/04_BitcoinAddressStep.ts new file mode 100644 index 000000000..b1a7ceb9a --- /dev/null +++ b/src/lib/tour/onboarding/04_BitcoinAddressStep.ts @@ -0,0 +1,86 @@ +import { defaultTooltipModifiers } from '..'; +import { + IOnboardingGetStepFnArgs, + ITooltipModifier, + ITourStep, + IWalletHTMLElements, + OnboardingTourStep, +} from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getBitcoinAddressStep( + { isSmallScreen, isMediumScreen, isLargeScreen }: IOnboardingGetStepFnArgs): ITourStep { + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + }; + + return { + path: '/', + tooltip: { + get target() { + return `.account-overview .bitcoin-account ${!isLargeScreen.value + ? '> .bitcoin-account-item > svg' : ''}`; + }, + content: getOnboardingTexts(OnboardingTourStep.BITCOIN_ADDRESS).default, + params: { + get placement() { + return !isLargeScreen.value ? 'bottom-start' : 'left-end'; + }, + get modifiers() { + let offset; + if (isSmallScreen.value) offset = [0, 40]; + else if (isMediumScreen.value) offset = [0, 20]; + else offset = [0, 40]; + + return [ + { + name: 'preventOverflow', + options: { + altAxis: false, + padding: 16, + }, + }, + { + name: 'offset', + options: { offset }, + } as ITooltipModifier, + ...defaultTooltipModifiers.filter(({ name }) => !['offset', 'preventOverflow'].includes(name)), + ]; + }, + }, + }, + ui, + } as ITourStep; +} diff --git a/src/lib/tour/onboarding/05_WalletBalanceStep.ts b/src/lib/tour/onboarding/05_WalletBalanceStep.ts new file mode 100644 index 000000000..3b6566e65 --- /dev/null +++ b/src/lib/tour/onboarding/05_WalletBalanceStep.ts @@ -0,0 +1,82 @@ +import { useAddressStore } from '@/stores/Address'; +import { useBtcAddressStore } from '@/stores/BtcAddress'; +import { defaultTooltipModifiers } from '..'; +import { IOnboardingGetStepFnArgs, ITourStep, IWalletHTMLElements, OnboardingTourStep } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getWalletBalanceStep({ isSmallScreen }: IOnboardingGetStepFnArgs): ITourStep { + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + }; + + const { accountBalance: btcAccountBalance } = useBtcAddressStore(); + const { accountBalance: nimAccountBalance } = useAddressStore(); + const hasOnlyNim = () => nimAccountBalance.value > 0 && btcAccountBalance.value === 0; + + return { + path: '/', + tooltip: { + get target() { + return `${IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE} .balance-distribution + ${!isSmallScreen.value ? '.btc .tooltip .bar' : ''}`; + }, + content: getOnboardingTexts(OnboardingTourStep.WALLET_BALANCE)[hasOnlyNim() ? 'default' : 'alternative'], + params: { + get placement() { + return isSmallScreen.value ? 'bottom' : 'right'; + }, + get modifiers() { + return [ + { + name: 'preventOverflow', + options: { + mainAxis: false, + padding: 8, + }, + }, + { + name: 'offset', + options: { + offset: [0, 16], + }, + }, + ...defaultTooltipModifiers.filter(({ name }) => !['offset', 'preventOverflow'].includes(name)), + ]; + }, + }, + }, + ui, + } as ITourStep; +} 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..18372caf5 --- /dev/null +++ b/src/lib/tour/onboarding/06_0_BackupAlertStep.ts @@ -0,0 +1,105 @@ +import { useAccountStore } from '@/stores/Account'; +import { watch } from '@vue/composition-api'; +import { defaultTooltipModifiers, ITourOrigin, IWalletHTMLElements, searchComponentByName } from '..'; +import { IOnboardingGetStepFnArgs, ITourStep, OnboardingTourStep } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getBackupAlertStep( + { isSmallScreen, startedFrom, toggleHighlightButton, root }: IOnboardingGetStepFnArgs): ITourStep { + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + }; + return { + path: '/', + tooltip: { + get target() { + return isSmallScreen.value + ? `${IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT} button` + : IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT; + }, + content: getOnboardingTexts(OnboardingTourStep.BACKUP_ALERT)[ + startedFrom === ITourOrigin.WELCOME_MODAL ? 'default' : 'alternative'] || [], + params: { + get placement() { + return isSmallScreen.value ? 'bottom' : 'right'; + }, + get modifiers() { + return [ + { + name: 'preventOverflow', + options: { + altAxis: false, + padding: 8, + }, + }, + { + name: 'offset', + options: { + offset: isSmallScreen.value ? [0, 10] : [0, 16], + }, + }, + ...defaultTooltipModifiers.filter(({ name }) => !['preventOverflow', 'offset'].includes(name)), + ]; + }, + }, + }, + ui, + lifecycle: { + mounted: () => { + const unwatch = watch(useAccountStore().activeAccountInfo, async (newVal) => { + if (newVal?.wordsExported) { + // If user clicks "save recover words" and completes the process, we + // no longer need to show the user this step, therefore, we execute + // setTourAsArray again, and go to the next step which in reality will have + // same index + const nimiqTourInstance = searchComponentByName(root, 'nimiq-tour') as any; + if (!nimiqTourInstance) return; + + nimiqTourInstance.setTourAsArray(); + nimiqTourInstance.currentStep -= 1; + + setTimeout(() => nimiqTourInstance.goToNextStep(), 500); + } + }); + + // hightlight 'Revover words' button + toggleHighlightButton(IWalletHTMLElements.BUTTON_ADDRESS_BACKUP_ALERT, true, 'orange'); + return () => { + unwatch(); + toggleHighlightButton(IWalletHTMLElements.BUTTON_ADDRESS_BACKUP_ALERT, false, 'orange'); + }; + }, + }, + } as ITourStep; +} 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..c3cefa462 --- /dev/null +++ b/src/lib/tour/onboarding/06_1_MenuIconStep.ts @@ -0,0 +1,72 @@ +import { defaultTooltipModifiers, IWalletHTMLElements } from '..'; +import { IOnboardingGetStepFnArgs, ITourStep, OnboardingTourStep } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getMenuIconStep({ toggleHighlightButton }: IOnboardingGetStepFnArgs): ITourStep { + return { + path: '/', + tooltip: { + target: `${IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR} > button.reset`, + content: getOnboardingTexts(OnboardingTourStep.MENU_ICON).default, + params: { + placement: 'bottom-start', + modifiers: [ + { + name: 'preventOverflow', + options: { + mainAxis: false, + padding: 8, + }, + }, + { + name: 'offset', + options: { + offset: [-20, 10], + }, + }, + ...defaultTooltipModifiers.filter(({ name }) => ['offset', 'preventOverflow'].includes(name)), + ], + }, + }, + lifecycle: { + mounted: async ({ goToNextStep }) => { + const hamburguerIconSelector = `${IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR} > button.reset`; + // User is expected to click the hamburguer icon to go to next step + const hamburguerIcon = document.querySelector(hamburguerIconSelector) as HTMLButtonElement; + + const onHamburguerClick = () => goToNextStep(); + hamburguerIcon!.addEventListener('click', onHamburguerClick, true); + + toggleHighlightButton(hamburguerIconSelector, true, 'gray'); + + return () => { + toggleHighlightButton(hamburguerIconSelector, false, 'gray'); + hamburguerIcon!.removeEventListener('click', onHamburguerClick, true); + }; + }, + }, + ui: { + fadedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + isNextStepDisabled: true, + }, + } as ITourStep; +} diff --git a/src/lib/tour/onboarding/07_1_BackupOptionNotLargeScreenStep.ts b/src/lib/tour/onboarding/07_1_BackupOptionNotLargeScreenStep.ts new file mode 100644 index 000000000..fb63436f6 --- /dev/null +++ b/src/lib/tour/onboarding/07_1_BackupOptionNotLargeScreenStep.ts @@ -0,0 +1,70 @@ +import { defaultTooltipModifiers, IWalletHTMLElements } from '..'; +import { IOnboardingGetStepFnArgs, OnboardingTourStep, ITourStep } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getBackupOptionNotLargeScreenStep({ isSmallScreen }: IOnboardingGetStepFnArgs): ITourStep { + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_MOBILE_TAP_AREA, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.MODAL_CONTAINER, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + }; + + return { + path: '/accounts?sidebar=true', + tooltip: { + target: `${IWalletHTMLElements.MODAL_PAGE} .current-account .item:nth-child(3) + ${isSmallScreen.value ? 'svg path:nth-child(2)' : ''}`, + content: getOnboardingTexts( + OnboardingTourStep.BACKUP_OPTION_FROM_OPTIONS).default, + params: { + get placement() { + return isSmallScreen.value ? 'top-start' : 'right'; + }, + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: isSmallScreen.value ? [-20, 16] : [0, -40], + }, + }, + ...defaultTooltipModifiers.filter(({ name }) => name !== 'offset'), + ]; + }, + }, + }, + ui, + } as ITourStep; +} diff --git a/src/lib/tour/onboarding/07_2_BackupOptionLargeScreenStep.ts b/src/lib/tour/onboarding/07_2_BackupOptionLargeScreenStep.ts new file mode 100644 index 000000000..bd863244c --- /dev/null +++ b/src/lib/tour/onboarding/07_2_BackupOptionLargeScreenStep.ts @@ -0,0 +1,63 @@ +import { defaultTooltipModifiers, IWalletHTMLElements } from '..'; +import { OnboardingTourStep, ITourStep } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getBackupOptionLargeScreenStep(): ITourStep { + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_MOBILE_TAP_AREA, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + }; + + return { + path: '/', + tooltip: { + target: IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + content: getOnboardingTexts(OnboardingTourStep.BACKUP_OPTION_FROM_OPTIONS).default, + params: { + placement: 'right', + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 32], + }, + }, + ...defaultTooltipModifiers.filter(({ name }) => name !== 'offset'), + ], + }, + }, + ui, + } as ITourStep; +} diff --git a/src/lib/tour/onboarding/07_AccountOptionsStep.ts b/src/lib/tour/onboarding/07_AccountOptionsStep.ts new file mode 100644 index 000000000..e5931054a --- /dev/null +++ b/src/lib/tour/onboarding/07_AccountOptionsStep.ts @@ -0,0 +1,79 @@ +import { defaultTooltipModifiers, IWalletHTMLElements } from '..'; +import { OnboardingTourStep, ITourStep, IOnboardingGetStepFnArgs } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getAccountOptionsStep({ isSmallScreen, isLargeScreen }: IOnboardingGetStepFnArgs): ITourStep { + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_MOBILE_TAP_AREA, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.MODAL_CONTAINER, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + }; + + return { + get path() { + return isLargeScreen.value ? '/accounts' : '/accounts?sidebar=true'; + }, + tooltip: { + get target() { + return isSmallScreen.value ? IWalletHTMLElements.MODAL_ACCOUNT_PICTURE : IWalletHTMLElements.MODAL_PAGE; + }, + content: getOnboardingTexts(OnboardingTourStep.ACCOUNT_OPTIONS).default, + params: { + get placement() { + return isSmallScreen.value ? 'bottom-start' : 'right'; + }, + get modifiers() { + return [ + { + name: 'offset', + options: { + offset: [isSmallScreen.value ? -13.5 : 0, 16], + }, + }, + { + name: 'preventOverflow', + options: { + altAxis: true, + padding: 8, + }, + }, + ...defaultTooltipModifiers.filter(({ name }) => !['offset', 'preventOverflow'].includes(name)), + ]; + }, + }, + }, + ui, + } as ITourStep; +} diff --git a/src/lib/tour/onboarding/08_OnboardingCompleted.ts b/src/lib/tour/onboarding/08_OnboardingCompleted.ts new file mode 100644 index 000000000..4ada0d978 --- /dev/null +++ b/src/lib/tour/onboarding/08_OnboardingCompleted.ts @@ -0,0 +1,93 @@ +import { defaultTooltipModifiers } from '..'; +import { IOnboardingGetStepFnArgs, OnboardingTourStep, ITourStep, IWalletHTMLElements } from '../types'; +import { getOnboardingTexts } from './OnboardingTourTexts'; + +export function getOnboardingCompletedStep( + { root, isLargeScreen }: IOnboardingGetStepFnArgs): ITourStep { + const ui: ITourStep['ui'] = { + fadedElements: [ + IWalletHTMLElements.SIDEBAR_TESTNET, + IWalletHTMLElements.SIDEBAR_LOGO, + IWalletHTMLElements.SIDEBAR_ANNOUNCMENT_BOX, + IWalletHTMLElements.SIDEBAR_PRICE_CHARTS, + IWalletHTMLElements.SIDEBAR_TRADE_ACTIONS, + IWalletHTMLElements.SIDEBAR_ACCOUNT_MENU, + IWalletHTMLElements.SIDEBAR_SETTINGS, + IWalletHTMLElements.ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledElements: [ + IWalletHTMLElements.SIDEBAR_NETWORK, + IWalletHTMLElements.SIDEBAR_MOBILE_TAP_AREA, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BACKUP_ALERT, + IWalletHTMLElements.ACCOUNT_OVERVIEW_TABLET_MENU_BAR, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BALANCE, + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + IWalletHTMLElements.ACCOUNT_OVERVIEW_BITCOIN, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS_MOBILE, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIVE_ADDRESS, + IWalletHTMLElements.ADDRESS_OVERVIEW_ACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS, + IWalletHTMLElements.ADDRESS_OVERVIEW_MOBILE_ACTION_BAR, + ], + disabledButtons: [ + IWalletHTMLElements.BUTTON_SIDEBAR_BUY, + IWalletHTMLElements.BUTTON_SIDEBAR_SELL, + IWalletHTMLElements.BUTTON_ADDRESS_OVERVIEW_BUY, + ], + scrollLockedElements: [ + IWalletHTMLElements.ACCOUNT_OVERVIEW_ADDRESS_LIST, + `${IWalletHTMLElements.ADDRESS_OVERVIEW_TRANSACTIONS} .vue-recycle-scroller`, + ], + }; + + return { + get path() { + return isLargeScreen.value ? '/' : '/?sidebar=true'; + }, + tooltip: { + get target() { + return `${IWalletHTMLElements.SIDEBAR_NETWORK} ${isLargeScreen.value ? 'span' : '.consensus-icon'}`; + }, + content: getOnboardingTexts(OnboardingTourStep.ONBOARDING_COMPLETED).default, + params: { + get placement() { + return isLargeScreen.value ? 'right' : 'top-start'; + }, + get modifiers() { + return [ + { + name: 'preventOverflow', + options: { + altAxis: false, + padding: 8, + }, + }, + { + name: 'offset', + options: { + offset: [-20, 16], + }, + }, + ...defaultTooltipModifiers.filter(({ name }) => !['offset', 'preventOverflow'].includes(name)), + ]; + }, + }, + button: { + text: root.$t('Go to Network'), + fn: async (endTour) => { + if (endTour) { + await endTour(); + } + root.$router.push({ + name: 'network', + params: { + showNetworkInfo: 'true', + }, + }); + }, + }, + }, + ui, + } as ITourStep; +} diff --git a/src/lib/tour/onboarding/OnboardingTourTexts.ts b/src/lib/tour/onboarding/OnboardingTourTexts.ts new file mode 100644 index 000000000..f17efca3e --- /dev/null +++ b/src/lib/tour/onboarding/OnboardingTourTexts.ts @@ -0,0 +1,85 @@ +import { ITourStepTexts, OnboardingTourStep } from '../types'; + +// This is used to trick the translation extraction script into extracting those strings +const $t = (s: string) => s; + +const texts: ITourStepTexts = { + [OnboardingTourStep.FIRST_ADDRESS]: { + default: [ + $t('This is your first address, represented by your avatar.'), + $t('You can click on the address to copy and share it.'), + ], + alternative: [ + $t('This is your first address, represented by your avatar.'), + $t('Tap it to open the address details.'), + ], + }, + [OnboardingTourStep.TRANSACTION_LIST]: { + default: [ + $t('This is where all your transactions will appear.'), + $t('Click the green button to receive a free NIM from Team Nimiq.'), + ], + alternative: [$t('This is where all your transactions will appear.')], + }, + [OnboardingTourStep.FIRST_TRANSACTION]: { + // If user has 1 tx + default: [ + $t('Here’s your first transaction with your first NIM.'), + $t('Every NIM address comes with an avatar. They help to make sure you got the right one.'), + ], + // If user has more than 1 tx + alternative: [ + $t('Here are your transactions.'), + $t('Every NIM address comes with an avatar. They help to make sure you got the right one.'), + ], + }, + [OnboardingTourStep.BITCOIN_ADDRESS]: { + default: [ + $t('This is your Bitcoin wallet. You get one with every Nimiq account.'), + ], + }, + [OnboardingTourStep.WALLET_BALANCE]: { + default: [ + $t('Check the bar-chart to see how your addresses compose your total balance.'), + $t('Currently you have 100% NIM, and no BTC.'), + ], + alternative: [ + $t('Check the bar-chart to see how your addresses compose your total balance.'), + ], + }, + [OnboardingTourStep.BACKUP_ALERT]: { + default: [ + $t('There is no ‘forgot password’. Create a backup to make sure you stay in control.'), + ], + alternative: [ + $t('Seriously! There is no ‘forgot password’! Create a backup to make sure you stay in control.'), + ], + }, + [OnboardingTourStep.MENU_ICON]: { + default: [ + $t('Tap on the menu icon to access your account and wallet settings.'), + ], + }, + [OnboardingTourStep.ACCOUNT_OPTIONS]: { + default: [ + $t('Create, switch and log out of accounts.'), + $t('Security options and backup can be found here.'), + ], + }, + [OnboardingTourStep.BACKUP_OPTION_FROM_OPTIONS]: { + default: [ + $t('You can always create a new backup. Simply click your account and select ‘Create backup’.'), + ], + }, + [OnboardingTourStep.ONBOARDING_COMPLETED]: { + default: [ + $t('Wallet tour completed!'), + $t('Nimiq is not just any crypto - Click on {network_icon} Network ' + + 'and discover true decentralization.'), + ], + }, +}; + +export function getOnboardingTexts(i: OnboardingTourStep) { + return texts[i]; +} diff --git a/src/lib/tour/onboarding/index.ts b/src/lib/tour/onboarding/index.ts new file mode 100644 index 000000000..b51b47f64 --- /dev/null +++ b/src/lib/tour/onboarding/index.ts @@ -0,0 +1,73 @@ +import { ScreenTypes } from '@/composables/useWindowSize'; +import { AccountType, useAccountStore } from '@/stores/Account'; +import { computed, SetupContext } from '@vue/composition-api'; +import { ITourOrigin, searchComponentByName } from '..'; +import { IOnboardingGetStepFnArgs, ITourSteps, OnboardingTourStep } 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 { getBackupOptionNotLargeScreenStep } from './07_1_BackupOptionNotLargeScreenStep'; +import { getBackupOptionLargeScreenStep } from './07_2_BackupOptionLargeScreenStep'; +import { getAccountOptionsStep } from './07_AccountOptionsStep'; +import { getOnboardingCompletedStep } from './08_OnboardingCompleted'; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export function getOnboardingTourSteps({ root }: SetupContext, screenTypes: ScreenTypes) + : ITourSteps { + const toggleHighlightButton = (element: string, highlight: boolean, color: 'gray' | 'orange' | 'green') => { + const receiveNim = document + .querySelector(element) as HTMLButtonElement; + if (!receiveNim) return; + receiveNim.classList[highlight ? 'add' : 'remove'](`${color}-highlight`); + }; + + // Returns the length of the transaction list for the current active account and active address + // Easier to access component transaction-list which has already been mounted and has the list + // of valid transactions for the current active account and active address + const txsLen = computed(() => (searchComponentByName(root, 'transactions-list') as any).txCount || 0); + + const { state, activeAccountInfo } = useAccountStore(); + const { startedFrom } = (state.tour as { startedFrom: ITourOrigin }); + const accountIsSecured = computed(() => + activeAccountInfo.value?.type === AccountType.BIP39 && activeAccountInfo.value?.wordsExported); + + const args: IOnboardingGetStepFnArgs = { + sleep, + root, + startedFrom, + toggleHighlightButton, + txsLen, + ...screenTypes, + }; + + return { + [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), + get [OnboardingTourStep.BACKUP_ALERT]() { + // if the user has already backed up their account, skip this step + return !accountIsSecured.value ? getBackupAlertStep(args) : undefined; + }, + get [OnboardingTourStep.MENU_ICON]() { + // show only this step if it is a new user and is not in a large screen + return (!screenTypes.isLargeScreen.value && startedFrom === ITourOrigin.WELCOME_MODAL) + ? getMenuIconStep(args) : undefined; + }, + [OnboardingTourStep.ACCOUNT_OPTIONS]: getAccountOptionsStep(args), + get [OnboardingTourStep.BACKUP_OPTION_FROM_OPTIONS]() { + // if the user has already backed up their account, remind the user that he can backup anytime + if (!accountIsSecured.value) return undefined; + return (screenTypes.isLargeScreen.value) + ? getBackupOptionLargeScreenStep() + : getBackupOptionNotLargeScreenStep(args); + }, + [OnboardingTourStep.ONBOARDING_COMPLETED]: getOnboardingCompletedStep(args), + }; +} diff --git a/src/lib/tour/types.ts b/src/lib/tour/types.ts new file mode 100644 index 000000000..40cf27870 --- /dev/null +++ b/src/lib/tour/types.ts @@ -0,0 +1,225 @@ +import { ScreenTypes } from '@/composables/useWindowSize'; +import { Ref, SetupContext } from '@vue/composition-api'; +import { NodeHexagon } from '../NetworkMap'; + +export enum TourName { ONBOARDING = 'onboarding', NETWORK = 'network' } + +export enum OnboardingTourStep { + FIRST_ADDRESS, + TRANSACTION_LIST, + FIRST_TRANSACTION, + BITCOIN_ADDRESS, + WALLET_BALANCE, + BACKUP_ALERT, + MENU_ICON, + BACKUP_OPTION_FROM_OPTIONS, + ACCOUNT_OPTIONS, + ONBOARDING_COMPLETED +} + +export enum NetworkTourStep { + YOUR_LOCATION, + BACKBONE_NODE, + METRICS, + NETWORK_COMPLETED +} + +export type TourStepIndex = OnboardingTourStep | NetworkTourStep; + +// Tooltip placement +// 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 ITooltipModifier { + name: 'preventOverflow' | 'offset' | 'arrow'; + options: any; +} + +export interface FutureStepArgs { + withDelay: boolean; +} + +export interface ILifecycleArgs { + goToNextStep: (args?: FutureStepArgs) => void; + goToPrevStep: (args?: FutureStepArgs) => void; + goingForward: boolean; + ending: boolean; + isNextStepDisabled: Ref; +} + +export type IUnmountedFn = ((args: Omit) => Promise | void); + +// This interface is the main interface for a step in the tour. It consists of: +// - path - the route that the step should be shown on +// - tooltip - the tooltip content and configuration for the step. It is using popperJS as dependency +// - ui - the UI elements that should be faded out and disabled during the step along with other UI config +// - lifecycle - the lifecycle hooks for the step. Read more at the top of the file of Tour.vue file +export interface ITourStep { + path: `/${'' | 'transactions' | 'accounts' | 'network'}${'' | '?sidebar=true'}`; + + // data for the steps of v-tour + tooltip: { + + // selector for the element that the tooltip should be attached to + target: string, + + // If the content is an array of string it will be displayed as a list + // otherwise it will be displayed as a paragraph. + // Some lines can use a string from type IContentSpecialItem + content: (string | string[] | IContentSpecialItem)[], + + // This configuration is the same as the one used in popperJS + params: { + placement: BasePlacement | AlignedPlacement, + modifiers?: ITooltipModifier[] | void, + }, + + // Some steps required a custom button in the bottom of the tooltip + button?: { + text: string, + fn: (callback?: () => Promise) => void, + }, + }; + + // function to call when the step is changed. Read more at Tour.vue file + lifecycle?: { + created?: (args: Omit) => Promise | void, + mounted?: (args: ILifecycleArgs) => IUnmountedFn | 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 + + // Allow user to go to the next step using buttons from the tour + // If false, user is required to make a click in a button which is not part of the tour + isNextStepDisabled?: boolean, + + // Buttons with primary classes are disabled + disabledButtons?: string[], + + // Elements that cannot be scrolled. See more at Tour.vue@_onScrollInLockedElement + scrollLockedElements?: string[], + + // Elements that are expected to be clicked by the user. If they are clicked, the tour + // will ignore them and will not make the flash animation + explicitInteractableElements?: string[], + }; +} + +// x will be the index of the step in the tour +// JSON objects have strings as keys normally, but JS also can work with +// numbers as keys which is more convinient for us in this case +export type ITourSteps = { + [x in T]?: ITourStep; +}; + +// From which part the user can start the tour +export enum ITourOrigin { + SETTINGS = 'settings', + WELCOME_MODAL = 'welcome-modal', +} + +export type ITourStepTexts = { + [x in T]: { + default: (string | string[])[], alternative?: string[], + } +} + +// Every step is defined in its own file as a function. The function receives this interface as a parameter +export type IOnboardingGetStepFnArgs = ScreenTypes & { + root: SetupContext['root'], + sleep: (ms: number) => Promise, + startedFrom: ITourOrigin, + toggleHighlightButton: (selector: string, highlight: boolean, color: 'gray' | 'orange' | 'green') => void, + txsLen: Readonly>, +}; + +// Every step is defined in its own file as a function. The function receives this interface as a parameter +export type INetworkGetStepFnArgs = ScreenTypes & { + root: SetupContext['root'], + nodes: () => NodeHexagon[], // nodes on the map + selfNodeIndex: number, + scrollIntoView: (x: number) => void, + sleep: (ms: number) => Promise, +} + +// Events that will be used to communicate between LargeScreenTourManager.vue and Tour.vue +export type ITourBroadcast = ITourBroadcastEnd | ITourBroadcastStepChanged | ITourBroadcastClickedOutsideTour + +interface ITourBroadcastEnd { + type: 'end-tour'; +} + +interface ITourBroadcastClickedOutsideTour { + type: 'clicked-outside-tour'; +} + +export interface ITourBroadcastStepChanged { + type: 'tour-step-changed'; + payload: { + currentStep: TourStepIndex, + nSteps: number, + }; +} + +// Some steps contain texts with special requirements +export enum IContentSpecialItem { + HR = '{HR}', + ICON_NETWORK_WORLD = '{network_icon}', + ICON_ACCOUNT = '{account_icon}', + BACK_TO_ADDRESSES = '{back_to_addresses}', +} + +// Elements that are need to be opacified or non-interactive at some point during the tour +export enum IWalletHTMLElements { + SIDEBAR_TESTNET = '.sidebar .testnet-notice', + SIDEBAR_LOGO = '.sidebar .logo', + SIDEBAR_ANNOUNCMENT_BOX = '.sidebar .announcement-box', + SIDEBAR_PRICE_CHARTS = '.sidebar .price-chart-wrapper', + SIDEBAR_TRADE_ACTIONS = '.sidebar .trade-actions', + SIDEBAR_ACCOUNT_MENU = '.sidebar .account-menu', + SIDEBAR_NETWORK = '.sidebar .network', + SIDEBAR_SETTINGS = '.sidebar .settings', + SIDEBAR_MOBILE_TAP_AREA = '.mobile-tap-area', + + ACCOUNT_OVERVIEW_BACKUP_ALERT = '.account-overview .backup-warning', + ACCOUNT_OVERVIEW_TABLET_MENU_BAR = '.account-overview .mobile-menu-bar', + ACCOUNT_OVERVIEW_BALANCE = '.account-overview .account-balance-container', + ACCOUNT_OVERVIEW_ADDRESS_LIST = '.account-overview .address-list', + ACCOUNT_OVERVIEW_BITCOIN = '.account-overview .bitcoin-account', + ACCOUNT_OVERVIEW_MOBILE_ACTION_BAR = '.account-overview .mobile-action-bar', + + ADDRESS_OVERVIEW_ACTIONS_MOBILE = '.address-overview .actions-mobile', + ADDRESS_OVERVIEW_ACTIVE_ADDRESS = '.address-overview .active-address', + ADDRESS_OVERVIEW_ACTIONS = '.address-overview .actions', + ADDRESS_OVERVIEW_TRANSACTIONS = '.address-overview .transaction-list', + ADDRESS_OVERVIEW_MOBILE_ACTION_BAR = '.address-overview .mobile-action-bar', + + BUTTON_SIDEBAR_BUY = '.sidebar .trade-actions button:nth-child(1)', + BUTTON_SIDEBAR_SELL = '.sidebar .trade-actions button:nth-child(2)', + BUTTON_ADDRESS_OVERVIEW_BUY = '.address-overview .transaction-list .after-first-tx button', + BUTTON_ADDRESS_OVERVIEW_RECEIVE_FREE_NIM = // The comma in document.querySelector works like an OR gate + '.address-overview .transaction-list .empty-state a,' // Mainnet uses an a element + + '.address-overview .transaction-list .empty-state button', // Devnet uses a button element + BUTTON_ADDRESS_BACKUP_ALERT = '.account-overview .backup-warning button', + + MODAL_CONTAINER = '.modal.backdrop', + MODAL_WRAPPER = '.modal .wrapper', + MODAL_PAGE = '.modal .small-page', + MODAL_ACCOUNT_PICTURE = '.modal .small-page .account-menu-item .icon svg', + MODAL_CLOSE_BUTTON = '.modal .close-button', + + 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/router.ts b/src/router.ts index c76359ace..9191e7338 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 WelcomeModal = () => + import(/* webpackChunkName: "discover-the-nimiq-wallet-modal" */ + './components/modals/WelcomeModal.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 = () => diff --git a/src/stores/Account.ts b/src/stores/Account.ts index 7fa9ffd91..116d00c54 100644 --- a/src/stores/Account.ts +++ b/src/stores/Account.ts @@ -1,12 +1,17 @@ -import { createStore } from 'pinia'; +import { ITourOrigin, TourName, TourStepIndex } from '@/lib/tour'; import { Account } from '@nimiq/hub-api'; -import { useAddressStore } from './Address'; +import { createStore } from 'pinia'; import { CryptoCurrency } from '../lib/Constants'; +import { useAddressStore } from './Address'; export type AccountState = { accountInfos: {[id: string]: AccountInfo}, activeAccountId: string | null, activeCurrency: CryptoCurrency, + tour: + { name: TourName.NETWORK, step: TourStepIndex } | + { name: TourName.ONBOARDING, step: TourStepIndex, startedFrom: ITourOrigin } | + null, } // Mirror of Hub WalletType, which is not exported @@ -29,6 +34,7 @@ export const useAccountStore = createStore({ accountInfos: {}, activeAccountId: null, activeCurrency: CryptoCurrency.NIM, + tour: null, } as AccountState), getters: { accountInfos: (state) => state.accountInfos, @@ -105,5 +111,8 @@ export const useAccountStore = createStore({ setActiveCurrency(currency: CryptoCurrency) { this.state.activeCurrency = currency; }, + setTour(tour: AccountState['tour']) { + this.state.tour = tour; + }, }, }); 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==