Skip to content

Commit c66288c

Browse files
authored
⛑️ YPP requirements rework (#5136)
* Revert "💇 Remove YPP requirements (#5008)" This reverts commit 3a26a46. * Rework hook * Update modal step * Linter * Add templating for requirements
1 parent ebc7f2e commit c66288c

File tree

7 files changed

+164
-23
lines changed

7 files changed

+164
-23
lines changed

packages/atlas/src/hooks/useSegmentAnalytics.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useCallback, useRef } from 'react'
22

33
import { useSegmentAnalyticsContext } from '@/providers/segmentAnalytics/useSegmentAnalyticsContext'
4+
import { YppRequirementsErrorCode } from '@/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.types'
45

56
export type videoPlaybackParams = {
67
videoId: string
@@ -349,6 +350,17 @@ export const useSegmentAnalytics = () => {
349350
[analytics]
350351
)
351352

353+
const trackYppReqsNotMet = useCallback(
354+
(
355+
errors: YppRequirementsErrorCode[],
356+
utmSource: string | null | undefined,
357+
utmCampaign: string | null | undefined
358+
) => {
359+
analytics.track('YPP Sign Up Failed - Reqs Not Met', { errors, utmSource, utmCampaign })
360+
},
361+
[analytics]
362+
)
363+
352364
const trackLogout = useCallback(() => {
353365
analytics.reset()
354366
}, [analytics])
@@ -420,6 +432,7 @@ export const useSegmentAnalytics = () => {
420432
trackVideoUpload,
421433
trackWithdrawnFunds,
422434
trackYppOptIn,
435+
trackYppReqsNotMet,
423436
trackYppSignInButtonClick,
424437
}
425438
}

packages/atlas/src/utils/misc.ts

+18
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ReactNode } from 'react'
2+
13
import { formatNumber } from '@/utils/number'
24

35
export class TimeoutError<T> extends Error {
@@ -91,3 +93,19 @@ export function convertUpperCamelToSentence(input?: string) {
9193
})
9294
.join(' ')
9395
}
96+
97+
export function replaceTemplateWithRenderer(
98+
template: string,
99+
variables: string[],
100+
render: (variable: string) => ReactNode
101+
) {
102+
const splitArray = template.split('{}')
103+
const result: ReactNode[] = []
104+
splitArray.forEach((part, idx) => {
105+
result.push(part)
106+
if (variables[idx]) {
107+
result.push(render(variables[idx]))
108+
}
109+
})
110+
return result
111+
}

packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx

+40-11
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
146146
trackPageView,
147147
trackYppOptIn,
148148
identifyUser,
149+
trackYppReqsNotMet,
149150
trackClickAuthModalSignUpButton,
150151
trackClickAuthModalSignInButton,
151152
} = useSegmentAnalytics()
@@ -154,9 +155,10 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
154155

155156
const { displaySnackbar } = useSnackbar()
156157

157-
const { handleAuthorizeClick, alreadyRegisteredChannel } = useYppGoogleAuth({
158-
channelsLoaded,
159-
})
158+
const { handleAuthorizeClick, ytRequirementsErrors, setYtRequirementsErrors, alreadyRegisteredChannel } =
159+
useYppGoogleAuth({
160+
channelsLoaded,
161+
})
160162

161163
useEffect(() => {
162164
if (searchParams.get('utm_source')) {
@@ -173,11 +175,18 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
173175
}, [trackPageView, yppModalOpenName])
174176

175177
const handleClose = useCallback(() => {
178+
setYtRequirementsErrors([])
176179
setReferrerId(null)
177180
setYppModalOpenName(null)
178181
setSelectedChannelId(null)
179182
setShouldContinueYppFlowAfterLogin(false)
180-
}, [setYppModalOpenName, setReferrerId, setSelectedChannelId, setShouldContinueYppFlowAfterLogin])
183+
}, [
184+
setYppModalOpenName,
185+
setReferrerId,
186+
setSelectedChannelId,
187+
setShouldContinueYppFlowAfterLogin,
188+
setYtRequirementsErrors,
189+
])
181190

