Skip to content

Commit

Permalink
First YPP flow
Browse files Browse the repository at this point in the history
  • Loading branch information
ikprk committed Jun 9, 2024
1 parent 36d1b98 commit afdb845
Show file tree
Hide file tree
Showing 11 changed files with 555 additions and 12 deletions.
4 changes: 2 additions & 2 deletions packages/atlas/src/.env
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ VITE_WALLET_CONNECT_PROJECT_ID=33b2609463e399daee8c51726546c8dd

# YPP configuration
VITE_GOOGLE_CONSOLE_CLIENT_ID=246331758613-rc1psegmsr9l4e33nqu8rre3gno5dsca.apps.googleusercontent.com
VITE_YOUTUBE_SYNC_API_URL=https://35.156.81.207.nip.io
VITE_YOUTUBE_COLLABORATOR_MEMBER_ID=18
VITE_YOUTUBE_SYNC_API_URL=https://50.19.175.219.nip.io
VITE_YOUTUBE_COLLABORATOR_MEMBER_ID=83

# Analytics tools
VITE_GA_ID=
Expand Down
4 changes: 4 additions & 0 deletions packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { AccountSetup } from '../AccountSetup'
import { EmailSetup } from '../EmailSetup'
import { ExternalAccountSetup } from '../ExternalAccountSetup'
import { ForgotPasswordModal } from '../ForgotPasswordModal/ForgotPasswordModal'
import { YppFinishFlowModal } from '../YppFinishFlowModal'
import { YppFirstFlowModal } from '../YppFirstFlowModal/YppFirstFlowModal'

