From 454e4d3a9f54251616530cad29b9049d4d512a76 Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Sat, 11 May 2024 09:57:17 +0200 Subject: [PATCH 01/22] ProveChannelOwnership --- .../genericSteps/ProveChannelOwnership.tsx | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx diff --git a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx new file mode 100644 index 0000000000..b08c665da6 --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx @@ -0,0 +1,63 @@ +import styled from '@emotion/styled' +import { useForm } from 'react-hook-form' + +import { AppLogo } from '@/components/AppLogo' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { FormField } from '@/components/_inputs/FormField' +import { Input } from '@/components/_inputs/Input' +import { useMountEffect } from '@/hooks/useMountEffect' +import { cVar } from '@/styles' + +type ProveChannelOwnershipProps = { + onSubmit: (videoUrl: string) => void + setActionButtonHandler: (fn: () => void | Promise) => void +} +export const ProveChannelOwnership = ({ onSubmit, setActionButtonHandler }: ProveChannelOwnershipProps) => { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm<{ videoUrl: string }>() + + useMountEffect(() => { + setActionButtonHandler( + handleSubmit((data) => { + onSubmit(data.videoUrl) + }) + ) + }) + + return ( + + + + Prove channel ownership + + + Link to unlisted video in your channel titled{' '} + + "Joining Gleev" + + . + + + + + + ) +} + +export const StyledAppLogo = styled(AppLogo)` + height: 36px; + width: auto; + + path { + fill: ${cVar('colorTextMuted')}; + } +` From 50a4902eb935162a5fb3b8aea9f76c1cb6d70e0b Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Sat, 11 May 2024 10:13:40 +0200 Subject: [PATCH 02/22] CheckEmailConfirmation --- .../genericSteps/CheckEmailConfirmation.tsx | 64 +++++++++++++++++++ .../genericSteps/ProveChannelOwnership.tsx | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx diff --git a/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx b/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx new file mode 100644 index 0000000000..3b4736e8ac --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx @@ -0,0 +1,64 @@ +import styled from '@emotion/styled' +import { useMutation } from 'react-query' + +import { AppLogo } from '@/components/AppLogo' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { useMountEffect } from '@/hooks/useMountEffect' +import { useSnackbar } from '@/providers/snackbars' +import { cVar } from '@/styles' +import { SentryLogger } from '@/utils/logs' + +type CheckEmailConfirmationProps = { + setActionButtonHandler: (fn: () => void | Promise) => void + onSuccess?: () => void + onFailure?: () => void +} + +export const CheckEmailConfirmation = ({ + setActionButtonHandler, + onSuccess, + onFailure, +}: CheckEmailConfirmationProps) => { + const { displaySnackbar } = useSnackbar() + const { mutateAsync } = useMutation({ + mutationKey: 'single', + mutationFn: async () => console.log('resending email confimation'), + }) + + useMountEffect(() => { + const handleMutation = () => + mutateAsync() + .then(onSuccess) + .catch((error) => { + onFailure?.() + displaySnackbar({ + iconType: 'error', + title: 'Failed to resend confirmation link', + }) + SentryLogger.error('Failed to resend confirmation link', 'CheckEmailConfirmation', error) + }) + setActionButtonHandler(() => handleMutation()) + }) + + return ( + + + + We sent you a confirmation link + + + Check your email and click the link we sent you to complete the process. + + + ) +} + +const StyledAppLogo = styled(AppLogo)` + height: 36px; + width: auto; + + path { + fill: ${cVar('colorTextMuted')}; + } +` diff --git a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx index b08c665da6..8f734dec58 100644 --- a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx @@ -53,7 +53,7 @@ export const ProveChannelOwnership = ({ onSubmit, setActionButtonHandler }: Prov ) } -export const StyledAppLogo = styled(AppLogo)` +const StyledAppLogo = styled(AppLogo)` height: 36px; width: auto; From 6a7aeb8d1ee815eeaf9beefbe0986bc5957489f3 Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Sat, 11 May 2024 10:16:12 +0200 Subject: [PATCH 03/22] EmailVerified --- .../genericSteps/CheckEmailConfirmation.tsx | 2 +- .../_auth/genericSteps/EmailVerified.tsx | 17 +++++++++++++++++ .../genericSteps/ProveChannelOwnership.tsx | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 packages/atlas/src/components/_auth/genericSteps/EmailVerified.tsx diff --git a/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx b/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx index 3b4736e8ac..9aab38af66 100644 --- a/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx @@ -42,7 +42,7 @@ export const CheckEmailConfirmation = ({ }) return ( - + We sent you a confirmation link diff --git a/packages/atlas/src/components/_auth/genericSteps/EmailVerified.tsx b/packages/atlas/src/components/_auth/genericSteps/EmailVerified.tsx new file mode 100644 index 0000000000..931c38bc5e --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/EmailVerified.tsx @@ -0,0 +1,17 @@ +import { SvgAlertsSuccess32 } from '@/assets/icons' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' + +export const EmailVerified = () => { + return ( + + + + Email verified + + + Your email has been successfully verified. + + + ) +} diff --git a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx index 8f734dec58..5c8ea44987 100644 --- a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx @@ -29,7 +29,7 @@ export const ProveChannelOwnership = ({ onSubmit, setActionButtonHandler }: Prov }) return ( - + Prove channel ownership From bb1bd9af9cf2fc46a94bdb65a45a1cdac490a51e Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Mon, 13 May 2024 06:55:52 +0200 Subject: [PATCH 04/22] CreatePassword --- .../_auth/genericSteps/CreatePassword.tsx | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx diff --git a/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx b/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx new file mode 100644 index 0000000000..891d3bca79 --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx @@ -0,0 +1,150 @@ +import styled from '@emotion/styled' +import HCaptcha from '@hcaptcha/react-hcaptcha' +import { zodResolver } from '@hookform/resolvers/zod' +import { RefObject, useEffect, useRef } from 'react' +import { Controller, FormProvider, useForm } from 'react-hook-form' + +import { AppLogo } from '@/components/AppLogo' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { PasswordCriterias } from '@/components/_auth/PasswordCriterias' +import { FormField } from '@/components/_inputs/FormField' +import { Input } from '@/components/_inputs/Input' +import { atlasConfig } from '@/config' +import { useHidePasswordInInput } from '@/hooks/useHidePasswordInInput' +import { useMountEffect } from '@/hooks/useMountEffect' +import { cVar, sizes } from '@/styles' +import { passwordAndRepeatPasswordSchema } from '@/utils/formValidationOptions' + +type PasswordStepForm = { + password: string + confirmPassword: string + captchaToken?: string +} + +type CreatePasswordProps = { + defaultValues: Omit + onSubmit: (data: PasswordStepForm) => void + setActionButtonHandler: (fn: () => void | Promise) => void + dialogContentRef?: RefObject +} + +export const CreatePassword = ({ + setActionButtonHandler, + onSubmit, + defaultValues, + dialogContentRef, +}: CreatePasswordProps) => { + const form = useForm({ + shouldFocusError: true, + reValidateMode: 'onSubmit', + defaultValues, + resolver: zodResolver(passwordAndRepeatPasswordSchema), + }) + const { + handleSubmit, + register, + formState: { errors }, + control, + trigger, + } = form + const [hidePasswordProps] = useHidePasswordInInput() + const [hideConfirmPasswordProps] = useHidePasswordInInput() + + const captchaRef = useRef(null) + const captchaInputRef = useRef(null) + + useMountEffect(() => { + setActionButtonHandler(() => { + handleSubmit((data) => { + onSubmit(data) + })() + captchaRef.current?.resetCaptcha() + }) + }) + + useEffect(() => { + if (errors.captchaToken) { + captchaInputRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + }, [errors.captchaToken]) + + // used to scroll the form to the bottom upon first handle field focus - this is done to let the user see password requirements & captcha + const hasDoneInitialScroll = useRef(false) + + return ( + + + + + Create a password + + + Please note that there is no option for us to recover your password if you forget it. . + + + + { + if (hasDoneInitialScroll.current || !dialogContentRef?.current) return + hasDoneInitialScroll.current = true + dialogContentRef.current.scrollTo({ top: dialogContentRef.current.scrollHeight, behavior: 'smooth' }) + }} + /> + + + + + + {atlasConfig.features.members.hcaptchaSiteKey && ( + ( + + { + onChange(token) + trigger('captchaToken') + }} + /> + + )} + /> + )} + + + + ) +} + +const StyledAppLogo = styled(AppLogo)` + height: 36px; + width: auto; + + path { + fill: ${cVar('colorTextMuted')}; + } +` + +const StyledSignUpForm = styled.form<{ additionalPaddingBottom?: boolean }>` + display: grid; + gap: ${sizes(6)}; + padding-bottom: ${({ additionalPaddingBottom }) => + additionalPaddingBottom ? 'var(--local-size-dialog-padding)' : 0}; +` From 93577420e22baa1d714bb79b681746f39047ac7d Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Mon, 13 May 2024 07:12:33 +0200 Subject: [PATCH 05/22] SeedGeneration and CreatePassword fixes --- .../_auth/genericSteps/CreatePassword.tsx | 23 ++-- .../_auth/genericSteps/SeedGeneration.tsx | 128 ++++++++++++++++++ 2 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 packages/atlas/src/components/_auth/genericSteps/SeedGeneration.tsx diff --git a/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx b/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx index 891d3bca79..ebe61edc7d 100644 --- a/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx @@ -16,15 +16,15 @@ import { useMountEffect } from '@/hooks/useMountEffect' import { cVar, sizes } from '@/styles' import { passwordAndRepeatPasswordSchema } from '@/utils/formValidationOptions' -type PasswordStepForm = { +type NewPasswordForm = { password: string confirmPassword: string captchaToken?: string } type CreatePasswordProps = { - defaultValues: Omit - onSubmit: (data: PasswordStepForm) => void + defaultValues?: Omit + onSubmit: (data: NewPasswordForm) => void setActionButtonHandler: (fn: () => void | Promise) => void dialogContentRef?: RefObject } @@ -35,7 +35,7 @@ export const CreatePassword = ({ defaultValues, dialogContentRef, }: CreatePasswordProps) => { - const form = useForm({ + const form = useForm({ shouldFocusError: true, reValidateMode: 'onSubmit', defaultValues, @@ -76,12 +76,14 @@ export const CreatePassword = ({ - - Create a password - - - Please note that there is no option for us to recover your password if you forget it. . - + + + Create a password + + + Please note that there is no option for us to recover your password if you forget it. + + ` + width: 100%; display: grid; gap: ${sizes(6)}; padding-bottom: ${({ additionalPaddingBottom }) => diff --git a/packages/atlas/src/components/_auth/genericSteps/SeedGeneration.tsx b/packages/atlas/src/components/_auth/genericSteps/SeedGeneration.tsx new file mode 100644 index 0000000000..fc052c00ab --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/SeedGeneration.tsx @@ -0,0 +1,128 @@ +import styled from '@emotion/styled' +import { mnemonicGenerate } from '@polkadot/util-crypto' +import { useCallback, useEffect, useRef } from 'react' +import { Controller, useForm } from 'react-hook-form' + +import { AppLogo } from '@/components/AppLogo' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { Checkbox } from '@/components/_inputs/Checkbox' +import { FormField } from '@/components/_inputs/FormField' +import { TextArea } from '@/components/_inputs/TextArea' +import { useMountEffect } from '@/hooks/useMountEffect' +import { cVar, sizes } from '@/styles' + +type SeedGenerationForm = { + allowDownload: boolean + mnemonic: string +} + +type SeedGenerationProps = { + onSubmit: (data: SeedGenerationForm) => void + setActionButtonHandler: (fn: () => void | Promise) => void + defaultValues?: SeedGenerationForm +} + +export const SeedGeneration = ({ setActionButtonHandler, onSubmit, defaultValues }: SeedGenerationProps) => { + const { control, register, handleSubmit, setValue } = useForm({ + shouldFocusError: true, + defaultValues: { + allowDownload: true, + mnemonic: defaultValues?.mnemonic, + }, + }) + const { mnemonic } = defaultValues ?? {} + const firstRender = useRef(true) + + useEffect(() => { + if (firstRender.current && !mnemonic) { + setValue('mnemonic', mnemonicGenerate()) + firstRender.current = false + } + }, [mnemonic, setValue]) + + const handleGoToNextStep = useCallback(() => { + handleSubmit((data) => { + const downloadSeed = () => { + const blobText = new Blob([data.mnemonic], { type: 'text/plain' }) + const url = URL.createObjectURL(blobText) + const link = document.createElement('a') + link.href = url + link.download = 'mnemonic.txt' + link.click() + + link.remove() + URL.revokeObjectURL(url) + } + + onSubmit(data) + if (data.allowDownload) { + downloadSeed() + } + })() + }, [handleSubmit, onSubmit]) + + useMountEffect(() => { + setActionButtonHandler(() => handleGoToNextStep()) + }) + + return ( + + + + + Write down your seed + + + Please write down your password recovery seed and keep it in a safe place. It's the only way to recover your + password if you forget it. + + + + + + + + ( + onChange(val)} + value={value} + label="Download the wallet seed as .txt file" + caption="Download will start after clicking continue" + /> + )} + /> + + + ) +} + +const StyledSignUpForm = styled.form<{ additionalPaddingBottom?: boolean }>` + display: grid; + width: 100%; + gap: ${sizes(6)}; + padding-bottom: ${({ additionalPaddingBottom }) => + additionalPaddingBottom ? 'var(--local-size-dialog-padding)' : 0}; +` + +const StyledTextArea = styled(TextArea)` + color: ${cVar('colorTextCaution')}; + resize: none; + + :disabled { + cursor: auto; + opacity: 1; + } +` + +const StyledAppLogo = styled(AppLogo)` + height: 36px; + width: auto; + + path { + fill: ${cVar('colorTextMuted')}; + } +` From a7e3bdbf7693845c434a3c3608a5fc6e15deca6e Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Mon, 13 May 2024 07:16:02 +0200 Subject: [PATCH 06/22] WaitingModal --- .../_auth/genericSteps/WaitingModal.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/atlas/src/components/_auth/genericSteps/WaitingModal.tsx diff --git a/packages/atlas/src/components/_auth/genericSteps/WaitingModal.tsx b/packages/atlas/src/components/_auth/genericSteps/WaitingModal.tsx new file mode 100644 index 0000000000..d540767c34 --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/WaitingModal.tsx @@ -0,0 +1,24 @@ +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { Loader } from '@/components/_loaders/Loader' + +export type WaitingModalProps = { + title?: string + description?: string +} + +export const WaitingModal = ({ title, description }: WaitingModalProps) => { + return ( + + + + + {title} + + + {description} + + + + ) +} From 723be41dee74d42ce0f799f289e7d2e324403608 Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Mon, 13 May 2024 07:25:28 +0200 Subject: [PATCH 07/22] ProvideEmailForLink --- .../genericSteps/ProveChannelOwnership.tsx | 5 ++ .../genericSteps/ProvideEmailForLink.tsx | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx diff --git a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx index 5c8ea44987..50d42bcd2d 100644 --- a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx @@ -19,6 +19,11 @@ export const ProveChannelOwnership = ({ onSubmit, setActionButtonHandler }: Prov formState: { errors }, handleSubmit, } = useForm<{ videoUrl: string }>() + // todo: validation + // 1. Not a link + // 2. Video has wrong title - show current title in error + // 3. Channel that owns the video is already part of the program + // 4. Other video conditions not met (not specified in the designs) useMountEffect(() => { setActionButtonHandler( diff --git a/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx b/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx new file mode 100644 index 0000000000..65a9f70294 --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx @@ -0,0 +1,56 @@ +import { useForm } from 'react-hook-form' + +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { FormField } from '@/components/_inputs/FormField' +import { Input } from '@/components/_inputs/Input' +import { Loader } from '@/components/_loaders/Loader' +import { useMountEffect } from '@/hooks/useMountEffect' + +type ProvideEmailForLinkProps = { + onSubmit: (email: string) => void + setActionButtonHandler: (fn: () => void | Promise) => void + resendEmailHandler?: (email: string) => void | Promise +} + +export const ProvideEmailForLink = ({ setActionButtonHandler, onSubmit }: ProvideEmailForLinkProps) => { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm<{ email: string }>() + // todo: validation + // 1. Not an email + // 2. Email already used (no idea how to check it?) - and allow to resend the link + + useMountEffect(() => { + setActionButtonHandler( + handleSubmit((data) => { + onSubmit(data.email) + }) + ) + }) + + return ( + + + + + Enter email address + + + Please enter your email address. We'll send you a link to complete the process. + + + + + + + + ) +} From 18e3334ad6bf114e9eaec67edd541cd4618da68b Mon Sep 17 00:00:00 2001 From: ikprk Date: Mon, 13 May 2024 20:38:12 +0200 Subject: [PATCH 08/22] Create email setup flow --- .../_auth/AuthModals/AuthModals.tsx | 2 + .../_auth/EmailSetup/EmailSetup.tsx | 112 ++++++++++++++++++ .../src/components/_auth/EmailSetup/index.ts | 1 + .../genericSteps/CheckEmailConfirmation.tsx | 18 +-- .../genericSteps/ProvideEmailForLink.tsx | 26 ++-- .../components/_auth/genericSteps/types.ts | 3 + .../atlas/src/providers/auth/auth.types.ts | 2 +- 7 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 packages/atlas/src/components/_auth/EmailSetup/EmailSetup.tsx create mode 100644 packages/atlas/src/components/_auth/EmailSetup/index.ts create mode 100644 packages/atlas/src/components/_auth/genericSteps/types.ts diff --git a/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx b/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx index 77516aacd4..d7b4920698 100644 --- a/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx +++ b/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx @@ -4,6 +4,7 @@ import { SignUpModal } from '@/components/_auth/SignUpModal' import { CreateChannelModal } from '@/components/_channel/CreateChannelModal' import { useAuthStore } from '@/providers/auth/auth.store' +import { EmailSetup } from '../EmailSetup' import { ForgotPasswordModal } from '../ForgotPasswordModal/ForgotPasswordModal' export const AuthModals = () => { @@ -17,6 +18,7 @@ export const AuthModals = () => { + ) } diff --git a/packages/atlas/src/components/_auth/EmailSetup/EmailSetup.tsx b/packages/atlas/src/components/_auth/EmailSetup/EmailSetup.tsx new file mode 100644 index 0000000000..1fbe457ce4 --- /dev/null +++ b/packages/atlas/src/components/_auth/EmailSetup/EmailSetup.tsx @@ -0,0 +1,112 @@ +import { useMemo, useRef, useState } from 'react' +import shallow from 'zustand/shallow' + +import { Button } from '@/components/_buttons/Button' +import { DialogModal } from '@/components/_overlays/DialogModal' +import { useAuthStore } from '@/providers/auth/auth.store' +import { formatDurationBiggestTick } from '@/utils/time' + +import { CheckEmailConfirmation } from '../genericSteps/CheckEmailConfirmation' +import { ProvideEmailForLink } from '../genericSteps/ProvideEmailForLink' +import { SetActionButtonHandler } from '../genericSteps/types' + +enum EmailSetupStep { + email, + confirmationLink, +} + +type EmailSetupForm = { + email?: string +} + +export const EmailSetup = () => { + const { authModalOpenName, setAuthModalOpenName } = useAuthStore( + (state) => ({ + authModalOpenName: state.authModalOpenName, + setAuthModalOpenName: state.actions.setAuthModalOpenName, + }), + shallow + ) + const [step, setStep] = useState(EmailSetupStep.email) + const [primaryAction, setPrimaryAction] = useState(undefined) + const formRef = useRef({}) + const [timeLeft, setTimeLeft] = useState('') + const [loading, setLoading] = useState(false) + + const primaryButton = useMemo(() => { + if (step === EmailSetupStep.confirmationLink) { + return { + text: loading ? 'Sending...' : timeLeft ? `Resend (${timeLeft.replace('seconds', 's')})` : 'Resend', + onClick: () => primaryAction?.(setLoading), + disabled: !!timeLeft || loading, + } + } + + return { + text: 'Continue', + onClick: () => primaryAction?.(setLoading), + } + }, [loading, primaryAction, step, timeLeft]) + + const secondaryButton = useMemo(() => { + if (step === EmailSetupStep.confirmationLink) { + return { + text: 'Go back', + onClick: () => setStep(EmailSetupStep.email), + } + } + + return undefined + }, [step]) + + return ( + setAuthModalOpenName(undefined)}> + Close + + } + > + {step === EmailSetupStep.email ? ( + { + setStep(EmailSetupStep.confirmationLink) + formRef.current = { + ...formRef.current, + email, + } + }} + setActionButtonHandler={(fn) => setPrimaryAction(() => fn)} + /> + ) : null} + {step === EmailSetupStep.confirmationLink ? ( + 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} + + ) +} diff --git a/packages/atlas/src/components/_auth/EmailSetup/index.ts b/packages/atlas/src/components/_auth/EmailSetup/index.ts new file mode 100644 index 0000000000..8d997d4887 --- /dev/null +++ b/packages/atlas/src/components/_auth/EmailSetup/index.ts @@ -0,0 +1 @@ +export * from './EmailSetup' diff --git a/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx b/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx index 9aab38af66..1f68f44eb9 100644 --- a/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx @@ -9,8 +9,10 @@ import { useSnackbar } from '@/providers/snackbars' import { cVar } from '@/styles' import { SentryLogger } from '@/utils/logs' +import { SetActionButtonHandlerSetter } from './types' + type CheckEmailConfirmationProps = { - setActionButtonHandler: (fn: () => void | Promise) => void + setActionButtonHandler: SetActionButtonHandlerSetter onSuccess?: () => void onFailure?: () => void } @@ -23,12 +25,13 @@ export const CheckEmailConfirmation = ({ const { displaySnackbar } = useSnackbar() const { mutateAsync } = useMutation({ mutationKey: 'single', - mutationFn: async () => console.log('resending email confimation'), + mutationFn: async () => new Promise((res) => setTimeout(res, 5000)), }) useMountEffect(() => { - const handleMutation = () => - mutateAsync() + setActionButtonHandler((setter) => { + setter?.(true) + return mutateAsync() .then(onSuccess) .catch((error) => { onFailure?.() @@ -38,16 +41,17 @@ export const CheckEmailConfirmation = ({ }) SentryLogger.error('Failed to resend confirmation link', 'CheckEmailConfirmation', error) }) - setActionButtonHandler(() => handleMutation()) + .finally(() => setter?.(false)) + }) }) return ( - + We sent you a confirmation link - + Check your email and click the link we sent you to complete the process. diff --git a/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx b/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx index 65a9f70294..d26350fe5d 100644 --- a/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx @@ -1,15 +1,19 @@ +import styled from '@emotion/styled' import { useForm } from 'react-hook-form' +import { AppLogo } from '@/components/AppLogo' import { FlexBox } from '@/components/FlexBox' import { Text } from '@/components/Text' import { FormField } from '@/components/_inputs/FormField' import { Input } from '@/components/_inputs/Input' -import { Loader } from '@/components/_loaders/Loader' import { useMountEffect } from '@/hooks/useMountEffect' +import { cVar } from '@/styles' + +import { SetActionButtonHandlerSetter } from './types' type ProvideEmailForLinkProps = { onSubmit: (email: string) => void - setActionButtonHandler: (fn: () => void | Promise) => void + setActionButtonHandler: SetActionButtonHandlerSetter resendEmailHandler?: (email: string) => void | Promise } @@ -24,16 +28,16 @@ export const ProvideEmailForLink = ({ setActionButtonHandler, onSubmit }: Provid // 2. Email already used (no idea how to check it?) - and allow to resend the link useMountEffect(() => { - setActionButtonHandler( + setActionButtonHandler(() => { handleSubmit((data) => { onSubmit(data.email) - }) - ) + })() + }) }) return ( - + Enter email address @@ -48,9 +52,17 @@ export const ProvideEmailForLink = ({ setActionButtonHandler, onSubmit }: Provid {...register('email', { required: 'Please provide an email.', })} - placeholder="Enter your YouTube video URL" + placeholder="Enter your email" /> ) } +const StyledAppLogo = styled(AppLogo)` + height: 36px; + width: auto; + + path { + fill: ${cVar('colorTextMuted')}; + } +` diff --git a/packages/atlas/src/components/_auth/genericSteps/types.ts b/packages/atlas/src/components/_auth/genericSteps/types.ts new file mode 100644 index 0000000000..3c11619428 --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/types.ts @@ -0,0 +1,3 @@ +type LoadingSetter = (value: boolean) => void +export type SetActionButtonHandler = (setLoading?: LoadingSetter) => void | Promise +export type SetActionButtonHandlerSetter = (fn: SetActionButtonHandler) => void diff --git a/packages/atlas/src/providers/auth/auth.types.ts b/packages/atlas/src/providers/auth/auth.types.ts index eba0a25c65..6480f67990 100644 --- a/packages/atlas/src/providers/auth/auth.types.ts +++ b/packages/atlas/src/providers/auth/auth.types.ts @@ -38,7 +38,7 @@ export type ExternalLogin = { export type LoginParams = InternalLogin | ExternalLogin -export type AuthModals = 'logIn' | 'externalLogIn' | 'signUp' | 'createChannel' | 'forgotPassword' +export type AuthModals = 'logIn' | 'externalLogIn' | 'signUp' | 'createChannel' | 'forgotPassword' | 'emailSetup' type EncryptionArtifacts = { id: string From 8b2acd691260a455dce4a2875773657332121307 Mon Sep 17 00:00:00 2001 From: ikprk Date: Wed, 15 May 2024 18:47:46 +0200 Subject: [PATCH 09/22] Post verification flow --- .../_auth/AccountSetup/AccountSetup.tsx | 117 ++++++++++++++++++ .../_auth/genericSteps/CreatePassword.tsx | 4 +- .../_auth/genericSteps/EmailVerified.tsx | 26 +++- .../_auth/genericSteps/SeedGeneration.tsx | 50 ++++---- 4 files changed, 164 insertions(+), 33 deletions(-) create mode 100644 packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx diff --git a/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx b/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx new file mode 100644 index 0000000000..cc73441995 --- /dev/null +++ b/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx @@ -0,0 +1,117 @@ +import { useMemo, useRef, useState } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { DialogModal } from '@/components/_overlays/DialogModal' + +import { CreatePassword } from '../genericSteps/CreatePassword' +import { EmailVerified } from '../genericSteps/EmailVerified' +import { SeedGeneration } from '../genericSteps/SeedGeneration' +import { WaitingModal } from '../genericSteps/WaitingModal' +import { SetActionButtonHandler } from '../genericSteps/types' + +enum AccountSetupStep { + verification, + password, + seed, + creation, +} + +type AccountSetupForm = { + password?: string + confirmPassword?: string + mnemonic?: string +} + +export const AccountSetup = () => { + const [searchParams] = useSearchParams() + const confirmationCode = searchParams.get('email-confirmation') + const [step, setStep] = useState(AccountSetupStep.verification) + const [loading, setLoading] = useState(true) + const [primaryAction, setPrimaryAction] = useState(undefined) + const formRef = useRef({}) + + const primaryButton = useMemo(() => { + if (step === AccountSetupStep.verification) { + return { + text: loading ? 'Verifying...' : 'Set password', + onClick: () => setStep(AccountSetupStep.password), + disabled: loading, + } + } + + if (step === AccountSetupStep.password || step === AccountSetupStep.seed) { + return { + text: 'Continue', + onClick: () => { + primaryAction?.() + }, + } + } + + return { + text: 'Continue', + onClick: () => primaryAction?.(setLoading), + } + }, [loading, primaryAction, step]) + + const secondaryButton = useMemo(() => { + if ([AccountSetupStep.verification, AccountSetupStep.password].includes(step)) { + return { + text: 'Cancel', + onClick: () => setStep(AccountSetupStep.verification), + } + } + + return { + text: 'Go back', + onClick: () => setStep((prev) => prev - 1), + } + }, [step]) + + if (!confirmationCode) { + return null + } + + return ( + + {step === AccountSetupStep.verification ? ( + setLoading(false)} code={confirmationCode} /> + ) : null} + + {step === AccountSetupStep.password ? ( + { + setStep(AccountSetupStep.seed) + formRef.current = { + ...formRef.current, + ...data, + } + }} + setActionButtonHandler={(fn) => setPrimaryAction(() => fn)} + /> + ) : null} + + {step === AccountSetupStep.seed ? ( + { + setStep(AccountSetupStep.creation) + formRef.current = { + ...formRef.current, + ...data, + } + }} + setActionButtonHandler={(fn) => setPrimaryAction(() => fn)} + /> + ) : null} + + {step === AccountSetupStep.creation ? ( + + ) : null} + + ) +} diff --git a/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx b/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx index ebe61edc7d..6c88ce07fe 100644 --- a/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx @@ -17,8 +17,8 @@ import { cVar, sizes } from '@/styles' import { passwordAndRepeatPasswordSchema } from '@/utils/formValidationOptions' type NewPasswordForm = { - password: string - confirmPassword: string + password?: string + confirmPassword?: string captchaToken?: string } diff --git a/packages/atlas/src/components/_auth/genericSteps/EmailVerified.tsx b/packages/atlas/src/components/_auth/genericSteps/EmailVerified.tsx index 931c38bc5e..1f48b29207 100644 --- a/packages/atlas/src/components/_auth/genericSteps/EmailVerified.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/EmailVerified.tsx @@ -1,16 +1,32 @@ +import { useQuery } from 'react-query' + import { SvgAlertsSuccess32 } from '@/assets/icons' import { FlexBox } from '@/components/FlexBox' import { Text } from '@/components/Text' +import { Loader } from '@/components/_loaders/Loader' + +type EmailVerifiedProps = { + code: string + onVerified: () => void +} + +export const EmailVerified = ({ code, onVerified }: EmailVerifiedProps) => { + const { isLoading } = useQuery({ + queryKey: code, + queryFn: () => new Promise((res) => setTimeout(res, 5000)), + onSuccess: () => { + onVerified() + }, + }) -export const EmailVerified = () => { return ( - - - Email verified + {isLoading ? : } + + Email {isLoading ? 'verification' : 'verified'} - Your email has been successfully verified. + {isLoading ? 'Your email is being verified...' : 'Your email has been successfully verified.'} ) diff --git a/packages/atlas/src/components/_auth/genericSteps/SeedGeneration.tsx b/packages/atlas/src/components/_auth/genericSteps/SeedGeneration.tsx index fc052c00ab..fba9d12355 100644 --- a/packages/atlas/src/components/_auth/genericSteps/SeedGeneration.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/SeedGeneration.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled' import { mnemonicGenerate } from '@polkadot/util-crypto' -import { useCallback, useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react' import { Controller, useForm } from 'react-hook-form' import { AppLogo } from '@/components/AppLogo' @@ -14,13 +14,13 @@ import { cVar, sizes } from '@/styles' type SeedGenerationForm = { allowDownload: boolean - mnemonic: string + mnemonic?: string } type SeedGenerationProps = { - onSubmit: (data: SeedGenerationForm) => void + onSubmit: (data: Pick) => void setActionButtonHandler: (fn: () => void | Promise) => void - defaultValues?: SeedGenerationForm + defaultValues?: Pick } export const SeedGeneration = ({ setActionButtonHandler, onSubmit, defaultValues }: SeedGenerationProps) => { @@ -41,29 +41,27 @@ export const SeedGeneration = ({ setActionButtonHandler, onSubmit, defaultValues } }, [mnemonic, setValue]) - const handleGoToNextStep = useCallback(() => { - handleSubmit((data) => { - const downloadSeed = () => { - const blobText = new Blob([data.mnemonic], { type: 'text/plain' }) - const url = URL.createObjectURL(blobText) - const link = document.createElement('a') - link.href = url - link.download = 'mnemonic.txt' - link.click() - - link.remove() - URL.revokeObjectURL(url) - } - - onSubmit(data) - if (data.allowDownload) { - downloadSeed() - } - })() - }, [handleSubmit, onSubmit]) - useMountEffect(() => { - setActionButtonHandler(() => handleGoToNextStep()) + setActionButtonHandler(() => { + handleSubmit((data) => { + const downloadSeed = () => { + const blobText = new Blob([data.mnemonic ?? ''], { type: 'text/plain' }) + const url = URL.createObjectURL(blobText) + const link = document.createElement('a') + link.href = url + link.download = 'mnemonic.txt' + link.click() + + link.remove() + URL.revokeObjectURL(url) + } + + onSubmit(data) + if (data.allowDownload) { + downloadSeed() + } + })() + }) }) return ( From d1df74fb8763db3bf3728a4ff90ab8c5dbce0679 Mon Sep 17 00:00:00 2001 From: ikprk Date: Sat, 18 May 2024 10:44:00 +0200 Subject: [PATCH 10/22] inital email send implementation --- .../_auth/AuthModals/AuthModals.tsx | 4 +- .../_auth/EmailSetup/EmailSetup.tsx | 4 +- .../genericSteps/CheckEmailConfirmation.tsx | 52 +++++++++++++------ .../genericSteps/ProvideEmailForLink.tsx | 30 ++++++++--- packages/atlas/src/hooks/useSendEmailToken.ts | 45 ++++++++++++++++ 5 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 packages/atlas/src/hooks/useSendEmailToken.ts diff --git a/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx b/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx index d7b4920698..a1902d853e 100644 --- a/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx +++ b/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx @@ -1,6 +1,6 @@ import { ExternalSignInModal } from '@/components/_auth/ExternalSignInModal' import { LogInModal } from '@/components/_auth/LogInModal' -import { SignUpModal } from '@/components/_auth/SignUpModal' +// import { SignUpModal } from '@/components/_auth/SignUpModal' import { CreateChannelModal } from '@/components/_channel/CreateChannelModal' import { useAuthStore } from '@/providers/auth/auth.store' @@ -15,7 +15,7 @@ export const AuthModals = () => { <> - + {/* old modal */} diff --git a/packages/atlas/src/components/_auth/EmailSetup/EmailSetup.tsx b/packages/atlas/src/components/_auth/EmailSetup/EmailSetup.tsx index 1fbe457ce4..7068896341 100644 --- a/packages/atlas/src/components/_auth/EmailSetup/EmailSetup.tsx +++ b/packages/atlas/src/components/_auth/EmailSetup/EmailSetup.tsx @@ -62,7 +62,7 @@ export const EmailSetup = () => { return ( { > {step === EmailSetupStep.email ? ( { setStep(EmailSetupStep.confirmationLink) formRef.current = { @@ -85,6 +86,7 @@ export const EmailSetup = () => { ) : null} {step === EmailSetupStep.confirmationLink ? ( setPrimaryAction(() => fn)} onSuccess={() => { const resendTimestamp = new Date() diff --git a/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx b/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx index 1f68f44eb9..7b419711e4 100644 --- a/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/CheckEmailConfirmation.tsx @@ -1,10 +1,11 @@ import styled from '@emotion/styled' -import { useMutation } from 'react-query' +import { useState } from 'react' import { AppLogo } from '@/components/AppLogo' import { FlexBox } from '@/components/FlexBox' import { Text } from '@/components/Text' import { useMountEffect } from '@/hooks/useMountEffect' +import { SendEmailTokenErrors, useSendEmailToken } from '@/hooks/useSendEmailToken' import { useSnackbar } from '@/providers/snackbars' import { cVar } from '@/styles' import { SentryLogger } from '@/utils/logs' @@ -15,33 +16,45 @@ type CheckEmailConfirmationProps = { setActionButtonHandler: SetActionButtonHandlerSetter onSuccess?: () => void onFailure?: () => void + email?: string } export const CheckEmailConfirmation = ({ + email, setActionButtonHandler, onSuccess, onFailure, }: CheckEmailConfirmationProps) => { const { displaySnackbar } = useSnackbar() - const { mutateAsync } = useMutation({ - mutationKey: 'single', - mutationFn: async () => new Promise((res) => setTimeout(res, 5000)), - }) + const { mutateAsync } = useSendEmailToken() + const [error, setError] = useState('') useMountEffect(() => { - setActionButtonHandler((setter) => { - setter?.(true) - return mutateAsync() - .then(onSuccess) - .catch((error) => { - onFailure?.() - displaySnackbar({ - iconType: 'error', - title: 'Failed to resend confirmation link', - }) - SentryLogger.error('Failed to resend confirmation link', 'CheckEmailConfirmation', error) + setActionButtonHandler(async (setLoading) => { + if (!email) { + displaySnackbar({ + iconType: 'error', + title: 'Missing email. Please start over.', + }) + return + } + try { + setError('') + setLoading?.(true) + await mutateAsync(email) + onSuccess?.() + } catch (e) { + onFailure?.() + displaySnackbar({ + iconType: 'error', + title: 'Failed to resend confirmation link', }) - .finally(() => setter?.(false)) + SentryLogger.error('Failed to resend confirmation link', 'CheckEmailConfirmation', error) + const handledError = e.message + if (handledError === SendEmailTokenErrors.TOO_MANY_REQUESTS) setError('Too many reqests, please wait.') + } finally { + setLoading?.(false) + } }) }) @@ -54,6 +67,11 @@ export const CheckEmailConfirmation = ({ Check your email and click the link we sent you to complete the process. + {error ? ( + + {error} + + ) : null} ) } diff --git a/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx b/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx index d26350fe5d..97bced2baa 100644 --- a/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx @@ -7,6 +7,7 @@ import { Text } from '@/components/Text' import { FormField } from '@/components/_inputs/FormField' import { Input } from '@/components/_inputs/Input' import { useMountEffect } from '@/hooks/useMountEffect' +import { SendEmailTokenErrors, useSendEmailToken } from '@/hooks/useSendEmailToken' import { cVar } from '@/styles' import { SetActionButtonHandlerSetter } from './types' @@ -14,23 +15,40 @@ import { SetActionButtonHandlerSetter } from './types' type ProvideEmailForLinkProps = { onSubmit: (email: string) => void setActionButtonHandler: SetActionButtonHandlerSetter - resendEmailHandler?: (email: string) => void | Promise + defaultEmail?: string } -export const ProvideEmailForLink = ({ setActionButtonHandler, onSubmit }: ProvideEmailForLinkProps) => { +export const ProvideEmailForLink = ({ setActionButtonHandler, onSubmit, defaultEmail }: ProvideEmailForLinkProps) => { const { register, formState: { errors }, + setError, handleSubmit, - } = useForm<{ email: string }>() + } = useForm<{ email: string }>({ + defaultValues: { + email: defaultEmail, + }, + }) + const { mutateAsync } = useSendEmailToken() // todo: validation // 1. Not an email // 2. Email already used (no idea how to check it?) - and allow to resend the link useMountEffect(() => { - setActionButtonHandler(() => { - handleSubmit((data) => { - onSubmit(data.email) + setActionButtonHandler((setLoading) => { + handleSubmit(async (data) => { + try { + setLoading?.(true) + await mutateAsync(data.email) + onSubmit(data.email) + } catch (e) { + const handledError = e.message + if (handledError === SendEmailTokenErrors.INVALID_EMAIL) setError('email', { message: 'Invalid email.' }) + if (handledError === SendEmailTokenErrors.TOO_MANY_REQUESTS) + setError('email', { message: 'Too many reqests, please wait.' }) + } finally { + setLoading?.(false) + } })() }) }) diff --git a/packages/atlas/src/hooks/useSendEmailToken.ts b/packages/atlas/src/hooks/useSendEmailToken.ts new file mode 100644 index 0000000000..149a604f9f --- /dev/null +++ b/packages/atlas/src/hooks/useSendEmailToken.ts @@ -0,0 +1,45 @@ +import { useMutation } from 'react-query' + +import { axiosInstance } from '@/api/axios' +import { ORION_AUTH_URL } from '@/config/env' +import { isAxiosError } from '@/utils/error' + +// code 400: Request is malformatted or provided e-mail address is not valid. +// code 429: Too many requests for a new token sent within a given timeframe. + +export enum SendEmailTokenErrors { + INVALID_EMAIL = 'INVALID_EMAIL', + TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS', +} + +export const useSendEmailToken = () => { + return useMutation({ + mutationFn: (email: string) => + axiosInstance + .post( + `${ORION_AUTH_URL}/request-email-confirmation-token`, + { + email, + }, + { + withCredentials: true, + headers: { + 'Content-Type': 'application/json', + }, + } + ) + .catch((e) => { + if (isAxiosError(e)) { + const code = e.response?.status + if (code === 400) { + throw new Error(SendEmailTokenErrors.INVALID_EMAIL) + } + if (code === 429) { + throw new Error(SendEmailTokenErrors.TOO_MANY_REQUESTS) + } + } + + throw e + }), + }) +} From db6b211d4e9adf50a778af69f74f961fb0ea857f Mon Sep 17 00:00:00 2001 From: ikprk Date: Mon, 20 May 2024 13:41:28 +0200 Subject: [PATCH 11/22] Initial acc cration step --- packages/atlas/codegen.config.ts | 3 +- .../__generated__/accounts.generated.tsx | 18 +- .../__generated__/baseTypes.generated.ts | 405 ++++++++++++------ .../__generated__/channels.generated.tsx | 54 ++- .../__generated__/fragments.generated.tsx | 6 +- .../__generated__/memberships.generated.tsx | 2 +- .../queries/__generated__/nfts.generated.tsx | 2 +- .../__generated__/videos.generated.tsx | 2 +- .../atlas/src/api/queries/accounts.graphql | 10 +- .../atlas/src/api/queries/channels.graphql | 36 +- .../atlas/src/api/queries/fragments.graphql | 4 +- packages/atlas/src/api/schemas/orion.json | 2 +- .../_auth/AccountSetup/AccountSetup.tsx | 62 ++- .../components/_auth/AccountSetup/index.ts | 1 + .../_auth/AuthModals/AuthModals.tsx | 4 +- .../_auth/SignUpModal/SignUpModal.tsx | 1 + .../_auth/genericSteps/EmailVerified.tsx | 2 +- packages/atlas/src/hooks/useCreateMember.ts | 3 +- .../atlas/src/providers/auth/auth.helpers.ts | 4 +- .../src/providers/auth/auth.provider.tsx | 16 +- .../atlas/src/providers/auth/auth.types.ts | 6 +- .../src/providers/user/user.provider.tsx | 8 +- .../ChangePasswordDialog.tsx | 2 +- .../MembershipWallet/MembershipWallet.tsx | 8 +- yarn.lock | 14 +- 25 files changed, 457 insertions(+), 218 deletions(-) create mode 100644 packages/atlas/src/components/_auth/AccountSetup/index.ts diff --git a/packages/atlas/codegen.config.ts b/packages/atlas/codegen.config.ts index 1b4711c7c6..0a5b9fbf24 100644 --- a/packages/atlas/codegen.config.ts +++ b/packages/atlas/codegen.config.ts @@ -2,7 +2,8 @@ import { CodegenConfig } from '@graphql-codegen/cli' import { customSchemaLoader } from './scripts/customSchemaLoader' -const ENV = process.env.VITE_DEFAULT_DATA_ENV?.toUpperCase() || process.env.VITE_ENV?.toUpperCase() || 'PRODUCTION' +// const ENV = process.env.VITE_DEFAULT_DATA_ENV?.toUpperCase() || process.env.VITE_ENV?.toUpperCase() || 'PRODUCTION' +const ENV = 'NEXT' const schemaUrl = process.env[`VITE_${ENV}_ORION_URL`] if (!schemaUrl) throw new Error(`VITE_${ENV}_ORION_URL is not defined`) diff --git a/packages/atlas/src/api/queries/__generated__/accounts.generated.tsx b/packages/atlas/src/api/queries/__generated__/accounts.generated.tsx index 44eebf1960..e432fcbb39 100644 --- a/packages/atlas/src/api/queries/__generated__/accounts.generated.tsx +++ b/packages/atlas/src/api/queries/__generated__/accounts.generated.tsx @@ -12,9 +12,11 @@ export type GetCurrentAccountQuery = { __typename?: 'AccountData' email: string id: string - isEmailConfirmed: boolean - joystreamAccount: string - membershipId: string + joystreamAccount: { + __typename?: 'BlockchainAccountType' + id: string + memberships: Array<{ __typename?: 'MembershipType'; id: string; controllerAccountId?: string | null }> + } followedChannels: Array<{ __typename?: 'FollowedChannel'; channelId: string; timestamp: string }> } } @@ -40,9 +42,13 @@ export const GetCurrentAccountDocument = gql` accountData { email id - isEmailConfirmed - joystreamAccount - membershipId + joystreamAccount { + id + memberships { + id + controllerAccountId + } + } followedChannels { channelId timestamp diff --git a/packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts b/packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts index 84c5d100ad..b612f8a211 100644 --- a/packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts +++ b/packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts @@ -23,18 +23,12 @@ export type Account = { id: Scalars['String'] /** Indicates whether the access to the gateway account is blocked */ isBlocked: Scalars['Boolean'] - /** Indicates whether the gateway account's e-mail has been confirmed or not. */ - isEmailConfirmed: Scalars['Boolean'] /** Blockchain (joystream) account associated with the gateway account */ - joystreamAccount: Scalars['String'] - /** On-chain membership associated with the gateway account */ - membership: Membership + joystreamAccount: BlockchainAccount /** notification preferences for the account */ notificationPreferences: AccountNotificationPreferences /** runtime notifications */ notifications: Array - /** ID of the channel which referred the user to the platform */ - referrerChannelId?: Maybe /** Time when the gateway account was registered */ registeredAt: Scalars['DateTime'] /** The user associated with the gateway account (the Gateway Account Owner) */ @@ -54,9 +48,7 @@ export type AccountData = { email: Scalars['String'] followedChannels: Array id: Scalars['String'] - isEmailConfirmed: Scalars['Boolean'] - joystreamAccount: Scalars['String'] - membershipId: Scalars['String'] + joystreamAccount: BlockchainAccountType preferences?: Maybe } @@ -279,24 +271,8 @@ export enum AccountOrderByInput { IdDesc = 'id_DESC', IsBlockedAsc = 'isBlocked_ASC', IsBlockedDesc = 'isBlocked_DESC', - IsEmailConfirmedAsc = 'isEmailConfirmed_ASC', - IsEmailConfirmedDesc = 'isEmailConfirmed_DESC', - JoystreamAccountAsc = 'joystreamAccount_ASC', - JoystreamAccountDesc = 'joystreamAccount_DESC', - MembershipControllerAccountAsc = 'membership_controllerAccount_ASC', - MembershipControllerAccountDesc = 'membership_controllerAccount_DESC', - MembershipCreatedAtAsc = 'membership_createdAt_ASC', - MembershipCreatedAtDesc = 'membership_createdAt_DESC', - MembershipHandleRawAsc = 'membership_handleRaw_ASC', - MembershipHandleRawDesc = 'membership_handleRaw_DESC', - MembershipHandleAsc = 'membership_handle_ASC', - MembershipHandleDesc = 'membership_handle_DESC', - MembershipIdAsc = 'membership_id_ASC', - MembershipIdDesc = 'membership_id_DESC', - MembershipTotalChannelsCreatedAsc = 'membership_totalChannelsCreated_ASC', - MembershipTotalChannelsCreatedDesc = 'membership_totalChannelsCreated_DESC', - ReferrerChannelIdAsc = 'referrerChannelId_ASC', - ReferrerChannelIdDesc = 'referrerChannelId_DESC', + JoystreamAccountIdAsc = 'joystreamAccount_id_ASC', + JoystreamAccountIdDesc = 'joystreamAccount_id_DESC', RegisteredAtAsc = 'registeredAt_ASC', RegisteredAtDesc = 'registeredAt_DESC', UserIdAsc = 'user_id_ASC', @@ -345,50 +321,13 @@ export type AccountWhereInput = { isBlocked_eq?: InputMaybe isBlocked_isNull?: InputMaybe isBlocked_not_eq?: InputMaybe - isEmailConfirmed_eq?: InputMaybe - isEmailConfirmed_isNull?: InputMaybe - isEmailConfirmed_not_eq?: InputMaybe - joystreamAccount_contains?: InputMaybe - joystreamAccount_containsInsensitive?: InputMaybe - joystreamAccount_endsWith?: InputMaybe - joystreamAccount_eq?: InputMaybe - joystreamAccount_gt?: InputMaybe - joystreamAccount_gte?: InputMaybe - joystreamAccount_in?: InputMaybe> + joystreamAccount?: InputMaybe joystreamAccount_isNull?: InputMaybe - joystreamAccount_lt?: InputMaybe - joystreamAccount_lte?: InputMaybe - joystreamAccount_not_contains?: InputMaybe - joystreamAccount_not_containsInsensitive?: InputMaybe - joystreamAccount_not_endsWith?: InputMaybe - joystreamAccount_not_eq?: InputMaybe - joystreamAccount_not_in?: InputMaybe> - joystreamAccount_not_startsWith?: InputMaybe - joystreamAccount_startsWith?: InputMaybe - membership?: InputMaybe - membership_isNull?: InputMaybe notificationPreferences?: InputMaybe notificationPreferences_isNull?: InputMaybe notifications_every?: InputMaybe notifications_none?: InputMaybe notifications_some?: InputMaybe - referrerChannelId_contains?: InputMaybe - referrerChannelId_containsInsensitive?: InputMaybe - referrerChannelId_endsWith?: InputMaybe - referrerChannelId_eq?: InputMaybe - referrerChannelId_gt?: InputMaybe - referrerChannelId_gte?: InputMaybe - referrerChannelId_in?: InputMaybe> - referrerChannelId_isNull?: InputMaybe - referrerChannelId_lt?: InputMaybe - referrerChannelId_lte?: InputMaybe - referrerChannelId_not_contains?: InputMaybe - referrerChannelId_not_containsInsensitive?: InputMaybe - referrerChannelId_not_endsWith?: InputMaybe - referrerChannelId_not_eq?: InputMaybe - referrerChannelId_not_in?: InputMaybe> - referrerChannelId_not_startsWith?: InputMaybe - referrerChannelId_startsWith?: InputMaybe registeredAt_eq?: InputMaybe registeredAt_gt?: InputMaybe registeredAt_gte?: InputMaybe @@ -781,8 +720,6 @@ export enum AppOrderByInput { NameDesc = 'name_DESC', OneLinerAsc = 'oneLiner_ASC', OneLinerDesc = 'oneLiner_DESC', - OwnerMemberControllerAccountAsc = 'ownerMember_controllerAccount_ASC', - OwnerMemberControllerAccountDesc = 'ownerMember_controllerAccount_DESC', OwnerMemberCreatedAtAsc = 'ownerMember_createdAt_ASC', OwnerMemberCreatedAtDesc = 'ownerMember_createdAt_DESC', OwnerMemberHandleRawAsc = 'ownerMember_handleRaw_ASC', @@ -1178,8 +1115,6 @@ export enum AuctionOrderByInput { TopBidIndexInBlockDesc = 'topBid_indexInBlock_DESC', TopBidIsCanceledAsc = 'topBid_isCanceled_ASC', TopBidIsCanceledDesc = 'topBid_isCanceled_DESC', - WinningMemberControllerAccountAsc = 'winningMember_controllerAccount_ASC', - WinningMemberControllerAccountDesc = 'winningMember_controllerAccount_DESC', WinningMemberCreatedAtAsc = 'winningMember_createdAt_ASC', WinningMemberCreatedAtDesc = 'winningMember_createdAt_DESC', WinningMemberHandleRawAsc = 'winningMember_handleRaw_ASC', @@ -1389,8 +1324,6 @@ export enum AuctionWhitelistedMemberOrderByInput { AuctionStartsAtBlockDesc = 'auction_startsAtBlock_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', - MemberControllerAccountAsc = 'member_controllerAccount_ASC', - MemberControllerAccountDesc = 'member_controllerAccount_DESC', MemberCreatedAtAsc = 'member_createdAt_ASC', MemberCreatedAtDesc = 'member_createdAt_DESC', MemberHandleRawAsc = 'member_handleRaw_ASC', @@ -1561,8 +1494,6 @@ export enum BannedMemberOrderByInput { ChannelVideoViewsNumDesc = 'channel_videoViewsNum_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', - MemberControllerAccountAsc = 'member_controllerAccount_ASC', - MemberControllerAccountDesc = 'member_controllerAccount_DESC', MemberCreatedAtAsc = 'member_createdAt_ASC', MemberCreatedAtDesc = 'member_createdAt_DESC', MemberHandleRawAsc = 'member_handleRaw_ASC', @@ -1824,8 +1755,6 @@ export enum BidOrderByInput { AuctionStartingPriceDesc = 'auction_startingPrice_DESC', AuctionStartsAtBlockAsc = 'auction_startsAtBlock_ASC', AuctionStartsAtBlockDesc = 'auction_startsAtBlock_DESC', - BidderControllerAccountAsc = 'bidder_controllerAccount_ASC', - BidderControllerAccountDesc = 'bidder_controllerAccount_DESC', BidderCreatedAtAsc = 'bidder_createdAt_ASC', BidderCreatedAtDesc = 'bidder_createdAt_DESC', BidderHandleRawAsc = 'bidder_handleRaw_ASC', @@ -1948,6 +1877,70 @@ export type BidsConnection = { totalCount: Scalars['Int'] } +export type BlockchainAccount = { + __typename?: 'BlockchainAccount' + /** The blockchain account id/address */ + id: Scalars['String'] + /** Membership associated with the blockchain account (controllerAccount) */ + memberships: Array +} + +export type BlockchainAccountMembershipsArgs = { + limit?: InputMaybe + offset?: InputMaybe + orderBy?: InputMaybe> + where?: InputMaybe +} + +export type BlockchainAccountEdge = { + __typename?: 'BlockchainAccountEdge' + cursor: Scalars['String'] + node: BlockchainAccount +} + +export enum BlockchainAccountOrderByInput { + IdAsc = 'id_ASC', + IdDesc = 'id_DESC', +} + +export type BlockchainAccountType = { + __typename?: 'BlockchainAccountType' + id: Scalars['String'] + memberships: Array +} + +export type BlockchainAccountWhereInput = { + AND?: InputMaybe> + OR?: InputMaybe> + id_contains?: InputMaybe + id_containsInsensitive?: InputMaybe + id_endsWith?: InputMaybe + id_eq?: InputMaybe + id_gt?: InputMaybe + id_gte?: InputMaybe + id_in?: InputMaybe> + id_isNull?: InputMaybe + id_lt?: InputMaybe + id_lte?: InputMaybe + id_not_contains?: InputMaybe + id_not_containsInsensitive?: InputMaybe + id_not_endsWith?: InputMaybe + id_not_eq?: InputMaybe + id_not_in?: InputMaybe> + id_not_startsWith?: InputMaybe + id_startsWith?: InputMaybe + memberships_every?: InputMaybe + memberships_none?: InputMaybe + memberships_some?: InputMaybe +} + +export type BlockchainAccountsConnection = { + __typename?: 'BlockchainAccountsConnection' + edges: Array + pageInfo: PageInfo + totalCount: Scalars['Int'] +} + export type BuyNowCanceledEventData = { __typename?: 'BuyNowCanceledEventData' /** Content actor acting as NFT owner. */ @@ -2288,8 +2281,6 @@ export enum ChannelOrderByInput { IsPublicDesc = 'isPublic_DESC', LanguageAsc = 'language_ASC', LanguageDesc = 'language_DESC', - OwnerMemberControllerAccountAsc = 'ownerMember_controllerAccount_ASC', - OwnerMemberControllerAccountDesc = 'ownerMember_controllerAccount_DESC', OwnerMemberCreatedAtAsc = 'ownerMember_createdAt_ASC', OwnerMemberCreatedAtDesc = 'ownerMember_createdAt_DESC', OwnerMemberHandleRawAsc = 'ownerMember_handleRaw_ASC', @@ -2929,8 +2920,6 @@ export type CommentEdge = { } export enum CommentOrderByInput { - AuthorControllerAccountAsc = 'author_controllerAccount_ASC', - AuthorControllerAccountDesc = 'author_controllerAccount_DESC', AuthorCreatedAtAsc = 'author_createdAt_ASC', AuthorCreatedAtDesc = 'author_createdAt_DESC', AuthorHandleRawAsc = 'author_handleRaw_ASC', @@ -3090,8 +3079,6 @@ export enum CommentReactionOrderByInput { CommentTextDesc = 'comment_text_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', - MemberControllerAccountAsc = 'member_controllerAccount_ASC', - MemberControllerAccountDesc = 'member_controllerAccount_DESC', MemberCreatedAtAsc = 'member_createdAt_ASC', MemberCreatedAtDesc = 'member_createdAt_DESC', MemberHandleRawAsc = 'member_handleRaw_ASC', @@ -3399,6 +3386,12 @@ export enum Continent { Sa = 'SA', } +export type CreateAccountMembershipResult = { + __typename?: 'CreateAccountMembershipResult' + accountId: Scalars['String'] + memberId: Scalars['Float'] +} + export type CreatorReceivesAuctionBid = { __typename?: 'CreatorReceivesAuctionBid' /** bid amount */ @@ -4785,6 +4778,99 @@ export type DistributionBucketsConnection = { totalCount: Scalars['Int'] } +export type EmailConfirmationToken = { + __typename?: 'EmailConfirmationToken' + /** The email the token was issued for */ + email: Scalars['String'] + /** When does the token expire or when has it expired */ + expiry: Scalars['DateTime'] + /** The token itself (32-byte string, securely random) */ + id: Scalars['String'] + /** When was the token issued */ + issuedAt: Scalars['DateTime'] +} + +export type EmailConfirmationTokenEdge = { + __typename?: 'EmailConfirmationTokenEdge' + cursor: Scalars['String'] + node: EmailConfirmationToken +} + +export enum EmailConfirmationTokenOrderByInput { + EmailAsc = 'email_ASC', + EmailDesc = 'email_DESC', + ExpiryAsc = 'expiry_ASC', + ExpiryDesc = 'expiry_DESC', + IdAsc = 'id_ASC', + IdDesc = 'id_DESC', + IssuedAtAsc = 'issuedAt_ASC', + IssuedAtDesc = 'issuedAt_DESC', +} + +export type EmailConfirmationTokenWhereInput = { + AND?: InputMaybe> + OR?: InputMaybe> + email_contains?: InputMaybe + email_containsInsensitive?: InputMaybe + email_endsWith?: InputMaybe + email_eq?: InputMaybe + email_gt?: InputMaybe + email_gte?: InputMaybe + email_in?: InputMaybe> + email_isNull?: InputMaybe + email_lt?: InputMaybe + email_lte?: InputMaybe + email_not_contains?: InputMaybe + email_not_containsInsensitive?: InputMaybe + email_not_endsWith?: InputMaybe + email_not_eq?: InputMaybe + email_not_in?: InputMaybe> + email_not_startsWith?: InputMaybe + email_startsWith?: InputMaybe + expiry_eq?: InputMaybe + expiry_gt?: InputMaybe + expiry_gte?: InputMaybe + expiry_in?: InputMaybe> + expiry_isNull?: InputMaybe + expiry_lt?: InputMaybe + expiry_lte?: InputMaybe + expiry_not_eq?: InputMaybe + expiry_not_in?: InputMaybe> + id_contains?: InputMaybe + id_containsInsensitive?: InputMaybe + id_endsWith?: InputMaybe + id_eq?: InputMaybe + id_gt?: InputMaybe + id_gte?: InputMaybe + id_in?: InputMaybe> + id_isNull?: InputMaybe + id_lt?: InputMaybe + id_lte?: InputMaybe + id_not_contains?: InputMaybe + id_not_containsInsensitive?: InputMaybe + id_not_endsWith?: InputMaybe + id_not_eq?: InputMaybe + id_not_in?: InputMaybe> + id_not_startsWith?: InputMaybe + id_startsWith?: InputMaybe + issuedAt_eq?: InputMaybe + issuedAt_gt?: InputMaybe + issuedAt_gte?: InputMaybe + issuedAt_in?: InputMaybe> + issuedAt_isNull?: InputMaybe + issuedAt_lt?: InputMaybe + issuedAt_lte?: InputMaybe + issuedAt_not_eq?: InputMaybe + issuedAt_not_in?: InputMaybe> +} + +export type EmailConfirmationTokensConnection = { + __typename?: 'EmailConfirmationTokensConnection' + edges: Array + pageInfo: PageInfo + totalCount: Scalars['Int'] +} + export type EmailDeliveryAttempt = { __typename?: 'EmailDeliveryAttempt' /** UUID */ @@ -4904,12 +4990,6 @@ export enum EncryptionArtifactsOrderByInput { AccountIdDesc = 'account_id_DESC', AccountIsBlockedAsc = 'account_isBlocked_ASC', AccountIsBlockedDesc = 'account_isBlocked_DESC', - AccountIsEmailConfirmedAsc = 'account_isEmailConfirmed_ASC', - AccountIsEmailConfirmedDesc = 'account_isEmailConfirmed_DESC', - AccountJoystreamAccountAsc = 'account_joystreamAccount_ASC', - AccountJoystreamAccountDesc = 'account_joystreamAccount_DESC', - AccountReferrerChannelIdAsc = 'account_referrerChannelId_ASC', - AccountReferrerChannelIdDesc = 'account_referrerChannelId_DESC', AccountRegisteredAtAsc = 'account_registeredAt_ASC', AccountRegisteredAtDesc = 'account_registeredAt_DESC', CipherIvAsc = 'cipherIv_ASC', @@ -5905,8 +5985,6 @@ export enum MemberMetadataOrderByInput { AvatarIsTypeOfDesc = 'avatar_isTypeOf_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', - MemberControllerAccountAsc = 'member_controllerAccount_ASC', - MemberControllerAccountDesc = 'member_controllerAccount_DESC', MemberCreatedAtAsc = 'member_createdAt_ASC', MemberCreatedAtDesc = 'member_createdAt_DESC', MemberHandleRawAsc = 'member_handleRaw_ASC', @@ -5995,7 +6073,7 @@ export type Membership = { /** Channels owned by this member */ channels: Array /** Member's controller account id */ - controllerAccount: Scalars['String'] + controllerAccount: BlockchainAccount /** Timestamp of the block the membership was created at */ createdAt: Scalars['DateTime'] /** The handle coming from decoded handleRaw if possible */ @@ -6053,8 +6131,8 @@ export type MembershipEdge = { } export enum MembershipOrderByInput { - ControllerAccountAsc = 'controllerAccount_ASC', - ControllerAccountDesc = 'controllerAccount_DESC', + ControllerAccountIdAsc = 'controllerAccount_id_ASC', + ControllerAccountIdDesc = 'controllerAccount_id_DESC', CreatedAtAsc = 'createdAt_ASC', CreatedAtDesc = 'createdAt_DESC', HandleRawAsc = 'handleRaw_ASC', @@ -6073,6 +6151,12 @@ export enum MembershipOrderByInput { TotalChannelsCreatedDesc = 'totalChannelsCreated_DESC', } +export type MembershipType = { + __typename?: 'MembershipType' + controllerAccountId?: Maybe + id: Scalars['String'] +} + export type MembershipWhereInput = { AND?: InputMaybe> OR?: InputMaybe> @@ -6082,23 +6166,8 @@ export type MembershipWhereInput = { channels_every?: InputMaybe channels_none?: InputMaybe channels_some?: InputMaybe - controllerAccount_contains?: InputMaybe - controllerAccount_containsInsensitive?: InputMaybe - controllerAccount_endsWith?: InputMaybe - controllerAccount_eq?: InputMaybe - controllerAccount_gt?: InputMaybe - controllerAccount_gte?: InputMaybe - controllerAccount_in?: InputMaybe> + controllerAccount?: InputMaybe controllerAccount_isNull?: InputMaybe - controllerAccount_lt?: InputMaybe - controllerAccount_lte?: InputMaybe - controllerAccount_not_contains?: InputMaybe - controllerAccount_not_containsInsensitive?: InputMaybe - controllerAccount_not_endsWith?: InputMaybe - controllerAccount_not_eq?: InputMaybe - controllerAccount_not_in?: InputMaybe> - controllerAccount_not_startsWith?: InputMaybe - controllerAccount_startsWith?: InputMaybe createdAt_eq?: InputMaybe createdAt_gt?: InputMaybe createdAt_gte?: InputMaybe @@ -6294,6 +6363,7 @@ export type MetaprotocolTransactionStatusEventData = { export type Mutation = { __typename?: 'Mutation' addVideoView: AddVideoViewResult + createAccountMembership: CreateAccountMembershipResult excludeChannel: ExcludeChannelResult excludeContent: ExcludeContentResult excludeVideo: ExcludeVideoInfo @@ -6332,6 +6402,13 @@ export type MutationAddVideoViewArgs = { videoId: Scalars['String'] } +export type MutationCreateAccountMembershipArgs = { + about: Scalars['String'] + avatar: Scalars['String'] + handle: Scalars['String'] + name: Scalars['String'] +} + export type MutationExcludeChannelArgs = { channelId: Scalars['String'] rationale: Scalars['String'] @@ -6554,8 +6631,6 @@ export enum NftActivityOrderByInput { EventTimestampDesc = 'event_timestamp_DESC', IdAsc = 'id_ASC', IdDesc = 'id_DESC', - MemberControllerAccountAsc = 'member_controllerAccount_ASC', - MemberControllerAccountDesc = 'member_controllerAccount_DESC', MemberCreatedAtAsc = 'member_createdAt_ASC', MemberCreatedAtDesc = 'member_createdAt_DESC', MemberHandleRawAsc = 'member_handleRaw_ASC', @@ -7056,12 +7131,6 @@ export enum NotificationOrderByInput { AccountIdDesc = 'account_id_DESC', AccountIsBlockedAsc = 'account_isBlocked_ASC', AccountIsBlockedDesc = 'account_isBlocked_DESC', - AccountIsEmailConfirmedAsc = 'account_isEmailConfirmed_ASC', - AccountIsEmailConfirmedDesc = 'account_isEmailConfirmed_DESC', - AccountJoystreamAccountAsc = 'account_joystreamAccount_ASC', - AccountJoystreamAccountDesc = 'account_joystreamAccount_DESC', - AccountReferrerChannelIdAsc = 'account_referrerChannelId_ASC', - AccountReferrerChannelIdDesc = 'account_referrerChannelId_DESC', AccountRegisteredAtAsc = 'account_registeredAt_ASC', AccountRegisteredAtDesc = 'account_registeredAt_DESC', CreatedAtAsc = 'createdAt_ASC', @@ -8144,6 +8213,11 @@ export type Query = { bidByUniqueInput?: Maybe bids: Array bidsConnection: BidsConnection + blockchainAccountById?: Maybe + /** @deprecated Use blockchainAccountById */ + blockchainAccountByUniqueInput?: Maybe + blockchainAccounts: Array + blockchainAccountsConnection: BlockchainAccountsConnection channelById?: Maybe /** @deprecated Use channelById */ channelByUniqueInput?: Maybe @@ -8221,6 +8295,11 @@ export type Query = { distributionBuckets: Array distributionBucketsConnection: DistributionBucketsConnection dumbPublicFeedVideos: Array {shortenString( - type === 'sell' ? formData.destinationAddress : activeMembership?.controllerAccount ?? '', + type === 'sell' ? formData.destinationAddress : activeMembership?.controllerAccount.id ?? '', 6, 6 )} diff --git a/packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx b/packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx index fa9dc31e17..5dfd4f4dab 100644 --- a/packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx +++ b/packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx @@ -107,14 +107,20 @@ const TableText = ({ text, onShowMoreClick }: { text?: string; onShowMoreClick: const Sender = ({ sender }: { sender: PaymentHistory['sender'] }) => { const { memberships } = useMemberships( - { where: { controllerAccount_eq: sender } }, + { + where: { + controllerAccount: { + id_eq: sender, + }, + }, + }, { onError: (error) => SentryLogger.error('Failed to fetch memberships', 'ActiveUserProvider', error), skip: sender === 'council', } ) const { activeChannel } = useUser() - const member = memberships?.find((member) => member.controllerAccount === sender) + const member = memberships?.find((member) => member.controllerAccount.id === sender) const { urls: avatarUrls, isLoadingAsset: avatarLoading } = getMemberAvatar(member) if (sender === 'council') { diff --git a/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx b/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx index 98586a892b..527999019b 100644 --- a/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx +++ b/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx @@ -3,9 +3,13 @@ import { useSearchParams } from 'react-router-dom' import { DialogModal } from '@/components/_overlays/DialogModal' import { useCreateMember } from '@/hooks/useCreateMember' +import { useAuth } from '@/providers/auth/auth.hooks' import { useSnackbar } from '@/providers/snackbars' +import { useUser } from '@/providers/user/user.hooks' +import { SentryLogger } from '@/utils/logs' -import { CreatePassword } from '../genericSteps/CreatePassword' +import { CreateHandle, NewHandleForm } from '../genericSteps/CreateHandle' +import { CreatePassword, NewPasswordForm } from '../genericSteps/CreatePassword' import { EmailVerified } from '../genericSteps/EmailVerified' import { SeedGeneration } from '../genericSteps/SeedGeneration' import { WaitingModal } from '../genericSteps/WaitingModal' @@ -14,22 +18,22 @@ import { SetActionButtonHandler } from '../genericSteps/types' enum AccountSetupStep { verification, password, + member, seed, creation, } type AccountSetupForm = { - password?: string - confirmPassword?: string mnemonic?: string - captchaToken?: string - email?: string -} +} & NewHandleForm & + NewPasswordForm export const AccountSetup = () => { const [searchParams, setSearchParams] = useSearchParams() - const confirmationCode = searchParams.get('email-token') // 5y6WUaZ5IxAGf4iLGarc1OHFHBWScJZ4/gxWGn4trq4= - console.log('xdd', confirmationCode) + const { refetchCurrentUser } = useAuth() + const { refetchUserMemberships } = useUser() + const confirmationCode = decodeURIComponent(searchParams.get('email-token') ?? '') // 5y6WUaZ5IxAGf4iLGarc1OHFHBWScJZ4/gxWGn4trq4= + const email = decodeURIComponent(searchParams.get('email') ?? '') const [step, setStep] = useState(AccountSetupStep.verification) const [loading, setLoading] = useState(true) const [primaryAction, setPrimaryAction] = useState(undefined) @@ -86,10 +90,20 @@ export const AccountSetup = () => { }, [resetSearchParams, step]) const handleAccountAndMemberCreation = async () => { + const { avatar, password, handle, mnemonic } = formRef.current + + if (!(avatar || password || email || handle || mnemonic)) { + displaySnackbar({ + title: 'Creation failed', + description: 'Missing required fields to create an account', + }) + SentryLogger.error('Missing fields during account creation', 'AccountSetup', { form: formRef.current }) + return + } await createNewOrionAccount({ data: { confirmedTerms: true, - email: formRef.current.email ?? 'alek12342@gmail.com', + email: email ?? '', mnemonic: formRef.current.mnemonic ?? '', password: formRef.current.password ?? '', emailConfimationToken: confirmationCode ?? '', @@ -98,21 +112,35 @@ export const AccountSetup = () => { resetSearchParams() }, onSuccess: async () => { - const membershipId = await createNewMember({ - data: { - handle: 'testttt1', - captchaToken: formRef.current.captchaToken ?? '', - allowDownload: true, - mnemonic: formRef.current.mnemonic ?? '', + await new Promise((res) => setTimeout(res, 5000)) + + await createNewMember( + { + data: { + handle: formRef.current.handle ?? '', + captchaToken: formRef.current.captchaToken ?? '', + allowDownload: true, + mnemonic: formRef.current.mnemonic ?? '', + avatar: formRef.current.avatar, + }, + onError: () => { + displaySnackbar({ + iconType: 'error', + title: 'Error during membership creation', + }) + resetSearchParams() + }, }, - onError: () => { + async () => { + await refetchCurrentUser() + await refetchUserMemberships() + resetSearchParams() displaySnackbar({ - iconType: 'error', - title: 'Error during membership creation', + iconType: 'success', + title: 'Account created!', }) - resetSearchParams() - }, - }) + } + ) }, }) } @@ -129,6 +157,20 @@ export const AccountSetup = () => { {step === AccountSetupStep.password ? ( { + setStep(AccountSetupStep.member) + formRef.current = { + ...formRef.current, + ...data, + } + }} + setActionButtonHandler={(fn) => setPrimaryAction(() => fn)} + /> + ) : null} + {step === AccountSetupStep.member ? ( + { setStep(AccountSetupStep.seed) diff --git a/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalWalletStep/ExternalSignInModalWalletStep.tsx b/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalWalletStep/ExternalSignInModalWalletStep.tsx index 842c055d2f..fc805070af 100644 --- a/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalWalletStep/ExternalSignInModalWalletStep.tsx +++ b/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalWalletStep/ExternalSignInModalWalletStep.tsx @@ -124,7 +124,9 @@ export const ExternalSignInModalWalletStep: FC acc.address), + controllerAccount: { + id_in: accounts.map((acc) => acc.address), + }, }, }, }) diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/SignUpPasswordStep.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/SignUpPasswordStep.tsx index 6827164a6f..d05d5e99bf 100644 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/SignUpPasswordStep.tsx +++ b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/SignUpPasswordStep.tsx @@ -42,7 +42,7 @@ export const SignUpPasswordStep: FC = ({ password, confirmPassword: password, }, - resolver: zodResolver(passwordAndRepeatPasswordSchema), + resolver: zodResolver(passwordAndRepeatPasswordSchema(true)), }) const { handleSubmit, diff --git a/packages/atlas/src/components/_auth/genericSteps/CreateHandle.tsx b/packages/atlas/src/components/_auth/genericSteps/CreateHandle.tsx new file mode 100644 index 0000000000..cab4268441 --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/CreateHandle.tsx @@ -0,0 +1,253 @@ +import styled from '@emotion/styled' +import debouncePromise from 'awesome-debounce-promise' +import { ChangeEventHandler, ForwardedRef, forwardRef, useEffect, useRef, useState } from 'react' +import { Controller, FormProvider, useController, useForm, useFormContext } from 'react-hook-form' + +import { AppLogo } from '@/components/AppLogo' +import { Avatar } from '@/components/Avatar' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { FormField } from '@/components/_inputs/FormField' +import { Input, InputProps } from '@/components/_inputs/Input' +import { ImageInputFile } from '@/components/_inputs/MultiFileSelect' +import { DialogModal } from '@/components/_overlays/DialogModal' +import { ImageCropModal, ImageCropModalImperativeHandle } from '@/components/_overlays/ImageCropModal' +import { MEMBERSHIP_NAME_PATTERN } from '@/config/regex' +import { useMountEffect } from '@/hooks/useMountEffect' +import { useUniqueMemberHandle } from '@/hooks/useUniqueMemberHandle' +import { cVar, media, sizes } from '@/styles' + +export type NewHandleForm = { + handle?: string + avatar?: ImageInputFile +} + +type CreatePasswordProps = { + defaultValues?: NewHandleForm + onSubmit: (data: NewHandleForm) => void + setActionButtonHandler: (fn: () => void | Promise) => void +} + +export const CreateHandle = ({ setActionButtonHandler, onSubmit, defaultValues }: CreatePasswordProps) => { + const form = useForm({ reValidateMode: 'onSubmit', defaultValues }) + const { + handleSubmit, + watch, + control, + formState: { errors, isSubmitting }, + } = form + const handleInputRef = useRef(null) + const avatarDialogRef = useRef(null) + const backgroundImage = watch('avatar')?.url || undefined + + const [isHandleValidating, setIsHandleValidating] = useState(false) + + useMountEffect(() => { + setActionButtonHandler(() => { + handleSubmit((data) => { + onSubmit(data) + })() + }) + }) + + useEffect(() => { + if (errors.handle) { + handleInputRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + }, [errors.handle]) + + return ( + <> + + {backgroundImage && } + + + + + Create membership + + + + + + + + ( + <> + + avatarDialogRef.current?.open( + imageInputFile?.originalBlob ? imageInputFile.originalBlob : imageInputFile?.blob, + imageInputFile?.imageCropData, + !!imageInputFile?.blob + ) + } + assetUrls={imageInputFile?.url ? [imageInputFile.url] : []} + editable + /> + { + onChange({ + blob, + url, + imageCropData, + originalBlob, + }) + }} + onDelete={() => { + onChange(undefined) + }} + ref={avatarDialogRef} + /> + + )} + /> + + + + + + + ) +} + +export const BackgroundImage = styled.img` + position: absolute; + filter: blur(${sizes(8)}); + opacity: 0.25; + top: 0; + left: 0; + object-fit: cover; + width: 100%; + height: 100%; + z-index: -1; +` + +type CustomBackgroundContainerProps = { + darkBackground?: boolean + hasNegativeBottomMargin?: boolean + hasDivider?: boolean + hasBottomPadding?: boolean +} + +export const CustomBackgroundContainer = styled.div` + position: relative; + overflow: hidden; + z-index: 0; + margin: calc(-1 * var(--local-size-dialog-padding)) calc(-1 * var(--local-size-dialog-padding)) 0 + calc(-1 * var(--local-size-dialog-padding)); + padding: var(--local-size-dialog-padding); + background-color: ${({ darkBackground }) => (darkBackground ? cVar('colorBackground') : 'unset')}; + box-shadow: cVar('effectDividersBottom'); +` + +export const StyledForm = styled.form` + position: relative; + padding-top: ${sizes(17)}; + display: grid; + gap: ${sizes(6)}; + margin-bottom: ${sizes(6)}; +` + +export const StyledDialogModal = styled(DialogModal)` + max-height: calc(100vh - 80px); + ${media.sm} { + max-height: 576px; + } +` + +export const SubtitleContainer = styled.div` + display: inline-block; + text-decoration: none; + margin-top: ${sizes(2)}; + margin-bottom: ${sizes(11)}; +` + +export const StyledAvatar = styled(Avatar)` + position: absolute; + transform: translateY(-50%); + top: 0; +` + +const StyledAppLogo = styled(AppLogo)` + height: 36px; + width: auto; + + path { + fill: ${cVar('colorTextMuted')}; + } +` + +type HandleInputProps = InputProps & { + name: string + setIsHandleValidating: (v: boolean) => void +} + +const HandleInput = forwardRef( + ( + { name, setIsHandleValidating, processing, ...inputProps }: HandleInputProps, + ref: ForwardedRef + ) => { + const { checkIfMemberIsAvailable } = useUniqueMemberHandle() + + const { control, trigger } = useFormContext() + const { field } = useController({ + name, + control, + rules: { + validate: { + valid: (value) => (!value ? true : MEMBERSHIP_NAME_PATTERN.test(value) || 'Enter a valid member handle.'), + unique: async (value) => (await checkIfMemberIsAvailable(value)) || 'This member handle is already in use.', + }, + required: { value: true, message: 'Member handle is required.' }, + minLength: { value: 5, message: 'Member handle must be at least 5 characters long.' }, + }, + }) + + const debouncedHandleValidation = useRef( + debouncePromise(async () => { + await trigger(name) + setIsHandleValidating(false) + }, 500) + ) + const handleChange: ChangeEventHandler = (evt) => { + const value = evt.target.value?.toLowerCase().replace(/[^0-9_a-z]/g, '_') + field.onChange(value) + if (!processing) setIsHandleValidating(true) + debouncedHandleValidation.current() + } + + return ( + { + field.ref(e) + if (ref && 'current' in ref) ref.current = e + else ref?.(e) + }} + processing={processing} + onChange={handleChange} + /> + ) + } +) +HandleInput.displayName = 'HandleInput' diff --git a/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx b/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx index 6c88ce07fe..c9180d5f70 100644 --- a/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/CreatePassword.tsx @@ -16,7 +16,7 @@ import { useMountEffect } from '@/hooks/useMountEffect' import { cVar, sizes } from '@/styles' import { passwordAndRepeatPasswordSchema } from '@/utils/formValidationOptions' -type NewPasswordForm = { +export type NewPasswordForm = { password?: string confirmPassword?: string captchaToken?: string @@ -27,6 +27,7 @@ type CreatePasswordProps = { onSubmit: (data: NewPasswordForm) => void setActionButtonHandler: (fn: () => void | Promise) => void dialogContentRef?: RefObject + withCaptcha?: boolean } export const CreatePassword = ({ @@ -34,12 +35,13 @@ export const CreatePassword = ({ onSubmit, defaultValues, dialogContentRef, + withCaptcha = true, }: CreatePasswordProps) => { const form = useForm({ shouldFocusError: true, reValidateMode: 'onSubmit', defaultValues, - resolver: zodResolver(passwordAndRepeatPasswordSchema), + resolver: zodResolver(passwordAndRepeatPasswordSchema(withCaptcha)), }) const { handleSubmit, @@ -108,7 +110,7 @@ export const CreatePassword = ({ /> - {atlasConfig.features.members.hcaptchaSiteKey && ( + {atlasConfig.features.members.hcaptchaSiteKey && withCaptcha && ( { const { isLoading } = useQuery({ queryKey: code, - queryFn: () => new Promise((res) => setTimeout(res, 5000)), // this is not implemented in the orion, it would be good to check if the token is valid before asking user to provide all the info + queryFn: () => new Promise((res) => setTimeout(res, 1000)), // this is not implemented in the orion, it would be good to check if the token is valid before asking user to provide all the info onSuccess: () => { onVerified() }, diff --git a/packages/atlas/src/components/_overlays/ClaimChannelPaymentsDialog/ClaimChannelPaymentsDialog.tsx b/packages/atlas/src/components/_overlays/ClaimChannelPaymentsDialog/ClaimChannelPaymentsDialog.tsx index cea058da6b..34ffcb6992 100644 --- a/packages/atlas/src/components/_overlays/ClaimChannelPaymentsDialog/ClaimChannelPaymentsDialog.tsx +++ b/packages/atlas/src/components/_overlays/ClaimChannelPaymentsDialog/ClaimChannelPaymentsDialog.tsx @@ -23,7 +23,7 @@ export const ClaimChannelPaymentsDialog = ({ onExit, show }: ClaimChannelPayment const { activeMembership } = useUser() const { availableAward, claimReward, isAwardLoading, txParams } = useChannelPayout(onExit) const { fullFee, loading: feeLoading } = useFee('claimRewardTx', txParams) - const { totalBalance } = useSubscribeAccountBalance(activeMembership?.controllerAccount) + const { totalBalance } = useSubscribeAccountBalance(activeMembership?.controllerAccount.id) const mdMatch = useMediaMatch('md') return ( diff --git a/packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx b/packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx index 59c01da553..6e7678a31a 100644 --- a/packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx +++ b/packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx @@ -187,7 +187,9 @@ export const MemberDropdownNav: FC = ({ diff --git a/packages/atlas/src/components/_overlays/SendTransferDialogs/SendFundsDialog.tsx b/packages/atlas/src/components/_overlays/SendTransferDialogs/SendFundsDialog.tsx index c8ffb8fdd0..dff321ffaf 100644 --- a/packages/atlas/src/components/_overlays/SendTransferDialogs/SendFundsDialog.tsx +++ b/packages/atlas/src/components/_overlays/SendTransferDialogs/SendFundsDialog.tsx @@ -102,7 +102,7 @@ export const SendFundsDialog: FC = ({ const isValidJoystreamAddress = (address: string) => isValidAddressPolkadotAddress(address) && address.startsWith(joystreamAddressPrefix) - const isOwnAccount = account === activeMembership?.controllerAccount + const isOwnAccount = account === activeMembership?.controllerAccount.id const fullFee = isWithdrawalMode ? (isOwnAccount ? withdrawFee : withdrawFee.add(transferFee)) : transferFee @@ -123,7 +123,13 @@ export const SendFundsDialog: FC = ({ data: { memberships }, } = await client.query({ query: GetMembershipsDocument, - variables: { where: { controllerAccount_eq: val } }, + variables: { + where: { + controllerAccount: { + id_eq: val, + }, + }, + }, }) setDestinationAccount(memberships.length ? memberships[0] : undefined) } catch (error) { @@ -169,7 +175,7 @@ export const SendFundsDialog: FC = ({ }, onTxSync: async () => { trackWithdrawnFunds( - (isWithdrawalMode ? channelId : activeMembership?.controllerAccount) || undefined, + (isWithdrawalMode ? channelId : activeMembership?.controllerAccount.id) || undefined, rawAmount?.toString(), isOwnAccount ) diff --git a/packages/atlas/src/hooks/useCreateMember.ts b/packages/atlas/src/hooks/useCreateMember.ts index 40a741e834..c93e678af4 100644 --- a/packages/atlas/src/hooks/useCreateMember.ts +++ b/packages/atlas/src/hooks/useCreateMember.ts @@ -5,6 +5,7 @@ import { useCallback } from 'react' import { useMutation } from 'react-query' import { axiosInstance } from '@/api/axios' +// import { useCreateAccountMembershipMutation } from '@/api/queries/__generated__/accounts.generated' import { ImageInputFile } from '@/components/_inputs/MultiFileSelect' import { FAUCET_URL, YPP_FAUCET_URL } from '@/config/env' import { keyring } from '@/joystream-lib/lib' @@ -72,6 +73,7 @@ export enum RegisterError { UnknownError = 'UnknownError', MembershipNotFound = 'MembershipNotFound', SessionRequired = 'SessionRequired', + TokenExpired = 'TokenExpired', } type SignUpParams = { @@ -95,6 +97,7 @@ export const useCreateMember = () => { const { displaySnackbar } = useSnackbar() const { addBlockAction } = useTransactionManagerStore((state) => state.actions) const ytResponseData = useYppStore((state) => state.ytResponseData) + // const [createAccountMembershipMutation] = useCreateAccountMembershipMutation() const { mutateAsync: avatarMutation } = useMutation('avatar-post', (croppedBlob: Blob) => uploadAvatarImage(croppedBlob) @@ -130,6 +133,14 @@ export const useCreateMember = () => { try { onStart?.() const response = await faucetMutation(body) + // const res = await createAccountMembershipMutation({ + // variables: { + // about: '', + // handle: data.handle, + // avatar: fileUrl ?? '', + // name: '', + // }, + // }) onBlockSync && addBlockAction({ callback: onBlockSync, targetBlock: response.data.block }) return String(response.data.memberId) @@ -233,7 +244,7 @@ export const useCreateMember = () => { const { lockedBalance } = await joystream.getAccountBalance(address) const amountOfTokens = hapiBnToTokenNumber(new BN(lockedBalance)) onSuccess?.({ amountOfTokens }) - handleLogin({ type: 'internal', ...data }) + await handleLogin({ type: 'internal', ...data }) } catch (error) { if (error instanceof OrionAccountError) { const errorCode = error.status @@ -253,6 +264,14 @@ export const useCreateMember = () => { } else if (errorMessage.startsWith("cookie 'session_id' required")) { onError?.(RegisterError.SessionRequired) return + } else if (errorMessage.includes('Token not found. Possibly expired or already used.')) { + onError?.(RegisterError.TokenExpired) + displaySnackbar({ + title: 'Something went wrong', + description: `Looks like your token is either invalid or expired. Try to get a new one, if problem persists contact support.`, + iconType: 'error', + }) + return } else { displaySnackbar({ title: 'Something went wrong', diff --git a/packages/atlas/src/providers/auth/auth.provider.tsx b/packages/atlas/src/providers/auth/auth.provider.tsx index be5f06a441..7700f193c7 100644 --- a/packages/atlas/src/providers/auth/auth.provider.tsx +++ b/packages/atlas/src/providers/auth/auth.provider.tsx @@ -47,7 +47,6 @@ export const AuthProvider: FC = ({ children }) => { const lastUsedWalletName = useWalletStore((store) => store.lastUsedWalletName) const currentWallet = useWalletStore((store) => store.wallet) const { signInToWallet } = useWallet() - console.log(loggedAddress) useMountEffect(() => { const init = async () => { await cryptoWaitReady() @@ -74,13 +73,13 @@ export const AuthProvider: FC = ({ children }) => { const mnemonic = await decodeSessionEncodedSeedToMnemonic(encodedSeed) if (mnemonic) { const keypair = keyring.addFromMnemonic(mnemonic) - if (keypair.address === data.accountData.joystreamAccount.id) { + if (keypair.address === data.accountData.joystreamAccountId ?? '') { setLoggedAddress(keypair.address) setCurrentUser(data.accountData) identifyUser({ name: 'Sign in', email: data.accountData.email, - memberId: data.accountData.joystreamAccount.memberships[0]?.id, + memberId: 'unknown', signInType: 'password', }) setApiActiveAccount('seed', mnemonic) @@ -94,16 +93,16 @@ export const AuthProvider: FC = ({ children }) => { setTimeout(async () => { // add a slight delay - sometimes the extension will not initialize by the time of this call and may appear unavailable const res = await signInToWallet(lastUsedWalletName, true) - if (res?.find((walletAcc) => walletAcc.address === data.accountData.joystreamAccount.id)) { - setLoggedAddress(data.accountData.joystreamAccount.id) + if (res?.find((walletAcc) => walletAcc.address === data.accountData.joystreamAccountId ?? '')) { + setLoggedAddress(data.accountData.joystreamAccountId ?? '') identifyUser({ name: 'Sign in', email: data.accountData.email, - memberId: data.accountData.joystreamAccount.memberships[0]?.id, + memberId: 'unknown', signInType: 'wallet', }) setCurrentUser(data.accountData) - setApiActiveAccount('address', data.accountData.joystreamAccount.id) + setApiActiveAccount('address', data.accountData.joystreamAccountId ?? '' ?? '') } setIsAuthenticating(false) }, 200) @@ -211,7 +210,7 @@ export const AuthProvider: FC = ({ children }) => { identifyUser({ name: 'Sign in', email: res.data.accountData.email, - memberId: res.data.accountData.joystreamAccount.memberships[0]?.id, + memberId: res.data.accountData.joystreamAccountId ?? '', signInType: params.type === 'external' ? 'wallet' : 'password', }) diff --git a/packages/atlas/src/providers/joystream/joystream.hooks.ts b/packages/atlas/src/providers/joystream/joystream.hooks.ts index 407e0fc99c..0dc605acc5 100644 --- a/packages/atlas/src/providers/joystream/joystream.hooks.ts +++ b/packages/atlas/src/providers/joystream/joystream.hooks.ts @@ -175,14 +175,14 @@ export const useSubscribeAccountBalance = ( const debt = totalInvitationLock && lockedAccountBalance && totalInvitationLock.sub(lockedAccountBalance) useEffect(() => { - if (!(controllerAccount || activeMembership?.controllerAccount) || !joystream) { + if (!(controllerAccount || activeMembership?.controllerAccount.id) || !joystream) { return } let unsubscribe: (() => void) | undefined const init = async () => { unsubscribe = await joystream.subscribeAccountBalance( - (controllerAccount || activeMembership?.controllerAccount) ?? '', + (controllerAccount || activeMembership?.controllerAccount.id) ?? '', proxyCallback(({ availableBalance, lockedBalance, totalInvitationLock }) => { setLockedAccountBalance(new BN(lockedBalance)) setTotalInvitationLock(new BN(totalInvitationLock)) diff --git a/packages/atlas/src/providers/user/user.provider.tsx b/packages/atlas/src/providers/user/user.provider.tsx index 6efc2f48bf..b6eb9249d4 100644 --- a/packages/atlas/src/providers/user/user.provider.tsx +++ b/packages/atlas/src/providers/user/user.provider.tsx @@ -21,6 +21,7 @@ export const UserProvider: FC = ({ children }) => { actions: { setLastUsedChannelId }, } = usePersonalDataStore((state) => state) const [channelId, setChannelId] = useState(lastUsedChannelId) + const [membershipId, setMemberId] = useState(lastUsedChannelId) const setActiveChannel = useCallback( (channelId: string) => { @@ -39,20 +40,20 @@ export const UserProvider: FC = ({ children }) => { } = useMemberships( { where: { - id_eq: currentUser?.membershipId, + controllerAccount: { + id_eq: currentUser?.joystreamAccountId, + }, }, }, { onCompleted: (data) => { - const activeMembership = - (currentUser?.membershipId && - data.memberships?.find((membership) => membership.id === currentUser?.membershipId)) || - null + const activeMembership = data.memberships[0] || null + setMemberId(activeMembership?.id) if (activeMembership && !activeMembership.channels.some((channel) => channel.id === channelId)) { setActiveChannel(activeMembership.channels[0].id) } }, - skip: !currentUser, + skip: !currentUser?.joystreamAccountId, onError: (error) => SentryLogger.error('Failed to fetch user memberships', 'UserProvider', error, { user: currentUser ?? {} }), } @@ -60,32 +61,25 @@ export const UserProvider: FC = ({ children }) => { const memberships = currentMemberships ?? previousData?.memberships - const refetchUserMemberships = useCallback(() => { - return refetch() + const refetchUserMemberships = useCallback(async () => { + const res = await refetch() + setMemberId(res.data.memberships[0]?.id) + return res }, [refetch]) // keep user used by loggers in sync useEffect(() => { const user = { - accountId: currentUser?.joystreamAccount.id, - memberId: currentUser?.membershipId, - channelId: currentMemberships?.[0].channels[0]?.id, + accountId: currentUser?.joystreamAccountId, + memberId: membershipId, + channelId: currentMemberships?.[0]?.channels[0]?.id, } SentryLogger.setUser(user) UserEventsLogger.setUser(user) - }, [ - currentMemberships, - currentUser?.email, - currentUser?.joystreamAccount.id, - currentUser?.membershipId, - setApiActiveAccount, - ]) - - const activeMembership = - (currentUser?.membershipId && memberships?.find((membership) => membership.id === currentUser?.membershipId)) || - null + }, [currentMemberships, currentUser?.email, currentUser?.joystreamAccountId, membershipId, setApiActiveAccount]) + const activeMembership = (membershipId && memberships?.find((membership) => membership.id === membershipId)) || null const activeChannel = (channelId ? activeMembership?.channels.find((channel) => channel.id === channelId) @@ -99,8 +93,8 @@ export const UserProvider: FC = ({ children }) => { activeMembership, activeChannel, refetchUserMemberships, - memberId: currentUser?.membershipId ?? null, - accountId: currentUser?.joystreamAccount.id ?? null, + memberId: membershipId ?? null, + accountId: currentUser?.joystreamAccountId ?? null, channelId, setActiveChannel, }), @@ -110,8 +104,8 @@ export const UserProvider: FC = ({ children }) => { membershipsLoading, activeChannel, refetchUserMemberships, - currentUser?.membershipId, - currentUser?.joystreamAccount.id, + membershipId, + currentUser?.joystreamAccountId, channelId, setActiveChannel, ] diff --git a/packages/atlas/src/utils/formValidationOptions.ts b/packages/atlas/src/utils/formValidationOptions.ts index 6f72946657..af172aaba8 100644 --- a/packages/atlas/src/utils/formValidationOptions.ts +++ b/packages/atlas/src/utils/formValidationOptions.ts @@ -62,33 +62,34 @@ export const pastDateValidation = (date: Date | null, required = false) => { const currentDate = new Date() return currentDate >= date } -export const passwordAndRepeatPasswordSchema = z - .object({ - password: z - .string() - .min(9, { message: 'Password has to meet requirements.' }) - .max(64, { message: 'Password has to meet requirements.' }), - confirmPassword: z.string(), - captchaToken: z.string().optional(), - }) - .refine( - (data) => { - return data.password === data.confirmPassword - }, - { - path: ['confirmPassword'], - message: 'Password address has to match.', - } - ) - .refine( - (data) => - !!data.password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,}$/)?.length, - { - path: ['password'], - message: 'Password has to meet requirements.', - } - ) - .refine((data) => (atlasConfig.features.members.hcaptchaSiteKey ? !!data.captchaToken : true), { - path: ['captchaToken'], - message: "Verify that you're not a robot.", - }) +export const passwordAndRepeatPasswordSchema = (withCaptcha: boolean) => + z + .object({ + password: z + .string() + .min(9, { message: 'Password has to meet requirements.' }) + .max(64, { message: 'Password has to meet requirements.' }), + confirmPassword: z.string(), + captchaToken: z.string().optional(), + }) + .refine( + (data) => { + return data.password === data.confirmPassword + }, + { + path: ['confirmPassword'], + message: 'Password address has to match.', + } + ) + .refine( + (data) => + !!data.password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,}$/)?.length, + { + path: ['password'], + message: 'Password has to meet requirements.', + } + ) + .refine((data) => (atlasConfig.features.members.hcaptchaSiteKey && withCaptcha ? !!data.captchaToken : true), { + path: ['captchaToken'], + message: "Verify that you're not a robot.", + }) diff --git a/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.utils.ts b/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.utils.ts index af97b604ac..fa8023ad41 100644 --- a/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.utils.ts +++ b/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.utils.ts @@ -70,17 +70,19 @@ const getAmount = (eventData: EventData, memberId: string): BN => { const getSender = (eventData: EventData) => { switch (eventData.__typename) { case 'NftBoughtEventData': - return eventData.buyer.controllerAccount + return eventData.buyer.controllerAccount.id case 'BidMadeCompletingAuctionEventData': case 'OpenAuctionBidAcceptedEventData': case 'EnglishAuctionSettledEventData': - return eventData.winningBid.bidder.controllerAccount + return eventData.winningBid.bidder.controllerAccount.id case 'ChannelRewardClaimedEventData': return 'council' case 'ChannelFundsWithdrawnEventData': - return eventData.actor.__typename === 'ContentActorMember' ? eventData.actor.member.controllerAccount : 'council' + return eventData.actor.__typename === 'ContentActorMember' + ? eventData.actor.member.controllerAccount.id + : 'council' case 'ChannelPaymentMadeEventData': - return eventData.payer.controllerAccount + return eventData.payer.controllerAccount.id case 'CreatorTokenRevenueSplitIssuedEventData': return 'own-channel' default: diff --git a/packages/atlas/src/views/viewer/MemberView/MemberView.tsx b/packages/atlas/src/views/viewer/MemberView/MemberView.tsx index 4ce28409e5..fe69ca1435 100644 --- a/packages/atlas/src/views/viewer/MemberView/MemberView.tsx +++ b/packages/atlas/src/views/viewer/MemberView/MemberView.tsx @@ -248,7 +248,7 @@ export const MemberView: FC = () => { avatarUrls={avatarUrls} avatarLoading={avatarLoading} handle={member?.handle} - address={member?.controllerAccount} + address={member?.controllerAccount.id} loading={loadingMember} isOwner={memberId === member?.id} /> diff --git a/packages/atlas/src/views/viewer/MembershipSettingsView/ChangePasswordDialog.tsx b/packages/atlas/src/views/viewer/MembershipSettingsView/ChangePasswordDialog.tsx index ddaf44c488..22ee762baa 100644 --- a/packages/atlas/src/views/viewer/MembershipSettingsView/ChangePasswordDialog.tsx +++ b/packages/atlas/src/views/viewer/MembershipSettingsView/ChangePasswordDialog.tsx @@ -65,7 +65,7 @@ export const ChangePasswordDialog: FC = ({ onClose, s const changePasswordForm = useForm({ shouldFocusError: true, - resolver: zodResolver(passwordAndRepeatPasswordSchema), + resolver: zodResolver(passwordAndRepeatPasswordSchema(true)), }) const [hideOldPasswordProps, resetHideOldPassword] = useHidePasswordInInput() @@ -91,7 +91,7 @@ export const ChangePasswordDialog: FC = ({ onClose, s const handleChangePassword = useCallback(() => { changePasswordForm.handleSubmit(async (data) => { - if (!currentUser || !mnemonic) { + if (!currentUser || !mnemonic || !currentUser.joystreamAccountId) { throw Error('Current user is not set or mnemonic is null') } try { @@ -101,7 +101,7 @@ export const ChangePasswordDialog: FC = ({ onClose, s mnemonic, newPassword: data.password, gatewayAccountId: currentUser.id, - joystreamAccountId: currentUser.joystreamAccount.id, + joystreamAccountId: currentUser.joystreamAccountId, }) handleClose() displaySnackbar({ diff --git a/packages/atlas/src/views/viewer/MembershipSettingsView/MembershipWallet/MembershipWallet.tsx b/packages/atlas/src/views/viewer/MembershipSettingsView/MembershipWallet/MembershipWallet.tsx index 9a04b9f9cb..0a8b5c511a 100644 --- a/packages/atlas/src/views/viewer/MembershipSettingsView/MembershipWallet/MembershipWallet.tsx +++ b/packages/atlas/src/views/viewer/MembershipSettingsView/MembershipWallet/MembershipWallet.tsx @@ -63,12 +63,12 @@ export const MembershipWallet = () => { const [isChangePasswordDialogOpen, setIsChangePasswordDialogOpen] = useState(false) const [isExportSeedDialogOpen, setIsExportSeedDialogOpen] = useState(false) const handleCopyToClipBoard = useCallback(() => { - if (!currentUser?.joystreamAccount.id) { + if (!currentUser?.joystreamAccountId) { return } setIsCopyClicked(true) - copyToClipboard(currentUser?.joystreamAccount.id) - }, [copyToClipboard, currentUser?.joystreamAccount.id]) + copyToClipboard(currentUser?.joystreamAccountId) + }, [copyToClipboard, currentUser?.joystreamAccountId]) return ( <> {!isWalletUser && ( @@ -89,7 +89,7 @@ export const MembershipWallet = () => { , From 075dd77defec3be4455b801068f344ab2d4db162 Mon Sep 17 00:00:00 2001 From: ikprk Date: Thu, 23 May 2024 06:58:32 +0200 Subject: [PATCH 13/22] Small account setup fixes --- .../src/components/_auth/AccountSetup/AccountSetup.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx b/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx index 527999019b..191f008b0d 100644 --- a/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx +++ b/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx @@ -92,12 +92,14 @@ export const AccountSetup = () => { const handleAccountAndMemberCreation = async () => { const { avatar, password, handle, mnemonic } = formRef.current - if (!(avatar || password || email || handle || mnemonic)) { + if (!(avatar && password && email && handle && mnemonic)) { displaySnackbar({ title: 'Creation failed', description: 'Missing required fields to create an account', + iconType: 'error', }) SentryLogger.error('Missing fields during account creation', 'AccountSetup', { form: formRef.current }) + setStep(AccountSetupStep.password) return } await createNewOrionAccount({ @@ -145,7 +147,7 @@ export const AccountSetup = () => { }) } - if (!confirmationCode) { + if (!confirmationCode || !email) { return null } From 4227f5e85399d04a12bae9436d798019512437cb Mon Sep 17 00:00:00 2001 From: ikprk Date: Thu, 23 May 2024 12:53:17 +0200 Subject: [PATCH 14/22] Initial setup for external wallets --- .../_auth/AccountSetup/AccountSetup.tsx | 82 ++++++--- .../_auth/AuthModals/AuthModals.tsx | 13 +- .../ExternalAccountSetup.tsx | 171 ++++++++++++++++++ .../_auth/ExternalAccountSetup/index.ts | 1 + .../ExternalSignInModal.tsx | 103 ++++++++--- .../ExternalSignInModalMembershipsStep.tsx | 15 +- .../ExternalSignInModalWalletStep.tsx | 15 +- .../ExternalSignInSteps.types.ts | 1 + .../genericSteps/ProvideEmailForLink.tsx | 7 + 9 files changed, 347 insertions(+), 61 deletions(-) create mode 100644 packages/atlas/src/components/_auth/ExternalAccountSetup/ExternalAccountSetup.tsx create mode 100644 packages/atlas/src/components/_auth/ExternalAccountSetup/index.ts diff --git a/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx b/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx index 191f008b0d..378bc8ba9c 100644 --- a/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx +++ b/packages/atlas/src/components/_auth/AccountSetup/AccountSetup.tsx @@ -102,6 +102,34 @@ export const AccountSetup = () => { setStep(AccountSetupStep.password) return } + await createNewMember( + { + data: { + handle: formRef.current.handle ?? '', + captchaToken: formRef.current.captchaToken ?? '', + allowDownload: true, + mnemonic: formRef.current.mnemonic ?? '', + avatar: formRef.current.avatar, + }, + onError: () => { + displaySnackbar({ + iconType: 'error', + title: 'Error during membership creation', + }) + resetSearchParams() + }, + }, + async () => { + await refetchCurrentUser() + await refetchUserMemberships() + resetSearchParams() + displaySnackbar({ + iconType: 'success', + title: 'Account created!', + }) + } + ) + return await createNewOrionAccount({ data: { confirmedTerms: true, @@ -116,33 +144,33 @@ export const AccountSetup = () => { onSuccess: async () => { await new Promise((res) => setTimeout(res, 5000)) - await createNewMember( - { - data: { - handle: formRef.current.handle ?? '', - captchaToken: formRef.current.captchaToken ?? '', - allowDownload: true, - mnemonic: formRef.current.mnemonic ?? '', - avatar: formRef.current.avatar, - }, - onError: () => { - displaySnackbar({ - iconType: 'error', - title: 'Error during membership creation', - }) - resetSearchParams() - }, - }, - async () => { - await refetchCurrentUser() - await refetchUserMemberships() - resetSearchParams() - displaySnackbar({ - iconType: 'success', - title: 'Account created!', - }) - } - ) + // await createNewMember( + // { + // data: { + // handle: formRef.current.handle ?? '', + // captchaToken: formRef.current.captchaToken ?? '', + // allowDownload: true, + // mnemonic: formRef.current.mnemonic ?? '', + // avatar: formRef.current.avatar, + // }, + // onError: () => { + // displaySnackbar({ + // iconType: 'error', + // title: 'Error during membership creation', + // }) + // resetSearchParams() + // }, + // }, + // async () => { + // await refetchCurrentUser() + // await refetchUserMemberships() + // resetSearchParams() + // displaySnackbar({ + // iconType: 'success', + // title: 'Account created!', + // }) + // } + // ) }, }) } diff --git a/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx b/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx index 294394fd6e..17913811fb 100644 --- a/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx +++ b/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx @@ -1,3 +1,5 @@ +import { useSearchParams } from 'react-router-dom' + import { ExternalSignInModal } from '@/components/_auth/ExternalSignInModal' import { LogInModal } from '@/components/_auth/LogInModal' // import { SignUpModal } from '@/components/_auth/SignUpModal' @@ -6,10 +8,14 @@ import { useAuthStore } from '@/providers/auth/auth.store' import { AccountSetup } from '../AccountSetup' import { EmailSetup } from '../EmailSetup' +import { ExternalAccountSetup } from '../ExternalAccountSetup' import { ForgotPasswordModal } from '../ForgotPasswordModal/ForgotPasswordModal' export const AuthModals = () => { const { authModalOpenName } = useAuthStore() + const [searchParams] = useSearchParams() + + const accountType = searchParams.get('account-type') if (authModalOpenName) { return ( @@ -25,5 +31,10 @@ export const AuthModals = () => { ) } - return + return ( + <> + {accountType === 'internal' ? : null} + {accountType === 'external' ? : null} + + ) } diff --git a/packages/atlas/src/components/_auth/ExternalAccountSetup/ExternalAccountSetup.tsx b/packages/atlas/src/components/_auth/ExternalAccountSetup/ExternalAccountSetup.tsx new file mode 100644 index 0000000000..397f242dde --- /dev/null +++ b/packages/atlas/src/components/_auth/ExternalAccountSetup/ExternalAccountSetup.tsx @@ -0,0 +1,171 @@ +import { useCallback, useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import shallow from 'zustand/shallow' + +import { GetMembershipsQuery } from '@/api/queries/__generated__/memberships.generated' +import { DialogButtonProps } from '@/components/_overlays/Dialog' +import { DialogModal } from '@/components/_overlays/DialogModal' +import { registerAccount } from '@/providers/auth/auth.helpers' +import { useAuth } from '@/providers/auth/auth.hooks' +import { useAuthStore } from '@/providers/auth/auth.store' +import { useJoystream } from '@/providers/joystream' +import { useSnackbar } from '@/providers/snackbars' + +import { AuthenticationModalStepTemplate } from '../AuthenticationModalStepTemplate' +import { + ExternalSignInModalMembershipsStep, + ExternalSignInModalWalletStep, +} from '../ExternalSignInModal/ExternalSignInSteps' +import { EmailVerified } from '../genericSteps/EmailVerified' +import { WaitingModal } from '../genericSteps/WaitingModal' + +enum ExternalAccountSetupStep { + Verification = 'Verification', + Wallet = 'Wallet', + Membership = 'Membership', + NoMembership = 'NoMembership', + ExtensionSigning = 'ExtensionSigning', +} + +export const ExternalAccountSetup = () => { + const [searchParams, setSearchParams] = useSearchParams() + const { handleLogin } = useAuth() + const { joystream } = useJoystream() + const { setAuthModalOpenName } = useAuthStore( + (state) => ({ + authModalOpenName: state.authModalOpenName, + setAuthModalOpenName: state.actions.setAuthModalOpenName, + }), + shallow + ) + const [primaryButtonProps, setPrimaryButtonProps] = useState({ text: 'Verifying...' }) // start with sensible default so that there are no jumps after first effect runs + const [availableMemberships, setAvailableMemberships] = useState(null) + const [selectedMembership, setSelectedMembership] = useState(null) + const confirmationCode = decodeURIComponent(searchParams.get('email-token') ?? '') // 5y6WUaZ5IxAGf4iLGarc1OHFHBWScJZ4/gxWGn4trq4= + const email = decodeURIComponent(searchParams.get('email') ?? '') + const [step, setStep] = useState(ExternalAccountSetupStep.Verification) + const [loading, setLoading] = useState(true) + const { displaySnackbar } = useSnackbar() + + const resetSearchParams = useCallback(() => setSearchParams(new URLSearchParams()), [setSearchParams]) + + useEffect(() => { + if (step === ExternalAccountSetupStep.Verification) { + setPrimaryButtonProps({ + text: loading ? 'Verifying...' : 'Create an account', + onClick: () => setStep(ExternalAccountSetupStep.Wallet), + disabled: loading, + }) + } + + if (step === ExternalAccountSetupStep.ExtensionSigning) { + return setPrimaryButtonProps({ + text: 'Waiting...', + onClick: undefined, + disabled: true, + }) + } + }, [loading, step]) + + const handleConfirm = useCallback(async () => { + const account = await joystream?.selectedAccountId + if (!joystream?.signMessage || !account || !email || !confirmationCode) return + const userAddress = typeof account === 'object' ? account.address : account + setStep(ExternalAccountSetupStep.ExtensionSigning) + + try { + const address = await registerAccount({ + type: 'external', + email: email, + address: userAddress, + signature: (data) => + joystream?.signMessage({ + type: 'payload', + data, + }), + emailConfimationToken: confirmationCode, + }) + + if (address) { + await handleLogin({ + type: 'external', + address, + sign: (data) => + joystream?.signMessage({ + type: 'payload', + data, + }), + }) + } + setAuthModalOpenName(undefined) + resetSearchParams() + } catch (error) { + if (error.message.includes('Token not found. Possibly expired or already used.')) { + displaySnackbar({ + iconType: 'error', + title: 'Token expired', + description: 'Please try again to generate a new one, if the problem persists contact support.', + }) + resetSearchParams() + return + } + displaySnackbar({ + iconType: 'error', + title: 'Something went wrong', + description: 'Please try again, if the problem persists contact support.', + }) + } + }, [confirmationCode, displaySnackbar, email, handleLogin, joystream, resetSearchParams, setAuthModalOpenName]) + + if (!confirmationCode || !email) { + return null + } + + return ( + resetSearchParams(), + }} + show + > + {step === ExternalAccountSetupStep.Verification ? ( + setLoading(false)} code={confirmationCode} /> + ) : null} + + {step === ExternalAccountSetupStep.Wallet ? ( + setStep(val as ExternalAccountSetupStep)} + hasNavigatedBack={false} + setPrimaryButtonProps={setPrimaryButtonProps} + setAvailableMemberships={setAvailableMemberships} + /> + ) : null} + + {step === ExternalAccountSetupStep.Membership ? ( + setStep(val as ExternalAccountSetupStep)} + memberId={selectedMembership} + setMemberId={setSelectedMembership} + handleNoAccount={handleConfirm} + /> + ) : null} + + {step === ExternalAccountSetupStep.ExtensionSigning ? ( + + ) : null} + + {step === ExternalAccountSetupStep.NoMembership ? ( + + ) : null} + + ) +} diff --git a/packages/atlas/src/components/_auth/ExternalAccountSetup/index.ts b/packages/atlas/src/components/_auth/ExternalAccountSetup/index.ts new file mode 100644 index 0000000000..9a83ff8f56 --- /dev/null +++ b/packages/atlas/src/components/_auth/ExternalAccountSetup/index.ts @@ -0,0 +1 @@ +export * from './ExternalAccountSetup' diff --git a/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInModal.tsx b/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInModal.tsx index 6a4580255f..03b8f0f2be 100644 --- a/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInModal.tsx +++ b/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInModal.tsx @@ -1,16 +1,13 @@ -import { zodResolver } from '@hookform/resolvers/zod/dist/zod' import { FC, useCallback, useEffect, useRef, useState } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import { z } from 'zod' import shallow from 'zustand/shallow' import { GetMembershipsQuery } from '@/api/queries/__generated__/memberships.generated' import { AuthenticationModalStepTemplate } from '@/components/_auth/AuthenticationModalStepTemplate' -import { ExternalSignInModalEmailStep } from '@/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalEmailStep' import { Button } from '@/components/_buttons/Button' import { DialogButtonProps } from '@/components/_overlays/Dialog' import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' import { useAuthStore } from '@/providers/auth/auth.store' +import { formatDurationBiggestTick } from '@/utils/time' import { StyledDialogModal } from './ExternalSignInModal.styles' import { @@ -20,6 +17,9 @@ import { SignInStepProps, } from './ExternalSignInSteps' +import { CheckEmailConfirmation } from '../genericSteps/CheckEmailConfirmation' +import { ProvideEmailForLink } from '../genericSteps/ProvideEmailForLink' + const stepToPageName: Partial> = { [ModalSteps.Email]: 'External sign in modal - Add email to existing membership', [ModalSteps.Logging]: 'External sign in modal - logging', @@ -36,13 +36,7 @@ export const ExternalSignInModal: FC = () => { const [availableMemberships, setAvailableMemberships] = useState(null) const dialogContentRef = useRef(null) const { trackPageView } = useSegmentAnalytics() - const form = useForm<{ email: string }>({ - resolver: zodResolver( - z.object({ - email: z.string().regex(/^\S+@\S+\.\S+$/, 'Enter valid email address.'), - }) - ), - }) + const formRef = useRef<{ email?: string }>({}) const { authModalOpenName, setAuthModalOpenName } = useAuthStore( (state) => ({ authModalOpenName: state.authModalOpenName, @@ -77,27 +71,89 @@ export const ExternalSignInModal: FC = () => { switch (currentStep) { case ModalSteps.Wallet: - return + return ( + setCurrentStep(val as ModalSteps)} + setAvailableMemberships={setAvailableMemberships} + /> + ) case ModalSteps.Membership: return ( setCurrentStep(val as ModalSteps)} memberId={selectedMembership} setMemberId={setSelectedMembership} + handleNoAccount={() => setCurrentStep(ModalSteps.Email)} /> ) case ModalSteps.Email: - return - case ModalSteps.Logging: return ( - { + formRef.current = { email } + setCurrentStep(ModalSteps.ConfirmationLink) + }} + setActionButtonHandler={(fn) => + setPrimaryButtonProps({ + text: 'Continue', + onClick: () => fn(), + }) + } + /> + ) + case ModalSteps.ConfirmationLink: + return ( + + setPrimaryButtonProps({ + text: 'Resend', + onClick: () => fn(), + }) + } + onSuccess={() => { + const resendTimestamp = new Date() + + const calcRemainingTime = (date: Date) => { + const difference = Date.now() - date.getTime() + if (difference > 30_000) { + clearInterval(id) + setPrimaryButtonProps((prev) => ({ + ...prev, + text: `Resend`, + disabled: false, + })) + return + } + const duration = formatDurationBiggestTick(Math.floor(30 - difference / 1000)) + setPrimaryButtonProps((prev) => ({ + ...prev, + text: `Resend (${duration.replace('seconds', 's')})`, + disabled: true, + })) + } + + calcRemainingTime(resendTimestamp) + + const id = setInterval(() => { + calcRemainingTime(resendTimestamp) + }, 1000) + }} /> ) + // return + // case ModalSteps.Logging: + // return ( + // + // ) case ModalSteps.ExtensionSigning: return ( { : [ModalSteps.Wallet, ModalSteps.NoMembership].includes(currentStep) ? { text: 'Use email & password', onClick: () => setAuthModalOpenName('logIn') } : undefined, - additionalActionsNode: [ModalSteps.Wallet, ModalSteps.Membership, ModalSteps.Email].includes(currentStep) && ( + additionalActionsNode: [ + ModalSteps.Wallet, + ModalSteps.Membership, + ModalSteps.Email, + ModalSteps.ConfirmationLink, + ].includes(currentStep) && ( + ) + } + dividers={[YppSetupModalStep.yppForm].includes(step)} + primaryButton={primaryButton} + > + {step === YppSetupModalStep.ytVideoUrl ? ( + setPrimaryAction(() => fn)} + onSubmit={(videoUrl) => { + formRef.current.videoUrl = videoUrl + setStep(YppSetupModalStep.channelVerification) + validateYtChannel(videoUrl) + .then(() => setStep(YppSetupModalStep.ownershipProved)) + .catch(() => setStep(YppSetupModalStep.ytVideoUrl)) // https://www.youtube.com/shorts/OQHvDRTK3Tk + }} + /> + ) : null} + {step === YppSetupModalStep.yppForm ? ( + setPrimaryAction(() => fn)} + onSubmit={(form) => { + formRef.current = { + ...formRef.current, + ...form, + } + + const onSuccessfulChannelCreation = () => { + if (!channelId) { + setStep(YppSetupModalStep.channelConnection) + } + + connectJoyChannelToYpp() + .then(() => onClose()) + .catch(() => setStep(YppSetupModalStep.ownershipProved)) + } + + updateOrCreateChannel(channelId ?? undefined, onSuccessfulChannelCreation).catch(() => { + setStep(YppSetupModalStep.ownershipProved) + }) + + if (!channelId) { + setStep(YppSetupModalStep.channelCreation) + return + } + + setStep(YppSetupModalStep.channelConnection) + }} + /> + ) : null} + {step === YppSetupModalStep.channelVerification ? : null} + {step === YppSetupModalStep.channelCreation ? ( + + ) : null} + {step === YppSetupModalStep.channelConnection ? : null} + {step === YppSetupModalStep.ownershipProved ? ( + + ) : null} + + ) +} diff --git a/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.types.ts b/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.types.ts new file mode 100644 index 0000000000..c200bfa9cc --- /dev/null +++ b/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.types.ts @@ -0,0 +1,39 @@ +export enum YppSetupModalStep { + ytVideoUrl = 'ytVideoUrl', + channelVerification = 'channelVerification', + ownershipProved = 'ownershipProved', + // user has no account + email = 'email', + + yppForm = 'yppForm', + // user has account, but no channel + channelCreation = 'channelCreation', + // user has channel + channelConnection = 'channelConnection', +} + +export type YppFormData = { + youtubeVideoUrl?: string + id?: string + joystreamChannelId?: string + email?: string + referrerChannelId?: string + shouldBeIngested?: boolean + videoCategoryId?: string +} + +export type YppResponseData = { + 'id': string + 'channelHandle': string + 'channelTitle': string + 'channelDescription': string + 'channelLanguage': string + 'avatarUrl': string + 'bannerUrl': string +} + +export type YppSetupForm = { + videoUrl?: string + channelTitle?: string +} & Partial & + YppFormData diff --git a/packages/atlas/src/components/_auth/YppSignUpSetupModal/index.ts b/packages/atlas/src/components/_auth/YppSignUpSetupModal/index.ts new file mode 100644 index 0000000000..c4289a32d3 --- /dev/null +++ b/packages/atlas/src/components/_auth/YppSignUpSetupModal/index.ts @@ -0,0 +1 @@ +export * from './YppSignUpSetupModal' diff --git a/packages/atlas/src/components/_auth/genericSteps/OwnershipVerified.tsx b/packages/atlas/src/components/_auth/genericSteps/OwnershipVerified.tsx new file mode 100644 index 0000000000..84a642f604 --- /dev/null +++ b/packages/atlas/src/components/_auth/genericSteps/OwnershipVerified.tsx @@ -0,0 +1,58 @@ +import styled from '@emotion/styled' + +import appKv from '@/assets/images/app-kv.webp' +import { Avatar } from '@/components/Avatar' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { sizes } from '@/styles' + +type OwnershipVerifiedProps = { + userHandle: string + userAvatar: string +} + +export const OwnershipVerified = ({ userHandle, userAvatar }: OwnershipVerifiedProps) => { + return ( + + + + + + {userHandle} + + + + + Ownership verified + + + Congratulations! We successfully verified your channel ownership. You can now create the account. + + + + ) +} + +const TextBox = styled(FlexBox)` + text-align: center; +` + +const BackgroundWrapper = styled.div` + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: calc(100% + ${sizes(12)}); + gap: ${sizes(2)}; + padding: ${sizes(8)} 0; + margin-left: ${sizes(-6)}; + margin-top: ${sizes(-6)}; + + img { + position: absolute; + object-fit: contain; + inset: 0; + } +` diff --git a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx index 50d42bcd2d..50f269082b 100644 --- a/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/ProveChannelOwnership.tsx @@ -36,7 +36,7 @@ export const ProveChannelOwnership = ({ onSubmit, setActionButtonHandler }: Prov return ( - + Prove channel ownership diff --git a/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx b/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx index 3efe424a56..1e9bfab4c3 100644 --- a/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx +++ b/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx @@ -37,7 +37,6 @@ import { } from './YppAuthorizationModal.styles' import { DetailsFormData, - YppAuthorizationDetailsFormStep, YppAuthorizationRequirementsStep, YppAuthorizationSelectChannelStep, YppSyncStepData, @@ -456,7 +455,8 @@ export const YppAuthorizationModal: FC = ({ unSynced onClick: () => handleCreateOrUpdateChannel(), text: 'Continue', }, - component: , + // component: , + component:
, } case 'ypp-speaking-to-backend': return { diff --git a/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep.tsx b/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep.tsx index c38b9e4152..55eaf45799 100644 --- a/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep.tsx +++ b/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep.tsx @@ -1,5 +1,6 @@ -import { FC, useCallback, useEffect, useState } from 'react' -import { Controller, useFormContext } from 'react-hook-form' +import styled from '@emotion/styled' +import { useCallback, useEffect, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' import shallow from 'zustand/shallow' import { @@ -11,14 +12,19 @@ import { ExtendedFullChannelFieldsFragment, FullChannelFieldsFragment, } from '@/api/queries/__generated__/fragments.generated' +import { AppLogo } from '@/components/AppLogo' import { Avatar } from '@/components/Avatar' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' import { FormField } from '@/components/_inputs/FormField' import { InputAutocomplete } from '@/components/_inputs/InputAutocomplete' import { Select, SelectItem } from '@/components/_inputs/Select' import { atlasConfig } from '@/config' import { displayCategories } from '@/config/categories' +import { useMountEffect } from '@/hooks/useMountEffect' import { useUser } from '@/providers/user/user.hooks' import { useYppStore } from '@/providers/ypp/ypp.store' +import { cVar } from '@/styles' import { FormFieldsWrapper } from './YppAuthorizationDetailsFormStep.styles' @@ -34,7 +40,12 @@ const categoriesSelectItems: SelectItem[] = value: c.defaultVideoCategory, })) || [] -export const YppAuthorizationDetailsFormStep: FC = () => { +type YppDetailsFormStepProps = { + onSubmit: (form: DetailsFormData) => void + setActionButtonHandler: (fn: () => void | Promise) => void +} + +export const YppDetailsFormStep = ({ onSubmit, setActionButtonHandler }: YppDetailsFormStepProps) => { const [foundChannel, setFoundChannel] = useState() const { memberId } = useUser() const { referrerId } = useYppStore((store) => store, shallow) @@ -42,7 +53,16 @@ export const YppAuthorizationDetailsFormStep: FC = () => { control, formState: { errors }, setValue, - } = useFormContext() + handleSubmit, + } = useForm() + + useMountEffect(() => { + setActionButtonHandler( + handleSubmit((data) => { + onSubmit(data) + }) + ) + }) useEffect(() => { if (referrerId) { @@ -65,90 +85,108 @@ export const YppAuthorizationDetailsFormStep: FC = () => { ) return ( - - + + + + YouTube Auto Sync + + + {atlasConfig.general.appName} automatically syncs all your YouTube videos. + + + + ( + - )} - /> - - - { - if (value && !foundChannel) { - return 'No channel with this title has been found.' - } - return true - }, - }} - render={({ field: { onChange, value } }) => ( - - ( + - notFoundLabel="Channel with this title not found, please check spelling and try again." - documentQuery={GetExtendedBasicChannelsDocument} - queryVariablesFactory={queryVariablesFactory} - perfectMatcher={(res, val) => { - const initialResults = res.extendedChannels.filter( - (extendedChannel) => extendedChannel.channel.title === val - ) - if (initialResults.length === 1 || !referrerId) { - return initialResults[0] - } + + notFoundLabel="Channel with this title not found, please check spelling and try again." + documentQuery={GetExtendedBasicChannelsDocument} + queryVariablesFactory={queryVariablesFactory} + perfectMatcher={(res, val) => { + const initialResults = res.extendedChannels.filter( + (extendedChannel) => extendedChannel.channel.title === val + ) + if (initialResults.length === 1 || !referrerId) { + return initialResults[0] + } - return ( - initialResults.find((extendedChannel) => extendedChannel.channel.id === referrerId) ?? - initialResults[0] - ) - }} - renderItem={(result) => - result.extendedChannels.map((extendedChannel) => ({ - ...extendedChannel, - label: extendedChannel.channel.title ?? '', - })) - } - placeholder="Channel Name" - value={value ?? ''} - onChange={onChange} - onItemSelect={(item) => { - if (item) { - setFoundChannel(item.channel) - onChange({ target: { value: item?.channel.title } }) - setValue('referrerChannelId', item.channel.id) + return ( + initialResults.find((extendedChannel) => extendedChannel.channel.id === referrerId) ?? + initialResults[0] + ) + }} + renderItem={(result) => + result.extendedChannels.map((extendedChannel) => ({ + ...extendedChannel, + label: extendedChannel.channel.title ?? '', + })) } - }} - nodeEnd={foundChannel && } - clearSelection={() => { - setFoundChannel(undefined) - }} - /> - - )} - /> - + placeholder="Channel Name" + value={value ?? ''} + onChange={onChange} + onItemSelect={(item) => { + if (item) { + setFoundChannel(item.channel) + onChange({ target: { value: item?.channel.title } }) + setValue('referrerChannelId', item.channel.id) + } + }} + nodeEnd={foundChannel && } + clearSelection={() => { + setFoundChannel(undefined) + }} + /> + + )} + /> + + ) } + +const StyledAppLogo = styled(AppLogo)` + height: 36px; + width: auto; + + path { + fill: ${cVar('colorTextMuted')}; + } +` From afdb84598bc8bed06ba2047b4975e071f37eaadb Mon Sep 17 00:00:00 2001 From: ikprk Date: Sun, 9 Jun 2024 15:41:12 +0200 Subject: [PATCH 18/22] First YPP flow --- packages/atlas/src/.env | 4 +- .../_auth/AuthModals/AuthModals.tsx | 4 + .../YppFirstFlowModal/YppFirstFlowModal.tsx | 233 +++++++++++++++ .../_auth/YppFirstFlowModal/index.ts | 1 + .../genericSteps/ProvideEmailForLink.tsx | 4 +- .../atlas/src/hooks/useChannelFormSubmit.ts | 4 +- packages/atlas/src/hooks/useSendEmailToken.ts | 8 +- .../atlas/src/hooks/useYppModalHandlers.ts | 282 ++++++++++++++++++ .../atlas/src/providers/auth/auth.types.ts | 9 +- .../src/providers/user/user.provider.tsx | 2 +- .../global/YppLandingView/YppLandingView.tsx | 16 +- 11 files changed, 555 insertions(+), 12 deletions(-) create mode 100644 packages/atlas/src/components/_auth/YppFirstFlowModal/YppFirstFlowModal.tsx create mode 100644 packages/atlas/src/components/_auth/YppFirstFlowModal/index.ts create mode 100644 packages/atlas/src/hooks/useYppModalHandlers.ts diff --git a/packages/atlas/src/.env b/packages/atlas/src/.env index 4ed1ec82cd..973cef3cb6 100644 --- a/packages/atlas/src/.env +++ b/packages/atlas/src/.env @@ -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= diff --git a/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx b/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx index 17913811fb..1aa7ac6b85 100644 --- a/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx +++ b/packages/atlas/src/components/_auth/AuthModals/AuthModals.tsx @@ -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() @@ -27,6 +29,7 @@ export const AuthModals = () => { + ) } @@ -35,6 +38,7 @@ export const AuthModals = () => { <> {accountType === 'internal' ? : null} {accountType === 'external' ? : null} + {accountType === 'ypp' ? : null} ) } diff --git a/packages/atlas/src/components/_auth/YppFirstFlowModal/YppFirstFlowModal.tsx b/packages/atlas/src/components/_auth/YppFirstFlowModal/YppFirstFlowModal.tsx new file mode 100644 index 0000000000..9a951f9ac8 --- /dev/null +++ b/packages/atlas/src/components/_auth/YppFirstFlowModal/YppFirstFlowModal.tsx @@ -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('') + const [primaryAction, setPrimaryAction] = useState(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 ( + setAuthModalOpenName(undefined)}> + Cancel + + ) + } + dividers={[YppFirstFlowStep.yppForm].includes(step)} + primaryButton={primaryButton} + > + {step === YppFirstFlowStep.ytVideoUrl ? ( + 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 ? ( + 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 ? ( + + ) : null} + {step === YppFirstFlowStep.channelCreation ? ( + + ) : null} + {step === YppFirstFlowStep.channelConnection ? ( + + ) : null} + {step === YppFirstFlowStep.ownershipProved ? ( + + ) : null} + + {step === YppFirstFlowStep.email ? ( + { + setStep(YppFirstFlowStep.confirmationLink) + formRef.current = { + ...formRef.current, + email, + } + }} + setActionButtonHandler={(fn) => setPrimaryAction(() => fn)} + /> + ) : null} + {step === YppFirstFlowStep.confirmationLink ? ( + 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} + + ) +} diff --git a/packages/atlas/src/components/_auth/YppFirstFlowModal/index.ts b/packages/atlas/src/components/_auth/YppFirstFlowModal/index.ts new file mode 100644 index 0000000000..af63ce703c --- /dev/null +++ b/packages/atlas/src/components/_auth/YppFirstFlowModal/index.ts @@ -0,0 +1 @@ +export * from './YppFirstFlowModal' diff --git a/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx b/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx index 0b8f616241..85d9e48ed7 100644 --- a/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx +++ b/packages/atlas/src/components/_auth/genericSteps/ProvideEmailForLink.tsx @@ -19,6 +19,7 @@ type ProvideEmailForLinkProps = { setActionButtonHandler: SetActionButtonHandlerSetter defaultEmail?: string isExternal?: boolean + yppVideoUrl?: string } export const ProvideEmailForLink = ({ @@ -26,6 +27,7 @@ export const ProvideEmailForLink = ({ onSubmit, defaultEmail, isExternal, + yppVideoUrl, }: ProvideEmailForLinkProps) => { const { register, @@ -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 diff --git a/packages/atlas/src/hooks/useChannelFormSubmit.ts b/packages/atlas/src/hooks/useChannelFormSubmit.ts index d6bc390c07..e1033200aa 100644 --- a/packages/atlas/src/hooks/useChannelFormSubmit.ts +++ b/packages/atlas/src/hooks/useChannelFormSubmit.ts @@ -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 minimized?: | { @@ -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 @@ -245,6 +246,7 @@ export const useCreateEditChannelSubmit = (initialChannelId?: string) => { data.collaboratorId, proxyCallback(updateStatus) ), + onError: onError, onTxSync: async (result) => { onTxSync?.(result) return refetchDataAndUploadAssets(result) diff --git a/packages/atlas/src/hooks/useSendEmailToken.ts b/packages/atlas/src/hooks/useSendEmailToken.ts index 248d7ca4a6..eb2c5e8efb 100644 --- a/packages/atlas/src/hooks/useSendEmailToken.ts +++ b/packages/atlas/src/hooks/useSendEmailToken.ts @@ -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`, @@ -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)}` : '' }` ) diff --git a/packages/atlas/src/hooks/useYppModalHandlers.ts b/packages/atlas/src/hooks/useYppModalHandlers.ts new file mode 100644 index 0000000000..ead2698795 --- /dev/null +++ b/packages/atlas/src/hooks/useYppModalHandlers.ts @@ -0,0 +1,282 @@ +import { useCallback, useRef } from 'react' +import { useNavigate } from 'react-router-dom' +import shallow from 'zustand/shallow' + +import { axiosInstance } from '@/api/axios' +import { useFullChannel } from '@/api/hooks/channel' +import { atlasConfig } from '@/config' +import { displayCategoriesLookup } from '@/config/categories' +import { absoluteRoutes } from '@/config/routes' +import { useCreateEditChannelSubmit } from '@/hooks/useChannelFormSubmit' +import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' +import { useAuth } from '@/providers/auth/auth.hooks' +import { useSnackbar } from '@/providers/snackbars' +import { useUser } from '@/providers/user/user.hooks' +import { useYppStore } from '@/providers/ypp/ypp.store' +import { createId } from '@/utils/createId' +import { imageUrlToBlob } from '@/utils/image' +import { SentryLogger } from '@/utils/logs' +import { useGetYppSyncedChannels } from '@/views/global/YppLandingView/useGetYppSyncedChannels' + +export type YppFormData = { + youtubeVideoUrl?: string + id?: string + joystreamChannelId?: string + email?: string + referrerChannelId?: string + shouldBeIngested?: boolean + videoCategoryId?: string +} + +export type YppResponseData = { + 'id': string + 'channelHandle': string + 'channelTitle': string + 'channelDescription': string + 'channelLanguage': string + 'avatarUrl': string + 'bannerUrl': string +} + +export type YppSetupForm = { + channelTitle?: string + email?: string +} & Partial & + YppFormData + +const YPP_SYNC_URL = atlasConfig.features.ypp.youtubeSyncApiUrl +const DEFAULT_LANGUAGE = atlasConfig.derived.popularLanguagesSelectValues[0].value +const COLLABORATOR_ID = atlasConfig.features.ypp.youtubeCollaboratorMemberId + +export const useYppModalHandlers = () => { + const formRef = useRef({}) + const { displaySnackbar } = useSnackbar() + const { currentUser } = useAuth() + const { memberId, refetchUserMemberships, setActiveChannel, channelId, isLoggedIn } = useUser() + const createOrUpdateChannel = useCreateEditChannelSubmit(undefined) + const { + referrerId, + utmSource, + utmCampaign, + utmContent, + actions: { setYtResponseData, setUtmSource, setUtmCampaign, setUtmContent }, + } = useYppStore((store) => store, shallow) + const createdChannelId = useRef(null) + const setReferrerId = useYppStore((store) => store.actions.setReferrerId) + const navigate = useNavigate() + const { refetchYppSyncedChannels } = useGetYppSyncedChannels() + const { trackYppOptIn, identifyUser } = useSegmentAnalytics() + + const { refetch: refetchChannel } = useFullChannel( + channelId || '', + { + skip: !channelId, + onError: (error) => + SentryLogger.error('Failed to fetch channel', 'UploadStatus', error, { + channel: { id: channelId }, + }), + }, + { where: { channel: { isPublic_eq: undefined, isCensored_eq: undefined } } } + ) + + const validateYtChannel = useCallback( + async (videoUrl: string) => { + try { + const res = await axiosInstance.post(`${YPP_SYNC_URL}/users`, { + youtubeVideoUrl: videoUrl, + }) + + formRef.current = { + ...formRef.current, + ...res.data, + } + } catch (e) { + displaySnackbar({ + iconType: 'error', + title: 'Ops, something went wrong', + description: e.response.data.message ?? `We couldn't verify your ownership. Please, try again.`, + }) + SentryLogger.error('Error validating YT ownership', 'useYppSetupModalHandlers', e) + throw e + } + }, + [displaySnackbar] + ) + + const updateOrCreateChannel = useCallback( + async (channelId?: string, onSuccess?: () => void) => { + try { + const avatarBlob = formRef.current?.avatarUrl + ? (await imageUrlToBlob(formRef.current?.avatarUrl).catch((err) => + SentryLogger.error('Failed to process YT avatar image', 'handleCreateOrUpdateChannel', err) + )) ?? null + : null + + const coverBlob = formRef.current?.bannerUrl + ? (await imageUrlToBlob(formRef.current?.bannerUrl, 1920, 480).catch((err) => + SentryLogger.error('Failed to process YT banner image', 'handleCreateOrUpdateChannel', err) + )) ?? null + : null + + const avatarContentId = `local-avatar-${createId()}` + const coverContentId = `local-cover-${createId()}` + + if (!memberId) { + throw Error('memberId id was not provided') + } + if (!COLLABORATOR_ID) { + throw Error('Collaborator member id was not provided') + } + + console.log('hmmm', channelId, COLLABORATOR_ID) + + await createOrUpdateChannel({ + minimized: { + errorMessage: 'Failed to create or update channel', + }, + data: { + collaboratorId: COLLABORATOR_ID, + metadata: channelId + ? { ownerAccount: memberId } + : { + ownerAccount: memberId, + description: formRef.current?.channelDescription, + isPublic: true, + language: formRef.current?.channelLanguage || DEFAULT_LANGUAGE, + title: formRef.current?.channelTitle || formRef.current?.channelHandle, + }, + refetchChannel, + newChannel: !channelId, + assets: channelId + ? {} + : { + avatarPhoto: { + assetDimensions: { height: 192, width: 192 }, + contentId: avatarContentId, + imageCropData: null, + croppedBlob: avatarBlob, + originalBlob: avatarBlob, + }, + coverPhoto: { + assetDimensions: { width: 1920, height: 480 }, + contentId: coverContentId, + imageCropData: null, + croppedBlob: coverBlob, + originalBlob: coverBlob, + }, + }, + }, + onTxSync: async ({ channelId }) => { + setActiveChannel(channelId) + createdChannelId.current = channelId + }, + onError: () => { + throw new Error('transaction error') + }, + onCompleted: async () => { + await refetchUserMemberships() + onSuccess?.() + }, + }) + } catch (error) { + displaySnackbar({ + title: 'Failed to create or update channel', + description: 'An unexpected error occurred. Please try again.', + iconType: 'error', + }) + throw error + } + }, + [createOrUpdateChannel, displaySnackbar, memberId, refetchChannel, refetchUserMemberships, setActiveChannel] + ) + + const connectJoyChannelToYpp = useCallback(async () => { + if (!memberId) { + throw Error('memberId id was not provided') + } + + if (!currentUser?.email) { + throw Error('email was not provided') + } + + const channelIdInAction = channelId ?? createdChannelId.current + + if (!channelIdInAction) { + throw Error('channel id was not provided') + } + + try { + const channelCreationResponse = await axiosInstance.post( + `${atlasConfig.features.ypp.youtubeSyncApiUrl}/channels`, + { + ...(channelIdInAction ? { joystreamChannelId: parseInt(channelIdInAction) } : {}), + ...(referrerId ? { referrerChannelId: parseInt(referrerId) } : {}), + youtubeVideoUrl: formRef.current.youtubeVideoUrl, + id: formRef.current?.id, + email: currentUser.email, + shouldBeIngested: true, + videoCategoryId: formRef.current.videoCategoryId, + } + ) + + await refetchYppSyncedChannels() + identifyUser({ + name: 'Sign up', + memberId: memberId, + email: formRef.current?.email || '', + isYppFlow: 'true', + signInType: 'password', + }) + trackYppOptIn({ + handle: formRef.current?.channelHandle, + email: currentUser.email, + category: formRef.current.videoCategoryId + ? displayCategoriesLookup[formRef.current.videoCategoryId]?.name + : undefined, + subscribersCount: channelCreationResponse.data.channel.subscribersCount, + referrerId: formRef.current.referrerChannelId, + utmSource: utmSource || undefined, + utmCampaign: utmCampaign || undefined, + utmContent: utmContent || undefined, + }) + setReferrerId(null) + setYtResponseData(null) + + navigate(absoluteRoutes.studio.yppDashboard()) + displaySnackbar({ + title: 'Sign up successful!', + description: 'It may take up to 24 hours after sign up for the videos to start syncing.', + iconType: 'success', + }) + } catch (e) { + displaySnackbar({ + title: 'Opss, sign up failed', + description: e.response.data.message ?? 'An unexpected error occurred. Please try again.', + iconType: 'error', + }) + throw e + } + }, [ + channelId, + currentUser?.email, + displaySnackbar, + identifyUser, + memberId, + navigate, + referrerId, + refetchYppSyncedChannels, + setReferrerId, + setYtResponseData, + trackYppOptIn, + utmCampaign, + utmContent, + utmSource, + ]) + + return { + connectJoyChannelToYpp, + formRef, + validateYtChannel, + updateOrCreateChannel, + } +} diff --git a/packages/atlas/src/providers/auth/auth.types.ts b/packages/atlas/src/providers/auth/auth.types.ts index 084c62fbfb..a2835a965c 100644 --- a/packages/atlas/src/providers/auth/auth.types.ts +++ b/packages/atlas/src/providers/auth/auth.types.ts @@ -38,7 +38,14 @@ export type ExternalLogin = { export type LoginParams = InternalLogin | ExternalLogin -export type AuthModals = 'logIn' | 'externalLogIn' | 'signUp' | 'createChannel' | 'forgotPassword' | 'emailSetup' +export type AuthModals = + | 'logIn' + | 'externalLogIn' + | 'signUp' + | 'createChannel' + | 'forgotPassword' + | 'emailSetup' + | 'yppFirstFlow' type EncryptionArtifacts = { id: string diff --git a/packages/atlas/src/providers/user/user.provider.tsx b/packages/atlas/src/providers/user/user.provider.tsx index b6eb9249d4..86b0ff2486 100644 --- a/packages/atlas/src/providers/user/user.provider.tsx +++ b/packages/atlas/src/providers/user/user.provider.tsx @@ -50,7 +50,7 @@ export const UserProvider: FC = ({ children }) => { const activeMembership = data.memberships[0] || null setMemberId(activeMembership?.id) if (activeMembership && !activeMembership.channels.some((channel) => channel.id === channelId)) { - setActiveChannel(activeMembership.channels[0].id) + setActiveChannel(activeMembership.channels[0]?.id) } }, skip: !currentUser?.joystreamAccountId, diff --git a/packages/atlas/src/views/global/YppLandingView/YppLandingView.tsx b/packages/atlas/src/views/global/YppLandingView/YppLandingView.tsx index 47ec3efd3d..8211e212aa 100644 --- a/packages/atlas/src/views/global/YppLandingView/YppLandingView.tsx +++ b/packages/atlas/src/views/global/YppLandingView/YppLandingView.tsx @@ -3,11 +3,13 @@ import 'aos/dist/aos.css' import { FC, useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { ParallaxProvider } from 'react-scroll-parallax' +import shallow from 'zustand/shallow' import { YppReferralBanner } from '@/components/_ypp/YppReferralBanner' import { absoluteRoutes } from '@/config/routes' import { useHeadTags } from '@/hooks/useHeadTags' import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' +import { useAuthStore } from '@/providers/auth/auth.store' import { useUser } from '@/providers/user/user.hooks' import { useYppStore } from '@/providers/ypp/ypp.store' import { CreatorOpportunities } from '@/views/global/YppLandingView/sections/CreatorOpportunities' @@ -32,6 +34,13 @@ export const YppLandingView: FC = () => { const { trackYppSignInButtonClick } = useSegmentAnalytics() const selectedChannelTitle = activeMembership?.channels.find((channel) => channel.id === channelId)?.title const viewerEarningsRef = useRef(null) + const { authModalOpenName, setAuthModalOpenName } = useAuthStore( + (state) => ({ + authModalOpenName: state.authModalOpenName, + setAuthModalOpenName: state.actions.setAuthModalOpenName, + }), + shallow + ) const [wasSignInTriggered, setWasSignInTriggered] = useState(false) const shouldContinueYppFlowAfterCreatingChannel = useYppStore( @@ -60,13 +69,14 @@ export const YppLandingView: FC = () => { navigate(absoluteRoutes.studio.yppDashboard()) return } + console.log('wtf') - if (!yppModalOpenName) { + if (!authModalOpenName) { trackYppSignInButtonClick() - setYppModalOpen('ypp-requirements') + setAuthModalOpenName('yppFirstFlow') return } - }, [isYppSigned, yppModalOpenName, navigate, trackYppSignInButtonClick, setYppModalOpen]) + }, [isYppSigned, authModalOpenName, navigate, trackYppSignInButtonClick, setAuthModalOpenName]) useEffect(() => { // rerun handleYppSignUpClick after sign in flow From 6fc7d7300edcaea0b24e5c71f7b00f917726451a Mon Sep 17 00:00:00 2001 From: ikprk Date: Sun, 9 Jun 2024 15:41:30 +0200 Subject: [PATCH 19/22] Second YPP flow --- .../YppFinishFlowModal/YppFinishFlowModal.tsx | 346 ++++++++++++++++++ .../_auth/YppFinishFlowModal/index.ts | 1 + .../YppSignUpSetupModa.hooks.ts | 281 -------------- .../YppSignUpSetupModal.tsx | 147 -------- .../YppSignUpSetupModal.types.ts | 39 -- .../_auth/YppSignUpSetupModal/index.ts | 1 - 6 files changed, 347 insertions(+), 468 deletions(-) create mode 100644 packages/atlas/src/components/_auth/YppFinishFlowModal/YppFinishFlowModal.tsx create mode 100644 packages/atlas/src/components/_auth/YppFinishFlowModal/index.ts delete mode 100644 packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModa.hooks.ts delete mode 100644 packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.tsx delete mode 100644 packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.types.ts delete mode 100644 packages/atlas/src/components/_auth/YppSignUpSetupModal/index.ts diff --git a/packages/atlas/src/components/_auth/YppFinishFlowModal/YppFinishFlowModal.tsx b/packages/atlas/src/components/_auth/YppFinishFlowModal/YppFinishFlowModal.tsx new file mode 100644 index 0000000000..7bbde0dc26 --- /dev/null +++ b/packages/atlas/src/components/_auth/YppFinishFlowModal/YppFinishFlowModal.tsx @@ -0,0 +1,346 @@ +import { useApolloClient } from '@apollo/client' +import { uniqueId } from 'lodash-es' +import { useCallback, useMemo, useRef, useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' + +import { GetCurrentAccountDocument, GetCurrentAccountQuery } from '@/api/queries/__generated__/accounts.generated' +import { Button } from '@/components/_buttons/Button' +import { DialogModal } from '@/components/_overlays/DialogModal' +import { atlasConfig } from '@/config' +import { absoluteRoutes } from '@/config/routes' +import { useCreateMember } from '@/hooks/useCreateMember' +import { useMountEffect } from '@/hooks/useMountEffect' +import { useUniqueMemberHandle } from '@/hooks/useUniqueMemberHandle' +import { useYppModalHandlers } from '@/hooks/useYppModalHandlers' +import { useAuth } from '@/providers/auth/auth.hooks' +import { useSnackbar } from '@/providers/snackbars' +import { useUser } from '@/providers/user/user.hooks' +import { SentryLogger } from '@/utils/logs' +import { YppDetailsFormStep } from '@/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps' + +import { NewHandleForm } from '../genericSteps/CreateHandle' +import { CreatePassword, NewPasswordForm } from '../genericSteps/CreatePassword' +import { EmailVerified } from '../genericSteps/EmailVerified' +import { SeedGeneration } from '../genericSteps/SeedGeneration' +import { WaitingModal } from '../genericSteps/WaitingModal' +import { SetActionButtonHandler } from '../genericSteps/types' + +export enum YppFinishFlowStep { + validationEmail = 'validationEmail', + password = 'password', + mnemonic = 'mnemonic', + accountCreation = 'accountCreation', + membershipCreation = 'membershipCreation', + yppForm = 'yppForm', + // user has account, but no channel + channelCreation = 'channelCreation', + // user has channel + channelConnection = 'channelConnection', +} + +type YppFinishFlowForm = { + mnemonic?: string +} & NewHandleForm & + NewPasswordForm + +const backStepLookup: Partial> = { + [YppFinishFlowStep.mnemonic]: YppFinishFlowStep.password, +} + +export const YppFinishFlowModal = () => { + const [searchParams, setSearchParams] = useSearchParams() + const confirmationCode = decodeURIComponent(searchParams.get('email-token') ?? '') // 5y6WUaZ5IxAGf4iLGarc1OHFHBWScJZ4/gxWGn4trq4= + const ytVideoUrl = decodeURIComponent(searchParams.get('ytVideo') ?? '') // 5y6WUaZ5IxAGf4iLGarc1OHFHBWScJZ4/gxWGn4trq4= + const [step, setStep] = useState(YppFinishFlowStep.validationEmail) + const [loading, setLoading] = useState(true) + const [primaryAction, setPrimaryAction] = useState(undefined) + const { formRef: accountForm, createAccount, createMembership } = useYppFinishFlowModalHandlers() + const { formRef: yppForm, validateYtChannel, connectJoyChannelToYpp, updateOrCreateChannel } = useYppModalHandlers() + const resetSearchParams = useCallback(() => setSearchParams(new URLSearchParams()), [setSearchParams]) + const navigate = useNavigate() + + useMountEffect(() => { + if (ytVideoUrl) { + validateYtChannel(ytVideoUrl) + yppForm.current.youtubeVideoUrl = ytVideoUrl + } + }) + + const primaryButton = useMemo(() => { + if (step === YppFinishFlowStep.validationEmail) { + return { + text: loading ? 'Verifying...' : 'Set password', + onClick: () => setStep(YppFinishFlowStep.password), + disabled: loading, + } + } + + if ( + [ + YppFinishFlowStep.accountCreation, + YppFinishFlowStep.membershipCreation, + YppFinishFlowStep.channelCreation, + YppFinishFlowStep.channelConnection, + ].includes(step) + ) { + return { + text: 'Waiting...', + onClick: () => undefined, + disabled: true, + } + } + + return { + text: 'Continue', + onClick: () => primaryAction?.(setLoading), + } + }, [loading, primaryAction, step]) + + const secondaryButton = useMemo(() => { + const backStep = backStepLookup[step] + + if (backStep) { + return { + text: 'Go back', + onClick: () => setStep(backStep), + } + } + }, [step]) + + return ( + + Cancel + + ) + } + > + {step === YppFinishFlowStep.validationEmail ? ( + setLoading(false)} code={confirmationCode} /> + ) : null} + + {step === YppFinishFlowStep.password ? ( + { + accountForm.current = { + ...accountForm.current, + ...data, + } + setStep(YppFinishFlowStep.mnemonic) + }} + setActionButtonHandler={(fn) => setPrimaryAction(() => fn)} + /> + ) : null} + + {step === YppFinishFlowStep.mnemonic ? ( + { + setStep(YppFinishFlowStep.membershipCreation) + accountForm.current = { + ...accountForm.current, + ...data, + } + try { + await createAccount() + await createMembership(yppForm.current.channelTitle ?? uniqueId(), () => { + setStep(YppFinishFlowStep.yppForm) + }) + } catch { + setStep(YppFinishFlowStep.password) + } + }} + setActionButtonHandler={(fn) => setPrimaryAction(() => fn)} + /> + ) : null} + + {step === YppFinishFlowStep.yppForm ? ( + setPrimaryAction(() => fn)} + onSubmit={(form) => { + yppForm.current = { + ...yppForm.current, + ...form, + } + + const onSuccessfulChannelCreation = () => { + setStep(YppFinishFlowStep.channelConnection) + connectJoyChannelToYpp() + .then(() => { + resetSearchParams() + navigate(absoluteRoutes.studio.yppDashboard()) + }) + .catch(() => setStep(YppFinishFlowStep.yppForm)) + } + + updateOrCreateChannel(undefined, onSuccessfulChannelCreation).catch(() => { + setStep(YppFinishFlowStep.yppForm) + }) + + setStep(YppFinishFlowStep.channelCreation) + }} + /> + ) : null} + + {step === YppFinishFlowStep.accountCreation ? ( + + ) : null} + + {step === YppFinishFlowStep.membershipCreation ? ( + + ) : null} + + {step === YppFinishFlowStep.channelCreation ? ( + + ) : null} + + {step === YppFinishFlowStep.channelConnection ? ( + + ) : null} + + ) +} + +const useYppFinishFlowModalHandlers = () => { + const formRef = useRef({}) + const [searchParams] = useSearchParams() + const { displaySnackbar } = useSnackbar() + const { createNewMember, createNewOrionAccount } = useCreateMember() + const confirmationCode = decodeURIComponent(searchParams.get('email-token') ?? '') // 5y6WUaZ5IxAGf4iLGarc1OHFHBWScJZ4/gxWGn4trq4= + const email = searchParams.get('email') ?? '' + const client = useApolloClient() + const { refetchCurrentUser } = useAuth() + const { generateUniqueMemberHandleBasedOnInput } = useUniqueMemberHandle() + const { refetchUserMemberships } = useUser() + + const checkAccountMembership = useCallback(async () => { + const { data } = await client.query({ + query: GetCurrentAccountDocument, + }) + + return data + }, [client]) + + const createMembership = useCallback( + async (handle: string, onSuccess?: () => void) => { + const { avatar, password, mnemonic } = formRef.current + if (!(password && email && handle && mnemonic)) { + displaySnackbar({ + title: 'Creation failed', + description: 'Missing required fields to create a membership', + iconType: 'error', + }) + SentryLogger.error('Missing fields during account creation', 'AccountSetup', { form: formRef.current }) + throw new Error('missing fields') + } + + const uniqueHandle = await generateUniqueMemberHandleBasedOnInput(handle) + + return createNewMember( + { + data: { + handle: uniqueHandle, + captchaToken: formRef.current.captchaToken ?? '', + allowDownload: true, + mnemonic: mnemonic, + avatar: avatar, + }, + onError: () => { + displaySnackbar({ + iconType: 'error', + title: 'Error during membership creation', + }) + throw new Error('member creation error') + }, + }, + async () => { + await refetchCurrentUser() + await refetchUserMemberships() + onSuccess?.() + } + ) + }, + [createNewMember, displaySnackbar, email, refetchCurrentUser, refetchUserMemberships] + ) + + const createAccount = useCallback( + async (onSuccess?: () => void) => { + const { password } = formRef.current + if (!(password && email)) { + displaySnackbar({ + title: 'Account creation blocked', + description: 'Missing required fields to create an account', + iconType: 'error', + }) + SentryLogger.error('Missing fields during account creation', 'AccountSetup', { form: formRef.current }) + return + } + console.log('creating account', email, password, confirmationCode) + + await createNewOrionAccount({ + data: { + confirmedTerms: true, + email: email ?? '', + mnemonic: formRef.current.mnemonic ?? '', + password: formRef.current.password ?? '', + emailConfimationToken: confirmationCode ?? '', + }, + onError: () => { + throw new Error('account creation error') + }, + onSuccess: async () => { + onSuccess?.() + }, + }) + }, + [confirmationCode, createNewOrionAccount, displaySnackbar, email] + ) + + return { + formRef, + checkAccountMembership, + createAccount, + createMembership, + } +} + +// export enum YppSignUpModalStep { +// ytVideo = 'ytVideo', +// channelVerification = 'channelVerification', +// emailInput = 'emailInput ', +// = 'ytVideo', +// ytVideo = 'ytVideo', +// ytVideo = 'ytVideo', +// } + +// export const YppSignUpModal = () => { +// return ( +// + +// +// ) +// } diff --git a/packages/atlas/src/components/_auth/YppFinishFlowModal/index.ts b/packages/atlas/src/components/_auth/YppFinishFlowModal/index.ts new file mode 100644 index 0000000000..6b193f0f93 --- /dev/null +++ b/packages/atlas/src/components/_auth/YppFinishFlowModal/index.ts @@ -0,0 +1 @@ +export * from './YppFinishFlowModal' diff --git a/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModa.hooks.ts b/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModa.hooks.ts deleted file mode 100644 index 5868590a1a..0000000000 --- a/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModa.hooks.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { useCallback, useRef } from 'react' -import { useNavigate } from 'react-router-dom' -import shallow from 'zustand/shallow' - -import { axiosInstance } from '@/api/axios' -import { useFullChannel } from '@/api/hooks/channel' -import { atlasConfig } from '@/config' -import { displayCategoriesLookup } from '@/config/categories' -import { absoluteRoutes } from '@/config/routes' -import { useCreateEditChannelSubmit } from '@/hooks/useChannelFormSubmit' -import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' -import { useAuth } from '@/providers/auth/auth.hooks' -import { useSnackbar } from '@/providers/snackbars' -import { useUser } from '@/providers/user/user.hooks' -import { useYppStore } from '@/providers/ypp/ypp.store' -import { createId } from '@/utils/createId' -import { imageUrlToBlob } from '@/utils/image' -import { SentryLogger } from '@/utils/logs' -import { useGetYppSyncedChannels } from '@/views/global/YppLandingView/useGetYppSyncedChannels' - -import { YppResponseData, YppSetupForm } from './YppSignUpSetupModal.types' - -const YPP_SYNC_URL = atlasConfig.features.ypp.youtubeSyncApiUrl -const DEFAULT_LANGUAGE = atlasConfig.derived.popularLanguagesSelectValues[0].value -const COLLABORATOR_ID = atlasConfig.features.ypp.youtubeCollaboratorMemberId - -export const useYppSetupModalHandlers = () => { - const formRef = useRef({}) - const { displaySnackbar } = useSnackbar() - const { currentUser } = useAuth() - const { memberId, refetchUserMemberships, setActiveChannel, channelId, isLoggedIn } = useUser() - const createOrUpdateChannel = useCreateEditChannelSubmit(undefined) - const { - referrerId, - ytResponseData, - utmSource, - utmCampaign, - utmContent, - actions: { setYtResponseData, setUtmSource, setUtmCampaign, setUtmContent }, - } = useYppStore((store) => store, shallow) - console.log('data', channelId) - const createdChannelId = useRef(null) - // const client = useApolloClient() - const setReferrerId = useYppStore((store) => store.actions.setReferrerId) - const navigate = useNavigate() - const { - unsyncedChannels: yppUnsyncedChannels, - currentChannel: yppCurrentChannel, - isLoading, - refetchYppSyncedChannels, - } = useGetYppSyncedChannels() - const { - trackPageView, - trackYppOptIn, - identifyUser, - trackYppReqsNotMet, - trackClickAuthModalSignUpButton, - trackClickAuthModalSignInButton, - } = useSegmentAnalytics() - - const { refetch: refetchChannel } = useFullChannel( - channelId || '', - { - skip: !channelId, - onError: (error) => - SentryLogger.error('Failed to fetch channel', 'UploadStatus', error, { - channel: { id: channelId }, - }), - }, - { where: { channel: { isPublic_eq: undefined, isCensored_eq: undefined } } } - ) - - const validateYtChannel = useCallback( - async (videoUrl: string) => { - try { - const res = await axiosInstance.post(`${YPP_SYNC_URL}/users`, { - youtubeVideoUrl: videoUrl, - }) - - formRef.current = { - ...formRef.current, - ...res.data, - } - } catch (e) { - // for testing only - // formRef.current = { - // ...formRef.current, - // id: '012938', - // channelHandle: 'test', - // channelTitle: 'test', - // channelDescription: 'test', - // channelLanguage: 'test', - // avatarUrl: 'https://atlas-services.joystream.org/avatars/migrated/151.webp', - // bannerUrl: 'test', - // } - // console.log('xd') - // setStep(YppSetupModalStep.ownershipProved) - displaySnackbar({ - iconType: 'error', - title: 'Ops, something went wrong', - description: "We couldn't verify your ownership. Please retry.", - }) - SentryLogger.error('Error validating YT ownership', 'useYppSetupModalHandlers', e) - throw e - } - }, - [displaySnackbar] - ) - - const updateOrCreateChannel = useCallback( - async (channelId?: string, onSuccess?: () => void) => { - try { - const avatarBlob = formRef.current?.avatarUrl - ? (await imageUrlToBlob(formRef.current?.avatarUrl).catch((err) => - SentryLogger.error('Failed to process YT avatar image', 'handleCreateOrUpdateChannel', err) - )) ?? null - : null - - const coverBlob = formRef.current?.bannerUrl - ? (await imageUrlToBlob(formRef.current?.bannerUrl, 1920, 480).catch((err) => - SentryLogger.error('Failed to process YT banner image', 'handleCreateOrUpdateChannel', err) - )) ?? null - : null - - const avatarContentId = `local-avatar-${createId()}` - const coverContentId = `local-cover-${createId()}` - - if (!memberId) { - throw Error('memberId id was not provided') - } - if (!COLLABORATOR_ID) { - throw Error('Collaborator member id was not provided') - } - - await createOrUpdateChannel({ - minimized: { - errorMessage: 'Failed to create or update channel', - }, - data: { - collaboratorId: COLLABORATOR_ID, - metadata: channelId - ? { ownerAccount: memberId } - : { - ownerAccount: memberId, - description: formRef.current?.channelDescription, - isPublic: true, - language: formRef.current?.channelLanguage || DEFAULT_LANGUAGE, - title: formRef.current?.channelTitle || formRef.current?.channelHandle, - }, - refetchChannel, - newChannel: !channelId, - assets: channelId - ? {} - : { - avatarPhoto: { - assetDimensions: { height: 192, width: 192 }, - contentId: avatarContentId, - imageCropData: null, - croppedBlob: avatarBlob, - originalBlob: avatarBlob, - }, - coverPhoto: { - assetDimensions: { width: 1920, height: 480 }, - contentId: coverContentId, - imageCropData: null, - croppedBlob: coverBlob, - originalBlob: coverBlob, - }, - }, - }, - onTxSync: async ({ channelId }) => { - setActiveChannel(channelId) - createdChannelId.current = channelId - }, - onCompleted: async () => { - await refetchUserMemberships() - onSuccess?.() - }, - }) - } catch (error) { - displaySnackbar({ - title: 'Failed to connect channel to the program', - description: 'An unexpected error occurred. Please try again.', - iconType: 'error', - }) - throw error - } - }, - [createOrUpdateChannel, displaySnackbar, memberId, refetchChannel, refetchUserMemberships, setActiveChannel] - ) - - const connectJoyChannelToYpp = useCallback(async () => { - if (!memberId) { - throw Error('memberId id was not provided') - } - - if (!currentUser?.email) { - throw Error('email was not provided') - } - - const channelIdInAction = channelId ?? createdChannelId.current - - if (!channelIdInAction) { - throw Error('channel id was not provided') - } - - try { - const channelCreationResponse = await axiosInstance.post( - `${atlasConfig.features.ypp.youtubeSyncApiUrl}/channels`, - { - ...(channelIdInAction ? { joystreamChannelId: parseInt(channelIdInAction) } : {}), - ...(referrerId ? { referrerChannelId: parseInt(referrerId) } : {}), - youtubeVideoUrl: formRef.current.videoUrl, - id: formRef.current?.id, - email: currentUser.email, - shouldBeIngested: true, - videoCategoryId: formRef.current.videoCategoryId, - } - ) - - await refetchYppSyncedChannels() - identifyUser({ - name: 'Sign up', - memberId: memberId, - email: formRef.current?.email || '', - isYppFlow: 'true', - signInType: 'password', - }) - trackYppOptIn({ - handle: formRef.current?.channelHandle, - email: currentUser.email, - category: formRef.current.videoCategoryId - ? displayCategoriesLookup[formRef.current.videoCategoryId]?.name - : undefined, - subscribersCount: channelCreationResponse.data.channel.subscribersCount, - referrerId: formRef.current.referrerChannelId, - utmSource: utmSource || undefined, - utmCampaign: utmCampaign || undefined, - utmContent: utmContent || undefined, - }) - setReferrerId(null) - setYtResponseData(null) - - navigate(absoluteRoutes.studio.yppDashboard()) - displaySnackbar({ - title: 'Sign up successful!', - description: 'It may take up to 24 hours after sign up for the videos to start syncing.', - iconType: 'success', - }) - } catch (e) { - displaySnackbar({ - title: 'Channel creation or update failed', - description: 'An unexpected error occurred. Please try again.', - iconType: 'error', - }) - throw e - } - }, [ - channelId, - currentUser?.email, - displaySnackbar, - identifyUser, - memberId, - navigate, - referrerId, - refetchYppSyncedChannels, - setReferrerId, - setYtResponseData, - trackYppOptIn, - utmCampaign, - utmContent, - utmSource, - ]) - - return { - connectJoyChannelToYpp, - formRef, - validateYtChannel, - updateOrCreateChannel, - } -} diff --git a/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.tsx b/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.tsx deleted file mode 100644 index 35622c14e3..0000000000 --- a/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useMemo, useState } from 'react' - -import { Button } from '@/components/_buttons/Button' -import { DialogModal } from '@/components/_overlays/DialogModal' -import { useAuth } from '@/providers/auth/auth.hooks' -import { useUser } from '@/providers/user/user.hooks' - -import { useYppSetupModalHandlers } from './YppSignUpSetupModa.hooks' -import { YppSetupModalStep } from './YppSignUpSetupModal.types' - -import { YppDetailsFormStep } from '../../../views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep' -import { OwnershipVerified } from '../genericSteps/OwnershipVerified' -import { ProveChannelOwnership } from '../genericSteps/ProveChannelOwnership' -import { WaitingModal } from '../genericSteps/WaitingModal' -import { SetActionButtonHandler } from '../genericSteps/types' - -export const YppSignUpSetupModal = ({ show, onClose }: { show?: boolean; onClose: () => void }) => { - const { isLoggedIn } = useAuth() - const { channelId } = useUser() - const [step, setStep] = useState(YppSetupModalStep.ytVideoUrl) - const [primaryAction, setPrimaryAction] = useState(undefined) - const { formRef, validateYtChannel, updateOrCreateChannel, connectJoyChannelToYpp } = useYppSetupModalHandlers() - - const primaryButton = useMemo(() => { - if (step === YppSetupModalStep.ytVideoUrl) { - return { - text: 'Verify video', - onClick: () => primaryAction?.(), - } - } - - if (step === YppSetupModalStep.yppForm) { - return { - text: 'Sign up', - onClick: () => primaryAction?.(), - } - } - - if (step === YppSetupModalStep.ownershipProved) { - return { - text: 'Continue', - onClick: () => { - if (!isLoggedIn) { - setStep(YppSetupModalStep.email) - return - } - - setStep(YppSetupModalStep.yppForm) - }, - } - } - - if ( - [ - YppSetupModalStep.channelVerification, - YppSetupModalStep.channelConnection, - YppSetupModalStep.channelCreation, - ].includes(step) - ) { - return { - text: 'Waiting...', - onClick: () => undefined, - disabled: true, - } - } - - return { - text: 'Continue', - onClick: () => primaryAction?.(), - } - }, [isLoggedIn, primaryAction, step]) - - return ( - - Cancel - - ) - } - dividers={[YppSetupModalStep.yppForm].includes(step)} - primaryButton={primaryButton} - > - {step === YppSetupModalStep.ytVideoUrl ? ( - setPrimaryAction(() => fn)} - onSubmit={(videoUrl) => { - formRef.current.videoUrl = videoUrl - setStep(YppSetupModalStep.channelVerification) - validateYtChannel(videoUrl) - .then(() => setStep(YppSetupModalStep.ownershipProved)) - .catch(() => setStep(YppSetupModalStep.ytVideoUrl)) // https://www.youtube.com/shorts/OQHvDRTK3Tk - }} - /> - ) : null} - {step === YppSetupModalStep.yppForm ? ( - setPrimaryAction(() => fn)} - onSubmit={(form) => { - formRef.current = { - ...formRef.current, - ...form, - } - - const onSuccessfulChannelCreation = () => { - if (!channelId) { - setStep(YppSetupModalStep.channelConnection) - } - - connectJoyChannelToYpp() - .then(() => onClose()) - .catch(() => setStep(YppSetupModalStep.ownershipProved)) - } - - updateOrCreateChannel(channelId ?? undefined, onSuccessfulChannelCreation).catch(() => { - setStep(YppSetupModalStep.ownershipProved) - }) - - if (!channelId) { - setStep(YppSetupModalStep.channelCreation) - return - } - - setStep(YppSetupModalStep.channelConnection) - }} - /> - ) : null} - {step === YppSetupModalStep.channelVerification ? : null} - {step === YppSetupModalStep.channelCreation ? ( - - ) : null} - {step === YppSetupModalStep.channelConnection ? : null} - {step === YppSetupModalStep.ownershipProved ? ( - - ) : null} - - ) -} diff --git a/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.types.ts b/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.types.ts deleted file mode 100644 index c200bfa9cc..0000000000 --- a/packages/atlas/src/components/_auth/YppSignUpSetupModal/YppSignUpSetupModal.types.ts +++ /dev/null @@ -1,39 +0,0 @@ -export enum YppSetupModalStep { - ytVideoUrl = 'ytVideoUrl', - channelVerification = 'channelVerification', - ownershipProved = 'ownershipProved', - // user has no account - email = 'email', - - yppForm = 'yppForm', - // user has account, but no channel - channelCreation = 'channelCreation', - // user has channel - channelConnection = 'channelConnection', -} - -export type YppFormData = { - youtubeVideoUrl?: string - id?: string - joystreamChannelId?: string - email?: string - referrerChannelId?: string - shouldBeIngested?: boolean - videoCategoryId?: string -} - -export type YppResponseData = { - 'id': string - 'channelHandle': string - 'channelTitle': string - 'channelDescription': string - 'channelLanguage': string - 'avatarUrl': string - 'bannerUrl': string -} - -export type YppSetupForm = { - videoUrl?: string - channelTitle?: string -} & Partial & - YppFormData diff --git a/packages/atlas/src/components/_auth/YppSignUpSetupModal/index.ts b/packages/atlas/src/components/_auth/YppSignUpSetupModal/index.ts deleted file mode 100644 index c4289a32d3..0000000000 --- a/packages/atlas/src/components/_auth/YppSignUpSetupModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './YppSignUpSetupModal' From 9be6877a67265fcf9c2948e64b753f73cfdaeff7 Mon Sep 17 00:00:00 2001 From: ikprk Date: Mon, 10 Jun 2024 15:42:23 +0200 Subject: [PATCH 20/22] Fix build --- .../__generated__/baseTypes.generated.ts | 2 + .../ExternalSignInModalEmailStep.tsx | 2 +- .../_auth/SignUpModal/SignUpModal.tsx | 434 ------------------ .../_auth/SignUpModal/SignUpModal.types.ts | 8 - .../SignUpSteps/SignUpCreatingMemberStep.tsx | 38 -- .../SignUpSteps/SignUpEmailStep.tsx | 134 ------ .../SignUpMembershipStep.styles.ts | 33 -- .../SignUpMembershipStep.tsx | 206 --------- .../SignUpSteps/SignUpMembershipStep/index.ts | 1 - .../SignUpPasswordStep/SignUpPasswordStep.tsx | 139 ------ .../SignUpSteps/SignUpPasswordStep/index.ts | 1 - .../SignUpSeedStep/SignUpSeedStep.tsx | 97 ---- .../SignUpSeedStep/SignupSeedStep.styles.ts | 19 - .../SignUpSteps/SignUpSeedStep/index.ts | 1 - .../SignUpSteps/SignUpSteps.styles.ts | 29 -- .../SignUpSteps/SignUpSteps.types.ts | 6 - .../SignUpSuccessStep.styles.ts | 38 -- .../SignUpSuccessStep/SignUpSuccessStep.tsx | 55 --- .../SignUpSteps/SignUpSuccessStep/index.ts | 1 - .../_auth/SignUpModal/SignUpSteps/index.ts | 6 - .../src/components/_auth/SignUpModal/index.ts | 1 - .../YppFinishFlowModal/YppFinishFlowModal.tsx | 10 +- .../steps/BuyMarketTokenSuccess.tsx | 7 +- .../steps/BuySaleTokenSuccess.tsx | 11 +- .../OnboardingProgressModal.tsx | 42 +- packages/atlas/src/embedded/index.html | 2 +- .../atlas/src/hooks/useYppModalHandlers.ts | 6 +- packages/atlas/src/index.html | 2 +- .../src/types/react-detectable-overflow.d.ts | 1 + .../global/YppLandingView/YppLandingView.tsx | 2 - 30 files changed, 61 insertions(+), 1273 deletions(-) delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpModal.tsx delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpModal.types.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpCreatingMemberStep.tsx delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpEmailStep.tsx delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/SignUpMembershipStep.styles.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/SignUpMembershipStep.tsx delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/index.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/SignUpPasswordStep.tsx delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/index.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/SignUpSeedStep.tsx delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/SignupSeedStep.styles.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/index.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.styles.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.types.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.tsx delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/index.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/index.ts delete mode 100644 packages/atlas/src/components/_auth/SignUpModal/index.ts diff --git a/packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts b/packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts index 919e9f9926..794661514c 100644 --- a/packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts +++ b/packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts @@ -8456,6 +8456,7 @@ export type Query = { tokens: Array tokensConnection: TokensConnection topSellingChannels: Array + // @ts-ignore working branch totalJoystreamEarnings: EarningStatsOutput trailerVideoById?: Maybe /** @deprecated Use trailerVideoById */ @@ -8463,6 +8464,7 @@ export type Query = { trailerVideos: Array trailerVideosConnection: TrailerVideosConnection userById?: Maybe + /** @deprecated Use userById */ userByUniqueInput?: Maybe users: Array diff --git a/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalEmailStep.tsx b/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalEmailStep.tsx index c077f689f8..129fe0644a 100644 --- a/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalEmailStep.tsx +++ b/packages/atlas/src/components/_auth/ExternalSignInModal/ExternalSignInSteps/ExternalSignInModalEmailStep.tsx @@ -47,13 +47,13 @@ export const ExternalSignInModalEmailStep: FC = ({ const address = await registerAccount({ type: 'external', email: data.email, + emailConfimationToken: '', address: userAddress, signature: (data) => joystream?.signMessage({ type: 'payload', data, }), - memberId, }) if (address) { diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.tsx deleted file mode 100644 index 40878d08c5..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import styled from '@emotion/styled' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { useOverflowDetector } from 'react-detectable-overflow' -import shallow from 'zustand/shallow' - -import { useGetMembershipsLazyQuery } from '@/api/queries/__generated__/memberships.generated' -import { Button } from '@/components/_buttons/Button' -import { DialogButtonProps } from '@/components/_overlays/Dialog' -import { DialogModal } from '@/components/_overlays/DialogModal' -import { AccountFormData, MemberFormData, RegisterError, useCreateMember } from '@/hooks/useCreateMember' -import { useMediaMatch } from '@/hooks/useMediaMatch' -import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' -import { useUniqueMemberHandle } from '@/hooks/useUniqueMemberHandle' -import { handleAnonymousAuth } from '@/providers/auth/auth.helpers' -import { useAuthStore } from '@/providers/auth/auth.store' -import { useSnackbar } from '@/providers/snackbars' -import { useYppStore } from '@/providers/ypp/ypp.store' -import { createId } from '@/utils/createId' -import { imageUrlToBlob } from '@/utils/image' -import { SentryLogger } from '@/utils/logs' - -import { SignUpSteps } from './SignUpModal.types' -import { - SignUpCreatingMemberStep, - SignUpEmailStep, - SignUpMembershipStep, - SignUpPasswordStep, - SignUpSeedStep, - SignUpSuccessStep, -} from './SignUpSteps' -import { SignUpStepsCommonProps } from './SignUpSteps/SignUpSteps.types' - -const SIGNUP_FORM_DATA_INITIAL_STATE: AccountFormData & MemberFormData = { - email: '', - password: '', - mnemonic: '', - handle: '', - avatar: undefined, - captchaToken: undefined, - confirmedTerms: false, - allowDownload: true, - memberId: '', - referrerChannelId: undefined, - emailConfimationToken: '', -} - -const stepToPageName: Partial> = { - [SignUpSteps.CreateMember]: 'Sign up modal - create member', - [SignUpSteps.SignUpSeed]: 'Signup modal - seed', - [SignUpSteps.SignUpPassword]: 'Signup modal - password', - [SignUpSteps.SignUpEmail]: 'Signup modal - email', - [SignUpSteps.Creating]: 'Signup modal - creating account', - [SignUpSteps.Success]: 'Signup modal - success', -} - -export const SignUpModal = () => { - const [currentStep, setCurrentStep] = useState(0) - const [emailAlreadyTakenError, setEmailAlreadyTakenError] = useState(false) - const [hasNavigatedBack, setHasNavigatedBack] = useState(false) - const [primaryButtonProps, setPrimaryButtonProps] = useState({ text: 'Continue' }) - const [amountOfTokens, setAmountofTokens] = useState() - const memberRef = useRef(null) - const memberPollingTries = useRef(0) - const haveTriedCreateSession = useRef(false) - const ytResponseData = useYppStore((state) => state.ytResponseData) - const isYppChannelFlow = useYppStore((state) => state.isYppChannelFlow) - const setYppModalOpenName = useYppStore((state) => state.actions.setYppModalOpenName) - const setYtResponseData = useYppStore((state) => state.actions.setYtResponseData) - const referrerChannelId = useYppStore((state) => state.referrerId) - const { - anonymousUserId, - actions: { setAnonymousUserId }, - } = useAuthStore() - const { displaySnackbar } = useSnackbar() - - const { generateUniqueMemberHandleBasedOnInput } = useUniqueMemberHandle() - - const { authModalOpenName, setAuthModalOpenName } = useAuthStore( - (state) => ({ - authModalOpenName: state.authModalOpenName, - setAuthModalOpenName: state.actions.setAuthModalOpenName, - }), - shallow - ) - const { ref, overflow } = useOverflowDetector({}) - - const signUpFormData = useRef>(SIGNUP_FORM_DATA_INITIAL_STATE) - const { createNewMember, createNewOrionAccount } = useCreateMember() - const { trackPageView, trackMembershipCreation } = useSegmentAnalytics() - - const goToNextStep = useCallback(() => { - setCurrentStep((previousIdx) => (previousIdx ?? -1) + 1) - setHasNavigatedBack(false) - }, []) - - const isYppFlow = ytResponseData !== null - - // skip the membership step when signing to ypp(ypp flow will provide handle and avatar for membership automatically) - useEffect(() => { - if (isYppFlow) { - setCurrentStep(SignUpSteps.SignUpSeed) - } - }, [isYppFlow]) - - const goToPreviousStep = useCallback(() => { - setCurrentStep((previousIdx) => (previousIdx ?? 1) - 1) - setHasNavigatedBack(true) - }, []) - - const goToStep = useCallback( - (step: SignUpSteps) => { - if (currentStep && currentStep < step) { - setHasNavigatedBack(true) - } - setCurrentStep(step) - }, - [currentStep] - ) - - const handleOrionAccountCreation = useCallback(() => { - if (!memberRef.current) { - return - } - - return createNewOrionAccount({ - data: { - ...signUpFormData.current, - memberId: memberRef.current, - ...(referrerChannelId ? { referrerChannelId } : {}), - }, - onError: (error) => { - if (error === RegisterError.EmailAlreadyExists) { - setEmailAlreadyTakenError(true) - goToStep(SignUpSteps.SignUpEmail) - return - } - if (error === RegisterError.MembershipNotFound) { - SentryLogger.error('Failed to create an account - missing membership', 'SignUpModal', error) - displaySnackbar({ - title: 'Something went wrong', - description: 'We could not find your membership. Please contact support.', - iconType: 'error', - }) - setAuthModalOpenName(undefined) - } - if (error === RegisterError.SessionRequired) { - if (!haveTriedCreateSession.current) { - haveTriedCreateSession.current = true - const anonymousReq = (id: string | null) => - handleAnonymousAuth(id).then((userId) => { - setAnonymousUserId(userId ?? null) - handleOrionAccountCreation() - }) - anonymousReq(anonymousUserId).catch((e) => { - if (e.response.status === 401) { - setAnonymousUserId(null) - anonymousReq(null) - } - }) - } else { - displaySnackbar({ - title: 'Something went wrong', - description: 'We could not create or find session. Please contact support.', - iconType: 'error', - }) - setAuthModalOpenName(undefined) - } - } - }, - onStart: () => { - goToStep(SignUpSteps.Creating) - }, - onSuccess: ({ amountOfTokens }) => { - // if this is ypp flow, overwrite ytResponseData.email - if (ytResponseData) { - setYtResponseData({ ...ytResponseData, email: signUpFormData.current.email }) - setAuthModalOpenName(undefined) - setYppModalOpenName('ypp-sync-options') - } else if (isYppChannelFlow) { - setAuthModalOpenName('createChannel') - } else { - setAmountofTokens(amountOfTokens) - goToNextStep() - } - }, - }) - }, [ - anonymousUserId, - createNewOrionAccount, - displaySnackbar, - goToNextStep, - goToStep, - isYppChannelFlow, - referrerChannelId, - setAnonymousUserId, - setAuthModalOpenName, - setYppModalOpenName, - setYtResponseData, - ytResponseData, - ]) - - const [getMembership, { startPolling }] = useGetMembershipsLazyQuery({ - onCompleted: (data) => { - if (!data.memberships[0].id) { - if (memberPollingTries.current > 5) { - handleOrionAccountCreation() - } - memberPollingTries.current++ - return - } - handleOrionAccountCreation() - }, - }) - - const handleEmailStepSubmit = useCallback( - (email: string, confirmedTerms: boolean) => { - signUpFormData.current = { ...signUpFormData.current, email, confirmedTerms } - if (memberRef.current && emailAlreadyTakenError) { - handleOrionAccountCreation() - return - } - - goToNextStep() - }, - [emailAlreadyTakenError, goToNextStep, handleOrionAccountCreation] - ) - - const handlePasswordStepSubmit = useCallback( - async (password: string, captchaToken?: string) => { - signUpFormData.current = { ...signUpFormData.current, password, captchaToken } - const memberId = await createNewMember({ - data: { - ...signUpFormData.current, - authorizationCode: ytResponseData?.authorizationCode, - userId: ytResponseData?.userId, - }, - onStart: () => { - goToStep(SignUpSteps.Creating) - }, - onError: () => { - setAuthModalOpenName(undefined) - }, - }) - if (memberId) { - memberRef.current = memberId ?? null - getMembership({ - variables: { - where: { - id_eq: memberId, - }, - }, - }) - startPolling(10_000) - } - }, - [ - createNewMember, - getMembership, - goToStep, - setAuthModalOpenName, - startPolling, - ytResponseData?.authorizationCode, - ytResponseData?.userId, - ] - ) - - const handleCreateMemberOnSeedStepSubmit = useCallback( - async (mnemonic: string, allowDownload: boolean) => { - let handle = signUpFormData.current.handle - let blob = signUpFormData.current.avatar?.blob - - if (ytResponseData) { - // replace handle and avatar if they are provided via ypp flow - blob = ytResponseData.avatarUrl - ? (await imageUrlToBlob(ytResponseData.avatarUrl).catch((err) => - SentryLogger.error('Failed to process YT avatar image', 'handleCreateOrUpdateChannel', err) - )) ?? null - : null - handle = ytResponseData.channelHandle - ? await generateUniqueMemberHandleBasedOnInput(ytResponseData.channelHandle) - : `user${createId()}` - } - - const memberData = { - mnemonic, - allowDownload, - handle, - avatar: blob ? { blob } : undefined, - } - - signUpFormData.current = { - ...signUpFormData.current, - ...memberData, - } - - goToNextStep() - }, - [ytResponseData, goToNextStep, generateUniqueMemberHandleBasedOnInput] - ) - - const handleMemberStepSubmit = useCallback( - (data: MemberFormData) => { - goToNextStep() - signUpFormData.current = { - ...signUpFormData.current, - handle: data.handle, - avatar: data.avatar, - captchaToken: data.captchaToken, - } - }, - [goToNextStep] - ) - - const commonProps: SignUpStepsCommonProps = useMemo( - () => ({ - setPrimaryButtonProps, - goToNextStep, - hasNavigatedBack, - }), - [goToNextStep, hasNavigatedBack] - ) - const backButtonVisible = useMemo(() => { - if (currentStep === SignUpSteps.SignUpSeed && ytResponseData) { - return undefined - } - - if (currentStep === SignUpSteps.CreateMember) { - return { text: 'Back', onClick: () => setAuthModalOpenName('logIn') } - } - - if ( - currentStep === SignUpSteps.SignUpEmail || - currentStep === SignUpSteps.SignUpPassword || - currentStep === SignUpSteps.SignUpSeed - ) { - return { text: 'Back', onClick: () => goToPreviousStep() } - } - }, [currentStep, goToPreviousStep, setAuthModalOpenName, ytResponseData]) - - const cancelButtonVisible = currentStep !== SignUpSteps.Success && currentStep !== SignUpSteps.Creating - const isSuccess = currentStep === SignUpSteps.Success - - useEffect(() => { - if (isSuccess) { - trackMembershipCreation(signUpFormData.current.handle, signUpFormData.current.email) - } - }, [isSuccess, signUpFormData.current.email, signUpFormData.current.handle, trackMembershipCreation]) - - useEffect(() => { - authModalOpenName === 'signUp' && - trackPageView(stepToPageName[currentStep] ?? 'Sign up - unknown page', { isYppFlow }) - }, [authModalOpenName, currentStep, isYppFlow, trackPageView]) - - const smMatch = useMediaMatch('sm') - return ( - { - signUpFormData.current = SIGNUP_FORM_DATA_INITIAL_STATE - setAuthModalOpenName(undefined) - }, - } - : primaryButtonProps - } - secondaryButton={backButtonVisible} - confetti={currentStep === SignUpSteps.Success && smMatch} - additionalActionsNode={ - cancelButtonVisible ? ( - - ) : undefined - } - additionalActionsNodeMobilePosition="bottom" - contentRef={ref} - > - {currentStep === SignUpSteps.CreateMember && ( - - )} - {currentStep === SignUpSteps.SignUpSeed && ( - - )} - {currentStep === SignUpSteps.SignUpPassword && ( - - )} - {currentStep === SignUpSteps.SignUpEmail && ( - - )} - {currentStep === SignUpSteps.Creating && } - {currentStep === SignUpSteps.Success && ( - - )} - - ) -} - -const StyledDialogModal = styled(DialogModal)` - max-height: calc(100vh - 80px); -` diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.types.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.types.ts deleted file mode 100644 index e2de7dc59b..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum SignUpSteps { - CreateMember, - SignUpSeed, - SignUpEmail, - SignUpPassword, - Creating, - Success, -} diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpCreatingMemberStep.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpCreatingMemberStep.tsx deleted file mode 100644 index 1a6c4c13e1..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpCreatingMemberStep.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FC, useEffect } from 'react' - -import { SvgAlertsWarning24 } from '@/assets/icons' -import { WarningContainer } from '@/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.styles' - -import { SignUpStepsCommonProps } from './SignUpSteps.types' - -import { AuthenticationModalStepTemplate } from '../../AuthenticationModalStepTemplate' - -export const SignUpCreatingMemberStep: FC = ({ setPrimaryButtonProps, hasNavigatedBack }) => { - // send updates to SignInModal on state of primary button - useEffect(() => { - setPrimaryButtonProps({ - text: 'Waiting...', - disabled: true, - }) - }, [setPrimaryButtonProps]) - - return ( - - Please wait while your membership is being created. Our faucet server will create it for you so you don't need - to worry about any fees. This may take up to 1 minute on a slow network. -
-
- - - Please do not close the browser tab or reload the page. - - - } - loader - hasNavigatedBack={hasNavigatedBack} - /> - ) -} diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpEmailStep.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpEmailStep.tsx deleted file mode 100644 index 7ebc641b79..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpEmailStep.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import { FC, useCallback, useEffect } from 'react' -import { Controller, useForm } from 'react-hook-form' -import * as z from 'zod' - -import { Checkbox } from '@/components/_inputs/Checkbox' -import { FormField } from '@/components/_inputs/FormField' -import { Input } from '@/components/_inputs/Input' -import { absoluteRoutes } from '@/config/routes' -import { AccountFormData } from '@/hooks/useCreateMember' -import { useMountEffect } from '@/hooks/useMountEffect' - -import { CheckboxWrapper, StyledLink, StyledSignUpForm } from './SignUpSteps.styles' -import { SignUpStepsCommonProps } from './SignUpSteps.types' - -import { AuthenticationModalStepTemplate } from '../../AuthenticationModalStepTemplate' - -const zodSchema = z - .object({ - email: z - .string() - .min(3, { message: 'Enter email address.' }) - .regex(/^\S+@\S+\.\S+$/, 'Enter valid email address.'), - confirmEmail: z - .string() - .min(1, { message: 'Enter email address.' }) - .regex(/^\S+@\S+\.\S+$/, 'Enter valid email address.'), - confirmedTerms: z.boolean().refine((value) => value, { message: 'Agree to Terms and Conditions to continue.' }), - }) - .refine( - (data) => { - return data.email === data.confirmEmail - }, - { - path: ['confirmEmail'], - message: 'Email address has to match.', - } - ) - -type EmailStepForm = z.infer - -type SignUpEmailStepProps = { - onEmailSubmit: (email: string, confirmedTerms: boolean) => void - isEmailAlreadyTakenError?: boolean - isOverflowing: boolean -} & SignUpStepsCommonProps & - Pick - -export const SignUpEmailStep: FC = ({ - setPrimaryButtonProps, - hasNavigatedBack, - isEmailAlreadyTakenError, - isOverflowing, - onEmailSubmit, - confirmedTerms, - email, -}) => { - const { - register, - handleSubmit, - control, - setError, - formState: { errors }, - } = useForm({ - criteriaMode: 'all', - mode: 'onBlur', - resolver: zodResolver(zodSchema), - shouldFocusError: true, - defaultValues: { - confirmedTerms: confirmedTerms || false, - confirmEmail: email, - email: email, - }, - }) - - const handleGoToNextStep = useCallback(() => { - handleSubmit((data) => { - onEmailSubmit(data.email, data.confirmedTerms) - })() - }, [handleSubmit, onEmailSubmit]) - - useEffect(() => { - setPrimaryButtonProps({ - text: 'Continue', - onClick: handleGoToNextStep, - }) - }, [handleGoToNextStep, isEmailAlreadyTakenError, setPrimaryButtonProps]) - - useMountEffect(() => { - if (isEmailAlreadyTakenError) { - setError('email', { message: 'This email is already in use.' }) - } - }) - - return ( - - - - - - - - - ( - - onChange(val)} - value={value} - caption={errors.confirmedTerms?.message} - label={ - <> - I have read and agree to{' '} - - Terms and conditions - - - } - /> - - )} - /> - - - ) -} diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/SignUpMembershipStep.styles.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/SignUpMembershipStep.styles.ts deleted file mode 100644 index 810aac6550..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/SignUpMembershipStep.styles.ts +++ /dev/null @@ -1,33 +0,0 @@ -import styled from '@emotion/styled' - -import { Avatar } from '@/components/Avatar' -import { DialogModal } from '@/components/_overlays/DialogModal' -import { media, sizes } from '@/styles' - -export const StyledDialogModal = styled(DialogModal)` - max-height: calc(100vh - 80px); - ${media.sm} { - max-height: 576px; - } -` - -export const StyledAvatar = styled(Avatar)` - position: absolute; - transform: translateY(-50%); - top: 0; -` - -export const StyledForm = styled.form` - position: relative; - padding-top: ${sizes(17)}; - display: grid; - gap: ${sizes(6)}; - margin-bottom: ${sizes(6)}; -` - -export const SubtitleContainer = styled.div` - display: inline-block; - text-decoration: none; - margin-top: ${sizes(2)}; - margin-bottom: ${sizes(11)}; -` diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/SignUpMembershipStep.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/SignUpMembershipStep.tsx deleted file mode 100644 index 34a7ad8d9a..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/SignUpMembershipStep.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import debouncePromise from 'awesome-debounce-promise' -import { - ChangeEventHandler, - FC, - ForwardedRef, - RefObject, - forwardRef, - useCallback, - useEffect, - useRef, - useState, -} from 'react' -import { Controller, FormProvider, useController, useForm, useFormContext } from 'react-hook-form' -import shallow from 'zustand/shallow' - -import { AuthenticationModalStepTemplate } from '@/components/_auth/AuthenticationModalStepTemplate' -import { TextButton } from '@/components/_buttons/Button' -import { FormField } from '@/components/_inputs/FormField' -import { Input, InputProps } from '@/components/_inputs/Input' -import { ImageCropModal, ImageCropModalImperativeHandle } from '@/components/_overlays/ImageCropModal' -import { MEMBERSHIP_NAME_PATTERN } from '@/config/regex' -import { MemberFormData } from '@/hooks/useCreateMember' -import { useUniqueMemberHandle } from '@/hooks/useUniqueMemberHandle' -import { useAuthStore } from '@/providers/auth/auth.store' - -import { StyledAvatar, StyledForm, SubtitleContainer } from './SignUpMembershipStep.styles' - -import { SignUpStepsCommonProps } from '../SignUpSteps.types' - -type SignInModalMembershipStepProps = SignUpStepsCommonProps & { - onSubmit: (data: MemberFormData) => void - dialogContentRef?: RefObject -} & Pick - -export const SignUpMembershipStep: FC = ({ - setPrimaryButtonProps, - onSubmit, - hasNavigatedBack, - avatar, - handle, -}) => { - const form = useForm({ reValidateMode: 'onSubmit', defaultValues: { avatar, handle } }) - const { - handleSubmit: createSubmitHandler, - watch, - control, - formState: { errors, isSubmitting }, - } = form - const { setAuthModalOpenName } = useAuthStore( - (state) => ({ - setAuthModalOpenName: state.actions.setAuthModalOpenName, - }), - shallow - ) - const handleInputRef = useRef(null) - const avatarDialogRef = useRef(null) - - const [isHandleValidating, setIsHandleValidating] = useState(false) - - const requestFormSubmit = useCallback(() => { - setIsHandleValidating(false) - createSubmitHandler(onSubmit)() - }, [onSubmit, createSubmitHandler]) - - // send updates to SignInModal on state of primary button - useEffect(() => { - setPrimaryButtonProps({ - text: 'Continue', - disabled: isSubmitting, - onClick: requestFormSubmit, - }) - }, [isHandleValidating, isSubmitting, requestFormSubmit, setPrimaryButtonProps]) - - useEffect(() => { - if (errors.handle) { - handleInputRef.current?.scrollIntoView({ behavior: 'smooth' }) - } - }, [errors.handle]) - - return ( - - Already have an account? setAuthModalOpenName('logIn')}>Sign in - - } - hasNavigatedBack={hasNavigatedBack} - formNode={ - - - ( - <> - - avatarDialogRef.current?.open( - imageInputFile?.originalBlob ? imageInputFile.originalBlob : imageInputFile?.blob, - imageInputFile?.imageCropData, - !!imageInputFile?.blob - ) - } - assetUrls={imageInputFile?.url ? [imageInputFile.url] : []} - editable - /> - { - onChange({ - blob, - url, - imageCropData, - originalBlob, - }) - }} - onDelete={() => { - onChange(undefined) - }} - ref={avatarDialogRef} - /> - - )} - /> - - - - - - } - /> - ) -} - -type HandleInputProps = InputProps & { - name: string - setIsHandleValidating: (v: boolean) => void -} - -const HandleInput = forwardRef( - ( - { name, setIsHandleValidating, processing, ...inputProps }: HandleInputProps, - ref: ForwardedRef - ) => { - const { checkIfMemberIsAvailable } = useUniqueMemberHandle() - - const { control, trigger } = useFormContext() - const { field } = useController({ - name, - control, - rules: { - validate: { - valid: (value) => (!value ? true : MEMBERSHIP_NAME_PATTERN.test(value) || 'Enter a valid member handle.'), - unique: async (value) => (await checkIfMemberIsAvailable(value)) || 'This member handle is already in use.', - }, - required: { value: true, message: 'Member handle is required.' }, - minLength: { value: 5, message: 'Member handle must be at least 5 characters long.' }, - }, - }) - - const debouncedHandleValidation = useRef( - debouncePromise(async () => { - await trigger(name) - setIsHandleValidating(false) - }, 500) - ) - const handleChange: ChangeEventHandler = (evt) => { - const value = evt.target.value?.toLowerCase().replace(/[^0-9_a-z]/g, '_') - field.onChange(value) - if (!processing) setIsHandleValidating(true) - debouncedHandleValidation.current() - } - - return ( - { - field.ref(e) - if (ref && 'current' in ref) ref.current = e - else ref?.(e) - }} - processing={processing} - onChange={handleChange} - /> - ) - } -) -HandleInput.displayName = 'HandleInput' diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/index.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/index.ts deleted file mode 100644 index be6737879a..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpMembershipStep/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SignUpMembershipStep' diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/SignUpPasswordStep.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/SignUpPasswordStep.tsx deleted file mode 100644 index d05d5e99bf..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/SignUpPasswordStep.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import HCaptcha from '@hcaptcha/react-hcaptcha' -import { zodResolver } from '@hookform/resolvers/zod' -import { FC, RefObject, useCallback, useEffect, useRef } from 'react' -import { Controller, FormProvider, useForm } from 'react-hook-form' - -import { AuthenticationModalStepTemplate } from '@/components/_auth/AuthenticationModalStepTemplate' -import { PasswordCriterias } from '@/components/_auth/PasswordCriterias' -import { FormField } from '@/components/_inputs/FormField' -import { Input } from '@/components/_inputs/Input' -import { atlasConfig } from '@/config' -import { AccountFormData } from '@/hooks/useCreateMember' -import { useHidePasswordInInput } from '@/hooks/useHidePasswordInInput' -import { passwordAndRepeatPasswordSchema } from '@/utils/formValidationOptions' - -import { StyledSignUpForm } from '../SignUpSteps.styles' -import { SignUpStepsCommonProps } from '../SignUpSteps.types' - -type PasswordStepForm = { - password: string - confirmPassword: string - captchaToken?: string -} - -type SignUpPasswordStepProps = { - onPasswordSubmit: (password: string, captchaToken?: string) => void - password?: string - dialogContentRef?: RefObject -} & SignUpStepsCommonProps & - Pick - -export const SignUpPasswordStep: FC = ({ - setPrimaryButtonProps, - hasNavigatedBack, - password, - dialogContentRef, - onPasswordSubmit, -}) => { - const form = useForm({ - shouldFocusError: true, - reValidateMode: 'onSubmit', - defaultValues: { - password, - confirmPassword: password, - }, - resolver: zodResolver(passwordAndRepeatPasswordSchema(true)), - }) - const { - handleSubmit, - register, - formState: { errors }, - control, - trigger, - } = form - const [hidePasswordProps] = useHidePasswordInInput() - const [hideConfirmPasswordProps] = useHidePasswordInInput() - - const captchaRef = useRef(null) - const captchaInputRef = useRef(null) - - const handleGoToNextStep = useCallback(() => { - handleSubmit((data) => { - onPasswordSubmit(data.password, data.captchaToken) - })() - captchaRef.current?.resetCaptcha() - }, [handleSubmit, onPasswordSubmit]) - - useEffect(() => { - setPrimaryButtonProps({ - text: 'Sign up', - onClick: handleGoToNextStep, - }) - }, [handleGoToNextStep, setPrimaryButtonProps]) - - useEffect(() => { - if (errors.captchaToken) { - captchaInputRef.current?.scrollIntoView({ behavior: 'smooth' }) - } - }, [errors.captchaToken]) - - // used to scroll the form to the bottom upon first handle field focus - this is done to let the user see password requirements & captcha - const hasDoneInitialScroll = useRef(false) - - return ( - - - - - { - if (hasDoneInitialScroll.current || !dialogContentRef?.current) return - hasDoneInitialScroll.current = true - dialogContentRef.current.scrollTo({ top: dialogContentRef.current.scrollHeight, behavior: 'smooth' }) - }} - /> - - - - - - {atlasConfig.features.members.hcaptchaSiteKey && ( - ( - - { - onChange(token) - trigger('captchaToken') - }} - /> - - )} - /> - )} - - - - ) -} diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/index.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/index.ts deleted file mode 100644 index 69a21393fd..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpPasswordStep/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SignUpPasswordStep' diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/SignUpSeedStep.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/SignUpSeedStep.tsx deleted file mode 100644 index 1c79135487..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/SignUpSeedStep.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { mnemonicGenerate } from '@polkadot/util-crypto' -import { FC, useCallback, useEffect, useRef } from 'react' -import { Controller, useForm } from 'react-hook-form' - -import { AuthenticationModalStepTemplate } from '@/components/_auth/AuthenticationModalStepTemplate' -import { Checkbox } from '@/components/_inputs/Checkbox' -import { FormField } from '@/components/_inputs/FormField' -import { MemberFormData } from '@/hooks/useCreateMember' - -import { StyledTextArea } from './SignupSeedStep.styles' - -import { StyledSignUpForm } from '../SignUpSteps.styles' -import { SignUpStepsCommonProps } from '../SignUpSteps.types' - -type FormData = Pick - -type SignUpSeedStepProps = { - onSeedSubmit: (mnemonic: string, allowDownload: boolean) => void -} & SignUpStepsCommonProps & - FormData - -export const SignUpSeedStep: FC = ({ - hasNavigatedBack, - setPrimaryButtonProps, - mnemonic, - onSeedSubmit, -}) => { - const { control, register, getValues, handleSubmit, setValue } = useForm({ - shouldFocusError: true, - defaultValues: { - allowDownload: true, - mnemonic, - }, - }) - const firstRender = useRef(true) - - useEffect(() => { - if (firstRender.current && !mnemonic) { - setValue('mnemonic', mnemonicGenerate()) - firstRender.current = false - } - }, [mnemonic, setValue]) - - const handleGoToNextStep = useCallback(() => { - const downloadSeed = () => { - const blobText = new Blob([getValues('mnemonic')], { type: 'text/plain' }) - const url = URL.createObjectURL(blobText) - const link = document.createElement('a') - link.href = url - link.download = 'mnemonic.txt' - link.click() - - link.remove() - URL.revokeObjectURL(url) - } - - handleSubmit((data) => { - onSeedSubmit(data.mnemonic, data.allowDownload) - if (data.allowDownload) { - downloadSeed() - } - })() - }, [getValues, handleSubmit, onSeedSubmit]) - - useEffect(() => { - setPrimaryButtonProps({ - text: 'Continue', - onClick: () => handleGoToNextStep(), - }) - }, [handleGoToNextStep, setPrimaryButtonProps]) - - return ( - - - - - - ( - onChange(val)} - value={value} - label="Download the wallet seed as .txt file" - caption="Download will start after clicking continue" - /> - )} - /> - - - ) -} diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/SignupSeedStep.styles.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/SignupSeedStep.styles.ts deleted file mode 100644 index 1b222cc268..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/SignupSeedStep.styles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import styled from '@emotion/styled' - -import { TextButton } from '@/components/_buttons/Button' -import { TextArea } from '@/components/_inputs/TextArea' -import { cVar } from '@/styles' - -export const StyledTextButton = styled(TextButton)` - justify-self: start; -` - -export const StyledTextArea = styled(TextArea)` - color: ${cVar('colorTextCaution')}; - resize: none; - - :disabled { - cursor: auto; - opacity: 1; - } -` diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/index.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/index.ts deleted file mode 100644 index 6fa88c026b..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSeedStep/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SignUpSeedStep' diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.styles.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.styles.ts deleted file mode 100644 index 1fc30796b4..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.styles.ts +++ /dev/null @@ -1,29 +0,0 @@ -import styled from '@emotion/styled' - -import { cVar, sizes } from '@/styles' - -export const StyledSignUpForm = styled.form<{ additionalPaddingBottom?: boolean }>` - display: grid; - gap: ${sizes(6)}; - padding-bottom: ${({ additionalPaddingBottom }) => - additionalPaddingBottom ? 'var(--local-size-dialog-padding)' : 0}; -` - -export const CheckboxWrapper = styled.div<{ isAccepted: boolean }>` - margin: 0 calc(-1 * var(--local-size-dialog-padding)); - display: flex; - align-items: center; - background-color: ${({ isAccepted }) => (isAccepted ? cVar('colorBackground') : cVar('colorBackgroundElevated'))}; - padding: ${sizes(4)} var(--local-size-dialog-padding); -` - -export const StyledLink = styled.a` - color: ${cVar('colorTextPrimary')}; - text-decoration: underline; -` - -export const WarningContainer = styled.span` - display: flex; - justify-content: flex-start; - align-items: center; -` diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.types.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.types.ts deleted file mode 100644 index 0b1ad4a772..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSteps.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DialogButtonProps } from '@/components/_overlays/Dialog' - -export type SignUpStepsCommonProps = { - setPrimaryButtonProps: (props: DialogButtonProps) => void - hasNavigatedBack: boolean -} diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles.ts deleted file mode 100644 index 0816fd854a..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles.ts +++ /dev/null @@ -1,38 +0,0 @@ -import styled from '@emotion/styled' - -import { cVar, media, sizes, zIndex } from '@/styles' - -export const IllustrationWrapper = styled.div` - margin: calc(var(--local-size-dialog-padding) * -1) calc(var(--local-size-dialog-padding) * -1) ${sizes(6)} - calc(var(--local-size-dialog-padding) * -1); - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - position: relative; - background-color: ${cVar('colorBackgroundMuted')}; - - > * { - width: 100%; - height: 208px; - - ${media.sm} { - height: 264px; - } - } -` - -export const LottieContainer = styled.div` - position: absolute; - bottom: 0; - left: 0; - right: 0; - width: 100%; - z-index: ${zIndex.nearOverlay}; - display: flex; - justify-content: center; -` - -export const ContentWrapper = styled.div` - text-align: center; -` diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.tsx deleted file mode 100644 index ea14d9ad9d..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { FC } from 'react' - -import { confettiAnimation } from '@/assets/animations' -import { AppKV } from '@/components/AppKV' -import { Avatar } from '@/components/Avatar' -import { LottiePlayer } from '@/components/LottiePlayer' -import { Text } from '@/components/Text' -import { atlasConfig } from '@/config' -import { useMediaMatch } from '@/hooks/useMediaMatch' -import { formatNumber } from '@/utils/number' - -import { ContentWrapper, IllustrationWrapper, LottieContainer } from './SignUpSuccessStep.styles' - -type SignUpSuccessStepProps = { - avatarUrl?: string - amountOfTokens?: number -} - -export const SignUpSuccessStep: FC = ({ avatarUrl, amountOfTokens }) => { - const smMatch = useMediaMatch('sm') - return ( - <> - - } /> - {!smMatch && ( - - - - )} - - - - Your membership has been created! - - - Congratulations! Now you can browse, watch, create, collect videos across the platform and have fun! - {amountOfTokens && amountOfTokens > 0 && ( - <> -
-
- Enjoy your {formatNumber(amountOfTokens)} {atlasConfig.joystream.tokenTicker} tokens to help you cover - transaction fees. These tokens are non-transferable and can't be spent on NFTs or other purchases. - - )} -
-
- - ) -} diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/index.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/index.ts deleted file mode 100644 index 3baa0a446d..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SignUpSuccessStep' diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/index.ts b/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/index.ts deleted file mode 100644 index 2e37508160..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpSteps/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './SignUpEmailStep' -export * from './SignUpPasswordStep' -export * from './SignUpSeedStep' -export * from './SignUpCreatingMemberStep' -export * from './SignUpMembershipStep' -export * from './SignUpSuccessStep' diff --git a/packages/atlas/src/components/_auth/SignUpModal/index.ts b/packages/atlas/src/components/_auth/SignUpModal/index.ts deleted file mode 100644 index 33e0624e22..0000000000 --- a/packages/atlas/src/components/_auth/SignUpModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SignUpModal' diff --git a/packages/atlas/src/components/_auth/YppFinishFlowModal/YppFinishFlowModal.tsx b/packages/atlas/src/components/_auth/YppFinishFlowModal/YppFinishFlowModal.tsx index 7bbde0dc26..73a69e3b9f 100644 --- a/packages/atlas/src/components/_auth/YppFinishFlowModal/YppFinishFlowModal.tsx +++ b/packages/atlas/src/components/_auth/YppFinishFlowModal/YppFinishFlowModal.tsx @@ -284,7 +284,14 @@ const useYppFinishFlowModalHandlers = () => { } ) }, - [createNewMember, displaySnackbar, email, refetchCurrentUser, refetchUserMemberships] + [ + createNewMember, + displaySnackbar, + email, + refetchCurrentUser, + refetchUserMemberships, + generateUniqueMemberHandleBasedOnInput, + ] ) const createAccount = useCallback( @@ -299,7 +306,6 @@ const useYppFinishFlowModalHandlers = () => { SentryLogger.error('Missing fields during account creation', 'AccountSetup', { form: formRef.current }) return } - console.log('creating account', email, password, confirmationCode) await createNewOrionAccount({ data: { diff --git a/packages/atlas/src/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenSuccess.tsx b/packages/atlas/src/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenSuccess.tsx index a6460f277e..b78b72c7bf 100644 --- a/packages/atlas/src/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenSuccess.tsx +++ b/packages/atlas/src/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenSuccess.tsx @@ -6,11 +6,6 @@ import confettiAnimation from '@/assets/animations/confetti.json' import { AppKV } from '@/components/AppKV' import { LottiePlayer } from '@/components/LottiePlayer' import { Text } from '@/components/Text' -import { - ContentWrapper, - IllustrationWrapper, - LottieContainer, -} from '@/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles' import { AnimatedCoin } from '@/components/_crt/AnimatedCoin/AnimatedCoin' import { absoluteRoutes } from '@/config/routes' import { useMediaMatch } from '@/hooks/useMediaMatch' @@ -19,6 +14,8 @@ import { useSnackbar } from '@/providers/snackbars' import { CommonProps } from './types' +import { ContentWrapper, IllustrationWrapper, LottieContainer } from '../../OnboardingProgressModal' + type SignUpSuccessStepProps = { tokenName?: string tokenAmount?: number diff --git a/packages/atlas/src/components/_crt/BuySaleTokenModal/steps/BuySaleTokenSuccess.tsx b/packages/atlas/src/components/_crt/BuySaleTokenModal/steps/BuySaleTokenSuccess.tsx index 32fee3d750..71e9ef4407 100644 --- a/packages/atlas/src/components/_crt/BuySaleTokenModal/steps/BuySaleTokenSuccess.tsx +++ b/packages/atlas/src/components/_crt/BuySaleTokenModal/steps/BuySaleTokenSuccess.tsx @@ -4,16 +4,17 @@ import confettiAnimation from '@/assets/animations/confetti.json' import { AppKV } from '@/components/AppKV' import { LottiePlayer } from '@/components/LottiePlayer' import { Text } from '@/components/Text' -import { - ContentWrapper, - IllustrationWrapper, - LottieContainer, -} from '@/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles' import { useMediaMatch } from '@/hooks/useMediaMatch' import { useMountEffect } from '@/hooks/useMountEffect' import { CommonProps } from './types' +import { + ContentWrapper, + IllustrationWrapper, + LottieContainer, +} from '../../OnboardingProgressModal/OnboardingProgressModal' + type SignUpSuccessStepProps = { tokenName?: string coinImageUrl?: string diff --git a/packages/atlas/src/components/_crt/OnboardingProgressModal/OnboardingProgressModal.tsx b/packages/atlas/src/components/_crt/OnboardingProgressModal/OnboardingProgressModal.tsx index 3c38d1571c..37e828ea08 100644 --- a/packages/atlas/src/components/_crt/OnboardingProgressModal/OnboardingProgressModal.tsx +++ b/packages/atlas/src/components/_crt/OnboardingProgressModal/OnboardingProgressModal.tsx @@ -1,19 +1,16 @@ +import styled from '@emotion/styled' import { useState } from 'react' import { confettiAnimation } from '@/assets/animations' import { AppKV } from '@/components/AppKV' import { LottiePlayer } from '@/components/LottiePlayer' import { Text } from '@/components/Text' -import { - ContentWrapper, - IllustrationWrapper, - LottieContainer, -} from '@/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles' import { TextButton } from '@/components/_buttons/Button' import { DialogModal } from '@/components/_overlays/DialogModal' import { useMediaMatch } from '@/hooks/useMediaMatch' import { useMountEffect } from '@/hooks/useMountEffect' import { useOverlayManager } from '@/providers/overlayManager' +import { cVar, media, sizes, zIndex } from '@/styles' const data = { master: { @@ -86,3 +83,38 @@ export const OnboardingProgressModal = ({ onContinue, type, show }: OnboardingPr ) } + +export const IllustrationWrapper = styled.div` + margin: calc(var(--local-size-dialog-padding) * -1) calc(var(--local-size-dialog-padding) * -1) ${sizes(6)} + calc(var(--local-size-dialog-padding) * -1); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; + background-color: ${cVar('colorBackgroundMuted')}; + + > * { + width: 100%; + height: 208px; + + ${media.sm} { + height: 264px; + } + } +` + +export const LottieContainer = styled.div` + position: absolute; + bottom: 0; + left: 0; + right: 0; + width: 100%; + z-index: ${zIndex.nearOverlay}; + display: flex; + justify-content: center; +` + +export const ContentWrapper = styled.div` + text-align: center; +` diff --git a/packages/atlas/src/embedded/index.html b/packages/atlas/src/embedded/index.html index 3f5f2ea902..e45e859c85 100644 --- a/packages/atlas/src/embedded/index.html +++ b/packages/atlas/src/embedded/index.html @@ -1,4 +1,4 @@ - + diff --git a/packages/atlas/src/hooks/useYppModalHandlers.ts b/packages/atlas/src/hooks/useYppModalHandlers.ts index ead2698795..b51cc1292e 100644 --- a/packages/atlas/src/hooks/useYppModalHandlers.ts +++ b/packages/atlas/src/hooks/useYppModalHandlers.ts @@ -52,14 +52,14 @@ export const useYppModalHandlers = () => { const formRef = useRef({}) const { displaySnackbar } = useSnackbar() const { currentUser } = useAuth() - const { memberId, refetchUserMemberships, setActiveChannel, channelId, isLoggedIn } = useUser() + const { memberId, refetchUserMemberships, setActiveChannel, channelId } = useUser() const createOrUpdateChannel = useCreateEditChannelSubmit(undefined) const { referrerId, utmSource, utmCampaign, utmContent, - actions: { setYtResponseData, setUtmSource, setUtmCampaign, setUtmContent }, + actions: { setYtResponseData }, } = useYppStore((store) => store, shallow) const createdChannelId = useRef(null) const setReferrerId = useYppStore((store) => store.actions.setReferrerId) @@ -128,8 +128,6 @@ export const useYppModalHandlers = () => { throw Error('Collaborator member id was not provided') } - console.log('hmmm', channelId, COLLABORATOR_ID) - await createOrUpdateChannel({ minimized: { errorMessage: 'Failed to create or update channel', diff --git a/packages/atlas/src/index.html b/packages/atlas/src/index.html index 26d366ff37..abd5c92d61 100644 --- a/packages/atlas/src/index.html +++ b/packages/atlas/src/index.html @@ -1,4 +1,4 @@ - + diff --git a/packages/atlas/src/types/react-detectable-overflow.d.ts b/packages/atlas/src/types/react-detectable-overflow.d.ts index afa22827be..9804023b12 100644 --- a/packages/atlas/src/types/react-detectable-overflow.d.ts +++ b/packages/atlas/src/types/react-detectable-overflow.d.ts @@ -5,6 +5,7 @@ declare module 'react-detectable-overflow' { props: useOverflowDetectorProps ): { overflow: boolean + // @ts-ignore not important ref: import('react').MutableRefObject } } diff --git a/packages/atlas/src/views/global/YppLandingView/YppLandingView.tsx b/packages/atlas/src/views/global/YppLandingView/YppLandingView.tsx index 8211e212aa..f7a5a856ce 100644 --- a/packages/atlas/src/views/global/YppLandingView/YppLandingView.tsx +++ b/packages/atlas/src/views/global/YppLandingView/YppLandingView.tsx @@ -26,7 +26,6 @@ import { useGetYppSyncedChannels } from './useGetYppSyncedChannels' export const YppLandingView: FC = () => { const headTags = useHeadTags('YouTube Partner Program') - const yppModalOpenName = useYppStore((state) => state.yppModalOpenName) const setYppModalOpen = useYppStore((state) => state.actions.setYppModalOpenName) const { activeMembership, channelId } = useUser() const { setSelectedChannelId, setShouldContinueYppFlowAfterCreatingChannel } = useYppStore((store) => store.actions) @@ -69,7 +68,6 @@ export const YppLandingView: FC = () => { navigate(absoluteRoutes.studio.yppDashboard()) return } - console.log('wtf') if (!authModalOpenName) { trackYppSignInButtonClick() From c230cb96e01b6f23530d27dcaca06588fec446c4 Mon Sep 17 00:00:00 2001 From: ikprk Date: Mon, 10 Jun 2024 16:06:49 +0200 Subject: [PATCH 21/22] Regen lock --- yarn.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2a573ce1ee..72579a0d15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10680,7 +10680,7 @@ __metadata: "blake3@patch:blake3@npm:2.1.7#.yarn/patches/blake3-npm-2.1.7-7bf40c44b4::locator=root-workspace-0b6124%40workspace%3A.": version: 2.1.7 - resolution: "blake3@patch:blake3@npm%3A2.1.7#.yarn/patches/blake3-npm-2.1.7-7bf40c44b4::version=2.1.7&hash=e42d7f&locator=root-workspace-0b6124%40workspace%3A." + resolution: "blake3@patch:blake3@npm%3A2.1.7#.yarn/patches/blake3-npm-2.1.7-7bf40c44b4::version=2.1.7&hash=c6a798&locator=root-workspace-0b6124%40workspace%3A." checksum: 2d3efd6a3d8506d094cfaea4137dc5f4f409107cc9fe4deed78854ffe84469647cf468e2c5d6ce4d2b0c1407d4a379996f12c6113286e7578784b9a3332fde09 languageName: node linkType: hard @@ -14158,7 +14158,7 @@ __metadata: "fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.3 - resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=df0bf1" + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=18f3a7" dependencies: node-gyp: latest conditions: os=darwin @@ -20198,7 +20198,7 @@ __metadata: "resolve@patch:resolve@^1.0.0#~builtin, resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.21.0#~builtin": version: 1.22.8 - resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=c3c19d" + resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=07638b" dependencies: is-core-module: ^2.13.0 path-parse: ^1.0.7 @@ -20211,7 +20211,7 @@ __metadata: "resolve@patch:resolve@^2.0.0-next.5#~builtin": version: 2.0.0-next.5 - resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#~builtin::version=2.0.0-next.5&hash=c3c19d" + resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#~builtin::version=2.0.0-next.5&hash=07638b" dependencies: is-core-module: ^2.13.0 path-parse: ^1.0.7 @@ -22294,17 +22294,17 @@ __metadata: "typescript@patch:typescript@5.1.6#~builtin": version: 5.1.6 - resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=5da071" + resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=7ad353" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: f53bfe97f7c8b2b6d23cf572750d4e7d1e0c5fff1c36d859d0ec84556a827b8785077bc27676bf7e71fae538e517c3ecc0f37e7f593be913d884805d931bc8be + checksum: 21e88b0a0c0226f9cb9fd25b9626fb05b4c0f3fddac521844a13e1f30beb8f14e90bd409a9ac43c812c5946d714d6e0dee12d5d02dfc1c562c5aacfa1f49b606 languageName: node linkType: hard "typescript@patch:typescript@^3.9.10#~builtin, typescript@patch:typescript@^3.9.5#~builtin, typescript@patch:typescript@^3.9.7#~builtin": version: 3.9.10 - resolution: "typescript@patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=3bd3d3" + resolution: "typescript@patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=7ad353" bin: tsc: bin/tsc tsserver: bin/tsserver From 791e86d759aa018376ac15be4960dcd02fb008ff Mon Sep 17 00:00:00 2001 From: ikprk Date: Thu, 27 Jun 2024 12:54:12 +0200 Subject: [PATCH 22/22] Quick fix for the validation --- packages/atlas/src/hooks/useYppModalHandlers.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/atlas/src/hooks/useYppModalHandlers.ts b/packages/atlas/src/hooks/useYppModalHandlers.ts index b51cc1292e..5f592d2b0b 100644 --- a/packages/atlas/src/hooks/useYppModalHandlers.ts +++ b/packages/atlas/src/hooks/useYppModalHandlers.ts @@ -94,7 +94,9 @@ export const useYppModalHandlers = () => { displaySnackbar({ iconType: 'error', title: 'Ops, something went wrong', - description: e.response.data.message ?? `We couldn't verify your ownership. Please, try again.`, + description: + JSON.stringify(e.response.data.message.map((err: { message: string }) => err.message).join(', \n')) ?? + `We couldn't verify your ownership. Please, try again.`, }) SentryLogger.error('Error validating YT ownership', 'useYppSetupModalHandlers', e) throw e