Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⛑️ YPP requirements rework #5136

Merged
merged 5 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/atlas/src/hooks/useSegmentAnalytics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useRef } from 'react'

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

export type videoPlaybackParams = {
videoId: string
Expand Down Expand Up @@ -349,6 +350,17 @@ export const useSegmentAnalytics = () => {
[analytics]
)

const trackYppReqsNotMet = useCallback(
(
errors: YppRequirementsErrorCode[],
utmSource: string | null | undefined,
utmCampaign: string | null | undefined
) => {
analytics.track('YPP Sign Up Failed - Reqs Not Met', { errors, utmSource, utmCampaign })
},
[analytics]
)

const trackLogout = useCallback(() => {
analytics.reset()
}, [analytics])
Expand Down Expand Up @@ -420,6 +432,7 @@ export const useSegmentAnalytics = () => {
trackVideoUpload,
trackWithdrawnFunds,
trackYppOptIn,
trackYppReqsNotMet,
trackYppSignInButtonClick,
}
}
18 changes: 18 additions & 0 deletions packages/atlas/src/utils/misc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ReactNode } from 'react'

import { formatNumber } from '@/utils/number'

export class TimeoutError<T> extends Error {
Expand Down Expand Up @@ -91,3 +93,19 @@ export function convertUpperCamelToSentence(input?: string) {
})
.join(' ')
}

export function replaceTemplateWithRenderer(
template: string,
variables: string[],
render: (variable: string) => ReactNode
) {
const splitArray = template.split('{}')
const result: ReactNode[] = []
splitArray.forEach((part, idx) => {
result.push(part)
if (variables[idx]) {
result.push(render(variables[idx]))
}
})
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
trackPageView,
trackYppOptIn,
identifyUser,
trackYppReqsNotMet,
trackClickAuthModalSignUpButton,
trackClickAuthModalSignInButton,
} = useSegmentAnalytics()
Expand All @@ -154,9 +155,10 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced

const { displaySnackbar } = useSnackbar()

const { handleAuthorizeClick, alreadyRegisteredChannel } = useYppGoogleAuth({
channelsLoaded,
})
const { handleAuthorizeClick, ytRequirementsErrors, setYtRequirementsErrors, alreadyRegisteredChannel } =
useYppGoogleAuth({
channelsLoaded,
})

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

const handleClose = useCallback(() => {
setYtRequirementsErrors([])
setReferrerId(null)
setYppModalOpenName(null)
setSelectedChannelId(null)
setShouldContinueYppFlowAfterLogin(false)
}, [setYppModalOpenName, setReferrerId, setSelectedChannelId, setShouldContinueYppFlowAfterLogin])
}, [
setYppModalOpenName,
setReferrerId,
setSelectedChannelId,
setShouldContinueYppFlowAfterLogin,
setYtRequirementsErrors,
])

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

const handleSelectChannel = useCallback(
(selectedChannelId: string) => {
setYtRequirementsErrors([])
setSelectedChannelId(selectedChannelId)
},
[setSelectedChannelId]
[setSelectedChannelId, setYtRequirementsErrors]
)

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

useEffect(() => {
if (ytRequirementsErrors?.length) {
trackPageView('YPP Reqs Not Met')
trackYppReqsNotMet(ytRequirementsErrors, utmSource, utmCampaign)
}
}, [trackPageView, trackYppReqsNotMet, utmCampaign, utmSource, ytRequirementsErrors])

