Merge pull request #8901 from scratchfoundation/onboarding-integration

Onboarding integration
This commit is contained in:
Christopher Willis-Ford 2024-11-08 10:50:42 -08:00 committed by GitHub
commit ce89e266d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1770 additions and 3203 deletions

View file

@ -68,8 +68,10 @@ jobs:
PROJECT_HOST: ${{ secrets.PROJECT_HOST }} PROJECT_HOST: ${{ secrets.PROJECT_HOST }}
STATIC_HOST: ${{ secrets.STATIC_HOST }} STATIC_HOST: ${{ secrets.STATIC_HOST }}
SCRATCH_ENV: ${{ vars.SCRATCH_ENV }} SCRATCH_ENV: ${{ vars.SCRATCH_ENV }}
SORTING_HAT_HOST: ${{ vars.SORTING_HAT_HOST }} ONBOARDING_TEST_ACTIVE: ${{ vars.ONBOARDING_TEST_ACTIVE }}
USER_GUIDING_ID: ${{ secrets.USER_GUIDING_ID }} ONBOARDING_TEST_PROJECT_IDS: ${{ vars.ONBOARDING_TEST_PROJECT_IDS }}
ONBOARDING_TESTING_STARTING_DATE: ${{ vars.ONBOARDING_TESTING_STARTING_DATE }}
ONBOARDING_TESTING_ENDING_DATE: ${{ vars.ONBOARDING_TESTING_ENDING_DATE }}
# used by src/template-config.js # used by src/template-config.js
GTM_ID: ${{ secrets.GTM_ID }} GTM_ID: ${{ secrets.GTM_ID }}

3788
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -46,6 +46,7 @@
"dependencies": { "dependencies": {
"bunyan": "1.8.15", "bunyan": "1.8.15",
"clipboard-copy": "2.0.1", "clipboard-copy": "2.0.1",
"driver.js": "^1.3.1",
"express": "4.21.1", "express": "4.21.1",
"express-http-proxy": "1.6.3", "express-http-proxy": "1.6.3",
"lodash.defaults": "4.2.0", "lodash.defaults": "4.2.0",
@ -127,7 +128,7 @@
"postcss-loader": "4.3.0", "postcss-loader": "4.3.0",
"postcss-simple-vars": "5.0.2", "postcss-simple-vars": "5.0.2",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "5.1.1", "query-string": "9.1.0",
"react": "16.14.0", "react": "16.14.0",
"react-dom": "16.14.0", "react-dom": "16.14.0",
"react-intl": "5.25.1", "react-intl": "5.25.1",

View file

@ -0,0 +1,69 @@
const React = require('react');
const {useState, useEffect, isValidElement} = require('react');
const {createPortal} = require('react-dom');
const PropTypes = require('prop-types');
require('driver.js/dist/driver.css');
const DriverJourney = ({configProps, driverObj}) => {
const [renderState, setRenderState] = useState();
useEffect(() => {
const {steps, ...restConfig} = configProps;
const driverSteps = steps.map((step, index) => {
const {sectionComponents = {}, callback, ...popoverProps} = step.popover;
return {
...step,
popover: {
...popoverProps,
onPopoverRender: popover => {
if (callback) {
callback();
}
const portalData = [];
for (const [section, component] of Object.entries(
sectionComponents
)) {
if (isValidElement(component)) {
popover[section].style.display = 'block';
popover[section].innerHTML = '';
portalData.push({
parentElement: popover[section],
childElement: component
});
}
}
setRenderState({components: portalData, stepIndex: index});
}
}
};
});
driverObj.setConfig({...restConfig, steps: driverSteps});
driverObj.drive();
}, [driverObj, configProps]);
if (!renderState) return null;
if (!configProps.steps[renderState.stepIndex]) return null;
return (
<>
{renderState.components.map(obj =>
createPortal(obj.childElement, obj.parentElement)
)}
</>
);
};
DriverJourney.propTypes = {
configProps: PropTypes.shape({
steps: PropTypes.arrayOf(PropTypes.object)
}),
driverObj: PropTypes.shape({
setConfig: PropTypes.func,
drive: PropTypes.func
})
};
module.exports = DriverJourney;

View file

@ -0,0 +1,358 @@
const React = require('react');
const {driver} = require('driver.js');
const FlexRow = require('../../flex-row/flex-row.jsx');
const Button = require('../../forms/button.jsx');
const DriverJourney = require('../driver-journey/driver-journey.jsx');
const {defineMessages, useIntl} = require('react-intl');
const {useMemo, useState, useCallback} = require('react');
const PropTypes = require('prop-types');
const {triggerAnalyticsEvent} = require('../../../lib/onboarding.js');
require('./editor-journey.scss');
const messages = defineMessages({
createStepTitle: {
id: 'gui.journey.controls.create',
defaultMessage: 'Create',
description: 'Create step title'
},
projectGenreStepTitle: {
id: 'gui.journey.controls.choose.projectGenre',
defaultMessage: 'What do you whant to create?',
description: 'Choose project genre step title'
},
typeStepTitle: {
id: 'gui.journey.controls.choose.type',
defaultMessage: 'Which type?',
description: 'Choose project type step title'
},
startStepTitle: {
id: 'gui.journey.controls.choose.start',
defaultMessage: 'How do you want to start?',
description: 'Choose way to start step title'
},
gameButtonText: {
id: 'gui.journey.controls.game',
defaultMessage: 'Game',
description: 'Game button text'
},
animiationButtonText: {
id: 'gui.journey.controls.animation',
defaultMessage: 'Animation',
description: 'Animation button text'
},
musicButtonText: {
id: 'gui.journey.controls.music',
defaultMessage: 'Music',
description: 'Music button text'
},
clickerGameButtonText: {
id: 'gui.journey.controls.game.clicker',
defaultMessage: 'Clicker Game',
description: 'Clicker game button text'
},
pongGameButtonText: {
id: 'gui.journey.controls.game.pong',
defaultMessage: 'Pong Game',
description: 'Pong game button text'
},
characterAnimationButtonText: {
id: 'gui.journey.controls.animation.character',
defaultMessage: 'Animate a character',
description: 'Animate a character button text'
},
flyAnimationButtonText: {
id: 'gui.journey.controls.animation.fly',
defaultMessage: 'Make it fly',
description: 'Make it fly animation button text'
},
recordSoundButtonText: {
id: 'gui.journey.controls.music.record',
defaultMessage: 'Record a sound',
description: 'Record a sound button text'
},
makeMusicButtonText: {
id: 'gui.journey.controls.music.make',
defaultMessage: 'Make music',
description: 'Make music button text'
},
tutorialButtonText: {
id: 'gui.journey.controls.tutorial',
defaultMessage: 'Tutorial',
description: 'Tutorial button text'
},
starterProjectButtonText: {
id: 'gui.journey.controls.starterProject',
defaultMessage: 'Starter project',
description: 'Starter project button text'
},
onMyOwnButtonText: {
id: 'gui.journey.controls.onMyOwn',
defaultMessage: 'On my own',
description: 'On my own button text'
}
});
const STEP_NAMES = [
'pick-genre-step',
'game-step',
'animation-step',
'music-step',
'clicker-game-step',
'pong-game-step',
'animate-character-step',
'make-fly-animation-step',
'record-sound-step',
'make-music-step'
];
const projectIds = JSON.parse(process.env.ONBOARDING_TEST_PROJECT_IDS);
const tutorialIds = {
clicker: 'Make-A-Game',
pong: 'pong',
animateCharacter: 'Animate-A-Character',
makeItFly: 'make-it-fly',
recordSound: 'record-a-sound',
makeMusic: 'Make-Music'
};
const EditorJourneyDescription = ({title, descriptionData}) => (
<>
<div className="title">{title}</div>
<FlexRow className="description-wrapper">
{
descriptionData.map((prop, index) => (
<FlexRow
key={index}
className="journey-option"
>
<img src={prop.imgSrc} />
<Button
className={'large'}
onClick={prop.handleOnClick}
>{prop.text}</Button>
</FlexRow>
))
}
</FlexRow>
</>
);
const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJourney}) => {
const [driverObj] = useState(() => (
driver()
));
const intl = useIntl();
const pickStep = useCallback((stepNumber, editorJourneyStep) => {
triggerAnalyticsEvent({
event: 'editor-journey-step',
editorJourneyStep: editorJourneyStep
});
driverObj.moveTo(stepNumber);
}, driverObj);
const createStep = useCallback((projectId, tutorialId) => ({
title: intl.formatMessage(messages.createStepTitle),
showButtons: ['close'],
sectionComponents: {
description: <EditorJourneyDescription
title={intl.formatMessage(messages.startStepTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.svg',
text: intl.formatMessage(messages.tutorialButtonText),
handleOnClick: () => {
triggerAnalyticsEvent({
event: 'editor-journey-step',
editorJourneyStep: `${tutorialId}-Open-Tutorial`
});
onActivateDeck(tutorialId);
setShowJourney(false);
driverObj.destroy();
}
},
{
imgSrc: '/images/onboarding-journeys/Starter-Projects-Icon.svg',
text: intl.formatMessage(messages.starterProjectButtonText),
handleOnClick: () => {
location.href = `/projects/${projectId}?showJourney=true`;
setShowJourney(false);
driverObj.destroy();
}
},
{
imgSrc: '/images/onboarding-journeys/On-Own-Icon.svg',
text: intl.formatMessage(messages.onMyOwnButtonText),
handleOnClick: () => {
triggerAnalyticsEvent({
event: 'editor-journey-step',
editorJourneyStep: `${tutorialId}-On-My-Own`
});
setCanViewTutorialsHighlight(true);
setShowJourney(false);
driverObj.destroy();
}
}
]}
/>
}
}), [onActivateDeck, setCanViewTutorialsHighlight, setShowJourney, driverObj, intl]);
const configProps = useMemo(
() => ({
popoverClass: 'gui-journey',
overlayOpacity: 0,
onDestroyStarted: () => {
const stepName = STEP_NAMES[driverObj.getActiveIndex()] || '';
triggerAnalyticsEvent({
event: 'editor-journey-step',
editorJourneyStep: `${stepName}-closed`
});
driverObj.destroy();
},
onDestroyed: () => {
setShowJourney(false);
},
steps: [{
popover: {
title: intl.formatMessage(messages.createStepTitle),
showButtons: ['close'],
sectionComponents: {
description: <EditorJourneyDescription
title={intl.formatMessage(messages.projectGenreStepTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Games-Icon.svg',
text: intl.formatMessage(messages.gameButtonText),
handleOnClick: () => pickStep(1, 'Games')
},
{
imgSrc: '/images/onboarding-journeys/Animation-Icon.svg',
text: intl.formatMessage(messages.animiationButtonText),
handleOnClick: () => pickStep(2, 'Animation')
},
{
imgSrc: '/images/onboarding-journeys/Music-Icon.svg',
text: intl.formatMessage(messages.musicButtonText),
handleOnClick: () => pickStep(3, 'Music')
}
]}
/>
}
}
},
{
popover: {
title: intl.formatMessage(messages.createStepTitle),
showButtons: ['close'],
sectionComponents: {
description: <EditorJourneyDescription
title={intl.formatMessage(messages.typeStepTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Clicker-Game.jpg',
text: intl.formatMessage(messages.clickerGameButtonText),
handleOnClick: () => pickStep(4, 'Clicker-Game')
},
{
imgSrc: '/images/onboarding-journeys/Pong-Game.jpg',
text: intl.formatMessage(messages.pongGameButtonText),
handleOnClick: () => pickStep(5, 'Pong-Game')
}
]}
/>
}
}
},
{
popover: {
title: intl.formatMessage(messages.createStepTitle),
showButtons: ['close'],
sectionComponents: {
description: <EditorJourneyDescription
title={intl.formatMessage(messages.typeStepTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Character-Animation.jpg',
text: intl.formatMessage(messages.characterAnimationButtonText),
handleOnClick: () => pickStep(6, 'Character-Animation')
},
{
imgSrc: '/images/onboarding-journeys/Fly-Animation.jpg',
text: intl.formatMessage(messages.flyAnimationButtonText),
handleOnClick: () => pickStep(7, 'Fly-Animation')
}
]}
/>
}
}
},
{
popover: {
title: intl.formatMessage(messages.createStepTitle),
showButtons: ['close'],
sectionComponents: {
description: <EditorJourneyDescription
title={intl.formatMessage(messages.typeStepTitle)}
descriptionData={[
{
imgSrc: '/images/onboarding-journeys/Record-Music.jpg',
text: intl.formatMessage(messages.recordSoundButtonText),
handleOnClick: () => pickStep(8, 'Record-Music')
},
{
imgSrc: '/images/onboarding-journeys/Make-Music.jpg',
text: intl.formatMessage(messages.makeMusicButtonText),
handleOnClick: () => pickStep(9, 'Make-Music')
}
]}
/>
}
}
},
{
popover: createStep(projectIds.clicker, tutorialIds.clicker)
},
{
popover: createStep(projectIds.pong, tutorialIds.pong)
},
{
popover: createStep(projectIds.animateCharacter, tutorialIds.animateCharacter)
},
{
popover: createStep(projectIds.makeItFly, tutorialIds.makeItFly)
},
{
popover: createStep(projectIds.recordSound, tutorialIds.recordSound)
},
{
popover: createStep(projectIds.makeMusic, tutorialIds.makeMusic)
}]}), [driverObj, intl, createStep, setShowJourney]
);
return (
<DriverJourney
configProps={configProps}
driverObj={driverObj}
/>
);
};
EditorJourneyDescription.propTypes = {
title: PropTypes.string,
descriptionData: PropTypes.arrayOf(PropTypes.shape({
imgSrc: PropTypes.string,
text: PropTypes.string,
handleOnClick: PropTypes.func
}))
};
EditorJourney.propTypes = {
onActivateDeck: PropTypes.func,
setCanViewTutorialsHighlight: PropTypes.func,
setShowJourney: PropTypes.func
};
module.exports = EditorJourney;

