From f61ef1be2ec92546eb237d24278e00a75df573ea Mon Sep 17 00:00:00 2001 From: Florian Rival Date: Tue, 16 Aug 2022 16:39:37 +0200 Subject: [PATCH] Add monetization tab in game dashboard * Allow to enable or disable ads on the game page if published on Liluo.io * In the future, games that generate enough revenue will be able to opt-in into "revenue share", so that as a creator you can start earning from your game sessions. * This also this allows to maintain free publishing on Liluo.io for all. --- .../src/GameDashboard/GameDetailsDialog.js | 10 +++ .../Monetization/GameMonetization.js | 86 +++++++++++++++++++ newIDE/app/src/UI/Checkbox.js | 2 +- newIDE/app/src/UI/InlineCheckbox.js | 2 +- newIDE/app/src/Utils/GDevelopServices/Game.js | 4 + .../GDevelopServicesTestData/index.js | 11 +++ .../Monetization/GameMonetization.stories.js | 82 ++++++++++++++++++ .../componentStories/UI/Checkboxes.stories.js | 7 ++ 8 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 newIDE/app/src/GameDashboard/Monetization/GameMonetization.js create mode 100644 newIDE/app/src/stories/componentStories/GameDashboard/Monetization/GameMonetization.stories.js diff --git a/newIDE/app/src/GameDashboard/GameDetailsDialog.js b/newIDE/app/src/GameDashboard/GameDetailsDialog.js index 7710d09c64..7cebf85f6a 100644 --- a/newIDE/app/src/GameDashboard/GameDetailsDialog.js +++ b/newIDE/app/src/GameDashboard/GameDetailsDialog.js @@ -45,6 +45,7 @@ import LeaderboardAdmin from './LeaderboardAdmin'; import { GameAnalyticsPanel } from './GameAnalyticsPanel'; import GameFeedback from './Feedbacks/GameFeedback'; import { useResponsiveWindowWidth } from '../UI/Reponsive/ResponsiveWindowMeasurer'; +import { GameMonetization } from './Monetization/GameMonetization'; export type GamesDetailsTab = | 'details' @@ -346,6 +347,7 @@ export const GameDetailsDialog = ({ Player Feedback} value="feedback" /> Statistics} value="analytics" /> Leaderboards} value="leaderboards" /> + Monetization} value="monetization" /> {currentTab === 'leaderboards' ? ( @@ -563,6 +565,14 @@ export const GameDetailsDialog = ({ game={game} /> ) : null} + {currentTab === 'monetization' ? ( + + + + ) : null} {publicGame && project && isPublicGamePropertiesDialogOpen && ( void, +|}; + +export const GameMonetization = ({ game, onGameUpdated }: Props) => { + const { getAuthorizationHeader, profile } = React.useContext( + AuthenticatedUserContext + ); + const [ + pendingDisplayAdsOnGamePage, + setPendingDisplayAdsOnGamePage, + ] = React.useState(null); + + if (!profile) return null; + + return ( + + {({ i18n }) => ( + + + + In the future, games that generate enough revenue will be able to + opt-in into "revenue share", so that as a creator you can start + earning from your game sessions. + + + { + setPendingDisplayAdsOnGamePage(enable); + const updatedGame = { ...game, displayAdsOnGamePage: enable }; + try { + await updateGame(getAuthorizationHeader, profile.id, game.id, { + displayAdsOnGamePage: enable, + }); + onGameUpdated(updatedGame); + } catch (error) { + showErrorBox({ + message: + i18n._(t`Unable to update game.`) + + ' ' + + i18n._( + t`Verify your internet connection or try again later.` + ), + rawError: error, + errorId: 'game-monetization-update-game-error', + }); + } finally { + setPendingDisplayAdsOnGamePage(null); + } + }} + label={ + + Allow to display advertisments on the game page on Liluo.io. + + } + tooltipOrHelperText={ + + This is recommended as this allows to maintain free publishing + on Liluo.io and allow to analyze if you could benefit from + revenue sharing. + + } + /> + + )} + + ); +}; diff --git a/newIDE/app/src/UI/Checkbox.js b/newIDE/app/src/UI/Checkbox.js index ac9657fd79..904e2304e0 100644 --- a/newIDE/app/src/UI/Checkbox.js +++ b/newIDE/app/src/UI/Checkbox.js @@ -25,7 +25,7 @@ const useFormGroupStyles = makeStyles({ type Props = {| label?: ?React.Node, checked: boolean, - onCheck?: (e: {||}, checked: boolean) => void, + onCheck?: (e: {||}, checked: boolean) => void | Promise, checkedIcon?: React.Node, uncheckedIcon?: React.Node, disabled?: boolean, diff --git a/newIDE/app/src/UI/InlineCheckbox.js b/newIDE/app/src/UI/InlineCheckbox.js index 05bc4704de..9f2dee0e83 100644 --- a/newIDE/app/src/UI/InlineCheckbox.js +++ b/newIDE/app/src/UI/InlineCheckbox.js @@ -22,7 +22,7 @@ const useFormGroupStyles = makeStyles({ type Props = {| label?: ?React.Node, checked: boolean, - onCheck?: (e: {||}, checked: boolean) => void, + onCheck?: (e: {||}, checked: boolean) => void | Promise, checkedIcon?: React.Node, uncheckedIcon?: React.Node, disabled?: boolean, diff --git a/newIDE/app/src/Utils/GDevelopServices/Game.js b/newIDE/app/src/Utils/GDevelopServices/Game.js index 28fd3413f5..deacd27407 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Game.js +++ b/newIDE/app/src/Utils/GDevelopServices/Game.js @@ -38,6 +38,7 @@ export type Game = { discoverable?: boolean, acceptsBuildComments?: boolean, acceptsGameComments?: boolean, + displayAdsOnGamePage?: boolean, }; export type GameSlug = { @@ -229,6 +230,7 @@ export const updateGame = ( discoverable, acceptsBuildComments, acceptsGameComments, + displayAdsOnGamePage, }: {| gameName?: string, categories?: string[], @@ -243,6 +245,7 @@ export const updateGame = ( discoverable?: boolean, acceptsBuildComments?: boolean, acceptsGameComments?: boolean, + displayAdsOnGamePage?: boolean, |} ): Promise => { return getAuthorizationHeader() @@ -263,6 +266,7 @@ export const updateGame = ( discoverable, acceptsBuildComments, acceptsGameComments, + displayAdsOnGamePage, }, { params: { diff --git a/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js b/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js index 939869d165..425ab69c6b 100644 --- a/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js +++ b/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js @@ -919,6 +919,7 @@ export const game1: Game = { gameName: 'My Great Game', createdAt: 1606065498, publicWebBuildId: 'fake-publicwebbuild-id', + displayAdsOnGamePage: true, }; export const game2: Game = { @@ -928,6 +929,16 @@ export const game2: Game = { createdAt: 1607065498, }; +export const gameWithDisplayAdsOnGamePageEnabled: Game = { + ...game1, + displayAdsOnGamePage: true, +}; + +export const gameWithDisplayAdsOnGamePageDisabled: Game = { + ...game1, + displayAdsOnGamePage: false, +}; + /** * It uses the ANSI C one because Number.MAX_SAFE_INTEGER is 2^53 * and this one multiply a seed of 2^15 with a multiplier of 2^32. diff --git a/newIDE/app/src/stories/componentStories/GameDashboard/Monetization/GameMonetization.stories.js b/newIDE/app/src/stories/componentStories/GameDashboard/Monetization/GameMonetization.stories.js new file mode 100644 index 0000000000..18431230c4 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/GameDashboard/Monetization/GameMonetization.stories.js @@ -0,0 +1,82 @@ +// @flow + +import * as React from 'react'; + +import { action } from '@storybook/addon-actions'; +import muiDecorator from '../../../ThemeDecorator'; + +import paperDecorator from '../../../PaperDecorator'; +import { GameMonetization } from '../../../../GameDashboard/Monetization/GameMonetization'; +import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext'; + +import { + fakeIndieAuthenticatedUser, + gameWithDisplayAdsOnGamePageEnabled, + gameWithDisplayAdsOnGamePageDisabled, +} from '../../../../fixtures/GDevelopServicesTestData'; +import { GDevelopGameApi } from '../../../../Utils/GDevelopServices/ApiConfigs'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; + +export default { + title: 'GameDashboard/Monetization/GameMonetization', + component: GameMonetization, + decorators: [paperDecorator, muiDecorator], +}; + +export const AdsEnabled = () => { + const game = gameWithDisplayAdsOnGamePageEnabled; + const mock = new MockAdapter(axios, { delayResponse: 1000 }); + mock + .onPatch(`${GDevelopGameApi.baseUrl}/game/${game.id}`) + .reply(200) + .onAny() + .reply(config => { + console.error(`Unexpected call to ${config.url} (${config.method})`); + return [504, null]; + }); + + return ( + + + + ); +}; + +export const AdsDisabled = () => { + const game = gameWithDisplayAdsOnGamePageDisabled; + const mock = new MockAdapter(axios, { delayResponse: 1000 }); + mock + .onPatch(`${GDevelopGameApi.baseUrl}/game/${game.id}`) + .reply(200) + .onAny() + .reply(config => { + console.error(`Unexpected call to ${config.url} (${config.method})`); + return [504, null]; + }); + + return ( + + + + ); +}; + +export const ErrorWhenUpdatingGame = () => { + const game = gameWithDisplayAdsOnGamePageEnabled; + const mock = new MockAdapter(axios, { delayResponse: 1000 }); + mock + .onPatch(`${GDevelopGameApi.baseUrl}/game/${game.id}`) + .reply(500) + .onAny() + .reply(config => { + console.error(`Unexpected call to ${config.url} (${config.method})`); + return [504, null]; + }); + + return ( + + + + ); +}; diff --git a/newIDE/app/src/stories/componentStories/UI/Checkboxes.stories.js b/newIDE/app/src/stories/componentStories/UI/Checkboxes.stories.js index f55c313d5e..a729482491 100644 --- a/newIDE/app/src/stories/componentStories/UI/Checkboxes.stories.js +++ b/newIDE/app/src/stories/componentStories/UI/Checkboxes.stories.js @@ -62,6 +62,13 @@ export const Default = () => { disabled /> + {}} + label="With some helper text" + tooltipOrHelperText="This is some helper text" + /> + Without label