const selectedChannel = useMemo(() => {
if (!unSyncedChannels || !selectedChannelId) {
return null
Expand All @@ -364,6 +381,13 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
setSelectedChannelId(yppUnsyncedChannels[0].id)
}

if (ytRequirementsErrors.length) {
return {
text: 'Close',
onClick: handleClose,
}
}

if (yppUnsyncedChannels && yppUnsyncedChannels.length > 1) {
return {
text: 'Select channel',
Expand All @@ -383,11 +407,13 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
}

return {
title: 'Requirements',
description:
'Before you can apply to the program, make sure your YouTube channel meets the below conditions.',
headerIcon: ytRequirementsErrors.length ? <SvgAlertsError32 /> : undefined,
title: ytRequirementsErrors.length ? 'Authorization failed' : 'Requirements',
description: ytRequirementsErrors.length
? '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.'
: 'Before you can apply to the program, make sure your YouTube channel meets the below conditions.',
primaryButton: getPrimaryButton(),
component: <YppAuthorizationRequirementsStep />,
component: <YppAuthorizationRequirementsStep requirmentsErrorCodes={ytRequirementsErrors} />,
}
}

Expand Down Expand Up @@ -467,6 +493,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
handleSelectChannel,
alreadyRegisteredChannel?.channelTitle,
alreadyRegisteredChannel?.ownerMemberHandle,
ytRequirementsErrors,
isLoading,
yppCurrentChannel,
yppUnsyncedChannels,
Expand All @@ -484,7 +511,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
const isLoadingModal = yppModalOpenName === 'ypp-fetching-data' || yppModalOpenName === 'ypp-speaking-to-backend'

const secondaryButton: DialogButtonProps | undefined = useMemo(() => {
if (isLoadingModal) return
if (isLoadingModal || ytRequirementsErrors.length) return

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

Expand Down Expand Up @@ -517,6 +544,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
}
}, [
isLoadingModal,
ytRequirementsErrors.length,
yppModalOpenName,
isLoggedIn,
isSubmitting,
Expand All @@ -540,7 +568,8 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({ unSynced
primaryButton={authorizationStep?.primaryButton}
secondaryButton={secondaryButton}
additionalActionsNode={
!isLoadingModal && (
!isLoadingModal &&
!ytRequirementsErrors.length && (
<Button variant="tertiary" disabled={isSubmitting} onClick={handleClose}>
Cancel
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,13 @@ export type ChannelVerificationSuccessResponse = {
}

export type ChannelRequirements = {
MINIMUM_SUBSCRIBERS_COUNT: number
MINIMUM_TOTAL_VIDEOS_COUNT: number
MINIMUM_VIDEO_AGE_HOURS: number
MINIMUM_CHANNEL_AGE_HOURS: number
MINIMUM_VIDEOS_PER_MONTH: number
MONTHS_TO_CONSIDER: number
requirements: {
errorCode: YppRequirementsErrorCode
template: string
variables: string[]
}[]
}

export type Requirements = Record<keyof ChannelRequirements, number | undefined>

type ChannelRequirementsFailedError = {
code: YppRequirementsErrorCode
message: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,65 @@ import { FC, PropsWithChildren } from 'react'

import { SvgActionCheck, SvgActionClose } from '@/assets/icons'
import { Banner } from '@/components/Banner'
import { FlexBox } from '@/components/FlexBox'
import { Text } from '@/components/Text'
import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader'
import { atlasConfig } from '@/config'
import { replaceTemplateWithRenderer } from '@/utils/misc'

import { ListItem, Paragraph, StyledList, TickWrapper } from './YppAuthorizationRequirementsStep.styles'
import { useGetYppChannelRequirements } from './useGetYppChannelRequirements'

export const YppAuthorizationRequirementsStep: FC = () => {
import { YppAuthorizationErrorCode, YppRequirementsErrorCode } from '../../YppAuthorizationModal.types'

type YppAuthorizationRequirementsStepProps = {
requirmentsErrorCodes: YppRequirementsErrorCode[]
}

export const YppAuthorizationRequirementsStep: FC<YppAuthorizationRequirementsStepProps> = ({
requirmentsErrorCodes,
}) => {
const { requirements, isLoading } = useGetYppChannelRequirements()
const checkRequirmentError = (errorCode: YppAuthorizationErrorCode) =>
!requirmentsErrorCodes.some((error) => error === errorCode)
const hasAtLeastOneError = !!requirmentsErrorCodes.length
return (
<>
<StyledList>
<SingleRequirement fulfilled>Original content, without reposts from other creators.</SingleRequirement>
<SingleRequirement fulfilled>
<SingleRequirement fulfilled={checkRequirmentError(YppAuthorizationErrorCode.CHANNEL_STATUS_SUSPENDED)}>
Original content, without reposts from other creators.
</SingleRequirement>
<SingleRequirement fulfilled={checkRequirmentError(YppAuthorizationErrorCode.CHANNEL_STATUS_SUSPENDED)}>
Organic audience, without bots, purchased subscribers and fake comments.
</SingleRequirement>
{isLoading ? (
<>
<FlexBox alignItems="center" gap={2}>
<SkeletonLoader width={24} height={24} rounded />
<SkeletonLoader width={24 * 3} height={14} />
</FlexBox>
<FlexBox alignItems="center" gap={2}>
<SkeletonLoader width={24} height={24} rounded />
<SkeletonLoader width={24 * 3} height={14} />
</FlexBox>
</>
) : requirements ? (
requirements.map((requirement, idx) => (
<SingleRequirement key={idx} fulfilled={checkRequirmentError(requirement.errorCode)}>
{replaceTemplateWithRenderer(requirement.template, requirement.variables, (variable) => (
<Text variant="t200" as="span" color="colorTextCaution">
{variable}
</Text>
))}
</SingleRequirement>
))
) : null}
</StyledList>
{!hasAtLeastOneError && (
<Banner
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.`}
/>
</StyledList>
)}
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useQuery } from 'react-query'

import { axiosInstance } from '@/api/axios'
import { atlasConfig } from '@/config'
import { SentryLogger } from '@/utils/logs'

import { ChannelRequirements } from '../../YppAuthorizationModal.types'

export const useGetYppChannelRequirements = () => {
const { data, isLoading } = useQuery('ypp-requirements-fetch', () =>
axiosInstance
.get<ChannelRequirements>(`${atlasConfig.features.ypp.youtubeSyncApiUrl}/channels/induction/requirements`)
.then((res) => res.data)
.catch((error) => SentryLogger.error("Couldn't fetch requirements", 'YppAuthorizationModal.hooks', error))
)

return {
requirements: data?.requirements,
isLoading,
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useApolloClient } from '@apollo/client'
import { isArray } from 'lodash-es'
import { useCallback, useEffect, useState } from 'react'
import { useMutation } from 'react-query'
import { useSearchParams } from 'react-router-dom'
Expand All @@ -25,6 +26,7 @@ import {
ChannelVerificationErrorResponse,
ChannelVerificationSuccessResponse,
YppAuthorizationErrorCode,
YppRequirementsErrorCode,
} from './YppAuthorizationModal.types'

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

const { setSelectedChannelId } = useYppStore((state) => state.actions)
const [ytRequirementsErrors, setYtRequirementsErrors] = useState<YppRequirementsErrorCode[]>([])

const [alreadyRegisteredChannel, setAlreadyRegisteredChannel] = useState<AlreadyRegisteredChannel | null>(null)
const { mutateAsync: authMutation } = useMutation('ypp-auth-post', (authorizationCode: string) =>
axiosInstance.post<ChannelVerificationSuccessResponse>(`${atlasConfig.features.ypp.youtubeSyncApiUrl}/users`, {
Expand Down Expand Up @@ -194,6 +198,15 @@ export const useYppGoogleAuth = ({ channelsLoaded }: { channelsLoaded: boolean }
return
}

const isRequirementsError = isArray(errorMessages)
if (isRequirementsError) {
const errorCodes = isRequirementsError ? errorMessages?.map((message) => message.code) : undefined

errorCodes && setYtRequirementsErrors(errorCodes)
setYppModalOpenName('ypp-requirements')
return
}

if (errorResponseData && 'code' in errorResponseData) {
switch (errorResponseData.code) {
case YppAuthorizationErrorCode.YOUTUBE_QUOTA_LIMIT_EXCEEDED:
Expand All @@ -211,6 +224,11 @@ export const useYppGoogleAuth = ({ channelsLoaded }: { channelsLoaded: boolean }
description: `You don't have a YouTube channel.`,
iconType: 'error',
})
setYtRequirementsErrors([
YppAuthorizationErrorCode.CHANNEL_CRITERIA_UNMET_CREATION_DATE,
YppAuthorizationErrorCode.CHANNEL_CRITERIA_UNMET_VIDEOS,
YppAuthorizationErrorCode.CHANNEL_CRITERIA_UNMET_SUBSCRIBERS,
])
setYppModalOpenName('ypp-requirements')
return
case YppAuthorizationErrorCode.CHANNEL_ALREADY_REGISTERED: {
Expand Down Expand Up @@ -294,6 +312,8 @@ export const useYppGoogleAuth = ({ channelsLoaded }: { channelsLoaded: boolean }

return {
handleAuthorizeClick,
setYtRequirementsErrors,
ytRequirementsErrors,
alreadyRegisteredChannel,
}
}