From d36f829d7e39261348fad4e6bb3b18cf8aa2504e Mon Sep 17 00:00:00 2001 From: Florian Rival Date: Wed, 22 May 2024 15:14:00 +0200 Subject: [PATCH] Add reward when following GDevelop Tiktok account --- .../src/Profile/AuthenticatedUserProvider.js | 34 +++ newIDE/app/src/Profile/CreateAccountForm.js | 10 +- newIDE/app/src/Profile/EditProfileDialog.js | 248 ++++++++++-------- newIDE/app/src/Profile/LoginForm.js | 10 +- newIDE/app/src/Profile/ProfileDetails.js | 187 ++++++------- newIDE/app/src/UI/CustomSvgIcons/Apple.js | 6 +- newIDE/app/src/UI/CustomSvgIcons/GitHub.js | 6 +- newIDE/app/src/UI/CustomSvgIcons/Google.js | 6 +- newIDE/app/src/Utils/Analytics/EventSender.js | 4 +- .../Utils/GDevelopServices/Authentication.js | 29 ++ newIDE/app/src/Utils/GDevelopServices/User.js | 65 ++++- 11 files changed, 388 insertions(+), 217 deletions(-) diff --git a/newIDE/app/src/Profile/AuthenticatedUserProvider.js b/newIDE/app/src/Profile/AuthenticatedUserProvider.js index 3153d3c1b2d5..cc0cc6f77aee 100644 --- a/newIDE/app/src/Profile/AuthenticatedUserProvider.js +++ b/newIDE/app/src/Profile/AuthenticatedUserProvider.js @@ -10,6 +10,7 @@ import { getUserBadges, listDefaultRecommendations, listRecommendations, + type CommunityLinks, } from '../Utils/GDevelopServices/User'; import { getAchievements } from '../Utils/GDevelopServices/Badge'; import Authentication, { @@ -1299,6 +1300,36 @@ export default class AuthenticatedUserProvider extends React.Component< } }; + _onUpdateTiktokFollow = async ( + communityLinks: CommunityLinks, + preferences: PreferencesValues + ) => { + const { authentication } = this.props; + + await this._doEdit( + { + communityLinks, + }, + preferences + ); + + this.setState({ + editInProgress: true, + }); + try { + const response = await authentication.updateTiktokFollow( + authentication.getAuthorizationHeader + ); + this._fetchUserBadges(); + + return response; + } finally { + this.setState({ + editInProgress: false, + }); + } + }; + render() { return ( @@ -1343,6 +1374,9 @@ export default class AuthenticatedUserProvider extends React.Component< onUpdateGitHubStar={githubUsername => this._onUpdateGithubStar(githubUsername, preferences) } + onUpdateTiktokFollow={communityLinks => + this._onUpdateTiktokFollow(communityLinks, preferences) + } onDelete={this._doDeleteAccount} actionInProgress={ this.state.editInProgress || this.state.deleteInProgress diff --git a/newIDE/app/src/Profile/CreateAccountForm.js b/newIDE/app/src/Profile/CreateAccountForm.js index 64c3d9caa0d1..9fdadfa29295 100644 --- a/newIDE/app/src/Profile/CreateAccountForm.js +++ b/newIDE/app/src/Profile/CreateAccountForm.js @@ -29,6 +29,10 @@ const styles = { alignItems: 'stretch', marginTop: 30, }, + icon: { + width: 16, + height: 16, + } }; type Props = {| @@ -139,7 +143,7 @@ const CreateAccountForm = ({ primary fullWidth label="Google" - leftIcon={} + leftIcon={} onClick={() => { onLoginWithProvider('google'); }} @@ -149,7 +153,7 @@ const CreateAccountForm = ({ primary fullWidth label="GitHub" - leftIcon={} + leftIcon={} onClick={() => { onLoginWithProvider('github'); }} @@ -159,7 +163,7 @@ const CreateAccountForm = ({ primary fullWidth label="Apple" - leftIcon={} + leftIcon={} onClick={() => { onLoginWithProvider('apple'); }} diff --git a/newIDE/app/src/Profile/EditProfileDialog.js b/newIDE/app/src/Profile/EditProfileDialog.js index 4ffa05182a99..28ece2b0dddc 100644 --- a/newIDE/app/src/Profile/EditProfileDialog.js +++ b/newIDE/app/src/Profile/EditProfileDialog.js @@ -4,6 +4,7 @@ import { Trans, t } from '@lingui/macro'; import * as React from 'react'; import { I18n } from '@lingui/react'; import { type I18n as I18nType } from '@lingui/core'; +import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow'; import FlatButton from '../UI/FlatButton'; import Dialog, { DialogPrimaryButton } from '../UI/Dialog'; import { @@ -11,14 +12,15 @@ import { type AuthError, type Profile, type UpdateGitHubStarResponse, + type UpdateTiktokFollowResponse, } from '../Utils/GDevelopServices/Authentication'; import { communityLinksConfig, donateLinkConfig, discordUsernameConfig, - githubUsernameConfig, type UsernameAvailability, type CommunityLinkType, + type CommunityLinks, } from '../Utils/GDevelopServices/User'; import { type Badge, type Achievement } from '../Utils/GDevelopServices/Badge'; import { @@ -45,7 +47,7 @@ import useAlertDialog from '../UI/Alert/useAlertDialog'; import Form from '../UI/Form'; import RaisedButton from '../UI/RaisedButton'; import Coin from '../Credits/Icons/Coin'; -import { sendGitHubStarUpdated } from '../Utils/Analytics/EventSender'; +import { sendSocialFollowUpdated } from '../Utils/Analytics/EventSender'; export type EditProfileDialogProps = {| profile: Profile, @@ -57,6 +59,9 @@ export type EditProfileDialogProps = {| onUpdateGitHubStar: ( githubUsername: string ) => Promise, + onUpdateTiktokFollow: ( + communityLinks: CommunityLinks + ) => Promise, onDelete: () => Promise, actionInProgress: boolean, error: ?AuthError, @@ -72,113 +77,120 @@ export const getUsernameErrorText = (error: ?AuthError) => { return undefined; }; -const GitHubUsernameField = ({ +const CommunityLinkWithFollow = ({ badges, achievements, - githubUsername, - onSetGithubUsername, - onUpdateGitHubStar, + achievementId, + value, + onChange, + onUpdateFollow, + getMessageFromUpdate, disabled, + maxLength, + prefix, + getRewardMessage, + translatableHintText, + icon, }: { badges: ?Array, achievements: ?Array, - githubUsername: string, - onSetGithubUsername: (username: string) => void, - onUpdateGitHubStar: ( - githubUsername: string - ) => Promise, + achievementId: string, + value: string, + onChange: (value: string) => void, + onUpdateFollow: () => Promise, + getMessageFromUpdate: ( + responseCode: string + ) => null | {| + title: MessageDescriptor, + message: MessageDescriptor, + |}, + getRewardMessage: ( + hasBadge: boolean, + rewardValueInCredits: string + ) => MessageDescriptor, disabled: boolean, + maxLength: number, + prefix: string, + translatableHintText?: string, + icon: React.Node, }) => { const { showAlert } = useAlertDialog(); - const hasGithubStarBadge = - !!badges && badges.some(badge => badge.achievementId === 'github-star'); - const githubStarAchievement = + const hasBadge = + !!badges && badges.some(badge => badge.achievementId === achievementId); + const achievement = (achievements && - achievements.find(achievement => achievement.id === 'github-star')) || + achievements.find(achievement => achievement.id === achievementId)) || null; const onClaim = React.useCallback( async () => { try { - const response = await onUpdateGitHubStar(githubUsername); - sendGitHubStarUpdated({ code: response.code }); + const response = await onUpdateFollow(); + sendSocialFollowUpdated(achievementId, { code: response.code }); - if ( - response.code === 'github-star/badge-given' || - response.code === 'github-star/badge-already-given' - ) { - showAlert({ - title: t`You're awesome!`, - message: t`Thanks for starring GDevelop repository. We added credits to your account as a thank you gift.`, - }); - } else if (response.code === 'github-star/repository-not-starred') { - showAlert({ - title: t`We could not find your GitHub star`, - message: t`Make sure you star the repository called 4ian/GDevelop with your GitHub user and try again.`, - }); - } else if (response.code === 'github-star/user-not-found') { - showAlert({ - title: t`We could not find your GitHub user and star`, - message: t`Make sure you create your GitHub account, star the repository called 4ian/GDevelop, enter your username here and try again.`, - }); + const messageAndTitle = getMessageFromUpdate(response.code); + if (messageAndTitle) { + showAlert({ ...messageAndTitle }); } else { throw new Error( - `Error while updating the GitHub star: ${response.code}.` + `Error while updating the social follow: ${response.code}.` ); } } catch (error) { - console.error('Error while updating GitHub star:', error); + console.error('Error while updating social follow:', error); showAlert({ title: t`Something went wrong`, message: t`Make sure you have a proper internet connection or try again later.`, }); } }, - [githubUsername, onUpdateGitHubStar, showAlert] + [onUpdateFollow, achievementId, getMessageFromUpdate, showAlert] ); return ( {({ i18n }) => ( - ( - } - label={ - hasGithubStarBadge ? ( - Credits given - ) : ( - Claim - ) - } - disabled={hasGithubStarBadge || disabled} - primary - style={style} - /> - )} - renderTextField={() => ( - GitHub username} - fullWidth - translatableHintText={t`Your GitHub username`} - onChange={(e, value) => { - onSetGithubUsername(value); - }} - disabled={disabled} - maxLength={githubUsernameConfig.maxLength} - helperMarkdownText={i18n._( - !hasGithubStarBadge - ? t`[Star the GDevelop repository](https://github.com/4ian/GDevelop) and add your GitHub username here to get ${(githubStarAchievement && - githubStarAchievement.rewardValueInCredits) || - '-'} free credits as a thank you!` - : t`Thank you for supporting the GDevelop open-source community. Credits were added to your account as a thank you.` - )} - /> - )} - /> + + {icon} + ( + } + label={ + hasBadge ? Credits given : Claim + } + disabled={hasBadge || disabled} + primary + style={style} + /> + )} + renderTextField={() => ( + { + onChange(value); + }} + startAdornment={ + prefix ? {prefix} : undefined + } + disabled={disabled} + maxLength={maxLength} + helperMarkdownText={i18n._( + getRewardMessage( + hasBadge, + achievement && achievement.rewardValueInCredits + ? achievement.rewardValueInCredits.toString() + : '-' + ) + )} + /> + )} + /> + )} ); @@ -230,6 +242,7 @@ const EditProfileDialog = ({ onClose, onEdit, onUpdateGitHubStar, + onUpdateTiktokFollow, onDelete, actionInProgress, error, @@ -321,6 +334,19 @@ const EditProfileDialog = ({ (!usernameAvailability || usernameAvailability.isAvailable) && !hasFormattingError; + const updatedCommunityLinks = { + personalWebsiteLink, + personalWebsite2Link, + twitterUsername, + facebookUsername, + youtubeUsername, + tiktokUsername, + instagramUsername, + redditUsername, + snapchatUsername, + discordServerLink, + }; + const edit = async () => { if (!canEdit) return; await onEdit({ @@ -331,18 +357,7 @@ const EditProfileDialog = ({ donateLink, discordUsername, githubUsername, - communityLinks: { - personalWebsiteLink, - personalWebsite2Link, - twitterUsername, - facebookUsername, - youtubeUsername, - tiktokUsername, - instagramUsername, - redditUsername, - snapchatUsername, - discordServerLink, - }, + communityLinks: updatedCommunityLinks, }); }; @@ -452,14 +467,6 @@ const EditProfileDialog = ({ t`Add your Discord username to get access to a dedicated channel if you have a Gold or Pro subscription! Join the [GDevelop Discord](https://discord.gg/gdevelop).` )} /> - Bio} @@ -478,6 +485,46 @@ const EditProfileDialog = ({ Socials + onUpdateGitHubStar(githubUsername)} + getMessageFromUpdate={ + communityLinksConfig.githubUsername.getMessageFromUpdate + } + disabled={actionInProgress} + maxLength={communityLinksConfig.githubUsername.maxLength} + prefix={communityLinksConfig.githubUsername.prefix} + getRewardMessage={ + communityLinksConfig.githubUsername.getRewardMessage + } + translatableHintText={t`username`} + icon={communityLinksConfig.githubUsername.icon} + /> + + onUpdateTiktokFollow(updatedCommunityLinks) + } + getMessageFromUpdate={ + communityLinksConfig.tiktokUsername.getMessageFromUpdate + } + disabled={actionInProgress} + maxLength={communityLinksConfig.tiktokUsername.maxLength} + prefix={communityLinksConfig.tiktokUsername.prefix} + getRewardMessage={ + communityLinksConfig.tiktokUsername.getRewardMessage + } + translatableHintText={t`username`} + icon={communityLinksConfig.tiktokUsername.icon} + /> - { - setTiktokUsername(value); - }} - disabled={actionInProgress} - /> } + leftIcon={} onClick={() => { onLoginWithProvider('google'); }} @@ -145,7 +149,7 @@ const LoginForm = ({ primary fullWidth label="GitHub" - leftIcon={} + leftIcon={} onClick={() => { onLoginWithProvider('github'); }} @@ -155,7 +159,7 @@ const LoginForm = ({ primary fullWidth label="Apple" - leftIcon={} + leftIcon={} onClick={() => { onLoginWithProvider('apple'); }} diff --git a/newIDE/app/src/Profile/ProfileDetails.js b/newIDE/app/src/Profile/ProfileDetails.js index 4058b1fa7f18..d01bb4659331 100644 --- a/newIDE/app/src/Profile/ProfileDetails.js +++ b/newIDE/app/src/Profile/ProfileDetails.js @@ -40,14 +40,20 @@ import { extractGDevelopApiErrorStatusAndCode } from '../Utils/GDevelopServices/ const CommunityLinksLines = ({ communityLinks, }: {| - communityLinks: Array<{ url: ?string, icon: React.Node }>, + communityLinks: Array<{ + text: ?React.Node, + isNotFilled?: boolean, + icon: React.Node, + }>, |}) => ( - {communityLinks.map(({ url, icon }, index) => - url ? ( + {communityLinks.map(({ text, isNotFilled, icon }, index) => + text ? ( {icon} - {url} + + {text} + ) : null )} @@ -110,6 +116,10 @@ const ProfileDetails = ({ (achievements && achievements.find(achievement => achievement.id === 'github-star')) || null; + const tiktokFollowAchievement = + (achievements && + achievements.find(achievement => achievement.id === 'tiktok-follow')) || + null; const [ discordUsernameSyncStatus, @@ -214,7 +224,7 @@ const ProfileDetails = ({ )} - + {!discordUsername ? ( !canUserBenefitFromDiscordRole ? ( - { - - - GitHub username - - - {!githubUsername ? ( - - ) : ( - githubUsername - )} - - - } Bio - + {profile.description || No bio defined.} - + + Socials + + + ) : ( + githubUsername + ), + isNotFilled: !githubUsername, + icon: communityLinksConfig.githubUsername.icon, + }, + { + text: !tiktokUsername ? ( + + ) : ( tiktokUsername - : undefined, - icon: communityLinksConfig.tiktokUsername.icon, - }, - { - url: instagramUsername - ? communityLinksConfig.instagramUsername.prefix + - instagramUsername - : undefined, - icon: communityLinksConfig.instagramUsername.icon, - }, - { - url: redditUsername - ? communityLinksConfig.redditUsername.prefix + - redditUsername - : undefined, - icon: communityLinksConfig.redditUsername.icon, - }, - { - url: snapchatUsername - ? communityLinksConfig.snapchatUsername.prefix + - snapchatUsername - : undefined, - icon: communityLinksConfig.snapchatUsername.icon, - }, - { - url: discordServerLink, - icon: communityLinksConfig.discordServerLink.icon, - }, - ]} - /> - + ), + isNotFilled: !tiktokUsername, + icon: communityLinksConfig.tiktokUsername.icon, + }, + { + text: personalWebsiteLink, + icon: communityLinksConfig.personalWebsiteLink.icon, + }, + { + text: personalWebsite2Link, + icon: communityLinksConfig.personalWebsite2Link.icon, + }, + { + text: twitterUsername, + icon: communityLinksConfig.twitterUsername.icon, + }, + { + text: facebookUsername, + icon: communityLinksConfig.facebookUsername.icon, + }, + { + text: youtubeUsername, + icon: communityLinksConfig.youtubeUsername.icon, + }, + { + text: instagramUsername, + icon: communityLinksConfig.instagramUsername.icon, + }, + { + text: redditUsername, + icon: communityLinksConfig.redditUsername.icon, + }, + { + text: snapchatUsername, + icon: communityLinksConfig.snapchatUsername.icon, + }, + { + text: discordServerLink, + icon: communityLinksConfig.discordServerLink.icon, + }, + ]} + /> + Donate link - {donateLink || No link defined.} + + {donateLink || No link defined.} + diff --git a/newIDE/app/src/UI/CustomSvgIcons/Apple.js b/newIDE/app/src/UI/CustomSvgIcons/Apple.js index 1b7f126701db..67f1014c4186 100644 --- a/newIDE/app/src/UI/CustomSvgIcons/Apple.js +++ b/newIDE/app/src/UI/CustomSvgIcons/Apple.js @@ -3,9 +3,9 @@ import SvgIcon from '@material-ui/core/SvgIcon'; export default React.memo(props => ( ( ( { - recordEvent('github-star-updated', metadata); +export const sendSocialFollowUpdated = (achievementId: string, metadata: {| code: string |}) => { + recordEvent(`${achievementId}-updated`, metadata); }; const inAppTutorialProgressLastFiredEvents: { diff --git a/newIDE/app/src/Utils/GDevelopServices/Authentication.js b/newIDE/app/src/Utils/GDevelopServices/Authentication.js index 09d28354ca26..967cb49ce037 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Authentication.js +++ b/newIDE/app/src/Utils/GDevelopServices/Authentication.js @@ -116,6 +116,14 @@ export type UpdateGitHubStarResponse = {| | 'github-star/user-not-found', |}; +export type UpdateTiktokFollowResponse = {| + +code: + | 'tiktok-follow/badge-already-given' + | 'tiktok-follow/badge-given' + | 'tiktok-follow/account-not-followed' + | 'tiktok-follow/user-not-found', +|}; + export type IdentityProvider = 'google' | 'apple' | 'github'; export default class Authentication { @@ -435,6 +443,27 @@ export default class Authentication { return response.data; }; + updateTiktokFollow = async ( + getAuthorizationHeader: () => Promise + ): Promise => { + const { currentUser } = this.auth; + if (!currentUser) + throw new Error('Tried to update tiktok follow while not authenticated.'); + const { uid } = currentUser; + + const authorizationHeader = await getAuthorizationHeader(); + const response = await axios.post( + `${GDevelopUserApi.baseUrl}/user/${uid}/action/update-tiktok-follow`, + {}, + { + params: { userId: uid }, + headers: { Authorization: authorizationHeader }, + } + ); + + return response.data; + }; + acceptGameStatsEmail = async ( getAuthorizationHeader: () => Promise, value: boolean diff --git a/newIDE/app/src/Utils/GDevelopServices/User.js b/newIDE/app/src/Utils/GDevelopServices/User.js index 972e0a5866a7..1cd3c05a060c 100644 --- a/newIDE/app/src/Utils/GDevelopServices/User.js +++ b/newIDE/app/src/Utils/GDevelopServices/User.js @@ -1,6 +1,6 @@ // @flow import * as React from 'react'; -import { Trans } from '@lingui/macro'; +import { Trans, t } from '@lingui/macro'; import axios from 'axios'; import Discord from '../../UI/CustomSvgIcons/Discord'; import Facebook from '../../UI/CustomSvgIcons/Facebook'; @@ -11,6 +11,7 @@ import Snapchat from '../../UI/CustomSvgIcons/Snapchat'; import TikTok from '../../UI/CustomSvgIcons/TikTok'; import Twitter from '../../UI/CustomSvgIcons/Twitter'; import YouTube from '../../UI/CustomSvgIcons/YouTube'; +import GitHub from '../../UI/CustomSvgIcons/GitHub'; import { GDevelopUserApi } from './ApiConfigs'; import { type MessageByLocale } from '../i18n/MessageByLocale'; @@ -399,10 +400,6 @@ export const discordUsernameConfig = { maxLength: 32, }; -export const githubUsernameConfig = { - maxLength: 39, -}; - export const communityLinksConfig = { personalWebsiteLink: { icon: , @@ -420,6 +417,37 @@ export const communityLinksConfig = { : undefined, maxLength: 150, }, + githubUsername: { + icon: , + prefix: 'https://github.com/', + maxLength: 39, + getMessageFromUpdate: (responseCode: string) => { + if ( + responseCode === 'github-star/badge-given' || + responseCode === 'github-star/badge-already-given' + ) { + return { + title: t`You're awesome!`, + message: t`Thanks for starring GDevelop repository. We added credits to your account as a thank you gift.`, + }; + } else if (responseCode === 'github-star/repository-not-starred') { + return { + title: t`We could not find your GitHub star`, + message: t`Make sure you star the repository called 4ian/GDevelop with your GitHub user and try again.`, + }; + } else if (responseCode === 'github-star/user-not-found') { + return { + title: t`We could not find your GitHub user and star`, + message: t`Make sure you create your GitHub account, star the repository called 4ian/GDevelop, enter your username here and try again.`, + }; + } + return null; + }, + getRewardMessage: (hasBadge: boolean, rewardValueInCredits: string) => + !hasBadge + ? t`[Star the GDevelop repository](https://github.com/4ian/GDevelop) and add your GitHub username here to get ${rewardValueInCredits} free credits as a thank you!` + : t`Thank you for supporting the GDevelop open-source community. Credits were added to your account as a thank you.`, + }, twitterUsername: { icon: , prefix: 'https://twitter.com/', @@ -443,6 +471,33 @@ export const communityLinksConfig = { ? tiktokUsernameFormattingErrorMessage : undefined, maxLength: 30, + getMessageFromUpdate: (responseCode: string) => { + if ( + responseCode === 'tiktok-follow/badge-given' || + responseCode === 'tiktok-follow/badge-already-given' + ) { + return { + title: t`You're awesome!`, + message: t`Thanks for following GDevelop. We added credits to your account as a thank you gift.`, + }; + } else if (responseCode === 'tiktok-follow/account-not-followed') { + return { + title: t`We could not check your follow`, + message: t`Make sure you follow the GDevelop account and try again.`, + }; + } else if (responseCode === 'tiktok-follow/user-not-found') { + return { + title: t`We could not find your user`, + message: t`Make sure your username is correct, follow the GDevelop account and try again.`, + }; + } + + return null; + }, + getRewardMessage: (hasBadge: boolean, rewardValueInCredits: string) => + !hasBadge + ? t`[Follow GDevelop](https://tiktok.com/@gdevelop) and enter your TikTok username here to get ${rewardValueInCredits} free credits as a thank you!` + : t`Thank you for supporting GDevelop. Credits were added to your account as a thank you.`, }, instagramUsername: { icon: ,