mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-21 09:35:27 -04:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
+82
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user