182191
const handleGoBack = useCallback(() => {
183192
if (yppModalOpenName === 'ypp-sync-options') {
@@ -190,9 +199,10 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
190199

191200
const handleSelectChannel = useCallback(
192201
(selectedChannelId: string) => {
202+
setYtRequirementsErrors([])
193203
setSelectedChannelId(selectedChannelId)
194204
},
195-
[setSelectedChannelId]
205+
[setSelectedChannelId, setYtRequirementsErrors]
196206
)
197207

198208
const createOrUpdateChannel = useCreateEditChannelSubmit()
@@ -340,6 +350,13 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
340350
}
341351
}, [channel, detailsFormMethods, referrerId])
342352

353+
useEffect(() => {
354+
if (ytRequirementsErrors?.length) {
355+
trackPageView('YPP Reqs Not Met')
356+
trackYppReqsNotMet(ytRequirementsErrors, utmSource, utmCampaign)
357+
}
358+
}, [trackPageView, trackYppReqsNotMet, utmCampaign, utmSource, ytRequirementsErrors])
359+
343360
const selectedChannel = useMemo(() => {
344361
if (!unSyncedChannels || !selectedChannelId) {
345362
return null
@@ -364,6 +381,13 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
364381
setSelectedChannelId(yppUnsyncedChannels[0].id)
365382
}
366383

384+
if (ytRequirementsErrors.length) {
385+
return {
386+
text: 'Close',
387+
onClick: handleClose,
388+
}
389+
}
390+
367391
if (yppUnsyncedChannels && yppUnsyncedChannels.length > 1) {
368392
return {
369393
text: 'Select channel',
@@ -383,11 +407,13 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
383407
}
384408

385409
return {
386-
title: 'Requirements',
387-
description:
388-
'Before you can apply to the program, make sure your YouTube channel meets the below conditions.',
410+
headerIcon: ytRequirementsErrors.length ? <SvgAlertsError32 /> : undefined,
411+
title: ytRequirementsErrors.length ? 'Authorization failed' : 'Requirements',
412+
description: ytRequirementsErrors.length
413+
? 'Looks like the YouTube channel you selected does not meet all conditions to be enrolled in the program. You can select another one or try again at a later time.'
414+
: 'Before you can apply to the program, make sure your YouTube channel meets the below conditions.',
389415
primaryButton: getPrimaryButton(),
390-
component: <YppAuthorizationRequirementsStep />,
416+
component: <YppAuthorizationRequirementsStep requirmentsErrorCodes={ytRequirementsErrors} />,
391417
}
392418
}
393419

@@ -467,6 +493,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
467493
handleSelectChannel,
468494
alreadyRegisteredChannel?.channelTitle,
469495
alreadyRegisteredChannel?.ownerMemberHandle,
496+
ytRequirementsErrors,
470497
isLoading,
471498
yppCurrentChannel,
472499
yppUnsyncedChannels,
@@ -484,7 +511,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
484511
const isLoadingModal = yppModalOpenName === 'ypp-fetching-data' || yppModalOpenName === 'ypp-speaking-to-backend'
485512

486513
const secondaryButton: DialogButtonProps | undefined = useMemo(() => {
487-
if (isLoadingModal) return
514+
if (isLoadingModal || ytRequirementsErrors.length) return
488515

489516
if (yppModalOpenName === 'ypp-requirements' && isLoggedIn) return
490517

@@ -517,6 +544,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
517544
}
518545
}, [
519546
isLoadingModal,
547+
ytRequirementsErrors.length,
520548
yppModalOpenName,
521549
isLoggedIn,
522550
isSubmitting,
@@ -540,7 +568,8 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
540568
primaryButton={authorizationStep?.primaryButton}
541569
secondaryButton={secondaryButton}
542570
additionalActionsNode={
543-
!isLoadingModal && (
571+
!isLoadingModal &&
572+
!ytRequirementsErrors.length && (
544573
<Button variant="tertiary" disabled={isSubmitting} onClick={handleClose}>
545574
Cancel
546575
</Button>

packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.types.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,13 @@ export type ChannelVerificationSuccessResponse = {
3636
}
3737

3838
export type ChannelRequirements = {
39-
MINIMUM_SUBSCRIBERS_COUNT: number
40-
MINIMUM_TOTAL_VIDEOS_COUNT: number
41-
MINIMUM_VIDEO_AGE_HOURS: number
42-
MINIMUM_CHANNEL_AGE_HOURS: number
43-
MINIMUM_VIDEOS_PER_MONTH: number
44-
MONTHS_TO_CONSIDER: number
39+
requirements: {
40+
errorCode: YppRequirementsErrorCode
41+
template: string
42+
variables: string[]
43+
}[]
4544
}
4645

47-
export type Requirements = Record<keyof ChannelRequirements, number | undefined>
48-
4946
type ChannelRequirementsFailedError = {
5047
code: YppRequirementsErrorCode
5148
message: string

packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationRequirementsStep/YppAuthorizationRequirementsStep.tsx

+47-4
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,65 @@ import { FC, PropsWithChildren } from 'react'
22

33
import { SvgActionCheck, SvgActionClose } from '@/assets/icons'
44
import { Banner } from '@/components/Banner'
5+
import { FlexBox } from '@/components/FlexBox'
6+
import { Text } from '@/components/Text'
7+
import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader'
58
import { atlasConfig } from '@/config'
9+
import { replaceTemplateWithRenderer } from '@/utils/misc'
610

711
import { ListItem, Paragraph, StyledList, TickWrapper } from './YppAuthorizationRequirementsStep.styles'
12+
import { useGetYppChannelRequirements } from './useGetYppChannelRequirements'
813

9-
export const YppAuthorizationRequirementsStep: FC = () => {
14+
import { YppAuthorizationErrorCode, YppRequirementsErrorCode } from '../../YppAuthorizationModal.types'
15+
16+
type YppAuthorizationRequirementsStepProps = {
17+
requirmentsErrorCodes: YppRequirementsErrorCode[]
18+
}
19+
20+
export const YppAuthorizationRequirementsStep: FC<YppAuthorizationRequirementsStepProps> = ({
21+
requirmentsErrorCodes,
22+
}) => {
23+
const { requirements, isLoading } = useGetYppChannelRequirements()
24+
const checkRequirmentError = (errorCode: YppAuthorizationErrorCode) =>
25+
!requirmentsErrorCodes.some((error) => error === errorCode)
26+
const hasAtLeastOneError = !!requirmentsErrorCodes.length
1027
return (
1128
<>
1229
<StyledList>
13-
<SingleRequirement fulfilled>Original content, without reposts from other creators.</SingleRequirement>
14-
<SingleRequirement fulfilled>
30+
<SingleRequirement fulfilled={checkRequirmentError(YppAuthorizationErrorCode.CHANNEL_STATUS_SUSPENDED)}>
31+
Original content, without reposts from other creators.
32+
</SingleRequirement>
33+
<SingleRequirement fulfilled={checkRequirmentError(YppAuthorizationErrorCode.CHANNEL_STATUS_SUSPENDED)}>
1534
Organic audience, without bots, purchased subscribers and fake comments.
1635
</SingleRequirement>
36+
{isLoading ? (
37+
<>
38+
<FlexBox alignItems="center" gap={2}>
39+
<SkeletonLoader width={24} height={24} rounded />
40+
<SkeletonLoader width={24 * 3} height={14} />
41+
</FlexBox>
42+
<FlexBox alignItems="center" gap={2}>
43+
<SkeletonLoader width={24} height={24} rounded />
44+
<SkeletonLoader width={24 * 3} height={14} />
45+
</FlexBox>
46+
</>
47+
) : requirements ? (
48+
requirements.map((requirement, idx) => (
49+
<SingleRequirement key={idx} fulfilled={checkRequirmentError(requirement.errorCode)}>
50+
{replaceTemplateWithRenderer(requirement.template, requirement.variables, (variable) => (
51+
<Text variant="t200" as="span" color="colorTextCaution">
52+
{variable}
53+
</Text>
54+
))}
55+
</SingleRequirement>
56+
))
57+
) : null}
58+
</StyledList>
59+
{!hasAtLeastOneError && (
1760
<Banner
1861
description={`${atlasConfig.general.appName} uses Google OAuth to get access to your public profile and account email address as part of sign up flow, and integrates with YouTube API to obtain details about your YouTube channel data, such as followers and video statistics.`}
1962
/>
20-
</StyledList>
63+
)}
2164
</>
2265
)
2366
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useQuery } from 'react-query'
2+
3+
import { axiosInstance } from '@/api/axios'
4+
import { atlasConfig } from '@/config'
5+
import { SentryLogger } from '@/utils/logs'
6+
7+
import { ChannelRequirements } from '../../YppAuthorizationModal.types'
8+
9+
export const useGetYppChannelRequirements = () => {
10+
const { data, isLoading } = useQuery('ypp-requirements-fetch', () =>
11+
axiosInstance
12+
.get<ChannelRequirements>(`${atlasConfig.features.ypp.youtubeSyncApiUrl}/channels/induction/requirements`)
13+
.then((res) => res.data)
14+
.catch((error) => SentryLogger.error("Couldn't fetch requirements", 'YppAuthorizationModal.hooks', error))
15+
)
16+
17+
return {
18+
requirements: data?.requirements,
19+
isLoading,
20+
}
21+
}

packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/useYppGoogleAuth.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useApolloClient } from '@apollo/client'
2+
import { isArray } from 'lodash-es'
23
import { useCallback, useEffect, useState } from 'react'
34
import { useMutation } from 'react-query'
45
import { useSearchParams } from 'react-router-dom'
@@ -25,6 +26,7 @@ import {
2526
ChannelVerificationErrorResponse,
2627
ChannelVerificationSuccessResponse,
2728
YppAuthorizationErrorCode,
29+
YppRequirementsErrorCode,
2830
} from './YppAuthorizationModal.types'
2931

3032
const GOOGLE_CONSOLE_CLIENT_ID = atlasConfig.features.ypp.googleConsoleClientId
@@ -51,6 +53,8 @@ export const useYppGoogleAuth = ({ channelsLoaded }: { channelsLoaded: boolean }
5153
const { isLoggedIn, isAuthenticating } = useAuth()
5254

5355
const { setSelectedChannelId } = useYppStore((state) => state.actions)
56+
const [ytRequirementsErrors, setYtRequirementsErrors] = useState<YppRequirementsErrorCode[]>([])
57+
5458
const [alreadyRegisteredChannel, setAlreadyRegisteredChannel] = useState<AlreadyRegisteredChannel | null>(null)
5559
const { mutateAsync: authMutation } = useMutation('ypp-auth-post', (authorizationCode: string) =>
5660
axiosInstance.post<ChannelVerificationSuccessResponse>(`${atlasConfig.features.ypp.youtubeSyncApiUrl}/users`, {
@@ -194,6 +198,15 @@ export const useYppGoogleAuth = ({ channelsLoaded }: { channelsLoaded: boolean }
194198
return
195199
}
196200

201+
const isRequirementsError = isArray(errorMessages)
202+
if (isRequirementsError) {
203+
const errorCodes = isRequirementsError ? errorMessages?.map((message) => message.code) : undefined
204+
205+
errorCodes && setYtRequirementsErrors(errorCodes)
206+
setYppModalOpenName('ypp-requirements')
207+
return
208+
}
209+
197210
if (errorResponseData && 'code' in errorResponseData) {
198211
switch (errorResponseData.code) {
199212
case YppAuthorizationErrorCode.YOUTUBE_QUOTA_LIMIT_EXCEEDED:
@@ -211,6 +224,11 @@ export const useYppGoogleAuth = ({ channelsLoaded }: { channelsLoaded: boolean }
211224
description: `You don't have a YouTube channel.`,
212225
iconType: 'error',
213226
})
227+
setYtRequirementsErrors([
228+
YppAuthorizationErrorCode.CHANNEL_CRITERIA_UNMET_CREATION_DATE,
229+
YppAuthorizationErrorCode.CHANNEL_CRITERIA_UNMET_VIDEOS,
230+
YppAuthorizationErrorCode.CHANNEL_CRITERIA_UNMET_SUBSCRIBERS,
231+
])
214232
setYppModalOpenName('ypp-requirements')
215233
return
216234
case YppAuthorizationErrorCode.CHANNEL_ALREADY_REGISTERED: {
@@ -294,6 +312,8 @@ export const useYppGoogleAuth = ({ channelsLoaded }: { channelsLoaded: boolean }
294312

295313
return {
296314
handleAuthorizeClick,
315+
setYtRequirementsErrors,
316+
ytRequirementsErrors,
297317
alreadyRegisteredChannel,
298318
}
299319
}

0 commit comments

Comments
 (0)