View file

@ -0,0 +1,65 @@
@import "../../../colors";
@import "../../../frameless";
.driver-popover.gui-journey {
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
max-width: unset;
padding: 0;
border-radius: 15px;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%);
.driver-popover-close-btn {
height: 2.5rem;
width: 2.5rem;
border-radius: 50%;
margin: 0.5rem;
font-size: 2rem;
font-weight: bold;
color: $type-white;
background-color: $ui-aqua-dark;
}
.driver-popover-title {
padding: 1rem 0;
font-size: 1rem;
font-weight: 700;
text-align: center;
color: $type-white;
margin: 0;
background-color: $ui-aqua;
border-radius: 15px 15px 0 0;
}
.driver-popover-title[style*=block]+.driver-popover-description {
margin: 0;
}
}
.title {
padding: 1rem 0;
font-size: 1.125rem;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
font-weight: 400;
text-align: center;
color: $type-gray;
background-color: $ui-light-primary;
}
.description-wrapper {
flex-direction: row;
justify-content: space-evenly;
gap: 3rem;
margin: 3rem 4rem;
.journey-option {
flex-direction: column;
justify-content: center;
gap: 1rem;
img {
height: $cols2;
}
}
}

View file

@ -0,0 +1,83 @@
const React = require('react');
const {driver} = require('driver.js');
const DriverJourney = require('../driver-journey/driver-journey.jsx');
const {defineMessages, useIntl} = require('react-intl');
const {useState} = require('react');
const PropTypes = require('prop-types');
require('./project-journey.scss');
const messages = defineMessages({
playProject: {
id: 'project.journey.play',
defaultMessage: 'Click the green flag to see what this project does.',
description: 'Play project'
},
remixProject: {
id: 'project.journey.remix',
defaultMessage: 'Make your own version!',
description: 'Remix project'
}
});
const ProjectJourney = ({setCanViewProjectJourney, setShouldStopProject}) => {
const [driverObj] = useState(() => (
driver()
));
const intl = useIntl();
const steps = [{
element: 'div[class^="stage_green-flag-overlay-wrapper"] > div',
popover: {
callback: () => {
const greenFlagButton = document.querySelector('div[class^="stage_green-flag-overlay-wrapper"] > div');
greenFlagButton.addEventListener('click', () => {
setCanViewProjectJourney(false);
driverObj.destroy();
setTimeout(() => {
setShouldStopProject(true);
driverObj.drive(1);
}, 8000);
});
},
description: intl.formatMessage(messages.playProject)
}
},
{
element: '.remix-button',
popover: {
callback: () => {
const remixButton = document.querySelector('.remix-button');
remixButton.addEventListener('click', () => {
setCanViewProjectJourney(false);
driverObj.destroy();
});
},
description: intl.formatMessage(messages.remixProject)
}
}];
return (
<DriverJourney
configProps={{
popoverClass: 'project-journey',
showButtons: [
'close'
],
onDestroyed: () => {
setCanViewProjectJourney(false);
},
showProgress: false,
steps: steps
}}
driverObj={driverObj}
/>
);
};
ProjectJourney.propTypes = {
setCanViewProjectJourney: PropTypes.func,
setShouldStopProject: PropTypes.func
};
module.exports = ProjectJourney;

View file

