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.
This commit is contained in:
Florian Rival
2022-08-16 16:39:37 +02:00
parent bf387631ec
commit f61ef1be2e
8 changed files with 202 additions and 2 deletions
@@ -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 = ({
<Tab label={<Trans>Player Feedback</Trans>} value="feedback" />
<Tab label={<Trans>Statistics</Trans>} value="analytics" />
<Tab label={<Trans>Leaderboards</Trans>} value="leaderboards" />
<Tab label={<Trans>Monetization</Trans>} value="monetization" />
</Tabs>
<Line expand>
{currentTab === 'leaderboards' ? (
@@ -563,6 +565,14 @@ export const GameDetailsDialog = ({
game={game}
/>
) : null}
{currentTab === 'monetization' ? (
<ColumnStackLayout>
<GameMonetization
game={game}
onGameUpdated={handleGameUpdated}
/>
</ColumnStackLayout>
) : null}
</Line>
{publicGame && project && isPublicGamePropertiesDialogOpen && (
<PublicGamePropertiesDialog
@@ -0,0 +1,86 @@
// @flow
import { t, Trans } from '@lingui/macro';
import * as React from 'react';
import { I18n } from '@lingui/react';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import AlertMessage from '../../UI/AlertMessage';
import InlineCheckbox from '../../UI/InlineCheckbox';
import { ColumnStackLayout } from '../../UI/Layout';
import { showErrorBox } from '../../UI/Messages/MessageBox';
import { type Game, updateGame } from '../../Utils/GDevelopServices/Game';
type Props = {|
game: Game,
onGameUpdated: (updatedGame: Game) => void,
|};
export const GameMonetization = ({ game, onGameUpdated }: Props) => {
const { getAuthorizationHeader, profile } = React.useContext(
AuthenticatedUserContext
);
const [
pendingDisplayAdsOnGamePage,
setPendingDisplayAdsOnGamePage,
] = React.useState<boolean | null>(null);
if (!profile) return null;
return (
<I18n>
{({ i18n }) => (
<ColumnStackLayout noMargin>
<AlertMessage kind="info">
<Trans>
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.
</Trans>
</AlertMessage>
<InlineCheckbox
checked={
pendingDisplayAdsOnGamePage !== null
? pendingDisplayAdsOnGamePage
: !!game.displayAdsOnGamePage
}
onCheck={async (e, enable) => {
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={
<Trans>
Allow to display advertisments on the game page on Liluo.io.
</Trans>
}
tooltipOrHelperText={
<Trans>
This is recommended as this allows to maintain free publishing
on Liluo.io and allow to analyze if you could benefit from
revenue sharing.
</Trans>
}
/>
</ColumnStackLayout>
)}
</I18n>
);
};
+1 -1
View File
@@ -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<void>,
checkedIcon?: React.Node,
uncheckedIcon?: React.Node,
disabled?: boolean,
+1 -1
View File
@@ -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<void>,
checkedIcon?: React.Node,
uncheckedIcon?: React.Node,
disabled?: boolean,
@@ -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<Game> => {
return getAuthorizationHeader()
@@ -263,6 +266,7 @@ export const updateGame = (
discoverable,
acceptsBuildComments,
acceptsGameComments,
displayAdsOnGamePage,
},
{
params: {
@@ -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.
@@ -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 (
<AuthenticatedUserContext.Provider value={fakeIndieAuthenticatedUser}>
<GameMonetization game={game} onGameUpdated={action('onGameUpdated')} />
</AuthenticatedUserContext.Provider>
);
};
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 (
<AuthenticatedUserContext.Provider value={fakeIndieAuthenticatedUser}>
<GameMonetization game={game} onGameUpdated={action('onGameUpdated')} />
</AuthenticatedUserContext.Provider>
);
};
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 (
<AuthenticatedUserContext.Provider value={fakeIndieAuthenticatedUser}>
<GameMonetization game={game} onGameUpdated={action('onGameUpdated')} />
</AuthenticatedUserContext.Provider>
);
};
@@ -62,6 +62,13 @@ export const Default = () => {
disabled
/>
<LargeSpacer />
<InlineCheckbox
checked={true}
onCheck={(e, value) => {}}
label="With some helper text"
tooltipOrHelperText="This is some helper text"
/>
<LargeSpacer />
<Text>Without label</Text>
<InlineCheckbox
checked={inlineValue}