Compare commits

...

1 Commits

Author SHA1 Message Date
Clément Pasteau
3625acf975 Simplify tabs by removing get started page 2025-04-23 16:07:25 +02:00
16 changed files with 101 additions and 1179 deletions

View File

@@ -28,7 +28,7 @@ import {
import { useDebounce } from '../Utils/UseDebounce';
import PromotionsSlideshow from '../Promotions/PromotionsSlideshow';
import { ColumnStackLayout } from '../UI/Layout';
import { EarnCredits } from '../MainFrame/EditorContainers/HomePage/GetStartedSection/EarnCredits';
import { EarnCredits } from '../GameDashboard/Wallet/EarnCredits';
const cellSpacing = 2;

View File

@@ -1,24 +1,24 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import Text from '../../../../UI/Text';
import Text from '../../UI/Text';
import {
ColumnStackLayout,
LineStackLayout,
ResponsiveLineStackLayout,
} from '../../../../UI/Layout';
} from '../../UI/Layout';
import {
type Badge,
type Achievement,
} from '../../../../Utils/GDevelopServices/Badge';
import { Column } from '../../../../UI/Grid';
import Window from '../../../../Utils/Window';
import Coin from '../../../../Credits/Icons/Coin';
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
} from '../../Utils/GDevelopServices/Badge';
import { Column } from '../../UI/Grid';
import Window from '../../Utils/Window';
import Coin from '../../Credits/Icons/Coin';
import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale';
import { I18n } from '@lingui/react';
import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
import TextButton from '../../../../UI/TextButton';
import RouterContext from '../../../RouterContext';
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
import TextButton from '../../UI/TextButton';
import RouterContext from '../../MainFrame/RouterContext';
type CreditItemType = 'badge' | 'feedback';
type BadgeInfo = {|

View File

@@ -7,7 +7,7 @@ import DashboardWidget, {
import { ColumnStackLayout } from '../../UI/Layout';
import Coin from '../../Credits/Icons/Coin';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import { EarnCredits } from '../../MainFrame/EditorContainers/HomePage/GetStartedSection/EarnCredits';
import { EarnCredits } from './EarnCredits';
import TextButton from '../../UI/TextButton';
import { Trans } from '@lingui/macro';

View File

@@ -1,185 +0,0 @@
// @flow
import * as React from 'react';
import { t, Trans } from '@lingui/macro';
import { I18n as I18nType } from '@lingui/core';
import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
import GDevelopThemeContext from '../../../../UI/Theme/GDevelopThemeContext';
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
import { Column, Line } from '../../../../UI/Grid';
import Paper from '../../../../UI/Paper';
import {
ColumnStackLayout,
ResponsiveLineStackLayout,
} from '../../../../UI/Layout';
import Text from '../../../../UI/Text';
import CheckCircle from '../../../../UI/CustomSvgIcons/CheckCircle';
import RaisedButton from '../../../../UI/RaisedButton';
import Window from '../../../../Utils/Window';
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
const styles = {
bulletIcon: { width: 20, height: 20, marginRight: 10 },
bulletText: { flex: 1 },
planRecommendationThumbnail: { maxWidth: 350, flex: 1 },
planRecommendationContainer: { borderRadius: 8, maxWidth: 850, padding: 8 },
};
const planImages = {
individual: {
path: 'res/plan-individual.svg',
alt: t`Red hero taking care of their diamond`,
},
education: {
path: 'res/plan-education.svg',
alt: t`Red hero sharing knowledge with pink cloud students`,
},
professional: {
path: 'res/plan-professional.svg',
alt: t`Red and Green heroes running side by side carrying their diamonds`,
},
};
const planDetailsById = {
silver: {
title: <Trans>GDevelop's Silver plan</Trans>,
description: (
<Trans>Unlock GDevelop's features to build more and faster.</Trans>
),
image: planImages.individual,
link: 'https://gdevelop.io/pricing/individual',
},
gold: {
title: <Trans>GDevelop's Gold plan</Trans>,
description: (
<Trans>Unlock GDevelop's features to build more and faster.</Trans>
),
image: planImages.individual,
link: 'https://gdevelop.io/pricing/individual',
},
education: {
title: <Trans>GDevelop's Education plan</Trans>,
description: (
<Trans>
For universities, extra curricular classes and summer camps.
</Trans>
),
image: planImages.education,
link: 'https://gdevelop.io/pricing/education',
},
startup: {
title: <Trans>GDevelop's Startup plan</Trans>,
description: (
<Trans>
Get the most out of GDevelop and get your games out in no time.
</Trans>
),
image: planImages.professional,
link: 'https://gdevelop.io/pricing/business',
},
business: {
title: <Trans>GDevelop's Business plan</Trans>,
description: (
<Trans>
Get the most out of GDevelop and get your games out in no time.
</Trans>
),
image: planImages.professional,
link: 'https://gdevelop.io/pricing/business',
},
};
const PlanRecommendationRow = ({
recommendationPlanId,
subscriptionPlansWithPricingSystems,
i18n,
}: {|
recommendationPlanId: string,
subscriptionPlansWithPricingSystems: SubscriptionPlanWithPricingSystems[],
i18n: I18nType,
|}) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const { isMobile } = useResponsiveWindowSize();
const planToUse =
recommendationPlanId === 'silver'
? 'gdevelop_silver'
: recommendationPlanId === 'gold'
? 'gdevelop_gold'
: recommendationPlanId === 'education'
? 'gdevelop_education'
: recommendationPlanId === 'startup' ||
recommendationPlanId === 'business'
? 'gdevelop_startup'
: null;
if (!planToUse) return null;
const plan = subscriptionPlansWithPricingSystems.find(
plan => plan.id === planToUse
);
if (!plan) return null;
const planDetails = planDetailsById[recommendationPlanId];
return (
<Line justifyContent="center">
<Paper
background="dark"
style={{
...styles.planRecommendationContainer,
border: `1px solid ${gdevelopTheme.palette.secondary}`,
}}
>
<ResponsiveLineStackLayout noColumnMargin noMargin>
<img
src={planDetails.image.path}
alt={i18n._(planDetails.image.alt)}
style={styles.planRecommendationThumbnail}
/>
<Line expand>
<ColumnStackLayout>
<Text
noMargin
align={isMobile ? 'center' : 'left'}
size="section-title"
>
{planDetails.title}
</Text>
<Text align={isMobile ? 'center' : 'left'}>
{planDetails.description}
</Text>
<div style={{ padding: `0 20px` }}>
<ColumnStackLayout noMargin>
{plan.bulletPointsByLocale.map(
(bulletPointByLocale, index) => (
<Column key={index} expand noMargin>
<Line noMargin alignItems="center">
<CheckCircle
style={{
...styles.bulletIcon,
color: gdevelopTheme.message.valid,
}}
/>
<Text style={styles.bulletText} size="body2" noMargin>
{selectMessageByLocale(i18n, bulletPointByLocale)}
</Text>
</Line>
</Column>
)
)}
</ColumnStackLayout>
</div>
<Column noMargin>
<RaisedButton
primary
label={<Trans>Learn More</Trans>}
onClick={() => Window.openExternalURL(planDetails.link)}
/>
</Column>
</ColumnStackLayout>
</Line>
</ResponsiveLineStackLayout>
</Paper>
</Line>
);
};
export default PlanRecommendationRow;

View File

@@ -1,419 +0,0 @@
// @flow
import * as React from 'react';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import { Trans } from '@lingui/macro';
import { makeStyles } from '@material-ui/styles';
import GridList from '@material-ui/core/GridList';
import GridListTile from '@material-ui/core/GridListTile';
import { type AuthenticatedUser } from '../../../../Profile/AuthenticatedUserContext';
import { type Subscription } from '../../../../Utils/GDevelopServices/Usage';
import { TutorialContext } from '../../../../Tutorial/TutorialContext';
import { SectionRow } from '../SectionContainer';
import GuidedLessons from '../InAppTutorials/GuidedLessons';
import { formatTutorialToImageTileComponent } from '../LearnSection';
import ImageTileRow from '../../../../UI/ImageTileRow';
import {
useResponsiveWindowSize,
type WindowSizeType,
} from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
import Text from '../../../../UI/Text';
import { Column, Line, Spacer } from '../../../../UI/Grid';
import { type Tutorial } from '../../../../Utils/GDevelopServices/Tutorial';
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
import { CardWidget } from '../CardWidget';
import Window from '../../../../Utils/Window';
import { ColumnStackLayout } from '../../../../UI/Layout';
import {
type GuidedLessonsRecommendation,
type PlanRecommendation,
} from '../../../../Utils/GDevelopServices/User';
import PreferencesContext from '../../../Preferences/PreferencesContext';
import PlanRecommendationRow from './PlanRecommendationRow';
import { SurveyCard } from './SurveyCard';
import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
import PromotionsSlideshow from '../../../../Promotions/PromotionsSlideshow';
import { PrivateTutorialViewDialog } from '../../../../AssetStore/PrivateTutorials/PrivateTutorialViewDialog';
import FlatButton from '../../../../UI/FlatButton';
import InAppTutorialContext from '../../../../InAppTutorial/InAppTutorialContext';
import { type NewProjectSetup } from '../../../../ProjectCreation/NewProjectSetupDialog';
import { type ExampleShortHeader } from '../../../../Utils/GDevelopServices/Example';
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
const styles = {
textTutorialContent: {
padding: 20,
flex: 1,
display: 'flex',
},
};
const useStyles = makeStyles({
tile: {
width: '100%',
display: 'flex',
justifyContent: 'center',
},
});
const getTextTutorialsColumnsFromWidth = (
windowSize: WindowSizeType,
isLandscape: boolean
) => {
switch (windowSize) {
case 'small':
return isLandscape ? 4 : 2;
case 'medium':
return 2;
case 'large':
return 4;
case 'xlarge':
return 5;
default:
return 3;
}
};
const getVideoTutorialsColumnsFromWidth = (
windowSize: WindowSizeType,
isLandscape: boolean
) => {
switch (windowSize) {
case 'small':
return isLandscape ? 5 : 2;
case 'medium':
return 3;
case 'large':
return 5;
case 'xlarge':
return 6;
default:
return 3;
}
};
const getTutorialsLimitsFromWidth = (
windowSize: WindowSizeType,
isLandscape: boolean
) => {
switch (windowSize) {
case 'small':
return isLandscape ? 5 : 3;
case 'medium':
return 3;
case 'large':
return 5;
case 'xlarge':
return 5;
default:
return 3;
}
};
const isPlanRecommendationRelevant = (
subscription: Subscription,
planRecommendation: PlanRecommendation
): boolean => {
// Don't recommend plans to holders of education plan.
if (subscription.planId === 'gdevelop_education') return false;
const relevantPlans =
subscription.planId === 'gdevelop_silver' ||
subscription.planId === 'gdevelop_indie'
? ['gold', 'startup', 'business', 'education']
: subscription.planId === 'gdevelop_gold' ||
subscription.planId === 'gdevelop_pro'
? ['startup', 'business', 'education']
: subscription.planId === 'gdevelop_startup'
? ['business']
: [];
return relevantPlans.includes(planRecommendation.id);
};
type TextTutorialsRowProps = {|
tutorials: Array<Tutorial>,
i18n: I18nType,
|};
const TextTutorialsRow = ({ tutorials, i18n }: TextTutorialsRowProps) => {
const classes = useStyles();
const { isLandscape, windowSize } = useResponsiveWindowSize();
return (
<>
<Column noMargin>
<Text size="section-title" noMargin>
<Trans>Read</Trans>
</Text>
<Text>
<Trans>
Text-based content directly from GDevelops site and Wiki.
</Trans>
</Text>
</Column>
<GridList
cols={getTextTutorialsColumnsFromWidth(windowSize, isLandscape)}
cellHeight="auto"
spacing={10}
>
{tutorials.map(tutorial => (
<GridListTile key={tutorial.id} classes={{ tile: classes.tile }}>
<CardWidget
onClick={() =>
Window.openExternalURL(
selectMessageByLocale(i18n, tutorial.linkByLocale)
)
}
size="large"
>
<div style={styles.textTutorialContent}>
<ColumnStackLayout expand justifyContent="center" useFullHeight>
<Text noMargin size="block-title">
{selectMessageByLocale(i18n, tutorial.titleByLocale)}
</Text>
<Text noMargin size="body" color="secondary">
{selectMessageByLocale(i18n, tutorial.descriptionByLocale)}
</Text>
</ColumnStackLayout>
</div>
</CardWidget>
</GridListTile>
))}
</GridList>
</>
);
};
type Props = {|
authenticatedUser: AuthenticatedUser,
selectInAppTutorial: (tutorialId: string) => void,
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
onStartSurvey: null | (() => void),
hasFilledSurveyAlready: boolean,
onOpenProfile: () => void,
onCreateProjectFromExample: (
exampleShortHeader: ExampleShortHeader,
newProjectSetup: NewProjectSetup,
i18n: I18nType,
isQuickCustomization?: boolean
) => Promise<void>,
askToCloseProject: () => Promise<boolean>,
|};
const RecommendationList = ({
authenticatedUser,
selectInAppTutorial,
subscriptionPlansWithPricingSystems,
onStartSurvey,
hasFilledSurveyAlready,
onOpenProfile,
onCreateProjectFromExample,
askToCloseProject,
}: Props) => {
const { recommendations, subscription, limits } = authenticatedUser;
const { tutorials } = React.useContext(TutorialContext);
const {
getTutorialProgress,
values: { showInAppTutorialDeveloperMode },
} = React.useContext(PreferencesContext);
const { onLoadInAppTutorialFromLocalFile } = React.useContext(
InAppTutorialContext
);
const [
selectedTutorial,
setSelectedTutorial,
] = React.useState<Tutorial | null>(null);
if (!recommendations) return <PlaceholderLoader />;
const recommendedTutorials = tutorials
? recommendations
.map(recommendation =>
recommendation.type === 'gdevelop-tutorial'
? tutorials.find(tutorial => tutorial.id === recommendation.id)
: null
)
.filter(Boolean)
: [];
const recommendedVideoTutorials = recommendedTutorials.filter(
tutorial => tutorial.type === 'video'
);
const recommendedTextTutorials = recommendedTutorials.filter(
tutorial => tutorial.type === 'text'
);
// $FlowIgnore
const guidedLessonsRecommendation: ?GuidedLessonsRecommendation = recommendations.find(
recommendation => recommendation.type === 'guided-lessons'
);
const guidedLessonsIds = guidedLessonsRecommendation
? guidedLessonsRecommendation.lessonsIds
: null;
// $FlowIgnore
const planRecommendation: ?PlanRecommendation = recommendations.find(
recommendation => recommendation.type === 'plan'
);
const getInAppTutorialPartProgress = ({
tutorialId,
}: {
tutorialId: string,
}) => {
const tutorialProgress = getTutorialProgress({
tutorialId,
userId: authenticatedUser.profile
? authenticatedUser.profile.id
: undefined,
});
if (!tutorialProgress || !tutorialProgress.progress) return 0;
return tutorialProgress.progress[0]; // guided lessons only have one part.
};
return (
<I18n>
{({ i18n }) => {
const items = [];
if (onStartSurvey && !hasFilledSurveyAlready)
items.push(
<SectionRow key="start-survey">
<SurveyCard
onStartSurvey={onStartSurvey}
hasFilledSurveyAlready={false}
/>
</SectionRow>
);
if (guidedLessonsRecommendation) {
const displayTextAfterGuidedLessons = guidedLessonsIds
? guidedLessonsIds
.map(tutorialId => getInAppTutorialPartProgress({ tutorialId }))
.every(progress => progress === 100)
: false;
items.push(
<SectionRow key="guided-lessons">
<Line justifyContent="space-between" noMargin alignItems="center">
<Text size="section-title" noMargin>
<Trans>Build game mechanics</Trans>
</Text>
{showInAppTutorialDeveloperMode && (
<FlatButton
label={<Trans>Load local lesson</Trans>}
onClick={onLoadInAppTutorialFromLocalFile}
/>
)}
</Line>
<GuidedLessons
selectInAppTutorial={selectInAppTutorial}
lessonsIds={guidedLessonsIds}
/>
{displayTextAfterGuidedLessons && (
<Text>
<Trans>
Congratulations on completing this selection of guided
lessons! Find all lessons in the Learn section.
</Trans>
</Text>
)}
</SectionRow>
);
}
if (recommendedVideoTutorials.length) {
items.push(
<SectionRow key="videos">
<ImageTileRow
title={<Trans>Get started with game creation</Trans>}
margin="dense"
items={recommendedVideoTutorials.map(tutorial =>
formatTutorialToImageTileComponent({
i18n,
limits,
tutorial,
onSelectTutorial: setSelectedTutorial,
})
)}
getColumnsFromWindowSize={getVideoTutorialsColumnsFromWidth}
getLimitFromWindowSize={getTutorialsLimitsFromWidth}
/>
</SectionRow>
);
}
if (onStartSurvey && hasFilledSurveyAlready)
items.push(
<SectionRow key="start-survey">
<SurveyCard
onStartSurvey={onStartSurvey}
hasFilledSurveyAlready
/>
</SectionRow>
);
items.push(
<SectionRow key="promotions">
<Text size="section-title" noMargin>
<Trans>Discover the ecosystem</Trans>
</Text>
<Spacer />
<PromotionsSlideshow />
</SectionRow>
);
if (recommendedTextTutorials.length) {
items.push(
<SectionRow key="texts">
<TextTutorialsRow
tutorials={recommendedTextTutorials}
i18n={i18n}
/>
</SectionRow>
);
}
if (planRecommendation) {
const shouldDisplayPlanRecommendation =
limits &&
!(
limits.capabilities.classrooms &&
limits.capabilities.classrooms.hideUpgradeNotice
) &&
(!subscription ||
isPlanRecommendationRelevant(subscription, planRecommendation));
if (
shouldDisplayPlanRecommendation &&
subscriptionPlansWithPricingSystems
) {
items.push(
<SectionRow key="plan">
<PlanRecommendationRow
recommendationPlanId={planRecommendation.id}
subscriptionPlansWithPricingSystems={
subscriptionPlansWithPricingSystems
}
i18n={i18n}
/>
</SectionRow>
);
}
}
return (
<>
{items}
{selectedTutorial && (
<PrivateTutorialViewDialog
tutorial={selectedTutorial}
onClose={() => setSelectedTutorial(null)}
/>
)}
</>
);
}}
</I18n>
);
};
export default RecommendationList;

View File

@@ -1,299 +0,0 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import Text from '../../../../UI/Text';
import { ColumnStackLayout } from '../../../../UI/Layout';
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
import { useOnlineStatus } from '../../../../Utils/OnlineStatus';
import TreeLeaves from '../../../../UI/CustomSvgIcons/TreeLeaves';
import SectionContainer from '../SectionContainer';
import RaisedButton from '../../../../UI/RaisedButton';
import useForceUpdate from '../../../../Utils/UseForceUpdate';
import { LargeSpacer, Line } from '../../../../UI/Grid';
import CircularProgress from '../../../../UI/CircularProgress';
import { type UserSurvey as UserSurveyType } from '../../../../Utils/GDevelopServices/User';
import UserSurvey from './UserSurvey';
import {
clearUserSurveyPersistedState,
hasStartedUserSurvey,
} from './UserSurveyStorage';
import LinearProgress from '../../../../UI/LinearProgress';
import PreferencesContext from '../../../Preferences/PreferencesContext';
import RecommendationList from './RecommendationList';
import ErrorBoundary from '../../../../UI/ErrorBoundary';
import { delay } from '../../../../Utils/Delay';
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
import Checkbox from '../../../../UI/Checkbox';
import { sendUserSurveyCompleted } from '../../../../Utils/Analytics/EventSender';
import { type NewProjectSetup } from '../../../../ProjectCreation/NewProjectSetupDialog';
import { type ExampleShortHeader } from '../../../../Utils/GDevelopServices/Example';
const styles = {
icon: {
width: 80,
height: 80,
margin: 20,
},
middlePageButtonContainer: {
width: '100%',
maxWidth: 300, // Make buttons larger but not too much.
marginBottom: '15%', // Used to display the content of the section higher than at the center.
},
bottomPageButtonContainer: {
width: '100%',
maxWidth: 300, // Make buttons larger but not too much.
marginBottom: 30, // Used to giver some space between the buttons and the screen bottom border.
},
linearProgress: { width: 200 },
getFormContainerStyle: (isMobile: boolean) => ({
marginTop: 20,
// Take full width on mobile.
width: isMobile ? '95%' : 300,
}),
questionnaireFinishedImage: { aspectRatio: '263 / 154' },
};
const questionnaireFinishedImageSource = 'res/questionnaire/welcome-back.svg';
type Props = {|
onUserSurveyStarted: () => void,
onUserSurveyHidden: () => void,
selectInAppTutorial: (tutorialId: string) => void,
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
onOpenProfile: () => void,
onCreateProjectFromExample: (
exampleShortHeader: ExampleShortHeader,
newProjectSetup: NewProjectSetup,
i18n: I18nType,
isQuickCustomization?: boolean
) => Promise<void>,
askToCloseProject: () => Promise<boolean>,
|};
const GetStartedSection = ({
selectInAppTutorial,
onUserSurveyStarted,
onUserSurveyHidden,
subscriptionPlansWithPricingSystems,
onOpenProfile,
onCreateProjectFromExample,
askToCloseProject,
}: Props) => {
const isFillingOutSurvey = hasStartedUserSurvey();
const isOnline = useOnlineStatus();
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const {
profile,
creatingOrLoggingInAccount,
onEditProfile,
loginState,
} = authenticatedUser;
const {
values: preferences,
setShowGetStartedSectionByDefault,
} = React.useContext(PreferencesContext);
const recommendationsGettingDelayPromise = React.useRef<?Promise<void>>(null);
const forceUpdate = useForceUpdate();
const [step, setStep] = React.useState<
'survey' | 'surveyFinished' | 'recommendations'
>(isFillingOutSurvey ? 'survey' : 'recommendations');
const [errorSendingSurvey, setErrorSendingSurvey] = React.useState<boolean>(
false
);
const onSurveyFinished = async (survey: UserSurveyType) => {
try {
setStep('surveyFinished');
// Artificial delay to build up expectations.
recommendationsGettingDelayPromise.current = delay(2500);
await Promise.all([
onEditProfile({ survey }, preferences),
recommendationsGettingDelayPromise.current,
]);
sendUserSurveyCompleted();
clearUserSurveyPersistedState();
} catch (error) {
console.error('An error occurred when sending survey:', error);
setErrorSendingSurvey(true);
} finally {
recommendationsGettingDelayPromise.current = null;
setStep('recommendations');
}
};
React.useEffect(
() => {
if (!authenticatedUser.authenticated) clearUserSurveyPersistedState();
},
[authenticatedUser.authenticated]
);
if (
(creatingOrLoggingInAccount || loginState === 'loggingIn') &&
// Do not display loader if the user is already seeing the recommendations.
// It can happen when the user profile is refreshed while the recommendations
// are displayed. This way, the loader is not displayed unnecessarily.
step !== 'recommendations' &&
!recommendationsGettingDelayPromise.current
) {
return (
<SectionContainer flexBody>
<ColumnStackLayout
noMargin
expand
justifyContent="center"
alignItems="center"
>
<ColumnStackLayout
noMargin
expand
justifyContent="center"
alignItems="center"
>
<CircularProgress size={40} />
</ColumnStackLayout>
</ColumnStackLayout>
</SectionContainer>
);
}
if (!isOnline || errorSendingSurvey) {
return (
<SectionContainer flexBody>
<ColumnStackLayout
noMargin
expand
justifyContent="center"
alignItems="center"
>
{errorSendingSurvey ? (
<>
<Text size="title" align="center">
<Trans>Error when sending survey.</Trans>
</Text>
<TreeLeaves style={styles.icon} />
<Text size="body2" noMargin align="center">
<Trans>
Verify your internet connection and try again later.
</Trans>
</Text>
</>
) : (
<>
<Text size="title" align="center">
<Trans>You seem to be offline</Trans>
</Text>
<TreeLeaves style={styles.icon} />
<Text size="body2" noMargin align="center">
<Trans>
Verify your internet connection to access your personalized
content.
</Trans>
</Text>
<div style={styles.middlePageButtonContainer}>
<Line expand>
<RaisedButton
primary
label={<Trans>Refresh</Trans>}
onClick={forceUpdate}
fullWidth
/>
</Line>
</div>
</>
)}
</ColumnStackLayout>
</SectionContainer>
);
}
if (step === 'surveyFinished') {
return (
<SectionContainer flexBody>
<ColumnStackLayout
noMargin
expand
justifyContent="center"
alignItems="center"
>
<Text size="title" align="center">
<Trans>Alright let's see what we have for you...</Trans>
</Text>
<img
src={questionnaireFinishedImageSource}
alt="You as the red hero coming back to life"
style={styles.questionnaireFinishedImage}
/>
<Text size="body2" noMargin align="center">
<Trans>Just one second please...</Trans>
</Text>
<Line>
<LinearProgress
variant="indeterminate"
style={styles.linearProgress}
/>
</Line>
</ColumnStackLayout>
</SectionContainer>
);
}
if (step === 'recommendations') {
return (
<>
<SectionContainer flexBody showUrgentAnnouncements>
<RecommendationList
authenticatedUser={authenticatedUser}
selectInAppTutorial={selectInAppTutorial}
subscriptionPlansWithPricingSystems={
subscriptionPlansWithPricingSystems
}
onOpenProfile={onOpenProfile}
onStartSurvey={
profile
? () => {
setStep('survey');
}
: null
}
hasFilledSurveyAlready={profile ? !!profile.survey : false}
onCreateProjectFromExample={onCreateProjectFromExample}
askToCloseProject={askToCloseProject}
/>
{authenticatedUser.recommendations && (
<Line justifyContent="center" alignItems="center">
<Checkbox
label={<Trans>Don't show this screen on next startup</Trans>}
checked={!preferences.showGetStartedSectionByDefault}
onCheck={(e, checked) =>
setShowGetStartedSectionByDefault(!checked)
}
/>
</Line>
)}
<LargeSpacer />
</SectionContainer>
</>
);
}
return (
<UserSurvey
onCompleted={onSurveyFinished}
onStarted={onUserSurveyStarted}
/>
);
};
const GetStartedSectionWithErrorBoundary = (props: Props) => (
<ErrorBoundary
componentTitle={<Trans>Get started section</Trans>}
scope="start-page-get-started"
>
<GetStartedSection {...props} />
</ErrorBoundary>
);
export default GetStartedSectionWithErrorBoundary;

View File

@@ -10,7 +10,6 @@ import HammerIcon from '../../../UI/CustomSvgIcons/Hammer';
import SchoolIcon from '../../../UI/CustomSvgIcons/School';
import ControllerIcon from '../../../UI/CustomSvgIcons/Controller';
import BookLeafIcon from '../../../UI/CustomSvgIcons/BookLeaf';
import SunIcon from '../../../UI/CustomSvgIcons/Sun';
import StoreIcon from '../../../UI/CustomSvgIcons/Store';
import Preferences from '../../../UI/CustomSvgIcons/Preferences';
import GDevelopGLogo from '../../../UI/CustomSvgIcons/GDevelopGLogo';
@@ -43,13 +42,7 @@ export const styles = {
},
};
export type HomeTab =
| 'get-started'
| 'create'
| 'learn'
| 'play'
| 'shop'
| 'team-view';
export type HomeTab = 'create' | 'learn' | 'play' | 'shop' | 'team-view';
export type GetIconFunction = ({
color: string,
@@ -64,14 +57,6 @@ export type HomePageMenuTab = {|
|};
const homePageMenuTabs: { [tab: HomeTab]: HomePageMenuTab } = {
'get-started': {
label: <Trans>Start</Trans>,
tab: 'get-started',
id: 'home-get-started-tab',
getIcon: ({ color, fontSize }) => (
<SunIcon fontSize={fontSize} color={color} />
),
},
create: {
label: <Trans>Create</Trans>,
tab: 'create',
@@ -131,15 +116,14 @@ export const getTabsToDisplay = ({
limits.capabilities.classrooms &&
limits.capabilities.classrooms.hidePremiumProducts
);
const displayTeachTab =
!shouldHideClassroomTab(limits) && !isNativeMobileApp();
const tabs: HomeTab[] = [
'get-started',
'create',
!shouldHideClassroomTab(limits) && !isNativeMobileApp()
? 'team-view'
: null,
displayShopTab ? 'shop' : null,
'learn',
'create',
displayPlayTab ? 'play' : null,
displayShopTab ? 'shop' : null,
displayTeachTab ? 'team-view' : null,
].filter(Boolean);
return tabs.map(tab => homePageMenuTabs[tab]);
};

View File

@@ -0,0 +1,60 @@
// @flow
import { getTabsToDisplay } from './HomePageMenu';
import { isNativeMobileApp } from '../../../Utils/Platform';
import { limitsForStudentUser } from '../../../fixtures/GDevelopServicesTestData';
jest.mock('../../../Utils/Platform');
const mockFn = (fn: Function): JestMockFn<any, any> => fn;
describe('HomePageMenu', () => {
describe('getTabsToDisplay', () => {
beforeEach(() => {
mockFn(isNativeMobileApp).mockReset();
});
test('Default desktop user', () => {
mockFn(isNativeMobileApp).mockReturnValue(false);
const tabs = getTabsToDisplay({ limits: null });
expect(tabs.map(tab => tab.tab)).toEqual([
'learn',
'create',
'play',
'shop',
'team-view',
]);
});
test('Desktop user student', () => {
mockFn(isNativeMobileApp).mockReturnValue(false);
const tabs = getTabsToDisplay({
limits: limitsForStudentUser,
});
expect(tabs.map(tab => tab.tab)).toEqual(['learn', 'create']);
});
test('Default mobile user', () => {
mockFn(isNativeMobileApp).mockReturnValue(true);
const tabs = getTabsToDisplay({ limits: null });
expect(tabs.map(tab => tab.tab)).toEqual([
'learn',
'create',
'play',
'shop',
]);
});
test('Mobile student user', () => {
mockFn(isNativeMobileApp).mockReturnValue(true);
const tabs = getTabsToDisplay({ limits: limitsForStudentUser });
expect(tabs.map(tab => tab.tab)).toEqual(['learn', 'create']);
});
});
});

View File

@@ -8,7 +8,6 @@ import {
type FileMetadata,
type StorageProvider,
} from '../../../ProjectsStorage';
import GetStartedSection from './GetStartedSection';
import LearnSection from './LearnSection';
import PlaySection from './PlaySection';
import CreateSection from './CreateSection';
@@ -27,13 +26,6 @@ import TeamProvider from '../../../Profile/Team/TeamProvider';
import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer';
import { type PrivateGameTemplateListingData } from '../../../Utils/GDevelopServices/Shop';
import { PrivateGameTemplateStoreContext } from '../../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
import PreferencesContext from '../../Preferences/PreferencesContext';
import useSubscriptionPlans from '../../../Utils/UseSubscriptionPlans';
import { incrementGetStartedSectionViewCount } from '../../../Utils/Analytics/LocalStats';
import {
sendUserSurveyHidden,
sendUserSurveyStarted,
} from '../../../Utils/Analytics/EventSender';
import RouterContext, { type RouteArguments } from '../../RouterContext';
import { type GameDetailsTab } from '../../../GameDashboard';
import { canUseClassroomFeature } from '../../../Utils/GDevelopServices/Usage';
@@ -45,6 +37,8 @@ import { type GamesList } from '../../../GameDashboard/UseGamesList';
import { type GamesPlatformFrameTools } from './PlaySection/UseGamesPlatformFrame';
import { type CourseChapter } from '../../../Utils/GDevelopServices/Asset';
import useCourses from './UseCourses';
import { getProgramOpeningCount } from '../../../Utils/Analytics/LocalStats';
import { isNativeMobileApp } from '../../../Utils/Platform';
const getRequestedTab = (routeArguments: RouteArguments): HomeTab | null => {
if (
@@ -66,8 +60,6 @@ const getRequestedTab = (routeArguments: RouteArguments): HomeTab | null => {
return 'play';
} else if (routeArguments['initial-dialog'] === 'learn') {
return 'learn';
} else if (routeArguments['initial-dialog'] === 'get-started') {
return 'get-started';
}
return null;
@@ -218,8 +210,6 @@ export const HomePage = React.memo<Props>(
startTimeoutToUnloadIframe,
loadIframeOrRemoveTimeout,
} = gamesPlatformFrameTools;
const userSurveyStartedRef = React.useRef<boolean>(false);
const userSurveyHiddenRef = React.useRef<boolean>(false);
const { fetchTutorials } = React.useContext(TutorialContext);
const { fetchExamplesAndFilters } = React.useContext(ExampleStoreContext);
const {
@@ -267,17 +257,17 @@ export const HomePage = React.memo<Props>(
] = React.useState<TutorialCategory | null>(null);
const { isMobile } = useResponsiveWindowSize();
const {
values: { showGetStartedSectionByDefault },
} = React.useContext(PreferencesContext);
const tabRequestedAtOpening = React.useRef<HomeTab | null>(
getRequestedTab(routeArguments)
);
const programOpeningCount = getProgramOpeningCount();
const initialTab = tabRequestedAtOpening.current
? tabRequestedAtOpening.current
: showGetStartedSectionByDefault
? 'get-started'
: 'create';
: isNativeMobileApp()
? 'play'
: programOpeningCount > 1
? 'create'
: 'learn';
const [activeTab, setActiveTab] = React.useState<HomeTab>(initialTab);
@@ -291,9 +281,6 @@ export const HomePage = React.memo<Props>(
: games.find(game => game.id === openedGameId),
[games, openedGameId]
);
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: false,
});
// Open the store and a pack or game template if asked to do so, either at
// app opening, either when the route changes (when clicking on an announcement
@@ -354,15 +341,6 @@ export const HomePage = React.memo<Props>(
]
);
React.useEffect(
() => {
if (initialTab === 'get-started') {
incrementGetStartedSectionViewCount();
}
},
[initialTab]
);
// Load everything when the user opens the home page, to avoid future loading times.
React.useEffect(
() => {
@@ -373,18 +351,6 @@ export const HomePage = React.memo<Props>(
[fetchExamplesAndFilters, fetchTutorials, fetchGameTemplates]
);
// Only fetch games if the user decides to open the games dashboard tab
// or the build tab to enable the context menu on project list items that
// redirects to the games dashboard.
React.useEffect(
() => {
if (activeTab === 'create' && !games) {
fetchGames();
}
},
[fetchGames, activeTab, games]
);
// Only fetch courses if the user decides to open the Learn section.
React.useEffect(
() => {
@@ -490,28 +456,6 @@ export const HomePage = React.memo<Props>(
onSceneObjectEdited,
}));
const onUserSurveyStarted = React.useCallback(() => {
if (userSurveyStartedRef.current) return;
sendUserSurveyStarted();
userSurveyStartedRef.current = true;
}, []);
const onUserSurveyHidden = React.useCallback(() => {
if (userSurveyHiddenRef.current) return;
sendUserSurveyHidden();
userSurveyHiddenRef.current = true;
}, []);
React.useEffect(
() => {
if (!authenticated) {
userSurveyStartedRef.current = false;
userSurveyHiddenRef.current = false;
}
},
// Reset flag that prevents multiple send of the same event on user change.
[authenticated]
);
// As the homepage is never unmounted, we need to ensure the games platform
// iframe is unloaded & loaded from here,
// allowing to handle when the user navigates to another tab.
@@ -581,19 +525,6 @@ export const HomePage = React.memo<Props>(
canSaveProject={canSave}
/>
)}
{activeTab === 'get-started' && (
<GetStartedSection
selectInAppTutorial={selectInAppTutorial}
onUserSurveyStarted={onUserSurveyStarted}
onUserSurveyHidden={onUserSurveyHidden}
subscriptionPlansWithPricingSystems={
subscriptionPlansWithPricingSystems
}
onOpenProfile={onOpenProfile}
onCreateProjectFromExample={onCreateProjectFromExample}
askToCloseProject={askToCloseProject}
/>
)}
{activeTab === 'learn' && (
<LearnSection
onTabChange={setActiveTab}

View File

@@ -216,7 +216,6 @@ export type PreferencesValues = {|
resourcesImporationBehavior: ResourceImportationBehavior,
eventsSheetCancelInlineParameter: 'cancel' | 'apply',
showCommunityExtensions: boolean,
showGetStartedSectionByDefault: boolean,
showInAppTutorialDeveloperMode: boolean,
showDeprecatedInstructionWarning: boolean,
openDiagnosticReportAutomatically: boolean,
@@ -302,7 +301,6 @@ export type Preferences = {|
setIsAlwaysOnTopInPreview: (enabled: boolean) => void,
setEventsSheetCancelInlineParameter: (value: string) => void,
setShowCommunityExtensions: (enabled: boolean) => void,
setShowGetStartedSectionByDefault: (enabled: boolean) => void,
setShowInAppTutorialDeveloperMode: (enabled: boolean) => void,
setOpenDiagnosticReportAutomatically: (enabled: boolean) => void,
getOpenDiagnosticReportAutomatically: () => boolean,
@@ -381,7 +379,6 @@ export const initialPreferences = {
resourcesImporationBehavior: 'ask',
eventsSheetCancelInlineParameter: 'apply',
showCommunityExtensions: false,
showGetStartedSectionByDefault: true,
showInAppTutorialDeveloperMode: false,
openDiagnosticReportAutomatically: true,
showDeprecatedInstructionWarning: false,
@@ -449,7 +446,6 @@ export const initialPreferences = {
setIsAlwaysOnTopInPreview: () => {},
setEventsSheetCancelInlineParameter: () => {},
setShowCommunityExtensions: () => {},
setShowGetStartedSectionByDefault: (enabled: boolean) => {},
setShowInAppTutorialDeveloperMode: (enabled: boolean) => {},
setShowDeprecatedInstructionWarning: (enabled: boolean) => {},
getOpenDiagnosticReportAutomatically: () => true,

View File

@@ -149,9 +149,6 @@ export default class PreferencesProvider extends React.Component<Props, State> {
this
),
setShowCommunityExtensions: this._setShowCommunityExtensions.bind(this),
setShowGetStartedSectionByDefault: this._setShowGetStartedSection.bind(
this
),
setShowInAppTutorialDeveloperMode: this._setShowInAppTutorialDeveloperMode.bind(
this
),
@@ -353,18 +350,6 @@ export default class PreferencesProvider extends React.Component<Props, State> {
);
}
_setShowGetStartedSection(showGetStartedSectionByDefault: boolean) {
this.setState(
state => ({
values: {
...state.values,
showGetStartedSectionByDefault,
},
}),
() => this._persistValuesToLocalStorage(this.state)
);
}
_setThemeName(themeName: string) {
this.setState(
state => ({

View File

@@ -12,8 +12,8 @@ export type Route =
| 'build' // Old way of opening the build section
| 'create' // New way of opening the build section
| 'education'
| 'play'
| 'get-started';
| 'learn'
| 'play';
type RouteKey =
| 'initial-dialog'
| 'game-id'

View File

@@ -1,7 +1,18 @@
// @flow
import * as React from 'react';
import RouterContext from './RouterContext';
import RouterContext, { type Route } from './RouterContext';
const homePageRoutes: Route[] = [
'play',
'learn',
'build',
'create',
'games-dashboard',
'asset-store',
'store',
'education',
];
/**
* This hook is used to be able to use route arguments from anywhere to open the homepage.
@@ -22,18 +33,7 @@ const useHomepageWitchForRouting = ({
const initialDialog = routeArguments['initial-dialog'];
if (!initialDialog) return;
if (
[
'games-dashboard',
'asset-store',
'store',
'build',
'education',
'play',
'community',
'get-started',
].includes(initialDialog)
) {
if (homePageRoutes.includes(initialDialog)) {
closeDialogs();
openHomePage();
}

View File

@@ -34,7 +34,6 @@ type ErrorBoundaryScope =
| 'app'
| 'editor'
| 'start-page'
| 'start-page-get-started'
| 'start-page-shop'
| 'start-page-learn'
| 'start-page-play'

View File

@@ -26,29 +26,3 @@ export const incrementProgramOpeningCount = () => {
console.warn('Unable to store program opening count', e);
}
};
export const getGetStartedSectionViewCount = (): number => {
try {
const count = localStorage.getItem(
`${localStorageLocalStatsPrefix}-get-started-section-view-count`
);
if (count !== null) return parseInt(count, 10);
} catch (e) {
console.warn('Unable to load stored get started section view count', e);
}
return 0;
};
export const incrementGetStartedSectionViewCount = () => {
const count = getGetStartedSectionViewCount() + 1;
try {
localStorage.setItem(
`${localStorageLocalStatsPrefix}-get-started-section-view-count`,
'' + count
);
} catch (e) {
console.warn('Unable to store get started section view count', e);
}
};

View File

@@ -1,104 +0,0 @@
// @flow
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import paperDecorator from '../../../PaperDecorator';
import { fakeAuthenticatedUserWithNoSubscription } from '../../../../fixtures/GDevelopServicesTestData';
import RecommendationList from '../../../../MainFrame/EditorContainers/HomePage/GetStartedSection/RecommendationList';
import PreferencesContext, {
initialPreferences,
} from '../../../../MainFrame/Preferences/PreferencesContext';
import inAppTutorialDecorator from '../../../InAppTutorialDecorator';
import { TutorialStateProvider } from '../../../../Tutorial/TutorialContext';
import useSubscriptionPlans, {
getAvailableSubscriptionPlansWithPrices,
} from '../../../../Utils/UseSubscriptionPlans';
import LoaderModal from '../../../../UI/LoaderModal';
export default {
title: 'HomePage/GetStartedSectionSection/RecommendationList',
component: RecommendationList,
decorators: [paperDecorator, inAppTutorialDecorator],
};
export const Default = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
<PreferencesContext.Provider value={initialPreferences}>
<TutorialStateProvider>
<RecommendationList
onOpenProfile={action('onOpenProfile')}
authenticatedUser={fakeAuthenticatedUserWithNoSubscription}
selectInAppTutorial={action('selectInAppTutorial')}
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
onStartSurvey={null}
hasFilledSurveyAlready={false}
askToCloseProject={async () => true}
onCreateProjectFromExample={action('onCreateProjectFromExample')}
/>
</TutorialStateProvider>
</PreferencesContext.Provider>
) : (
<LoaderModal show />
);
};
export const WithSurvey = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
<PreferencesContext.Provider value={initialPreferences}>
<TutorialStateProvider>
<RecommendationList
onOpenProfile={action('onOpenProfile')}
authenticatedUser={fakeAuthenticatedUserWithNoSubscription}
selectInAppTutorial={action('selectInAppTutorial')}
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
onStartSurvey={action('onStartSurvey')}
hasFilledSurveyAlready={false}
askToCloseProject={async () => true}
onCreateProjectFromExample={action('onCreateProjectFromExample')}
/>
</TutorialStateProvider>
</PreferencesContext.Provider>
) : (
<LoaderModal show />
);
};
export const WithSurveyAlreadyFilled = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
<PreferencesContext.Provider value={initialPreferences}>
<TutorialStateProvider>
<RecommendationList
onOpenProfile={action('onOpenProfile')}
authenticatedUser={fakeAuthenticatedUserWithNoSubscription}
selectInAppTutorial={action('selectInAppTutorial')}
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
onStartSurvey={action('onStartSurvey')}
hasFilledSurveyAlready={true}
askToCloseProject={async () => true}
onCreateProjectFromExample={action('onCreateProjectFromExample')}
/>
</TutorialStateProvider>
</PreferencesContext.Provider>
) : (
<LoaderModal show />
);
};