@ -0,0 +1,41 @@
@import "../../../colors";
.driver-popover.project-journey {
display: flex;
flex-direction: column;
background-color: $ui-purple-dark;
.driver-popover-close-btn {
height: 2.5rem;
width: 2.5rem;
border-radius: 50%;
margin: 0.5rem;
font-size: 2rem;
font-weight: bold;
color: $type-white;
background-color: $ui-purple-dark;
}
.driver-popover-arrow-side-left.driver-popover-arrow {
border-left-color: $ui-purple-dark;;
}
.driver-popover-arrow-side-right.driver-popover-arrow {
border-right-color: $ui-purple-dark;;
}
.driver-popover-arrow-side-top.driver-popover-arrow {
border-top-color: $ui-purple-dark;;
}
.driver-popover-arrow-side-bottom.driver-popover-arrow {
border-bottom-color: $ui-purple-dark;;
}
.driver-popover-description {
color: $ui-white;
font-size: 1rem;
font-weight: 700;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
}
}

View file

@ -0,0 +1,60 @@
const React = require('react');
const {driver} = require('driver.js');
const DriverJourney = require('../driver-journey/driver-journey.jsx');
const {defineMessages, useIntl} = require('react-intl');
const PropTypes = require('prop-types');
const {useState} = require('react');
require('./tutorials-highlight.scss');
const messages = defineMessages({
tutorialsHighlight: {
id: 'gui.highlight.tutorials',
defaultMessage: 'Click here for tutorials',
description: 'Tutorials highlight'
}
});
const TutorialsHighlight = ({setCanViewTutorialsHighlight}) => {
const [driverObj] = useState(() => (
driver()
));
const intl = useIntl();
const steps = [{
element: '.tutorials-button',
popover: {
showButtons: ['close'],
callback: () => {
const tutorialsButton = document.querySelector('.tutorials-button');
tutorialsButton.addEventListener('click', () => {
setCanViewTutorialsHighlight(false);
driverObj.destroy();
});
},
side: 'bottom',
description: intl.formatMessage(messages.tutorialsHighlight)
}
}];
return (
<DriverJourney
configProps={{
popoverClass: 'tutorials-highlight',
showProgress: false,
overlayOpacity: 0,
onDestroyed: () => {
setCanViewTutorialsHighlight(false);
},
steps: steps
}}
driverObj={driverObj}
/>
);
};
TutorialsHighlight.propTypes = {
setCanViewTutorialsHighlight: PropTypes.func
};
module.exports = TutorialsHighlight;

View file

@ -0,0 +1,41 @@
@import "../../../colors";
.driver-popover.tutorials-highlight {
display: flex;
flex-direction: column;
background-color: $ui-purple-dark;
.driver-popover-close-btn {
height: 2.5rem;
width: 2.5rem;
border-radius: 50%;
margin: 0.5rem;
font-size: 2rem;
font-weight: bold;
color: $type-white;
background-color: $ui-purple-dark;
}
.driver-popover-arrow-side-left.driver-popover-arrow {
border-left-color: $ui-purple-dark;;
}
.driver-popover-arrow-side-right.driver-popover-arrow {
border-right-color: $ui-purple-dark;;
}
.driver-popover-arrow-side-top.driver-popover-arrow {
border-top-color: $ui-purple-dark;;
}
.driver-popover-arrow-side-bottom.driver-popover-arrow {
border-bottom-color: $ui-purple-dark;;
}
.driver-popover-description {
color: $ui-white;
font-size: 1rem;
font-weight: 700;
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
}
}

92
src/lib/onboarding.js Normal file
View file

@ -0,0 +1,92 @@
const ONBOARDING_TESTING_GROUP_A_NAME = 'Displayed Onboarding Journeys';
const ONBOARDING_TESTING_GROUP_B_NAME = 'Not Displayed Onboarding Journeys';
const isBanned = user => user.banned;
const isAdmin = permissions => permissions.admin;
const isMuted = permissions => !!Object.keys(permissions.mute_status).length;
const isDateInOnboardingRange = date => {
const dateToCompare = Date.parse(date);
const startDate = Date.parse(process.env.ONBOARDING_TESTING_STARTING_DATE);
const endDate = Date.parse(process.env.ONBOARDING_TESTING_ENDING_DATE);
return dateToCompare >= startDate && dateToCompare <= endDate;
};
const isRegisteredInRange = user => {
const dateOfJoin = user.dateJoined.split('T')[0];
return isDateInOnboardingRange(dateOfJoin);
};
const isCurrentDayInRange = () => {
const currentDate = new Date().toJSON()
.split('T')[0];
return isDateInOnboardingRange(currentDate);
};
const isUserEligible = (user, permissions) =>
Object.keys(user).length !== 0 &&
Object.keys(permissions).length !== 0 &&
JSON.parse(process.env.ONBOARDING_TEST_ACTIVE) &&
isRegisteredInRange(user) &&
isCurrentDayInRange() &&
!isAdmin(permissions) &&
!isMuted(permissions) &&
!isBanned(user);
const calculateAgeGroup = (birthYear, birthMonth) => {
const today = new Date();
let age = today.getFullYear() - parseInt(birthYear, 10);
const monthDiff = today.getMonth() + 1 - parseInt(birthMonth, 10);
if (monthDiff < 0) {
age--;
}
if (age <= 10) {
return '[00-10]';
} else if (age <= 16) {
return '[11-16]';
}
return '[17+]';
};
const onboardingTestGroup = user =>
(user.id % 2 === 0 ?
ONBOARDING_TESTING_GROUP_A_NAME :
ONBOARDING_TESTING_GROUP_B_NAME);
export const shouldDisplayOnboarding = (user, permissions) =>
user.id % 2 === 0 && isUserEligible(user, permissions);
export const triggerAnalyticsEvent = eventVaribles => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
...eventVaribles
});
};
export const sendUserProperties = (user, permissions) => {
if (!isUserEligible(user, permissions)) {
window.dataLayer.push({
testGroup: null,
ageGroup: null,
gender: null
});
return;
}
window.dataLayer = window.dataLayer || [];
const {gender, birthYear, birthMonth} = user;
window.dataLayer.push({
testGroup: onboardingTestGroup(user),
ageGroup: calculateAgeGroup(birthYear, birthMonth),
gender: gender
});
};

View file

@ -1,124 +0,0 @@
const api = require('./api');
const sample = require('lodash.sample');
const USER_GUIDING_ID = process.env.USER_GUIDING_ID;
const AUTONOMY_SURVEY_ID = 3677;
const RELATIONSHIP_SURVEY_ID = 3678;
const JOY_SURVEY_ID = 3679;
const COMPETENCE_SURVEY_ID = 3676;
const EDITOR_INTERACTION_SURVEY_IDS = [COMPETENCE_SURVEY_ID, JOY_SURVEY_ID];
const CONDITIONS = {condition_list: [
'IsLoggedIn',
'NotAdmin',
'NotMuted'
]};
const USER_GUIDING_SNIPPET = `
(function(g, u, i, d, e, s) {
g[e] = g[e] || [];
var f = u.getElementsByTagName(i)[0];
var k = u.createElement(i);
k.async = true;
k.src = 'https://static.userguiding.com/media/user-guiding-' + s + '-embedded.js';
f.parentNode.insertBefore(k, f);
if (g[d]) return;
var ug = g[d] = {
q: []
};
ug.c = function(n) {
return function() {
ug.q.push([n, arguments])
};
};
var m = ['previewGuide', 'finishPreview', 'track', 'identify', 'hideChecklist', 'launchChecklist'];
for (var j = 0; j < m.length; j += 1) {
ug[m[j]] = ug.c(m[j]);
}
})(window, document, 'script', 'userGuiding', 'userGuidingLayer', '${USER_GUIDING_ID}');
`;
const activateUserGuiding = (userId, callback) => {
if (window.userGuiding) {
callback();
return;
}
const userGuidingScript = document.createElement('script');
userGuidingScript.innerHTML = USER_GUIDING_SNIPPET;
document.head.insertBefore(userGuidingScript, document.head.firstChild);
window.userGuidingSettings = {disablePageViewAutoCapture: true};
window.userGuidingLayer.push({
event: 'onload',
fn: () => window.userGuiding.identify(userId.toString())
});
window.userGuidingLayer.push({
event: 'onIdentificationComplete',
fn: callback
});
};
const attemptDisplayUserGuidingSurvey = (userId, permissions, guideId, callback) => {
if (!USER_GUIDING_ID || !process.env.SORTING_HAT_HOST) {
return;
}
api({
uri: '/user_guiding',
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-USERID': userId,
'X-PERMISSIONS': JSON.stringify(permissions),
'X-CONDITIONS': JSON.stringify(CONDITIONS),
'X-QUESTION-NUMBER': guideId
},
host: process.env.SORTING_HAT_HOST
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
return;
}
if (body?.result === 'true') {
activateUserGuiding(userId, callback);
}
});
};
const onCommented = (userId, permissions) => {
attemptDisplayUserGuidingSurvey(
userId,
permissions,
AUTONOMY_SURVEY_ID,
() => window.userGuiding.launchSurvey(AUTONOMY_SURVEY_ID)
);
};
const onProjectShared = (userId, permissions) => {
attemptDisplayUserGuidingSurvey(
userId,
permissions,
RELATIONSHIP_SURVEY_ID,
() => window.userGuiding.launchSurvey(RELATIONSHIP_SURVEY_ID)
);
};
const onProjectLoaded = (userId, permissions) => {
const surveyId = sample(EDITOR_INTERACTION_SURVEY_IDS);
attemptDisplayUserGuidingSurvey(
userId,
permissions,
surveyId,
() => window.userGuiding.launchSurvey(surveyId)
);
};
module.exports = {
onProjectLoaded,
onCommented,
onProjectShared
};