export const AuthModals = () => {
const { authModalOpenName } = useAuthStore()
Expand All @@ -27,6 +29,7 @@ export const AuthModals = () => {
<CreateChannelModal />
<EmailSetup />
<AccountSetup />
<YppFirstFlowModal />
</>
)
}
Expand All @@ -35,6 +38,7 @@ export const AuthModals = () => {
<>
{accountType === 'internal' ? <AccountSetup /> : null}
{accountType === 'external' ? <ExternalAccountSetup /> : null}
{accountType === 'ypp' ? <YppFinishFlowModal /> : null}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { useMemo, useState } from 'react'
import shallow from 'zustand/shallow'

import { Button } from '@/components/_buttons/Button'
import { DialogModal } from '@/components/_overlays/DialogModal'
import { useYppModalHandlers } from '@/hooks/useYppModalHandlers'
import { useAuth } from '@/providers/auth/auth.hooks'
import { useAuthStore } from '@/providers/auth/auth.store'
import { useUser } from '@/providers/user/user.hooks'
import { formatDurationBiggestTick } from '@/utils/time'

import { YppDetailsFormStep } from '../../../views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep'
import { CheckEmailConfirmation } from '../genericSteps/CheckEmailConfirmation'
import { OwnershipVerified } from '../genericSteps/OwnershipVerified'
import { ProveChannelOwnership } from '../genericSteps/ProveChannelOwnership'
import { ProvideEmailForLink } from '../genericSteps/ProvideEmailForLink'
import { WaitingModal } from '../genericSteps/WaitingModal'
import { SetActionButtonHandler } from '../genericSteps/types'

export enum YppFirstFlowStep {
ytVideoUrl = 'ytVideoUrl',
channelVerification = 'channelVerification',
ownershipProved = 'ownershipProved',
// user has no account
email = 'email',
confirmationLink = 'confirmationLink',

yppForm = 'yppForm',
// user has account, but no channel
channelCreation = 'channelCreation',
// user has channel
channelConnection = 'channelConnection',
}

export const YppFirstFlowModal = () => {
const { authModalOpenName, setAuthModalOpenName } = useAuthStore(
(state) => ({
authModalOpenName: state.authModalOpenName,
setAuthModalOpenName: state.actions.setAuthModalOpenName,
}),
shallow
)
const show = authModalOpenName === 'yppFirstFlow'
const { isLoggedIn } = useAuth()
const { channelId } = useUser()
const [step, setStep] = useState(YppFirstFlowStep.ytVideoUrl)
const [timeLeft, setTimeLeft] = useState<string>('')
const [primaryAction, setPrimaryAction] = useState<undefined | SetActionButtonHandler>(undefined)
const { formRef, validateYtChannel, updateOrCreateChannel, connectJoyChannelToYpp } = useYppModalHandlers()
const [loading, setLoading] = useState(false)

const primaryButton = useMemo(() => {
if (step === YppFirstFlowStep.ytVideoUrl) {
return {
text: 'Verify video',
onClick: () => primaryAction?.(),
}
}

if (step === YppFirstFlowStep.yppForm) {
return {
text: 'Sign up',
onClick: () => primaryAction?.(),
}
}

if (step === YppFirstFlowStep.ownershipProved) {
return {
text: 'Continue',
onClick: () => {
if (!isLoggedIn) {
setStep(YppFirstFlowStep.email)
return
}

setStep(YppFirstFlowStep.yppForm)
},
}
}

if (
[
YppFirstFlowStep.channelVerification,
YppFirstFlowStep.channelConnection,
YppFirstFlowStep.channelCreation,
].includes(step)
) {
return {
text: 'Waiting...',
onClick: () => undefined,
disabled: true,
}
}

if (step === YppFirstFlowStep.confirmationLink) {
return {
text: loading ? 'Sending...' : timeLeft ? `Resend (${timeLeft.replace('seconds', 's')})` : 'Resend',
onClick: () => primaryAction?.(setLoading),
disabled: !!timeLeft || loading,
}
}

return {
text: 'Continue',
onClick: () => primaryAction?.(setLoading),
}
}, [isLoggedIn, loading, primaryAction, step, timeLeft])

return (
<DialogModal
show={show}
additionalActionsNode={
![
YppFirstFlowStep.channelConnection,
YppFirstFlowStep.channelConnection,
YppFirstFlowStep.channelVerification,
].includes(step) && (
<Button variant="secondary" onClick={() => setAuthModalOpenName(undefined)}>
Cancel
</Button>
)
}
dividers={[YppFirstFlowStep.yppForm].includes(step)}
primaryButton={primaryButton}
>
{step === YppFirstFlowStep.ytVideoUrl ? (
<ProveChannelOwnership
setActionButtonHandler={(fn) => setPrimaryAction(() => fn)}
onSubmit={(youtubeVideoUrl) => {
formRef.current.youtubeVideoUrl = youtubeVideoUrl
setStep(YppFirstFlowStep.channelVerification)
validateYtChannel(youtubeVideoUrl)
.then(() => setStep(YppFirstFlowStep.ownershipProved))
.catch(() => setStep(YppFirstFlowStep.ytVideoUrl)) // https://www.youtube.com/shorts/OQHvDRTK3Tk
}}
/>
) : null}
{step === YppFirstFlowStep.yppForm ? (
<YppDetailsFormStep
setActionButtonHandler={(fn) => setPrimaryAction(() => fn)}
onSubmit={(form) => {
formRef.current = {
...formRef.current,
...form,
}

const onSuccessfulChannelCreation = () => {
if (!channelId) {
setStep(YppFirstFlowStep.channelConnection)
}

connectJoyChannelToYpp()
.then(() => {
setAuthModalOpenName(undefined)
})
.catch(() => setStep(YppFirstFlowStep.ownershipProved))
}

updateOrCreateChannel(channelId ?? undefined, onSuccessfulChannelCreation).catch(() => {
setStep(YppFirstFlowStep.ownershipProved)
})

setStep(channelId ? YppFirstFlowStep.channelConnection : YppFirstFlowStep.channelCreation)
}}
/>
) : null}
{step === YppFirstFlowStep.channelVerification ? (
<WaitingModal
title="Verifing your YouTube video..."
description="Please wait and don't close this tab as we're verifing your YouTube video."
/>
) : null}
{step === YppFirstFlowStep.channelCreation ? (
<WaitingModal
title="Creating Joystream channel for you..."
description="Please wait and don't close this tab as we're creating your Joystream channel."
/>
) : null}
{step === YppFirstFlowStep.channelConnection ? (
<WaitingModal
title="Connecting your channel to YPP..."
description="Please wait and don't close this tab as we're creating a connection between your YouTube and Joystream channel."
/>
) : null}
{step === YppFirstFlowStep.ownershipProved ? (
<OwnershipVerified
userAvatar={formRef.current.avatarUrl ?? ''}
userHandle={formRef.current.channelHandle ?? ''}
/>
) : null}

{step === YppFirstFlowStep.email ? (
<ProvideEmailForLink
defaultEmail={formRef.current.email}
yppVideoUrl={formRef.current.youtubeVideoUrl}
onSubmit={(email) => {
setStep(YppFirstFlowStep.confirmationLink)
formRef.current = {
...formRef.current,
email,
}
}}
setActionButtonHandler={(fn) => setPrimaryAction(() => fn)}
/>
) : null}
{step === YppFirstFlowStep.confirmationLink ? (
<CheckEmailConfirmation
email={formRef.current.email}
setActionButtonHandler={(fn) => setPrimaryAction(() => fn)}
onSuccess={() => {
const resendTimestamp = new Date()

const calcRemainingTime = (date: Date) => {
const difference = Date.now() - date.getTime()
if (difference > 30_000) {
clearInterval(id)
setTimeLeft('')
return
}
const duration = formatDurationBiggestTick(Math.floor(30 - difference / 1000))
setTimeLeft(duration)
}

calcRemainingTime(resendTimestamp)
const id = setInterval(() => {
calcRemainingTime(resendTimestamp)
}, 1000)
}}
/>
) : null}
</DialogModal>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './YppFirstFlowModal'
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ type ProvideEmailForLinkProps = {
setActionButtonHandler: SetActionButtonHandlerSetter
defaultEmail?: string
isExternal?: boolean
yppVideoUrl?: string
}

export const ProvideEmailForLink = ({
setActionButtonHandler,
onSubmit,
defaultEmail,
isExternal,
yppVideoUrl,
}: ProvideEmailForLinkProps) => {
const {
register,
Expand All @@ -52,7 +54,7 @@ export const ProvideEmailForLink = ({
handleSubmit(async (data) => {
try {
setLoading?.(true)
await mutateAsync({ email: data.email, isExternal })
await mutateAsync({ email: data.email, isExternal, yppVideoUrl })
onSubmit(data.email)
} catch (e) {
const handledError = e.message
Expand Down
4 changes: 3 additions & 1 deletion packages/atlas/src/hooks/useChannelFormSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type CreateEditChannelSubmitParams = {
data: CreateEditChannelData
onUploadAssets?: (field: 'avatar.contentId' | 'cover.contentId', data: string) => void
onCompleted?: () => void
onError?: () => void
onTxSync?: (result: { block: number } & { channelId: string; assetsIds: ChannelAssetsIds }) => void | Promise<void>
minimized?:
| {
Expand Down Expand Up @@ -87,7 +88,7 @@ export const useCreateEditChannelSubmit = (initialChannelId?: string) => {
)

return useCallback(
async ({ data, onCompleted, onUploadAssets, minimized, onTxSync }: CreateEditChannelSubmitParams) => {
async ({ data, onCompleted, onUploadAssets, minimized, onTxSync, onError }: CreateEditChannelSubmitParams) => {
if (!joystream) {
ConsoleLogger.error('No Joystream instance! Has webworker been initialized?')
return
Expand Down Expand Up @@ -245,6 +246,7 @@ export const useCreateEditChannelSubmit = (initialChannelId?: string) => {
data.collaboratorId,
proxyCallback(updateStatus)
),
onError: onError,
onTxSync: async (result) => {
onTxSync?.(result)
return refetchDataAndUploadAssets(result)
Expand Down
8 changes: 5 additions & 3 deletions packages/atlas/src/hooks/useSendEmailToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export enum SendEmailTokenErrors {

export const useSendEmailToken = () => {
return useMutation({
mutationFn: async (props: { email: string; isExternal?: boolean }) => {
mutationFn: async (props: { email: string; isExternal?: boolean; yppVideoUrl?: string }) => {
const res = await axiosInstance
.post(
`${ORION_AUTH_URL}/request-email-confirmation-token`,
Expand Down Expand Up @@ -43,8 +43,10 @@ export const useSendEmailToken = () => {
})
const data = JSON.parse(res.data.payload)
alert(
`${location.host}?email=${encodeURI(data.email)}&email-token=${encodeURI(data.id)}&account-type=${
props.isExternal ? 'external' : 'internal'
`${location.host}?email=${encodeURIComponent(data.email)}&email-token=${encodeURIComponent(
data.id
)}&account-type=${props.isExternal ? 'external' : props.yppVideoUrl ? 'ypp' : 'internal'}${
props.yppVideoUrl ? `&ytVideo=${encodeURIComponent(props.yppVideoUrl)}` : ''
}`
)

Expand Down
Loading

0 comments on commit afdb845

Please sign in to comment.