Skip to content

Commit 11df078

Browse files
Merge pull request #7463 from 4ian/youtube-sub-badge
New badge can be unlocked when subscribing to YouTube channel
2 parents 9001bfd + a2e48fd commit 11df078

File tree

6 files changed

+159
-31
lines changed

6 files changed

+159
-31
lines changed

newIDE/app/src/MainFrame/EditorContainers/HomePage/GetStartedSection/EarnCredits.js

+31-21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as React from 'react';
33
import { Trans } from '@lingui/macro';
44
import Text from '../../../../UI/Text';
55
import {
6+
ColumnStackLayout,
67
LineStackLayout,
78
ResponsiveLineStackLayout,
89
} from '../../../../UI/Layout';
@@ -34,6 +35,9 @@ type FeedbackInfo = {|
3435
type CreditItem = BadgeInfo | FeedbackInfo;
3536

3637
const styles = {
38+
widgetContainer: {
39+
maxWidth: 1800, // To avoid taking too much space on large screens.
40+
},
3741
badgeContainer: {
3842
position: 'relative',
3943
width: 65,
@@ -111,7 +115,7 @@ const FeedbackItem = () => {
111115
const allBadgesInfo: BadgeInfo[] = [
112116
{
113117
id: 'github-star',
114-
label: <Trans>Star GDevelop</Trans>,
118+
label: 'Star GDevelop', // Do not translate "Star".
115119
linkUrl: 'https://github.com/4ian/GDevelop',
116120
type: 'badge',
117121
},
@@ -127,6 +131,12 @@ const allBadgesInfo: BadgeInfo[] = [
127131
linkUrl: 'https://x.com/GDevelopApp',
128132
type: 'badge',
129133
},
134+
{
135+
id: 'youtube-subscription',
136+
label: <Trans>Subscribe</Trans>,
137+
linkUrl: 'https://x.com/GDevelopApp',
138+
type: 'badge',
139+
},
130140
];
131141

132142
const getAllBadgesWithOwnedStatus = (badges: ?Array<Badge>): BadgeInfo[] => {
@@ -235,9 +245,8 @@ export const EarnCredits = ({
235245
showRandomItem,
236246
showAllItems,
237247
}: Props) => {
238-
const { windowSize, isMobile } = useResponsiveWindowSize();
239-
const isMobileOrMediumWidth =
240-
windowSize === 'small' || windowSize === 'medium';
248+
const { isMobile, windowSize } = useResponsiveWindowSize();
249+
const isExtraLargeScreen = windowSize === 'xlarge';
241250

242251
const allBadgesWithOwnedStatus = React.useMemo(
243252
() => getAllBadgesWithOwnedStatus(badges),
@@ -318,28 +327,24 @@ export const EarnCredits = ({
318327
[badgesToShow, feedbackItemsToShow]
319328
);
320329

330+
const onlyOneItemDisplayed = allItemsToShow.length === 1;
331+
const itemsPerRow = onlyOneItemDisplayed ? 1 : isExtraLargeScreen ? 3 : 2;
321332
// Slice items in arrays of two to display them in a responsive way.
322-
const itemsSlicedInArraysOfTwo: CreditItem[][] = React.useMemo(
333+
const itemsSlicedInArrays: CreditItem[][] = React.useMemo(
323334
() => {
324335
const slicedItems: CreditItem[][] = [];
325-
for (let i = 0; i < allItemsToShow.length; i += 2) {
326-
slicedItems.push(allItemsToShow.slice(i, i + 2));
336+
for (let i = 0; i < allItemsToShow.length; i += itemsPerRow) {
337+
slicedItems.push(allItemsToShow.slice(i, i + itemsPerRow));
327338
}
328339
return slicedItems;
329340
},
330-
[allItemsToShow]
341+
[allItemsToShow, itemsPerRow]
331342
);
332343

333-
const onlyOneItemDisplayed = allItemsToShow.length === 1;
334-
335344
return (
336-
<Column noMargin expand>
337-
<ResponsiveLineStackLayout
338-
noMargin
339-
expand
340-
forceMobileLayout={isMobileOrMediumWidth}
341-
>
342-
{itemsSlicedInArraysOfTwo.map((items, index) => (
345+
<div style={styles.widgetContainer}>
346+
<ColumnStackLayout noMargin expand>
347+
{itemsSlicedInArrays.map((items, index) => (
343348
<ResponsiveLineStackLayout
344349
noMargin
345350
expand={onlyOneItemDisplayed}
@@ -366,12 +371,17 @@ export const EarnCredits = ({
366371
return null;
367372
})
368373
.filter(Boolean)}
369-
{items.length === 1 &&
374+
{items.length < itemsPerRow &&
370375
!onlyOneItemDisplayed &&
371-
isMobileOrMediumWidth && <div style={styles.itemPlaceholder} />}
376+
Array.from(
377+
{ length: itemsPerRow - items.length },
378+
(_, i) => i
379+
).map(i => (
380+
<div key={`filler-${i}`} style={styles.itemPlaceholder} />
381+
))}
372382
</ResponsiveLineStackLayout>
373383
))}
374-
</ResponsiveLineStackLayout>
375-
</Column>
384+
</ColumnStackLayout>
385+
</div>
376386
);
377387
};

newIDE/app/src/Profile/AuthenticatedUserProvider.js

+36
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,36 @@ export default class AuthenticatedUserProvider extends React.Component<
14521452
}
14531453
};
14541454

1455+
_onUpdateYoutubeSubscription = async (
1456+
communityLinks: CommunityLinks,
1457+
preferences: PreferencesValues
1458+
) => {
1459+
const { authentication } = this.props;
1460+
1461+
await this._doEdit(
1462+
{
1463+
communityLinks,
1464+
},
1465+
preferences
1466+
);
1467+
1468+
this.setState({
1469+
editInProgress: true,
1470+
});
1471+
try {
1472+
const response = await authentication.updateYoutubeSubscription(
1473+
authentication.getAuthorizationHeader
1474+
);
1475+
this._fetchUserBadges();
1476+
1477+
return response;
1478+
} finally {
1479+
this.setState({
1480+
editInProgress: false,
1481+
});
1482+
}
1483+
};
1484+
14551485
render() {
14561486
return (
14571487
<PreferencesContext.Consumer>
@@ -1509,6 +1539,12 @@ export default class AuthenticatedUserProvider extends React.Component<
15091539
onUpdateTwitterFollow={communityLinks =>
15101540
this._onUpdateTwitterFollow(communityLinks, preferences)
15111541
}
1542+
onUpdateYoutubeSubscription={communityLinks =>
1543+
this._onUpdateYoutubeSubscription(
1544+
communityLinks,
1545+
preferences
1546+
)
1547+
}
15121548
onDelete={this._doDeleteAccount}
15131549
actionInProgress={
15141550
this.state.editInProgress || this.state.deleteInProgress

newIDE/app/src/Profile/EditProfileDialog.js

+26-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type UpdateGitHubStarResponse,
1515
type UpdateTiktokFollowResponse,
1616
type UpdateTwitterFollowResponse,
17+
type UpdateYoutubeSubscriptionResponse,
1718
} from '../Utils/GDevelopServices/Authentication';
1819
import {
1920
communityLinksConfig,
@@ -69,6 +70,9 @@ export type EditProfileDialogProps = {|
6970
onUpdateTwitterFollow: (
7071
communityLinks: CommunityLinks
7172
) => Promise<UpdateTwitterFollowResponse>,
73+
onUpdateYoutubeSubscription: (
74+
communityLinks: CommunityLinks
75+
) => Promise<UpdateYoutubeSubscriptionResponse>,
7276
onDelete: () => Promise<void>,
7377
actionInProgress: boolean,
7478
error: ?AuthError,
@@ -252,6 +256,7 @@ const EditProfileDialog = ({
252256
onUpdateGitHubStar,
253257
onUpdateTiktokFollow,
254258
onUpdateTwitterFollow,
259+
onUpdateYoutubeSubscription,
255260
onDelete,
256261
actionInProgress,
257262
error,
@@ -551,6 +556,27 @@ const EditProfileDialog = ({
551556
translatableHintText={t`username`}
552557
icon={communityLinksConfig.twitterUsername.icon}
553558
/>
559+
<CommunityLinkWithFollow
560+
badges={badges}
561+
achievements={achievements}
562+
achievementId="youtube-subscription"
563+
value={youtubeUsername}
564+
onChange={setYoutubeUsername}
565+
onUpdateFollow={() =>
566+
onUpdateYoutubeSubscription(updatedCommunityLinks)
567+
}
568+
getMessageFromUpdate={
569+
communityLinksConfig.youtubeUsername.getMessageFromUpdate
570+
}
571+
disabled={actionInProgress}
572+
maxLength={communityLinksConfig.youtubeUsername.maxLength}
573+
prefix={communityLinksConfig.youtubeUsername.prefix}
574+
getRewardMessage={
575+
communityLinksConfig.youtubeUsername.getRewardMessage
576+
}
577+
translatableHintText={t`username`}
578+
icon={communityLinksConfig.youtubeUsername.icon}
579+
/>
554580
<CommunityLinkWithFollow
555581
badges={badges}
556582
achievements={achievements}
@@ -599,15 +625,6 @@ const EditProfileDialog = ({
599625
}}
600626
disabled={actionInProgress}
601627
/>
602-
<CommunityLinkLine
603-
id="youtubeUsername"
604-
value={youtubeUsername}
605-
translatableHintText={t`username`}
606-
onChange={(e, value) => {
607-
setYoutubeUsername(value);
608-
}}
609-
disabled={actionInProgress}
610-
/>
611628
<CommunityLinkLine
612629
id="instagramUsername"
613630
value={instagramUsername}

newIDE/app/src/Utils/GDevelopServices/Authentication.js

+33
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ export type UpdateTwitterFollowResponse = {|
134134
| 'twitter-follow/user-not-found',
135135
|};
136136

137+
export type UpdateYoutubeSubscriptionResponse = {|
138+
+code:
139+
| 'youtube-subscription/badge-already-given'
140+
| 'youtube-subscription/badge-given'
141+
| 'youtube-subscription/channel-not-subscribed'
142+
| 'youtube-subscription/user-not-found',
143+
|};
144+
137145
export type IdentityProvider = 'google' | 'apple' | 'github';
138146

139147
export default class Authentication {
@@ -499,6 +507,31 @@ export default class Authentication {
499507
return response.data;
500508
};
501509

510+
updateYoutubeSubscription = async (
511+
getAuthorizationHeader: () => Promise<string>
512+
): Promise<UpdateYoutubeSubscriptionResponse> => {
513+
const { currentUser } = this.auth;
514+
if (!currentUser)
515+
throw new Error(
516+
'Tried to update youtube subscription while not authenticated.'
517+
);
518+
const { uid } = currentUser;
519+
520+
const authorizationHeader = await getAuthorizationHeader();
521+
const response = await axios.post(
522+
`${
523+
GDevelopUserApi.baseUrl
524+
}/user/${uid}/action/update-youtube-subscription`,
525+
{},
526+
{
527+
params: { userId: uid },
528+
headers: { Authorization: authorizationHeader },
529+
}
530+
);
531+
532+
return response.data;
533+
};
534+
502535
acceptGameStatsEmail = async (
503536
getAuthorizationHeader: () => Promise<string>,
504537
value: boolean

newIDE/app/src/Utils/GDevelopServices/User.js

+30-1
Original file line numberDiff line numberDiff line change
@@ -656,8 +656,37 @@ export const communityLinksConfig = {
656656
},
657657
youtubeUsername: {
658658
icon: <YouTube />,
659-
prefix: 'https://youtube.com/',
659+
prefix: 'https://youtube.com/@',
660660
maxLength: 100,
661+
getMessageFromUpdate: (responseCode: string) => {
662+
if (
663+
responseCode === 'youtube-subscription/badge-given' ||
664+
responseCode === 'youtube-subscription/badge-already-given'
665+
) {
666+
return {
667+
title: t`You're awesome!`,
668+
message: t`Thanks for following GDevelop. We added credits to your account as a thank you gift.`,
669+
};
670+
} else if (
671+
responseCode === 'youtube-subscription/channel-not-subscribed'
672+
) {
673+
return {
674+
title: t`We could not check your subscription`,
675+
message: t`Make sure you subscribed to the GDevelop channel and try again.`,
676+
};
677+
} else if (responseCode === 'youtube-subscription/user-not-found') {
678+
return {
679+
title: t`We could not find your user`,
680+
message: t`Make sure your username is correct, subscribe to the GDevelop channel and try again.`,
681+
};
682+
}
683+
684+
return null;
685+
},
686+
getRewardMessage: (hasBadge: boolean, rewardValueInCredits: string) =>
687+
!hasBadge
688+
? t`[Subscribe to GDevelop](https://youtube.com/@gdevelopapp) and enter your YouTube username here to get ${rewardValueInCredits} free credits as a thank you!`
689+
: t`Thank you for supporting GDevelop. Credits were added to your account as a thank you.`,
661690
},
662691
tiktokUsername: {
663692
icon: <TikTok />,

newIDE/app/src/stories/componentStories/Profile/EditProfileDialog.stories.js

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const defaultProps: EditProfileDialogProps = {
3131
onUpdateTwitterFollow: async () => ({
3232
code: 'twitter-follow/badge-already-given',
3333
}),
34+
onUpdateYoutubeSubscription: async () => ({
35+
code: 'youtube-subscription/badge-already-given',
36+
}),
3437
achievements: fakeAchievements,
3538
badges: [
3639
{

0 commit comments

Comments
 (0)