View file

@ -34,13 +34,15 @@ const thumbnailUrl = require('../../lib/user-thumbnail');
const FormsyProjectUpdater = require('./formsy-project-updater.jsx'); const FormsyProjectUpdater = require('./formsy-project-updater.jsx');
const EmailConfirmationModal = require('../../components/modal/email-confirmation/modal.jsx'); const EmailConfirmationModal = require('../../components/modal/email-confirmation/modal.jsx');
const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx'); const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx');
const {onCommented} = require('../../lib/user-guiding.js'); const queryString = require('query-string').default;
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
require('./preview.scss'); require('./preview.scss');
const frameless = require('../../lib/frameless'); const frameless = require('../../lib/frameless');
const {useState, useCallback} = require('react'); const {useState, useEffect} = require('react');
const ProjectJourney = require('../../components/journeys/project-journey/project-journey.jsx');
const {triggerAnalyticsEvent, shouldDisplayOnboarding} = require('../../lib/onboarding.js');
// disable enter key submission on formsy input fields; otherwise formsy thinks // disable enter key submission on formsy input fields; otherwise formsy thinks
// we meant to trigger the "See inside" button. Instead, treat these keypresses // we meant to trigger the "See inside" button. Instead, treat these keypresses
@ -148,7 +150,15 @@ const PreviewPresentation = ({
userUsesParentEmail, userUsesParentEmail,
visibilityInfo visibilityInfo
}) => { }) => {
const [hasSubmittedComment, setHasSubmittedComment] = useState(false); const [canViewProjectJourney, setCanViewProjectJourney] = useState(false);
const [shouldStopProject, setShouldStopProject] = useState(false);
useEffect(() => {
setCanViewProjectJourney(
queryString.parse(location.search, {parseBooleans: true}).showJourney &&
!userOwnsProject &&
shouldDisplayOnboarding(user, permissions)
);
}, [userOwnsProject, user, permissions]);
const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : ''; const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : '';
const revisedDate = ((projectInfo.history && projectInfo.history.modified)) ? projectInfo.history.modified : ''; const revisedDate = ((projectInfo.history && projectInfo.history.modified)) ? projectInfo.history.modified : '';
const showInstructions = editable || projectInfo.instructions || const showInstructions = editable || projectInfo.instructions ||
@ -222,13 +232,14 @@ const PreviewPresentation = ({
</FlexRow> </FlexRow>
); );
const onAddCommentWrapper = useCallback(body => { useEffect(() => {
onAddComment(body); if (canViewProjectJourney && projectInfo.title) {
if (!hasSubmittedComment && user) { triggerAnalyticsEvent({
setHasSubmittedComment(true); event: 'editor-journey-step',
onCommented(user.id, permissions); editorJourneyStep: `${projectInfo.title}-Starter-Project`
});
} }
}, [hasSubmittedComment, user]); }, [canViewProjectJourney, projectInfo.title]);
return ( return (
<div className="preview"> <div className="preview">
@ -256,6 +267,14 @@ const PreviewPresentation = ({
)} )}
{ projectInfo && projectInfo.author && projectInfo.author.id && ( { projectInfo && projectInfo.author && projectInfo.author.id && (
<React.Fragment> <React.Fragment>
{
isProjectLoaded &&
canViewProjectJourney &&
<ProjectJourney
setCanViewProjectJourney={setCanViewProjectJourney}
setShouldStopProject={setShouldStopProject}
/>
}
{showEmailConfirmationBanner && <EmailConfirmationBanner {showEmailConfirmationBanner && <EmailConfirmationBanner
userUsesParentEmail={userUsesParentEmail} userUsesParentEmail={userUsesParentEmail}
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
@ -392,6 +411,7 @@ const PreviewPresentation = ({
onUpdateProjectData={onUpdateProjectData} onUpdateProjectData={onUpdateProjectData}
onUpdateProjectId={onUpdateProjectId} onUpdateProjectId={onUpdateProjectId}
onUpdateProjectThumbnail={onUpdateProjectThumbnail} onUpdateProjectThumbnail={onUpdateProjectThumbnail}
shouldStopProject={shouldStopProject}
/> />
</div> </div>
<MediaQuery maxWidth={frameless.tabletPortrait - 1}> <MediaQuery maxWidth={frameless.tabletPortrait - 1}>
@ -626,7 +646,7 @@ const PreviewPresentation = ({
isLoggedIn ? ( isLoggedIn ? (
isShared && <ComposeComment isShared && <ComposeComment
postURI={`/proxy/comments/project/${projectId}`} postURI={`/proxy/comments/project/${projectId}`}
onAddComment={onAddCommentWrapper} onAddComment={onAddComment}
/> />
) : ( ) : (
/* TODO add box for signing in to leave a comment */ /* TODO add box for signing in to leave a comment */

View file

@ -8,7 +8,7 @@ const PropTypes = require('prop-types');
const connect = require('react-redux').connect; const connect = require('react-redux').connect;
const injectIntl = require('react-intl').injectIntl; const injectIntl = require('react-intl').injectIntl;
const parser = require('scratch-parser'); const parser = require('scratch-parser');
const queryString = require('query-string'); const queryString = require('query-string').default;
const api = require('../../lib/api'); const api = require('../../lib/api');
const Page = require('../../components/page/www/page.jsx'); const Page = require('../../components/page/www/page.jsx');
@ -25,10 +25,6 @@ const ConnectedLogin = require('../../components/login/connected-login.jsx');
const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx'); const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx');
const NotAvailable = require('../../components/not-available/not-available.jsx'); const NotAvailable = require('../../components/not-available/not-available.jsx');
const Meta = require('./meta.jsx'); const Meta = require('./meta.jsx');
const {
onProjectShared,
onProjectLoaded
} = require('../../lib/user-guiding.js');
const sessionActions = require('../../redux/session.js'); const sessionActions = require('../../redux/session.js');
const {selectProjectCommentsGloballyEnabled, selectIsTotallyNormal} = require('../../redux/session'); const {selectProjectCommentsGloballyEnabled, selectIsTotallyNormal} = require('../../redux/session');
@ -44,16 +40,48 @@ const IntlGUI = injectIntl(GUI.default);
const localStorageAvailable = 'localStorage' in window && window.localStorage !== null; const localStorageAvailable = 'localStorage' in window && window.localStorage !== null;
const xhr = require('xhr'); const xhr = require('xhr');
const {useEffect} = require('react'); const {useEffect, useState} = require('react');
const EditorJourney = require('../../components/journeys/editor-journey/editor-journey.jsx');
const {usePrevious} = require('react-use');
const TutorialsHighlight = require('../../components/journeys/tutorials-highlight/tutorials-highlight.jsx');
const {triggerAnalyticsEvent, sendUserProperties, shouldDisplayOnboarding} = require('../../lib/onboarding.js');
const IntlGUIWithProjectHandler = ({...props}) => {
const [showJourney, setShowJourney] = useState(false);
const [canViewTutorialsHighlight, setCanViewTutorialsHighlight] = useState(false);
const prevProjectId = usePrevious(props.projectId);
const IntlGUIWithProjectHandler = ({user, permissions, ...props}) => {
useEffect(() => { useEffect(() => {
if (props.projectId && props.projectId !== '0') { const isTutorialOpen = !!queryString.parse(location.search).tutorial;
onProjectLoaded(user.id, permissions);
}
}, [props.projectId, user.id, permissions]);
return <IntlGUI {...props} />; if (
(prevProjectId === 0 || prevProjectId === '0') &&
props.projectId &&
props.projectId !== '0' &&
!isTutorialOpen &&
shouldDisplayOnboarding(props.user, props.permissions)
) {
setShowJourney(true);
}
}, [props.projectId, prevProjectId, props.user, props.permissions]);
return (
<>
<IntlGUI {...props} />
{showJourney && (
<EditorJourney
onActivateDeck={props.onActivateDeck}
setCanViewTutorialsHighlight={setCanViewTutorialsHighlight}
setShowJourney={setShowJourney}
/>
)}
{canViewTutorialsHighlight && (
<TutorialsHighlight
setCanViewTutorialsHighlight={setCanViewTutorialsHighlight}
/>
)}
</>
);
}; };
IntlGUIWithProjectHandler.propTypes = { IntlGUIWithProjectHandler.propTypes = {
@ -226,6 +254,10 @@ class Preview extends React.Component {
false // Do not show cloud/username alerts again false // Do not show cloud/username alerts again
); );
} }
if (!prevProps.user.id && this.props.user.id && this.props.permissions) {
sendUserProperties(this.props.user, this.props.permissions);
}
} }
componentWillUnmount () { componentWillUnmount () {
this.removeEventListeners(); this.removeEventListeners();
@ -511,6 +543,15 @@ class Preview extends React.Component {
if (!this.state.greenFlagRecorded) { if (!this.state.greenFlagRecorded) {
this.props.logProjectView(this.props.projectInfo.id, this.props.authorUsername, this.props.user.token); this.props.logProjectView(this.props.projectInfo.id, this.props.authorUsername, this.props.user.token);
} }
const showJourney = queryString.parse(location.search, {parseBooleans: true}).showJourney;
if (showJourney && shouldDisplayOnboarding(this.props.user, this.props.permissions)) {
triggerAnalyticsEvent({
event: 'tutorial-played',
playedProject: this.props.projectInfo.title
});
}
this.setState({ this.setState({
showUsernameBlockAlert: false, showUsernameBlockAlert: false,
showCloudDataAlert: false, showCloudDataAlert: false,
@ -622,6 +663,14 @@ class Preview extends React.Component {
} }
} }
handleRemix () { handleRemix () {
const showJourney = queryString.parse(location.search, {parseBooleans: true}).showJourney;
if (showJourney && shouldDisplayOnboarding(this.props.user, this.props.permissions)) {
triggerAnalyticsEvent({
event: 'tutorial-remixed',
remixedProject: this.props.projectInfo.title
});
}
// Update the state first before starting the remix to show spinner // Update the state first before starting the remix to show spinner
this.setState({isRemixing: true}, () => { this.setState({isRemixing: true}, () => {
this.props.remixProject(); this.props.remixProject();
@ -650,7 +699,6 @@ class Preview extends React.Component {
justRemixed: false, justRemixed: false,
justShared: true justShared: true
}); });
onProjectShared(this.props.user.id, this.props.permissions);
} }
handleShareAttempt () { handleShareAttempt () {
this.setState({ this.setState({
@ -691,6 +739,7 @@ class Preview extends React.Component {
const parts = window.location.pathname.toLowerCase() const parts = window.location.pathname.toLowerCase()
.split('/') .split('/')
.filter(Boolean); .filter(Boolean);
const queryParams = location.search;
let newUrl; let newUrl;
if (projectId === '0') { if (projectId === '0') {
newUrl = `/${parts[0]}/editor`; newUrl = `/${parts[0]}/editor`;
@ -702,7 +751,7 @@ class Preview extends React.Component {
history.pushState( history.pushState(
{projectId: projectId}, {projectId: projectId},
{projectId: projectId}, {projectId: projectId},
newUrl `${newUrl}${queryParams}`
); );
if (callback) callback(); if (callback) callback();
}); });
@ -908,6 +957,7 @@ class Preview extends React.Component {
onUpdateProjectTitle={this.handleUpdateProjectTitle} onUpdateProjectTitle={this.handleUpdateProjectTitle}
user={this.props.user} user={this.props.user}
permissions={this.props.permissions} permissions={this.props.permissions}
onActivateDeck={this.props.onActivateDeck}
/> />
)} )}
{this.props.registrationOpen && ( {this.props.registrationOpen && (
@ -985,6 +1035,7 @@ Preview.propTypes = {
lovedLoaded: PropTypes.bool, lovedLoaded: PropTypes.bool,
moreCommentsToLoad: PropTypes.bool, moreCommentsToLoad: PropTypes.bool,
original: projectShape, original: projectShape,
onActivateDeck: PropTypes.func,
parent: projectShape, parent: projectShape,
permissions: PropTypes.object, permissions: PropTypes.object,
playerMode: PropTypes.bool, playerMode: PropTypes.bool,
@ -1241,6 +1292,9 @@ const mapDispatchToProps = dispatch => ({
}, },
setFullScreen: fullscreen => { setFullScreen: fullscreen => {
dispatch(GUI.setFullScreen(fullscreen)); dispatch(GUI.setFullScreen(fullscreen));
},
onActivateDeck: id => {
dispatch(GUI.activateDeck(id));
} }
}); });

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 287.25 289.31"><g id="Layer_1-2"><g><polygon points="5.62 66.87 17.11 111.29 263.82 47.87 254.29 3.94 5.62 66.87" fill="#fbaa1e"/><circle cx="152.92" cy="203.26" r="62.8" fill="#fbaa1e" stroke="#1e1e1e" stroke-miterlimit="10" stroke-width="8.09"/><g><path d="M283.6,112.42H40.71l228.64-58.13c1.95-.53,3.09-2.54,2.56-4.48l-10.48-38.46c-2.23-8.2-10.72-13.05-18.91-10.81L11.36,59.36C3.16,61.59-1.69,70.08,.54,78.27l10.05,36.91c-.07,.29-.12,.58-.12,.9v157.83c0,8.49,6.91,15.41,15.41,15.41h245.97c8.49,0,15.41-6.91,15.41-15.41V116.07c0-2.02-1.63-3.65-3.65-3.65ZM108.92,42.22l42.38,34.38-44.76,10.23-42.38-34.38,44.76-10.23Zm48.04-12.6l46.88,34.38-42.06,10.23-42.38-34.38,37.57-10.23Zm56.13-13.5l42.38,34.38-42.96,11.13-42.38-34.38,42.96-11.13Zm31.34-8.54c4.31-1.18,8.78,1.38,9.95,5.69l9.42,34.59L221.77,13.76l22.67-6.18ZM55.49,54.82l42.38,34.38-43.86,12.03L11.9,66.92c.44-.21,.9-.39,1.38-.52l42.21-11.58ZM7.45,72.72l37.9,30.88-28.24,7.7L7.59,76.35c-.33-1.21-.37-2.44-.14-3.64Zm272.5,201.19c0,4.47-3.64,8.1-8.11,8.1H25.88c-4.47,0-8.1-3.64-8.1-8.1V119.72H279.95v154.18Z" fill="#1e1e1e"/><path d="M125.19,240.95c1.16,.67,2.42,1,3.69,1s2.54-.33,3.69-1l56.09-32.38c2.31-1.34,3.69-3.73,3.69-6.4s-1.38-5.06-3.69-6.4l-56.09-32.38c-2.31-1.34-5.07-1.34-7.39,0-2.31,1.33-3.69,3.73-3.69,6.4v64.77c0,2.67,1.38,5.06,3.69,6.4Zm3.61-71.21s.04-.04,.09-.04c.01,0,.02,0,.04,0l56.09,32.38,1.83-3.16-1.79,3.18s.01,.08-.03,.13l-56.13,32.4s-.08-.03-.09-.09v-64.81Z" fill="#1e1e1e"/></g><path d="M128.8,169.74s.04-.04,.09-.04c.01,0,.02,0,.04,0l56.09,32.38,1.83-3.16-1.79,3.18s.01,.08-.03,.13l-56.13,32.4s-.08-.03-.09-.09v-64.81Z" fill="#7860aa"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 257.29 252.8"><g id="Layer_1-2"><g><circle cx="130.2" cy="124.6" r="120.27" fill="#fbaa1e"/><circle cx="132.35" cy="122.46" r="95.72" fill="#fff"/><circle cx="131.41" cy="125.24" r="30.02" fill="#7e69af"/><polygon points="13.77 75.45 38.17 94.28 60.83 82.42 32.94 65.34 13.77 75.45" fill="#7e69af"/><polygon points="35.58 37.94 63.22 51.56 62.56 77.13 34.94 59.61 35.58 37.94" fill="#7e69af"/><g><path d="M104.12,140.6l-1.56-4.03c-.21-.54-.4-1.08-.58-1.64l-1.33-4.11,8.23-2.66,1.33,4.11c.13,.4,.27,.79,.42,1.17l1.56,4.03-8.06,3.13Z" fill="#1e1e1e"/><path d="M131.7,156.54c-7.21,0-14.25-2.52-19.81-7.08l-3.34-2.74,5.49-6.68,3.34,2.74c4.02,3.3,9.11,5.12,14.33,5.12,12.46,0,22.6-10.14,22.6-22.6s-10.14-22.6-22.6-22.6c-1.1,0-2.21,.08-3.3,.24l-4.28,.62-1.24-8.56,4.28-.62c1.5-.22,3.02-.33,4.54-.33,17.23,0,31.25,14.02,31.25,31.25s-14.02,31.25-31.25,31.25Z" fill="#1e1e1e"/><path d="M75.74,120.35l-8.51-1.51,.75-4.26c.3-1.7,.68-3.41,1.11-5.07l1.1-4.18,8.36,2.19-1.1,4.18c-.38,1.44-.7,2.92-.96,4.39l-.75,4.26Z" fill="#1e1e1e"/><path d="M131.05,189.86c-15.49,0-30.44-5.6-42.1-15.78-11.54-10.08-19.1-23.94-21.29-39.02l-.62-4.28,8.56-1.24,.62,4.28c3.91,27.02,27.48,47.4,54.83,47.4,30.55,0,55.4-24.85,55.4-55.4s-24.85-55.4-55.4-55.4c-4.97,0-9.9,.66-14.65,1.96l-4.17,1.14-2.28-8.34,4.17-1.14c5.49-1.5,11.19-2.26,16.93-2.26,35.32,0,64.05,28.73,64.05,64.05s-28.73,64.05-64.05,64.05Z" fill="#1e1e1e"/><path d="M94.55,83.91l-5.16-6.94,3.47-2.58c1.46-1.08,2.98-2.11,4.52-3.07l3.68-2.27,4.55,7.35-3.68,2.27c-1.33,.82-2.65,1.72-3.91,2.65l-3.47,2.58Z" fill="#1e1e1e"/><path d="M40.15,123.75l-8.63-.57,.28-4.31c.1-1.54,.24-3.11,.42-4.64l.49-4.3,8.59,.97-.49,4.3c-.16,1.4-.29,2.83-.38,4.24l-.28,4.31Z" fill="#1e1e1e"/><path d="M130.89,224.83c-24.68,0-48.34-9.12-66.62-25.67-18.15-16.43-29.56-38.83-32.12-63.08l-.45-4.3,8.6-.91,.45,4.3c4.88,46.18,43.64,81.01,90.15,81.01,49.99,0,90.66-40.67,90.66-90.65s-40.67-90.66-90.66-90.66c-11.07,0-21.89,1.97-32.15,5.86l-4.04,1.53-3.07-8.08,4.04-1.53c11.24-4.26,23.09-6.43,35.21-6.43,54.76,0,99.3,44.55,99.3,99.3s-44.55,99.3-99.3,99.3Z" fill="#1e1e1e"/><path d="M79.04,51.02l-4.6-7.32,3.66-2.3c.94-.59,1.9-1.17,2.86-1.73l3.73-2.18,4.35,7.47-3.73,2.18c-.88,.51-1.75,1.04-2.61,1.58l-3.66,2.3Z" fill="#1e1e1e"/><rect x="69.62" y="14.07" width="8.65" height="147.15" transform="translate(-39.88 101.61) rotate(-56.84)" fill="#1e1e1e"/><polygon points="62.89 87.44 58.54 53.99 40.37 41.47 39.34 67.5 30.7 67.16 32.35 25.45 66.62 49.05 71.47 86.32 62.89 87.44" fill="#1e1e1e"/><polygon points="36.32 99.83 0 73.51 37.2 55.49 40.97 63.27 16.78 74.99 37.56 90.05 66.76 78.55 69.93 86.59 36.32 99.83" fill="#1e1e1e"/><path d="M14.47,108.08l-8.49-1.65,.82-4.24c.29-1.52,.62-3.05,.97-4.56l.98-4.21,8.42,1.96-.98,4.21c-.33,1.41-.63,2.84-.91,4.25l-.82,4.24Z" fill="#1e1e1e"/><path d="M130.89,252.8c-69.7,0-126.4-56.7-126.4-126.4,0-1.98,.05-3.98,.14-5.95l.2-4.32,8.64,.4-.2,4.32c-.09,1.84-.13,3.7-.13,5.55,0,64.93,52.82,117.75,117.75,117.75s117.75-52.82,117.75-117.75S195.82,8.65,130.89,8.65c-17.33,0-34.01,3.67-49.6,10.92l-3.92,1.82-3.65-7.84,3.92-1.82C94.38,3.95,112.29,0,130.89,0c69.7,0,126.4,56.7,126.4,126.4s-56.7,126.4-126.4,126.4Z" fill="#1e1e1e"/><path d="M59.39,32.73l-4.99-7.06,3.53-2.5c1.16-.82,2.34-1.63,3.53-2.41l3.61-2.38,4.76,7.22-3.61,2.38c-1.11,.73-2.22,1.49-3.29,2.25l-3.53,2.5Z" fill="#1e1e1e"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 328.03 253.62"><g id="Layer_1-2"><g><polygon points="84.02 101.63 76.8 115.85 109.59 139.6 119.32 126.19 84.02 101.63" fill="#f68c24"/><polygon points="75.64 121.2 2.61 236.4 21.81 251.09 105.73 138.5 76.8 115.85 75.64 121.2" fill="#7e69af" fill-rule="evenodd"/><g><path d="M299.39,50.51c.82-5.54,5.98-9.36,11.51-8.54l-15.08,101.51c-.15,10.79-13.15,20.55-30.01,22.18-17.64,1.71-32.84-6.12-33.94-17.49-1.1-11.37,12.31-21.97,29.95-23.68,10.14-.98,19.47,1.19,25.78,5.37l11.79-79.35Zm11.52-8.53c10.39,2.61,26.86,18.97,9.64,63.48-1.08,2.47-2.8,5.96-4.9,9.55-2.77,4.72-6.29,2-4.53-3.18,.86-2.54,1.6-5.08,2.13-7.45,2.52-11.39,7.88-36.27-4.83-45.65l2.49-16.75Z" fill="#231f20" fill-rule="evenodd"/><path d="M208.72,10.36c.64-2.98,3.39-4.96,6.35-4.71v-.05s.77,.16,.77,.16l45.66,9.84,5.26,1.13h0s.05,.01,.08,.02l-12.78,59.31c-.5,6.36-8.54,11.62-18.55,11.94-10.47,.33-19.14-4.87-19.35-11.62-.22-6.75,8.1-12.49,18.58-12.83,6.02-.19,11.44,1.44,15,4.15l7.94-36.86-45.14-9.72-9.47,43.95c-.5,6.36-8.54,11.62-18.55,11.94-10.47,.33-19.14-4.87-19.35-11.62-.22-6.75,8.1-12.49,18.58-12.83,6.02-.19,11.44,1.44,15,4.15l9.99-46.36Z" fill="#231f20" fill-rule="evenodd"/></g><path d="M152.13,63.42l-.64-.65c-8.53-8.02-17.19-13.27-25.99-15.74-2.6-.79-5.68-.99-9.22-.6-2.48,.13-5.88,1.56-10.19,4.31-3.88,2.12-9.01,7.3-15.39,15.54-5.26,7.09-8.64,13.12-10.13,18.1-1.33,4.93-1.54,8.61-.63,11.05,.5,1.57,1.14,3.07,1.9,4.49-2.62,3.48-4.42,6.45-5.36,8.89-.71,2.14-.61,4.49,.31,7.05,.08,.18,.18,.36,.28,.55-22.03,34.63-43.68,68.65-56.45,88.35-3.65,5.63-6.6,10.13-8.94,13.7-1.57,2.4-2.88,4.39-3.98,6.08-3.19,4.92-4.67,7.38-5.32,9.37-.96,2.96-.09,4.9,1.58,6.62,.82,.85,1.83,1.64,2.9,2.48,.35,.28,.73,.57,1.12,.87,3.3,2.53,7.93,5.91,10.9,7.17,4.43,1.88,6.26-.96,10.74-6.82l.33-.44c5.15-6.74,14.56-18.92,23.97-31.12h.01s55.17-73.13,55.17-73.13h0s.01-.02,.01-.02c.15,.03,.3,.06,.45,.08,1.6,.27,3.07-.03,4.41-.9,1.5-.77,3.29-2.36,5.38-4.75,.73-.95,2.11-3,4.12-6.13,4.39,.45,8.24-.47,11.57-2.74,.73-.28,2.45-1.8,5.16-4.54,3.18-3.75,5.78-7.28,7.81-10.59,5.45-8.01,9.27-15.56,11.46-22.65,2.34-8.79-.12-16.76-7.38-23.88Zm-43,76.11h0s0,0,0,0h0Zm-57.83,71.11c-9.41,12.19-18.82,24.37-23.97,31.12l-.33,.44c-5.24,6.85-5.25,6.43-6.79,5.77-2.5-1.06-6.79-4.15-10.17-6.74-.38-.29-.75-.58-1.1-.85-1.18-.92-2.02-1.62-2.59-2.21-1.13-1.17-1.2-1.92-.77-3.24,.48-1.48,1.67-3.53,4.94-8.58,1.09-1.69,2.39-3.67,3.97-6.07,2.34-3.57,5.29-8.07,8.95-13.71,12.65-19.51,33.98-53.04,55.77-87.29,1.02,1.1,2.29,2.27,3.85,3.51,.58,.48,1.19,.96,1.83,1.43l17.18,12.35c1.21,.8,2.43,1.45,3.66,1.95l-54.43,72.14Zm61.86-74.98c-.44,.26-.95,.43-1.51,.53l.06-.09h0c-.56,.16-1.14,.2-1.77,.15-.04,0-.07,0-.11,0-.29-.03-.58-.09-.88-.16-.12-.03-.24-.07-.36-.1-.18-.05-.36-.11-.55-.17-1.24-.44-2.67-1.15-4.3-2.12l-15.99-11.51-3.48-2.73c-1.89-1.53-3.33-3.17-4.34-4.9-.29-.58-.48-1.17-.57-1.76h0s0,0,0,0c-.03-.25-.04-.51-.03-.78,.03-.83,.23-1.75,.6-2.75l.68-1.57c.72-1.45,1.76-3.06,3.09-4.82,1.05,1.46,2.25,2.82,3.61,4.1,7.63,7.58,16.93,13.79,27.91,18.63,1.64,.67,3.22,1.19,4.74,1.56-3.26,4.88-5.53,7.71-6.8,8.47Z" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><path d="M152.75,150.65c-3.52-.05-6.79-2.27-8.02-5.79-1.56-4.49,.81-9.4,5.31-10.97,9.77-3.4,16.89-11.91,18.58-22.21,.77-4.7,5.19-7.88,9.89-7.12,4.7,.77,7.88,5.2,7.11,9.89-2.7,16.54-14.16,30.23-29.92,35.71-.98,.34-1.97,.49-2.95,.48Z" fill="#fbaa1e"/><path d="M166.39,181.86c-3.49-.05-6.74-2.23-7.99-5.71-1.61-4.48,.72-9.41,5.19-11.02,20.13-7.23,34.67-24.76,37.93-45.74,.73-4.7,5.11-7.92,9.84-7.19,4.7,.73,7.92,5.14,7.19,9.84-4.23,27.22-23.06,49.95-49.13,59.31-1,.36-2.03,.52-3.03,.51Z" fill="#fbaa1e"/><path d="M70.5,31.21c3.52,.05,6.79,2.27,8.02,5.79,1.56,4.49-.81,9.4-5.31,10.97-9.77,3.4-16.89,11.91-18.58,22.21-.77,4.7-5.19,7.88-9.89,7.12-4.7-.77-7.88-5.2-7.11-9.89,2.7-16.54,14.16-30.23,29.92-35.71,.98-.34,1.97-.49,2.95-.48Z" fill="#fbaa1e"/><path d="M56.87,0c3.49,.05,6.74,2.23,7.99,5.71,1.61,4.48-.72,9.41-5.19,11.02-20.13,7.23-34.67,24.76-37.93,45.74-.73,4.7-5.11,7.92-9.84,7.19-4.7-.73-7.92-5.14-7.19-9.84C8.93,32.6,27.76,9.87,53.83,.51,54.84,.15,55.86-.01,56.87,0Z" fill="#fbaa1e"/></g></g></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 331.84 300.12"><g id="Layer_1-2"><g><path d="M183.85,193.08l-41.73,72.19c-2.09,3.61-8.19,4.02-13.64,.87-5.45-3.15-8.14-8.64-6.05-12.25l41.73-72.19,19.69,11.38Z" fill="#7e69af"/><g><g><path d="M162.09,138.64l4.81-8.32c3.3-5.71,4.88-12.39,3.77-18.9-1.66-9.7,.56-20.14,7.33-28.83,10.09-12.95,28.17-17.51,43.23-10.96,20.44,8.9,28.05,33.23,17.22,51.96-4.45,7.7-11.27,13.17-19.01,16.02-6.12,2.26-11.12,6.83-14.38,12.49l-4.93,8.54" fill="#febf10" stroke="#ffd996" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><path d="M182.72,190.73l-41.7,72.13c-.9,1.55-2.06,2.91-3.48,4.04l-32.5,27.98c-5.12,4.1-12.85-.37-11.85-6.85l8.03-42.12c.27-1.79,.87-3.47,1.76-5.03l33.46-57.87,8.25-14.26" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><path d="M184.63,172.81l1.58,.92c4.38,2.53,5.87,8.13,3.34,12.5h0c-2.53,4.38-8.13,5.87-12.5,3.34l-28.52-16.49c-4.38-2.53-5.87-8.13-3.34-12.5h0c2.53-4.38,8.13-5.87,12.5-3.34l1.58,.92" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><path d="M159.27,158.15l-4.75-2.75c-4.38-2.53-5.87-8.13-3.34-12.5h0c2.53-4.38,8.13-5.87,12.5-3.34l34.86,20.16c4.38,2.53,5.87,8.13,3.34,12.5h0c-2.53,4.38-8.13,5.87-12.5,3.34l-4.75-2.75" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><polygon points="119.01 282.87 107.82 276.4 96.63 269.93 97.37 295.78 119.01 282.87" fill="#231f20" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><path d="M162.09,138.64l4.81-8.32c3.3-5.71,4.88-12.39,3.77-18.9-1.66-9.7,.56-20.14,7.33-28.83,10.09-12.95,28.17-17.51,43.23-10.96,20.44,8.9,28.05,33.23,17.22,51.96-4.45,7.7-11.27,13.17-19.01,16.02-6.12,2.26-11.12,6.83-14.38,12.49l-4.93,8.54" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><polyline points="191.76 109.27 193.94 127.45 210.77 120.27" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><line x1="193.94" y1="127.45" x2="188.44" y2="136.95" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><line x1="232.41" y1="60.89" x2="236.08" y2="54.56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><line x1="149.22" y1="80.45" x2="155.56" y2="84.12" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><line x1="250.64" y1="139.09" x2="256.97" y2="142.75" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><line x1="260.79" y1="100.56" x2="268.11" y2="100.56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><line x1="180.22" y1="49.75" x2="183.88" y2="56.09" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><path d="M162.79,181.32l-40.31,69.72c-2.02,3.49-7.91,3.88-13.17,.84-5.26-3.04-7.86-8.35-5.84-11.84l40.31-69.72,19.02,10.99Z" fill="#7e69af" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/><path d="M181.8,192.32l-40.31,69.72c-2.02,3.49-7.91,3.88-13.17,.84-5.26-3.04-7.86-8.35-5.84-11.84l40.31-69.72,19.02,10.99Z" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="7.32"/></g><g><path d="M312.92,249.61c-1.78,0-3.23-1.45-3.23-3.23v-62.08c0-1.78,1.45-3.23,3.23-3.23s3.23,1.45,3.23,3.23v62.08c0,1.78-1.45,3.23-3.23,3.23Z" fill="#1e1e1e"/><path d="M328.61,265.3H179.95c-1.78,0-3.23-1.45-3.23-3.23s1.45-3.23,3.23-3.23h145.43V39.45H46.3c-1.78,0-3.23-1.45-3.23-3.23s1.45-3.23,3.23-3.23H328.61c1.78,0,3.23,1.45,3.23,3.23v225.85c0,1.78-1.45,3.23-3.23,3.23Z" fill="#1e1e1e"/><path d="M65.4,265.3H33.14c-18.28,0-33.14-14.87-33.14-33.14,0-14.98,12.18-27.16,27.16-27.16,12.34,0,22.37,10.04,22.37,22.37,0,10.23-8.32,18.54-18.54,18.54-8.54,0-15.48-6.94-15.48-15.48,0-1.78,1.45-3.23,3.23-3.23s3.23,1.45,3.23,3.23c0,4.98,4.04,9.02,9.02,9.02,6.66,0,12.08-5.42,12.08-12.08,0-8.78-7.14-15.91-15.91-15.91-11.41,0-20.7,9.29-20.7,20.7,0,14.71,11.97,26.68,26.68,26.68h32.26c1.78,0,3.23,1.45,3.23,3.23s-1.45,3.23-3.23,3.23Z" fill="#1e1e1e"/><path d="M3.23,235.38c-1.78,0-3.23-1.45-3.23-3.23V27.16C0,12.18,12.18,0,27.16,0c12.34,0,22.37,10.04,22.37,22.37V227.37c0,1.78-1.45,3.23-3.23,3.23s-3.23-1.45-3.23-3.23V22.37c0-8.78-7.14-15.91-15.91-15.91C15.74,6.46,6.46,15.75,6.46,27.16V232.15c0,1.78-1.45,3.23-3.23,3.23Z" fill="#1e1e1e"/><g><path d="M18.92,107.72c-1.78,0-3.23-1.45-3.23-3.23V33.22c0-1.78,1.45-3.23,3.23-3.23s3.23,1.45,3.23,3.23V104.49c0,1.78-1.45,3.23-3.23,3.23Z" fill="#1e1e1e"/><path d="M18.92,129.78c-1.78,0-3.23-1.45-3.23-3.23v-7.34c0-1.78,1.45-3.23,3.23-3.23s3.23,1.45,3.23,3.23v7.34c0,1.78-1.45,3.23-3.23,3.23Z" fill="#1e1e1e"/></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 349.79 255.18"><g id="Layer_1-2"><g><path d="M94.31,91.76s13.65-16.35,49.71-15.95c36.06,.41,49.29,17.02,49.33,17.06l67.63,.76c2.28,.03,4.11,1.9,4.08,4.17l-.56,49.38c-.03,2.28-1.89,4.11-4.17,4.08l-113.03-1.28h0s-2.42-.03-2.42-.03c-1.09-.01-2.15,.41-2.93,1.18l-5.9,5.77c-.78,.77-1.84,1.19-2.93,1.18l-13.09-.15c-1.09-.01-2.14-.46-2.9-1.24l-5.77-5.9c-.76-.78-1.81-1.23-2.9-1.24l-2.42-.03h0l-8.25-.09c-2.28-.03-4.1-1.89-4.08-4.17l.6-53.5h0Z" fill="#f9a11b" fill-rule="evenodd" stroke="#000" stroke-miterlimit="10" stroke-width="3.79"/><path d="M147.74,149.98h1.97s0-.01,0-.01l80.1-.51c2.36-.02,4.28,1.88,4.29,4.24l.19,43.66c0,2.36-1.89,4.28-4.25,4.3l-80.1,.51h0s-1.97,.01-1.97,.01c-1.54,0-3.02,.63-4.1,1.73l-5.32,5.38c-1.08,1.1-2.56,1.72-4.1,1.73l-12.68,.08c-1.54,.01-3.02-.59-4.11-1.67l-5.37-5.31c-1.09-1.08-2.57-1.68-4.11-1.67h-1.97s0,.01,0,.01l-8.85,.06c-2.36,.01-4.28-1.89-4.29-4.24l-.19-43.66c-.01-2.36,1.89-4.28,4.25-4.3l8.85-.06h0s1.97-.01,1.97-.01c1.54,0,3.02,.59,4.11,1.67l5.37,5.31c1.09,1.08,2.57,1.68,4.11,1.67l12.68-.08c1.54,0,3.02-.63,4.1-1.73l5.32-5.38c1.08-1.1,2.56-1.72,4.1-1.73Z" fill="#7e69af" fill-rule="evenodd" stroke="#000" stroke-miterlimit="10" stroke-width="3.79"/><g><rect x="27.95" y="59.08" width="293.9" height="168.16" fill="none" stroke="#000" stroke-linejoin="round" stroke-width="6.07"/><rect x="3.03" y="3.03" width="343.72" height="249.11" fill="none" stroke="#000" stroke-linejoin="round" stroke-width="6.07"/><line x1="3.03" y1="40.4" x2="346.76" y2="40.4" fill="none" stroke="#000" stroke-linejoin="round" stroke-width="6.07"/><line x1="21.72" y1="21.72" x2="34.17" y2="21.72" fill="none" stroke="#000" stroke-linejoin="round" stroke-width="6.07"/><line x1="46.63" y1="21.72" x2="59.08" y2="21.72" fill="none" stroke="#000" stroke-linejoin="round" stroke-width="6.07"/><line x1="71.54" y1="21.72" x2="84" y2="21.72" fill="none" stroke="#000" stroke-linejoin="round" stroke-width="6.07"/><polygon points="160.79 121.68 160.79 171.51 204.39 146.59 160.79 121.68" fill="#1e1e1e" stroke="#1e1e1e" stroke-linejoin="round" stroke-width="6.07"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,89 @@
import {shouldDisplayOnboarding} from '../../../src/lib/onboarding';
describe('unit test lib/onboarding.js', () => {
const startDate = new Date();
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 7);
let user;
let permissions;
beforeEach(() => {
process.env.ONBOARDING_TEST_ACTIVE = 'true';
process.env.ONBOARDING_TESTING_STARTING_DATE = startDate.toJSON().split('T')[0];
process.env.ONBOARDING_TESTING_ENDING_DATE = endDate.toJSON().split('T')[0];
user = {id: 2, dateJoined: startDate.toJSON(), banned: false};
permissions = {admin: false, mute_status: {}, new_scratcher: true};
});
describe('#shouldDisplayOnboarding', () => {
describe('when user is eligible to view onboarding journeys', () => {
describe('when there is time frame for A/B testing', () => {
test('returns true', () => {
expect(shouldDisplayOnboarding(user, permissions)).toBeTruthy();
});
});
});
describe('when user is not eligible to view onboarding journeys', () => {
describe('when feature flag is toggled off', () => {
test('returns false', () => {
process.env.ONBOARDING_TEST_ACTIVE = 'false';
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
});
});
describe('when user is in other testing group', () => {
test('returns false', () => {
user.id = 1;
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
});
});
describe('when user is registered outside of time frame', () => {
test('returns false', () => {
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() - 1);
user.dateJoined = currentDate.toJSON();
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
});
});
describe('when user is admin', () => {
test('returns false', () => {
permissions.admin = true;
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
});
});
describe('when user is muted', () => {
test('returns false', () => {
permissions.mute_status = {showWarning: true};
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
});
});
describe('when user is banned', () => {
test('returns false', () => {
user.banned = true;
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
});
});
describe('when user is empty', () => {
test('returns false', () => {
user = {};
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
});
});
describe('when permissions is empty', () => {
test('returns false', () => {
permissions = {};
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
});
});
});
});
});

View file

@ -171,6 +171,7 @@ module.exports = {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
modules: { modules: {
auto: true,
localIdentName: '[name]_[local]_[hash:base64:5]', localIdentName: '[name]_[local]_[hash:base64:5]',
exportLocalsConvention: 'camelCase' exportLocalsConvention: 'camelCase'
}, },
@ -284,8 +285,25 @@ module.exports = {
'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"`, 'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"`,
'process.env.GTM_ENV_AUTH': `"${process.env.GTM_ENV_AUTH || ''}"`, 'process.env.GTM_ENV_AUTH': `"${process.env.GTM_ENV_AUTH || ''}"`,
'process.env.GTM_ID': process.env.GTM_ID ? `"${process.env.GTM_ID}"` : null, 'process.env.GTM_ID': process.env.GTM_ID ? `"${process.env.GTM_ID}"` : null,
'process.env.USER_GUIDING_ID': `"${process.env.USER_GUIDING_ID || ''}"`, 'process.env.ONBOARDING_TEST_ACTIVE': `"${
'process.env.SORTING_HAT_HOST': `"${process.env.SORTING_HAT_HOST || ''}"` process.env.ONBOARDING_TEST_ACTIVE || false
}"`,
'process.env.ONBOARDING_TEST_PROJECT_IDS': `'${process.env.ONBOARDING_TEST_PROJECT_IDS || JSON.stringify(
{
clicker: '10128368',
pong: '10128515',
animateCharacter: '10128067',
makeItFly: '114019829',
recordSound: '1031325137',
makeMusic: '10012676'
}
)}'`,
'process.env.ONBOARDING_TESTING_STARTING_DATE': `"${
process.env.ONBOARDING_TESTING_STARTING_DATE || '2024-01-20'
}"`,
'process.env.ONBOARDING_TESTING_ENDING_DATE': `"${
process.env.ONBOARDING_TESTING_ENDING_DATE || '2030-11-20'
}"`
}) })
]) ])
.concat(process.env.ANALYZE_BUNDLE === 'true' ? [ .concat(process.env.ANALYZE_BUNDLE === 'true' ? [