diff --git a/package.json b/package.json index a480c36d29..0b475f7243 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hyperplay", - "version": "0.25.1", + "version": "0.26.0", "private": true, "main": "build/main/main.js", "homepage": "./", @@ -119,6 +119,8 @@ "i18next-fs-backend": "^2.3.2", "i18next-http-backend": "^2.5.2", "ini": "^3.0.1", + "intro.js": "^7.2.0", + "intro.js-react": "^1.0.0", "jest-environment-jsdom": "^29.7.0", "jsdom": "^20.0.0", "json5": "^2.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b75741538..ec17dbecb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,12 @@ importers: ini: specifier: ^3.0.1 version: 3.0.1 + intro.js: + specifier: ^7.2.0 + version: 7.2.0 + intro.js-react: + specifier: ^1.0.0 + version: 1.0.0(intro.js@7.2.0)(react@18.3.1) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -5841,6 +5847,15 @@ packages: resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==} engines: {node: '>=10'} + intro.js-react@1.0.0: + resolution: {integrity: sha512-zR8pbTyX20RnCZpJMc0nuHBpsjcr1wFkj3ZookV6Ly4eE/LGpFTQwPsaA61Cryzwiy/tTFsusf4hPU9NpI9UOg==} + peerDependencies: + intro.js: '>=2.5.0' + react: '>=0.14.0' + + intro.js@7.2.0: + resolution: {integrity: sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ==} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -16856,6 +16871,13 @@ snapshots: from2: 2.3.0 p-is-promise: 3.0.0 + intro.js-react@1.0.0(intro.js@7.2.0)(react@18.3.1): + dependencies: + intro.js: 7.2.0 + react: 18.3.1 + + intro.js@7.2.0: {} + invariant@2.2.4: dependencies: loose-envify: 1.4.0 diff --git a/public/locales/en/tour.json b/public/locales/en/tour.json new file mode 100644 index 0000000000..f73a0ce1e5 --- /dev/null +++ b/public/locales/en/tour.json @@ -0,0 +1,17 @@ +{ + "tour": { + "back": "Back", + "done": "Done", + "first-welcome": { + "account": "Login and connect your primary wallet to track progress.", + "account-title": "Start Your Journey", + "links-title": "A Galaxy of Games", + "quests": "Take on quests, make leaderboards, and earn along the way.", + "rewards-title": "Earn Real Rewards 🎁", + "stores": "Explore all the titles from HyperPlay, Epic Games, and GOG.", + "welcome": "Begin your journey through the Web3 galaxy and discover top-tier games.", + "welcome-title": "Ready for Takeoff 🚀" + }, + "next": "Next" + } +} diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index aca462d790..2a565de214 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -41,6 +41,7 @@ import { QuestsPage } from './screens/Quests' import { NavigateListener } from './NavigateListener' import G7Webview from './screens/G7Webview' import CardPrivacyPolicy from './screens/Onboarding/analytics/CardPrivacyPolicy' +import { TourProvider } from './components/TourGuide/TourContext' function App() { const { sidebarCollapsed, isSettingsModalOpen, connectivity } = @@ -50,106 +51,111 @@ function App() { return (
- - - - -
- - - - - - - - - - - {isSettingsModalOpen.gameInfo && ( - - )} - - } - /> - } /> - - - - } - > - } /> + + + + + +
+ + + + + + + + + + + {isSettingsModalOpen.gameInfo && ( + + )} + } + path="/" + element={} /> - - } /> - } - /> - } /> - } /> - } /> - } /> - } - /> - } /> - }> - } /> - - - - } /> + } /> + + + + } + > + } /> + } + /> + + } /> + } + /> + } /> + } /> + } /> + } /> + } + /> + } /> + }> + } /> + + + + } /> + + + } + /> + + } /> - - } /> - - } /> - - - - - } /> + + + + } /> + - - } /> - }> - } /> - - -
-
- -
-
- - {onboardingStore.isOnboardingOpen && ( - { - if (disableReason === 'skipped') { - window.api.trackEvent({ event: 'Onboarding Skipped' }) - } - onboardingStore.closeOnboarding() - }} - /> - )} -
- - - - + } /> + }> + } /> + +
+
+
+ +
+
+ + {onboardingStore.isOnboardingOpen && ( + { + if (disableReason === 'skipped') { + window.api.trackEvent({ event: 'Onboarding Skipped' }) + } + onboardingStore.closeOnboarding() + }} + /> + )} +
+ + + + +
) } diff --git a/src/frontend/components/TourGuide/CustomButtons.scss b/src/frontend/components/TourGuide/CustomButtons.scss new file mode 100644 index 0000000000..093f8b4008 --- /dev/null +++ b/src/frontend/components/TourGuide/CustomButtons.scss @@ -0,0 +1,40 @@ +.hp-custom-buttons-active { + .introjs-button { + opacity: 0 !important; + pointer-events: none !important; + position: absolute; + height: 0; + } + + .hp-custom-buttons-container { + position: relative; + min-height: 60px; + } +} + +.custom-tour-buttons { + width: 100%; + box-sizing: border-box; + display: flex; + align-items: center; + z-index: 10; + + button { + margin-left: 12px; + min-width: 90px; + font-weight: 500; + } + + &.two-buttons { + justify-content: space-between; + + button:first-child { + margin-right: auto; + margin-left: 0; + } + } + + &.one-button { + justify-content: flex-end; + } +} diff --git a/src/frontend/components/TourGuide/CustomButtons.tsx b/src/frontend/components/TourGuide/CustomButtons.tsx new file mode 100644 index 0000000000..11d58bc322 --- /dev/null +++ b/src/frontend/components/TourGuide/CustomButtons.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useRef } from 'react' +import { createPortal } from 'react-dom' +import { Button } from '@hyperplay/ui' +import { useTranslation } from 'react-i18next' +import './CustomButtons.scss' + +interface CustomTourButtonsProps { + onNext?: () => void + onPrev?: () => void + onFinish?: () => void + isFirstStep: boolean + isFinalStep: boolean +} + +const CustomTourButtons: React.FC = ({ + onNext, + onPrev, + onFinish, + isFirstStep, + isFinalStep +}) => { + const { t } = useTranslation('tour') + const containerRef = useRef(null) + const [mounted, setMounted] = React.useState(false) + const mountRetryCountRef = useRef(0) + + const findAndSetupButtons = () => { + const tooltipButtons = document.querySelector('.introjs-tooltipbuttons') + + if (tooltipButtons) { + containerRef.current = tooltipButtons + setMounted(true) + document.documentElement.classList.add('hp-custom-buttons-active') + tooltipButtons.classList.add('hp-custom-buttons-container') + return true + } + return false + } + + useEffect(() => { + if (!findAndSetupButtons()) { + const intervalId = setInterval(() => { + if (findAndSetupButtons() || mountRetryCountRef.current > 10) { + clearInterval(intervalId) + } + mountRetryCountRef.current += 1 + }, 100) + + return () => clearInterval(intervalId) + } + + return () => { + document.documentElement.classList.remove('hp-custom-buttons-active') + if (containerRef.current) { + containerRef.current.classList.remove('hp-custom-buttons-container') + } + } + }, []) + + const handleNext = () => { + if (isFinalStep) { + const doneBtn = document.querySelector('.introjs-donebutton') + if (doneBtn && doneBtn instanceof HTMLElement) { + doneBtn.click() + } + if (onFinish) onFinish() + } else { + const nextBtn = document.querySelector('.introjs-nextbutton') + if (nextBtn && nextBtn instanceof HTMLElement) { + nextBtn.click() + } + if (onNext) onNext() + } + } + + const handlePrev = () => { + const prevBtn = document.querySelector('.introjs-prevbutton') + if (prevBtn && prevBtn instanceof HTMLElement) { + prevBtn.click() + } + if (onPrev) onPrev() + } + + if (!mounted || !containerRef.current) { + return null + } + + const containerClass = isFirstStep + ? 'custom-tour-buttons one-button' + : 'custom-tour-buttons two-buttons' + + return createPortal( +
+ {!isFirstStep && ( + + )} + +
, + containerRef.current + ) +} + +export default CustomTourButtons diff --git a/src/frontend/components/TourGuide/TourContext.tsx b/src/frontend/components/TourGuide/TourContext.tsx new file mode 100644 index 0000000000..7d63344a7e --- /dev/null +++ b/src/frontend/components/TourGuide/TourContext.tsx @@ -0,0 +1,77 @@ +import React, { createContext, useState, useContext, ReactNode } from 'react' + +type TourState = { + isTourActive: boolean + currentTour: string | null + completedTours: string[] + activateTour: (tourId: string) => void + deactivateTour: () => void + markTourAsComplete: (tourId: string) => void + isTourCompleted: (tourId: string) => boolean +} + +const initialState: TourState = { + isTourActive: false, + currentTour: null, + completedTours: [], + activateTour: () => {}, + deactivateTour: () => {}, + markTourAsComplete: () => {}, + isTourCompleted: () => false +} + +const TourContext = createContext(initialState) + +interface TourProviderProps { + children: ReactNode +} + +export const TourProvider: React.FC = ({ children }) => { + const [isTourActive, setIsTourActive] = useState(false) + const [currentTour, setCurrentTour] = useState(null) + const [completedTours, setCompletedTours] = useState(() => { + // Load completed tours from localStorage if available + const saved = localStorage.getItem('hp-completed-tours') + return saved ? JSON.parse(saved) : [] + }) + + const activateTour = (tourId: string) => { + setCurrentTour(tourId) + setIsTourActive(true) + } + + const deactivateTour = () => { + setIsTourActive(false) + setCurrentTour(null) + } + + const markTourAsComplete = (tourId: string) => { + if (!completedTours.includes(tourId)) { + const updatedTours = [...completedTours, tourId] + setCompletedTours(updatedTours) + localStorage.setItem('hp-completed-tours', JSON.stringify(updatedTours)) + } + } + + const isTourCompleted = (tourId: string): boolean => { + return completedTours.includes(tourId) + } + + return ( + + {children} + + ) +} + +export const useTourGuide = (): TourState => useContext(TourContext) diff --git a/src/frontend/components/TourGuide/TourGuide.scss b/src/frontend/components/TourGuide/TourGuide.scss new file mode 100644 index 0000000000..df90b21bb5 --- /dev/null +++ b/src/frontend/components/TourGuide/TourGuide.scss @@ -0,0 +1,155 @@ +.introjs-tooltip { + background: var(--color-neutral-600); + border-radius: var(--space-md); + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.25); + min-width: 320px; +} + +.introjs-tooltiptext { + color: var(--text-weak); + font-size: var(--text-xl); + line-height: 1.5; + padding: var(--space-md); +} + +.introjs-tooltipbuttons { + border-top: 1px solid var(--color-neutral-500); +} + +.introjs-bullets ul { + scale: 1.5; +} + +.introjs-button { + box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); + font-family: var(--font-family-body, 'Poppins'); + font-weight: 500; + cursor: pointer; + position: relative; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-shadow: none; + border-radius: var(--space-xs); + border: none; + + &:active { + color: var(--color-neutral-200); + } + + border-image-source: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0) 100% + ); + + color: var(--text-strong); + background-color: var(--fill-brand-strong); + position: relative; + isolation: isolate; + + &::before { + content: ''; + position: absolute; + inset: 0; + background-color: var(--fill-hover); + opacity: 0; + transition: opacity 0.25s ease-in-out; + } + + &:focus-visible, + &.focusVisible { + outline-offset: 2px; + outline: 2px solid var(--color-primary-500); + } + + &:focus, + &:hover { + background-color: var(--color-primary-600); + color: var(--text-strong); + opacity: 80%; + } +} + +.introjs-prevbutton { + box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); + cursor: pointer; + position: relative; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-shadow: none; + color: var(--text-brand); + + &:active { + color: var(--color-neutral-200); + } + + &:disabled, + &[disabled] { + cursor: not-allowed; + } + + border-image-source: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0) 100% + ); + + border: 2px solid var(--color-stroke-brand-strong); + background: transparent; + position: relative; + isolation: isolate; + cursor: pointer; + + &:hover { + background-color: var(--fill-hover); + color: var(--text-brand); + opacity: 80%; + z-index: 1; + cursor: pointer; + border: 2px solid var(--color-stroke-brand-strong); + } +} + +.introjs-bullets ul li a { + background-color: var(--background-lighter); + + &.active { + background-color: var(--primary); + } +} + +.introjs-arrow { + border: 10px solid transparent; + + &.top, + &.top-right, + &.top-middle { + border-bottom-color: var(--color-neutral-600); + top: -18px; + } + + &.bottom, + &.bottom-right, + &.bottom-middle { + border-top-color: var(--color-neutral-600); + } + + &.left { + border-right-color: var(--color-neutral-600); + left: -18px; + } + + &.right { + right: -18px; + border-left-color: var(--color-neutral-600); + } +} + +.introjs-skipbutton { + color: var(--text-default); + + &:hover { + color: var(--text-default); + opacity: 0.8; + } +} diff --git a/src/frontend/components/TourGuide/TourGuide.tsx b/src/frontend/components/TourGuide/TourGuide.tsx new file mode 100644 index 0000000000..83f73dd406 --- /dev/null +++ b/src/frontend/components/TourGuide/TourGuide.tsx @@ -0,0 +1,91 @@ +import React, { useState, useEffect } from 'react' +import { Steps } from 'intro.js-react' +import 'intro.js/introjs.css' +import { useTranslation } from 'react-i18next' +import { useTourGuide } from './TourContext' +import { firstWelcomeTourSteps, TourStep } from './TourSteps' +import './TourGuide.scss' +import { FIRST_TIME_TOUR } from './constants' +import CustomTourButtons from './CustomButtons' + +export const TourGuide: React.FC = () => { + const { t } = useTranslation('tour') + const { isTourActive, currentTour, deactivateTour, markTourAsComplete } = + useTourGuide() + const [stepsEnabled, setStepsEnabled] = useState(false) + const [currentSteps, setCurrentSteps] = useState([]) + const [initialStep, setInitialStep] = useState(0) + const [currentStepIndex, setCurrentStepIndex] = useState(0) + + useEffect(() => { + if (isTourActive && currentTour) { + let steps: TourStep[] = [] + + // Add more tours here as needed + switch (currentTour) { + case FIRST_TIME_TOUR: + steps = firstWelcomeTourSteps(t) + break + default: + steps = [] + } + + setCurrentSteps(steps) + setInitialStep(0) + setStepsEnabled(true) + } else { + setStepsEnabled(false) + } + }, [isTourActive, currentTour, t]) + + const onExit = () => { + setStepsEnabled(false) + deactivateTour() + } + + const onComplete = () => { + if (currentTour) { + markTourAsComplete(currentTour) + } + deactivateTour() + } + + const onChange = (nextStepIndex: number) => { + setCurrentStepIndex(nextStepIndex) + } + + const options = { + showStepNumbers: false, + showBullets: true, + showProgress: false, + exitOnEsc: true, + exitOnOverlayClick: false, + nextLabel: t('tour.next', 'Next'), + prevLabel: t('tour.back', 'Back'), + doneLabel: t('tour.done', 'Done'), + overlayOpacity: 0.9, + scrollToElement: true + } + + return ( + <> + + {stepsEnabled && ( + + )} + + ) +} + +export default TourGuide diff --git a/src/frontend/components/TourGuide/TourSteps.ts b/src/frontend/components/TourGuide/TourSteps.ts new file mode 100644 index 0000000000..0ab2b55485 --- /dev/null +++ b/src/frontend/components/TourGuide/TourSteps.ts @@ -0,0 +1,49 @@ +import { TFunction } from 'i18next' + +export interface TourStep { + element?: string + intro: string + title?: string + position?: 'top' | 'bottom' | 'left' | 'right' | 'center' + tooltipClass?: string + highlightClass?: string +} + +// First Welcome tour steps for new users landing on the Store Page +export const firstWelcomeTourSteps = (t: TFunction<'tour'>): TourStep[] => [ + { + title: t('tour.first-welcome.welcome-title', 'Ready for Takeoff 🚀'), + intro: t( + 'tour.first-welcome.welcome', + 'Begin your journey through the Web3 galaxy and discover top-tier games.' + ), + position: 'center' + }, + { + element: '[data-tour="topnav-store-links"]', + title: t('tour.first-welcome.links-title', 'A Galaxy of Games'), + intro: t( + 'tour.first-welcome.stores', + 'Explore all the titles from HyperPlay, Epic Games, and GOG.' + ), + position: 'bottom' + }, + { + element: '[data-tour="sidebar-quests"]', + title: t('tour.first-welcome.rewards-title', 'Earn Real Rewards 🎁'), + intro: t( + 'tour.first-welcome.quests', + 'Take on quests, make leaderboards, and earn along the way.' + ), + position: 'right' + }, + { + element: '[data-tour="topnav-account"]', + title: t('tour.first-welcome.account-title', 'Start Your Journey'), + intro: t( + 'tour.first-welcome.account', + 'Login and connect your primary wallet to track progress.' + ), + position: 'bottom' + } +] diff --git a/src/frontend/components/TourGuide/constants.ts b/src/frontend/components/TourGuide/constants.ts new file mode 100644 index 0000000000..020bdf9077 --- /dev/null +++ b/src/frontend/components/TourGuide/constants.ts @@ -0,0 +1 @@ +export const FIRST_TIME_TOUR = 'first_time_tour' diff --git a/src/frontend/components/TourGuide/index.tsx b/src/frontend/components/TourGuide/index.tsx new file mode 100644 index 0000000000..31a94e0ba4 --- /dev/null +++ b/src/frontend/components/TourGuide/index.tsx @@ -0,0 +1,4 @@ +import { TourProvider, useTourGuide } from './TourContext' +import TourGuide from './TourGuide' + +export { TourProvider, useTourGuide, TourGuide } diff --git a/src/frontend/components/UI/AccountDropdown/index.tsx b/src/frontend/components/UI/AccountDropdown/index.tsx index c2e2d3dab6..99aadbcae8 100644 --- a/src/frontend/components/UI/AccountDropdown/index.tsx +++ b/src/frontend/components/UI/AccountDropdown/index.tsx @@ -17,15 +17,18 @@ import useAuthSession from '../../../hooks/useAuthSession' function NavigationMenuItem({ label, to, - showMetaMaskExtensionLinks + showMetaMaskExtensionLinks, + dataTour }: { label: string to: string showMetaMaskExtensionLinks: boolean + dataTour?: string }) { return ( { onboardingStore.openOnboarding()} + data-tour="account-wallet-connect" >
{showWalletConnectedLinks @@ -87,7 +91,8 @@ const WalletDropdown: React.FC = observer(() => { label={t('hyperplay.viewFullscreen', `View fullscreen`)} to={'/metamaskHome'} showMetaMaskExtensionLinks={showMetaMaskExtensionLinks} - > + dataTour="account-metamask-fullscreen" + /> { })} showMetaMaskExtensionLinks={showMetaMaskExtensionLinks} to={'/metamaskSnaps'} - > + dataTour="account-metamask-snaps" + /> )} {showMetaMaskExtensionLinks && ( { )} Epic/GoG {t('accounts', `accounts`)} - +
{t('userselector.manageStore', `Manage stores`)} @@ -140,7 +147,10 @@ const WalletDropdown: React.FC = observer(() => { HyperPlay {t('profile', `Profile`)} {isSignedIn ? ( <> - authState.openSignInModal()}> + authState.openSignInModal()} + data-tour="account-manage-accounts" + >
{t('userselector.manageaccounts', `Manage accounts`)}
@@ -150,6 +160,7 @@ const WalletDropdown: React.FC = observer(() => { await window.api.logOut() await invalidateQuery() }} + data-tour="account-logout" >
{ ) : ( - authState.openSignInModal()}> + authState.openSignInModal()} + data-tour="account-login" + >
{t('userselector.logIn', `Log in`)}
diff --git a/src/frontend/components/UI/Sidebar/components/SidebarLinks/index.tsx b/src/frontend/components/UI/Sidebar/components/SidebarLinks/index.tsx index bf331ee001..ff73781918 100644 --- a/src/frontend/components/UI/Sidebar/components/SidebarLinks/index.tsx +++ b/src/frontend/components/UI/Sidebar/components/SidebarLinks/index.tsx @@ -76,7 +76,7 @@ export default observer(function SidebarLinks() { return ( <>
-
+
@@ -98,7 +98,7 @@ export default observer(function SidebarLinks() {
-
+
@@ -118,7 +118,10 @@ export default observer(function SidebarLinks() {
{SHOW_ACHIEVEMENTS && ( -
+
)} {SHOW_QUESTS ? ( -
+
) : null} -
+
-
+
-
+
handleExternalLink(window.api.openDiscordLink)} + data-tour="sidebar-discord" >
@@ -208,8 +221,9 @@ export default observer(function SidebarLinks() {
handleExternalLink(window.api.openTwitterLink)} + data-tour="sidebar-x" >
@@ -219,7 +233,7 @@ export default observer(function SidebarLinks() {
-
+
-
+
{(isFullscreen || activeController) && }
diff --git a/src/frontend/components/UI/Sidebar/index.module.scss b/src/frontend/components/UI/Sidebar/index.module.scss index 89e952d2ee..91f2ee4d53 100644 --- a/src/frontend/components/UI/Sidebar/index.module.scss +++ b/src/frontend/components/UI/Sidebar/index.module.scss @@ -25,3 +25,37 @@ .Sidebar::-webkit-scrollbar { width: 0px; } + +.sidebarContent { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + justify-content: space-between; +} + +.sidebarTourButtonWrapper { + display: flex; + justify-content: center; + margin-top: auto; + padding: var(--space-sm) 0; +} + +.sidebarTourButton { + margin-top: var(--space-md); + background-color: var(--transparent); + color: var(--text-default); + font-weight: 500; + text-align: center; + transition: background-color 0.2s, color 0.2s; + + svg { + width: 24px; + height: 24px; + } + + &:hover { + background-color: var(--primary); + color: var(--text-title); + } +} diff --git a/src/frontend/components/UI/Sidebar/index.tsx b/src/frontend/components/UI/Sidebar/index.tsx index 47c5bcf547..cdfc25601e 100644 --- a/src/frontend/components/UI/Sidebar/index.tsx +++ b/src/frontend/components/UI/Sidebar/index.tsx @@ -5,8 +5,10 @@ import styles from './index.module.scss' const Sidebar = () => { return ( -
) }) diff --git a/src/frontend/screens/Game/GamePage/index.tsx b/src/frontend/screens/Game/GamePage/index.tsx index 1a518a45dd..564b4de9ba 100644 --- a/src/frontend/screens/Game/GamePage/index.tsx +++ b/src/frontend/screens/Game/GamePage/index.tsx @@ -69,6 +69,7 @@ import libraryState from 'frontend/state/libraryState' import DMQueueState from 'frontend/state/DMQueueState' import { useEstimatedUncompressedSize } from 'frontend/hooks/useEstimatedUncompressedSize' import authState from 'frontend/state/authState' +import { TourGuide } from 'frontend/components/TourGuide' type locationState = { fromDM?: boolean @@ -399,7 +400,7 @@ export default observer(function GamePage(): JSX.Element | null { t('generic.noDescription', 'No description available') return ( -
+
{showStopInstallModal ? ( setShowStopInstallModal(false)} @@ -420,7 +421,11 @@ export default observer(function GamePage(): JSX.Element | null { )} {title ? ( <> - + - + -
+
-
+
-

{title}

+

+ {title} +

{is_installed && !isBrowserGame && ( )} -
+
@@ -480,10 +488,12 @@ export default observer(function GamePage(): JSX.Element | null { />
-
+
{developer}
-
{description}
-
+
+ {description} +
+
{!is_installed && !isSideloaded && ( <> {downloadSize !== 0 ? ( @@ -600,7 +610,7 @@ export default observer(function GamePage(): JSX.Element | null { />
-
+

setLaunchArguments(event.target.value)} value={launchArguments} prompt={t('launch.options', 'Launch Options...')} + data-tour="game-page-launch-options" > {launchOptions.map(({ name, parameters }) => (

+
{is_installed && !isQueued && (
+ ) }) diff --git a/src/frontend/screens/WebView/index.tsx b/src/frontend/screens/WebView/index.tsx index 1cc226e052..1d54381911 100644 --- a/src/frontend/screens/WebView/index.tsx +++ b/src/frontend/screens/WebView/index.tsx @@ -13,6 +13,7 @@ import { UpdateComponent } from 'frontend/components/UI' import WebviewControls from 'frontend/components/UI/WebviewControls' import ContextProvider from 'frontend/state/ContextProvider' import webviewNavigationStore from 'frontend/store/WebviewNavigationStore' +import { useTourGuide } from 'frontend/components/TourGuide/TourContext' import { Runner } from 'common/types' import './index.css' import LoginWarning from '../Login/components/LoginWarning' @@ -31,6 +32,7 @@ import { METAMASK_SNAPS_URL } from 'common/constants' import storeAuthState from 'frontend/state/storeAuthState' import { getGameInfo } from 'frontend/helpers' import cn from 'classnames' +import { FIRST_TIME_TOUR } from 'frontend/components/TourGuide/constants' function urlIsHpUrl(url: string) { const urlToTest = new URL(url) @@ -50,6 +52,7 @@ function WebView({ const { pathname, search } = useLocation() const { t } = useTranslation() const { epic, gog, connectivity } = useContext(ContextProvider) + const { isTourCompleted, activateTour } = useTourGuide() const [loading, setLoading] = useState<{ refresh: boolean message: string @@ -138,6 +141,19 @@ function WebView({ window.api.trackScreen('WebView', { url: startUrl, runner }) }, [startUrl, runner]) + // Check if on a store page and trigger first-welcome tour if not completed + useEffect(() => { + const isStorePage = pathname === '/hyperplaystore' + + if (isStorePage && !loading.refresh) { + if (!isTourCompleted(FIRST_TIME_TOUR)) { + setTimeout(() => { + activateTour(FIRST_TIME_TOUR) + }, 1000) + } + } + }, [pathname, loading.refresh, isTourCompleted]) + useEffect(() => { if (!urlIsHpUrl(startUrl) && pathname !== '/game7Portal') { return