diff --git a/web/.env.ci b/web/.env.ci index 21a4386aa..24265b79e 100644 --- a/web/.env.ci +++ b/web/.env.ci @@ -6,4 +6,5 @@ REACT_APP_IPFS = https://ipfs.network.gosh.sh REACT_APP_GOSHAI_PROFILE = REACT_APP_GOSHAI_NAME = REACT_APP_ISDOCKEREXT = false -REACT_APP_MAINTENANCE = 0 # 0 - none | 1 - alert | 2 - full \ No newline at end of file +REACT_APP_MAINTENANCE = 0 # 0 - none | 1 - alert | 2 - full +REACT_APP_DAO_PARTNER = [] \ No newline at end of file diff --git a/web/.env.development b/web/.env.development index 102927d79..42f79cca6 100644 --- a/web/.env.development +++ b/web/.env.development @@ -1,9 +1,10 @@ PUBLIC_URL=/ REACT_APP_GOSH_NETWORK = http://localhost -REACT_APP_GOSH_ROOTADDR = 0:38f66aaea340f6022c2c007a6748db6f6e4695ed887b9fb428ddae735409f850 -REACT_APP_GOSH = {"6.0.0": "0:3947ce84d7b11e5e5dc2f2089a6e33486727b1baf7772918b7126710fee25b20", "6.1.0": "0:fa4201a1fcaa671f5acae9e34f6f180b2dc3ee41ee326502a24c9ed491fbf35a"} +REACT_APP_GOSH_ROOTADDR = 0:8e4ac7d5eb19dd608ae9b8c82773bb171dd756186ae938356229db5ae0ca335d +REACT_APP_GOSH = {"6.1.0": "0:468a6f03824c527038baca8d0bce32e37875d5c9e2c4da20e5f2ad401b447135"} REACT_APP_IPFS = https://ipfs.network.gosh.sh REACT_APP_GOSHAI_PROFILE = 0:3b3484aa0585b43397e864d74cf20df53e20411201573344910a155aff61e507 REACT_APP_GOSHAI_NAME = gosh-ai REACT_APP_ISDOCKEREXT = false REACT_APP_MAINTENANCE = 0 # 0 - none | 1 - alert | 2 - full +REACT_APP_DAO_PARTNER = [] diff --git a/web/.env.docker.development b/web/.env.docker.development index 8ba5903e0..bcc032511 100644 --- a/web/.env.docker.development +++ b/web/.env.docker.development @@ -7,3 +7,4 @@ REACT_APP_GOSHAI_PROFILE = 0:e8db351957df2342f9196fde67cda7719326e3da04d93d23f8c REACT_APP_GOSHAI_NAME = gosh-ai REACT_APP_ISDOCKEREXT = true REACT_APP_MAINTENANCE = 0 # 0 - none | 1 - alert | 2 - full +REACT_APP_DAO_PARTNER = [] diff --git a/web/.env.docker.production b/web/.env.docker.production index bffdcffee..244e98157 100644 --- a/web/.env.docker.production +++ b/web/.env.docker.production @@ -7,3 +7,4 @@ REACT_APP_GOSHAI_PROFILE = 0:7de5c13a1ab0f38232f94f4ba2e520bca9dcc30eb5134e98559 REACT_APP_GOSHAI_NAME = gosh-ai-bot REACT_APP_ISDOCKEREXT = true REACT_APP_MAINTENANCE = 0 # 0 - none | 1 - alert | 2 - full +REACT_APP_DAO_PARTNER = [] diff --git a/web/.env.production b/web/.env.production index 12708d78c..76fb0ca0c 100644 --- a/web/.env.production +++ b/web/.env.production @@ -7,3 +7,4 @@ REACT_APP_GOSHAI_PROFILE = 0:7de5c13a1ab0f38232f94f4ba2e520bca9dcc30eb5134e98559 REACT_APP_GOSHAI_NAME = gosh-ai-bot REACT_APP_ISDOCKEREXT = false REACT_APP_MAINTENANCE = 0 # 0 - none | 1 - alert | 2 - full +REACT_APP_DAO_PARTNER = [] diff --git a/web/public/images/github.webp b/web/public/images/github.webp new file mode 100644 index 000000000..32c6b4ba3 Binary files /dev/null and b/web/public/images/github.webp differ diff --git a/web/src/appconfig.ts b/web/src/appconfig.ts index 1de641133..51ba2afa5 100644 --- a/web/src/appconfig.ts +++ b/web/src/appconfig.ts @@ -69,6 +69,19 @@ export class AppConfig { AppConfig._setupReactGosh() } + static getVersions(options: { reverse?: boolean } = {}) { + const { reverse } = options + + let active = Object.keys(AppConfig.versions).filter((v) => { + return DISABLED_VERSIONS.indexOf(v) < 0 + }) + if (reverse) { + active = active.reverse() + } + + return Object.fromEntries(active.map((v) => [v, AppConfig.versions[v]])) + } + static getLatestVersion() { return Object.keys(AppConfig.versions) .reverse() diff --git a/web/src/constants.ts b/web/src/constants.ts index 95551a879..d8bd1dd01 100644 --- a/web/src/constants.ts +++ b/web/src/constants.ts @@ -8,6 +8,10 @@ export const DAO_TOKEN_TRANSFER_TAG = '___!daotokentransfer!___' export const VESTING_BALANCE_TAG = '___!vestingbalance!___' export const DISABLED_VERSIONS = ['5.0.0', '6.0.0'] +export const PARTNER_DAO_NAMES: string[] = JSON.parse( + import.meta.env.REACT_APP_DAO_PARTNER || '[]', +) + export const DaoEventType: { [key: number]: string } = { 1: 'Pull request', 2: 'Add branch protection', diff --git a/web/src/pages/Home/Home.tsx b/web/src/pages/Home/Home.tsx index 50d3455b7..bcee390d8 100644 --- a/web/src/pages/Home/Home.tsx +++ b/web/src/pages/Home/Home.tsx @@ -1,48 +1,39 @@ -import { Link } from 'react-router-dom' import { useRecoilValue } from 'recoil' -import { userPersistAtom } from 'react-gosh' +import { ButtonLink } from '../../components/Form' +import { userPersistAtom } from '../../store/user.state' +import { Navigate } from 'react-router-dom' const HomePage = () => { - const userStatePersist = useRecoilValue(userPersistAtom) + const user = useRecoilValue(userPersistAtom) + + if (user.phrase) { + return + } return ( -
-
-

- Git Open Source Hodler -

-
-

GOSH secures delivery and decentralization of your code.

-

- The first development platform blockchain, purpose-built for - securing the software supply chain and extracting the value locked - in your projects. -

-
-
- {userStatePersist.phrase ? ( - - Organizations - - ) : ( - <> - - Sign in - - - Create account - - - )} +
+
+
+

+ Git Open +
+ Source Hodler +

+ +
+

GOSH secures delivery and decentralization of your code.

+

+ The first development platform blockchain, purpose-built for + securing the software supply chain and extracting the value + locked in your projects. +

+
+ +
+ + Create account + +
diff --git a/web/src/pages/OnboardingAi/OnboardingAi.tsx b/web/src/pages/OnboardingAi/OnboardingAi.tsx deleted file mode 100644 index b910754b2..000000000 --- a/web/src/pages/OnboardingAi/OnboardingAi.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useState } from 'react' -import GoshPhrase from './components/GoshPhrase' -import { useUser } from 'react-gosh' -import GoshUsername from './components/GoshUsername' -import { Navigate } from 'react-router-dom' -import GoshPhraseCheck from './components/GoshPhraseCheck' - -const OnboardingAiPage = () => { - const { persist } = useUser() - const [step, setStep] = useState<'phrase' | 'phrase-check' | 'username'>('phrase') - const [signupState, setSignupState] = useState<{ - phrase: string[] - username: string - }>({ phrase: [], username: '' }) - - if (persist.pin && persist.username) { - return - } - return ( -
- {step === 'phrase' && ( - - )} - {step === 'phrase-check' && ( - - )} - {step === 'username' && ( - - )} -
- ) -} - -export default OnboardingAiPage diff --git a/web/src/pages/OnboardingAi/components/GoshPhrase.tsx b/web/src/pages/OnboardingAi/components/GoshPhrase.tsx deleted file mode 100644 index 91f5ec28c..000000000 --- a/web/src/pages/OnboardingAi/components/GoshPhrase.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Field } from 'formik' -import { useCallback, useEffect } from 'react' -import { AppConfig, EGoshError, GoshError } from 'react-gosh' -import { toast } from 'react-toastify' -import { ToastError } from '../../../components/Toast' -import { FormikCheckbox } from '../../../components/Formik' -import { Link } from 'react-router-dom' -import PhraseForm from '../../../components/PhraseForm' -import yup from '../../../yup-extended' - -type TGoshPhraseProps = { - signupState: { - phrase: string[] - username: string - } - setSignupState: React.Dispatch< - React.SetStateAction<{ - phrase: string[] - username: string - }> - > - setStep: React.Dispatch> -} - -const GoshPhrase = (props: TGoshPhraseProps) => { - const { signupState, setSignupState, setStep } = props - - const setRandomPhrase = useCallback(async () => { - const result = await AppConfig.goshclient.crypto.mnemonic_from_random({}) - setSignupState((state) => ({ ...state, phrase: result.phrase.split(' ') })) - }, [setSignupState]) - - const onFormSubmit = async (values: { words: string[] }) => { - try { - const { words } = values - const { valid } = await AppConfig.goshclient.crypto.mnemonic_verify({ - phrase: words.join(' '), - }) - if (!valid) { - throw new GoshError(EGoshError.PHRASE_INVALID) - } - setSignupState((state) => ({ ...state, phrase: words })) - setStep('phrase-check') - } catch (e: any) { - console.error(e.message) - toast.error() - } - } - - useEffect(() => { - if (!signupState.phrase.length) { - setRandomPhrase() - } - }, [signupState.phrase, setRandomPhrase]) - - return ( -
-
-

Save the phrase

-
- The secret phrase is a crucial element for the security of your - account -
-
- Already have an account on Gosh?{' '} - - Log in - -
-
- -
-
- { - setSignupState((state) => ({ ...state, phrase: words })) - }} - > -
- -
-
-
-
-
- ) -} - -export default GoshPhrase diff --git a/web/src/pages/OnboardingAi/components/GoshPhraseCheck.tsx b/web/src/pages/OnboardingAi/components/GoshPhraseCheck.tsx deleted file mode 100644 index 924eedf59..000000000 --- a/web/src/pages/OnboardingAi/components/GoshPhraseCheck.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useEffect, useState } from 'react' -import { toast } from 'react-toastify' - -import PreviousStep from './PreviousStep' -import { ToastError } from '../../../components/Toast' -import PhraseForm from '../../../components/PhraseForm' -import { GoshError } from 'react-gosh' -import { Link } from 'react-router-dom' - -const generateRandomWordNumbers = () => { - const min = 0 - const max = 11 - const numbers: number[] = [] - while (true) { - const num = Math.floor(Math.random() * (max - min + 1)) + min - if (numbers.indexOf(num) < 0) { - numbers.push(num) - } - if (numbers.length === 3) { - break - } - } - return numbers.sort((a, b) => a - b) -} - -type TGoshPhraseCheckProps = { - signupState: { - phrase: string[] - username: string - } - setStep: React.Dispatch> -} - -const GoshPhraseCheck = (props: TGoshPhraseCheckProps) => { - const { signupState, setStep } = props - const [rndNumbers, setRndNumbers] = useState([]) - - const onBackClick = () => { - setStep('phrase') - } - - const onFormSubmit = async (values: { words: string[] }) => { - try { - const { words } = values - const validated = rndNumbers.map((n, index) => { - return words[index] === signupState.phrase[n] - }) - if (!validated.every((v) => !!v)) { - throw new GoshError('Words check failed') - } - setStep('username') - } catch (e: any) { - console.error(e.message) - toast.error() - } - } - - useEffect(() => { - setRndNumbers(generateRandomWordNumbers()) - }, []) - - return ( -
-
-
- -
-

Verify the secret

-
Enter required words
-
- Already have an account on Gosh?{' '} - - Log in - -
-
- -
-
-

- Input words{' '} - - {rndNumbers.map((n) => n + 1).join(' - ')} - {' '} - of your phrase -

- -
-
-
- ) -} - -export default GoshPhraseCheck diff --git a/web/src/pages/OnboardingAi/components/GoshUsername.tsx b/web/src/pages/OnboardingAi/components/GoshUsername.tsx deleted file mode 100644 index b1bb56bc5..000000000 --- a/web/src/pages/OnboardingAi/components/GoshUsername.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { Field, Form, Formik } from 'formik' -import { GoshAdapterFactory, GoshError, useDaoCreate, useUser } from 'react-gosh' -import { useSetRecoilState } from 'recoil' -import yup from '../../../yup-extended' -import { PinCodeModal } from '../../../components/Modal' -import { SignupProgress } from '../../Signup/components/SignupProgress' -import { appModalStateAtom } from '../../../store/app.state' -import { toast } from 'react-toastify' -import { ToastError } from '../../../components/Toast' -import PreviousStep from './PreviousStep' -import { FormikInput } from '../../../components/Formik' -import { Button } from '../../../components/Form' -import Alert from '../../../components/Alert' -import DaoCreateProgress from '../../DaoCreate/2.0.0/DaoCreateProgress' -import { useNavigate } from 'react-router-dom' - -type TGoshUsernameProps = { - signupState: { - phrase: string[] - username: string - } - setSignupState: React.Dispatch< - React.SetStateAction<{ - phrase: string[] - username: string - }> - > - setStep: React.Dispatch> -} - -const GoshUsername = (props: TGoshUsernameProps) => { - const { signupState, setSignupState, setStep } = props - const setModal = useSetRecoilState(appModalStateAtom) - const { signup, signupProgress } = useUser() - const { create: createDao, progress: createDaoProgress } = useDaoCreate() - const navigate = useNavigate() - - const onBackClick = () => { - setStep('phrase') - } - - const onFormSubmit = async (values: { username: string }) => { - try { - // Prepare data - const username = values.username.trim().toLowerCase() - const seed = signupState.phrase.join(' ') - const daoname = `${username}-ai` - const gosh = GoshAdapterFactory.createLatest() - - // Check if DAO exists - const _dao = await gosh.getDao({ name: daoname, useAuth: false }) - if (await _dao.isDeployed()) { - throw new GoshError('DAO create error', 'DAO already exists') - } - - // Deploy GOSH account - const auth = await signup({ phrase: seed, username }) - setSignupState((state) => ({ ...state, username })) - - // Create DAO - const dao = await createDao(daoname, { tags: ['GoshAI'], auth }) - const aiUsername = import.meta.env.REACT_APP_GOSHAI_NAME - // TODO: Add gosh-ai bot as dao member - // await dao.createMember({ - // alone: true, - // members: [ - // { - // user: { name: aiUsername, type: 'user' }, - // allowance: 0, - // comment: '', - // expired: 0, - // }, - // ], - // }) - - // Create PIN-code - setModal({ - static: true, - isOpen: true, - element: navigate('/ai')} />, - }) - } catch (e: any) { - console.error(e.message) - toast.error() - } - } - - return ( - <> -
-
-
- -
-

Choose a short nickname

-
- It will be visible to all gosh users -
-
-
-
- - {({ isSubmitting, setFieldValue }) => ( -
-
- - setFieldValue( - 'username', - e.target.value.toLowerCase(), - ) - } - disabled={isSubmitting} - /> -
- - - This is your unique cryptographic identifier in - Gosh. -
- Please note that after creating your username it - will be impossible to change it in the future -
- -
- -
-
- )} -
- - - -
-
-
- - ) -} - -export default GoshUsername diff --git a/web/src/pages/OnboardingAi/components/PreviousStep.tsx b/web/src/pages/OnboardingAi/components/PreviousStep.tsx deleted file mode 100644 index 6fe84c427..000000000 --- a/web/src/pages/OnboardingAi/components/PreviousStep.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { faArrowLeft } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { classNames } from 'react-gosh' - -type TPreviousStepProps = { - onClick(): void | Promise -} - -const PreviousStep = (props: TPreviousStepProps) => { - const { onClick } = props - - return ( -
- -
- ) -} - -export default PreviousStep diff --git a/web/src/pages/OnboardingAi/index.ts b/web/src/pages/OnboardingAi/index.ts deleted file mode 100644 index 446c693be..000000000 --- a/web/src/pages/OnboardingAi/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './OnboardingAi' -export { default } from './OnboardingAi' diff --git a/web/src/supabase.ts b/web/src/supabase.ts index 3998e02ed..ee1e91bdf 100644 --- a/web/src/supabase.ts +++ b/web/src/supabase.ts @@ -4,8 +4,9 @@ import { GoshError } from './errors' export const supabase = { client: AppConfig.supabase, - singinOAuth: async (provider: Provider) => { + singinOAuth: async (provider: Provider, options?: { redirectTo?: string }) => { const scopes = 'read:user read:org' + const { redirectTo } = options || {} if (AppConfig.dockerclient) { const nounce = Date.now() @@ -13,7 +14,9 @@ export const supabase = { const { data, error } = await AppConfig.supabase.auth.signInWithOAuth({ provider, options: { - redirectTo: `https://open.docker.com/dashboard/extension-tab?extensionId=teamgosh/docker-extension&nounce=${nounce}`, + redirectTo: + redirectTo || + `https://open.docker.com/dashboard/extension-tab?extensionId=teamgosh/docker-extension&nounce=${nounce}`, scopes, skipBrowserRedirect: true, }, @@ -29,7 +32,7 @@ export const supabase = { const { error } = await AppConfig.supabase.auth.signInWithOAuth({ provider, options: { - redirectTo: document.location.href, + redirectTo: redirectTo || document.location.href, scopes, }, }) diff --git a/web/src/v1.0.0/blockchain/systemcontract.ts b/web/src/v1.0.0/blockchain/systemcontract.ts index 6ba5b01f0..cd93171a0 100644 --- a/web/src/v1.0.0/blockchain/systemcontract.ts +++ b/web/src/v1.0.0/blockchain/systemcontract.ts @@ -7,6 +7,7 @@ import { GoshRepository } from './repository' import { AppConfig } from '../../appconfig' import { VersionController } from '../../blockchain/versioncontroller' import { whileFinite } from '../../utils' +import { DaoProfile } from '../../blockchain/daoprofile' export class SystemContract extends BaseContract { versionController: VersionController @@ -16,6 +17,13 @@ export class SystemContract extends BaseContract { this.versionController = AppConfig.goshroot } + async getDaoProfile(name: string) { + const { value0 } = await this.runLocal('getProfileDaoAddr', { name }, undefined, { + useCachedBoc: true, + }) + return new DaoProfile(this.account.client, value0) + } + async getDao(params: { name?: string; address?: string }) { const { name, address } = params diff --git a/web/src/v1.0.0/validators.ts b/web/src/v1.0.0/validators.ts index 0ed7f76fe..9109a7105 100644 --- a/web/src/v1.0.0/validators.ts +++ b/web/src/v1.0.0/validators.ts @@ -116,7 +116,7 @@ export const validateOnboardingDao = async (name: string): Promise daonames)) await member.wallet!.createDaoMember({ members, daonames, comment }) - } else { + } else if (profiles.length > 0) { const memberAddCells = profiles.map(({ profile, daonames }) => ({ type: EDaoEventType.DAO_MEMBER_ADD, params: { members: [{ profile, allowance: 0 }], daonames }, diff --git a/web/src/v3.0.0/validators.ts b/web/src/v3.0.0/validators.ts index 0ed7f76fe..9109a7105 100644 --- a/web/src/v3.0.0/validators.ts +++ b/web/src/v3.0.0/validators.ts @@ -116,7 +116,7 @@ export const validateOnboardingDao = async (name: string): Promise daonames)) await member.wallet!.createDaoMember({ members, daonames, comment }) - } else { + } else if (profiles.length > 0) { const memberAddCells = profiles.map(({ profile, daonames }) => ({ type: EDaoEventType.DAO_MEMBER_ADD, params: { members: [{ profile, allowance: 0 }], daonames }, diff --git a/web/src/v4.0.0/validators.ts b/web/src/v4.0.0/validators.ts index 0ed7f76fe..9109a7105 100644 --- a/web/src/v4.0.0/validators.ts +++ b/web/src/v4.0.0/validators.ts @@ -116,7 +116,7 @@ export const validateOnboardingDao = async (name: string): Promise daonames)) await member.wallet!.createDaoMember({ members, daonames, comment }) - } else { + } else if (profiles.length > 0) { const memberAddCells = profiles.map(({ profile, daonames }) => ({ type: EDaoEventType.DAO_MEMBER_ADD, params: { diff --git a/web/src/v5.0.0/validators.ts b/web/src/v5.0.0/validators.ts index 0ed7f76fe..9109a7105 100644 --- a/web/src/v5.0.0/validators.ts +++ b/web/src/v5.0.0/validators.ts @@ -116,7 +116,7 @@ export const validateOnboardingDao = async (name: string): Promise daonames)) await member.wallet!.createDaoMember({ members, daonames, comment }) - } else { + } else if (profiles.length > 0) { const memberAddCells = profiles.map(({ profile, daonames }) => ({ type: EDaoEventType.DAO_MEMBER_ADD, params: { diff --git a/web/src/v5.1.0/validators.ts b/web/src/v5.1.0/validators.ts index 0ed7f76fe..9109a7105 100644 --- a/web/src/v5.1.0/validators.ts +++ b/web/src/v5.1.0/validators.ts @@ -116,7 +116,7 @@ export const validateOnboardingDao = async (name: string): Promise daonames)) await member.wallet!.createDaoMember({ members, daonames, comment }) - } else { + } else if (profiles.length > 0) { const memberAddCells = profiles.map(({ profile, daonames }) => ({ type: EDaoEventType.DAO_MEMBER_ADD, params: { diff --git a/web/src/v6.0.0/validators.ts b/web/src/v6.0.0/validators.ts index 0ed7f76fe..9109a7105 100644 --- a/web/src/v6.0.0/validators.ts +++ b/web/src/v6.0.0/validators.ts @@ -116,7 +116,7 @@ export const validateOnboardingDao = async (name: string): Promise {
- } /> + } /> } /> } /> diff --git a/web/src/v6.1.0/blockchain/systemcontract.ts b/web/src/v6.1.0/blockchain/systemcontract.ts index 7f48d2d3d..c33459280 100644 --- a/web/src/v6.1.0/blockchain/systemcontract.ts +++ b/web/src/v6.1.0/blockchain/systemcontract.ts @@ -6,13 +6,11 @@ import { Dao } from './dao' import { GoshRepository } from './repository' import { AppConfig } from '../../appconfig' import { VersionController } from '../../blockchain/versioncontroller' -import { executeByChunk, whileFinite } from '../../utils' +import { whileFinite } from '../../utils' import { GoshTag } from './goshtag' import { Task } from './task' -import { contextVersion } from '../constants' -import { getAllAccounts } from '../../blockchain/utils' -import { MAX_PARALLEL_READ } from '../../constants' import { GoshCommitTag } from './committag' +import { DaoProfile } from '../../blockchain/daoprofile' export class SystemContract extends BaseContract { versionController: VersionController @@ -51,6 +49,13 @@ export class SystemContract extends BaseContract { return new GoshCommitTag(this.client, value0) } + async getDaoProfile(name: string) { + const { value0 } = await this.runLocal('getProfileDaoAddr', { name }, undefined, { + useCachedBoc: true, + }) + return new DaoProfile(this.account.client, value0) + } + async getDao(params: { name?: string; address?: string }) { const { name, address } = params diff --git a/web/src/v6.1.0/components/Header/Header.tsx b/web/src/v6.1.0/components/Header/Header.tsx index acc8a8e49..668a4449f 100644 --- a/web/src/v6.1.0/components/Header/Header.tsx +++ b/web/src/v6.1.0/components/Header/Header.tsx @@ -19,6 +19,9 @@ const Header = () => { const location = useLocation() const setModal = useSetRecoilState(appModalStateAtom) + const isSignin = location.pathname.search('/signin') >= 0 + const isSignup = location.pathname.search('/signup') >= 0 + return (
{ {!user.persist.phrase && - location.pathname.search(/signin|signup/) < 0 && ( + ((!isSignin && !isSignup) || isSignup) && ( Sign in )} - {location.pathname.search('/signin') >= 0 && ( - Sign up - )} - {location.pathname.search('/signup') >= 0 && ( - Sign in + {!user.persist.phrase && isSignin && ( + Sign up )} {/* Mobile menu button. Simple dropdown menu is used for now */} diff --git a/web/src/v6.1.0/hooks/dao.hooks.ts b/web/src/v6.1.0/hooks/dao.hooks.ts index 75b213f23..67753617f 100644 --- a/web/src/v6.1.0/hooks/dao.hooks.ts +++ b/web/src/v6.1.0/hooks/dao.hooks.ts @@ -18,6 +18,7 @@ import { DISABLED_VERSIONS, MAX_PARALLEL_READ, MAX_PARALLEL_WRITE, + PARTNER_DAO_NAMES, SYSTEM_TAG, } from '../../constants' import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' @@ -30,10 +31,12 @@ import { daoMemberSelector, daoTaskListSelector, daoTaskSelector, + partnerDaoListAtom, userDaoListAtom, } from '../store/dao.state' import { EDaoMemberType, + EDaoInviteStatus, ETaskReward, TDaoDetailsMemberItem, TDaoEventDetails, @@ -54,12 +57,65 @@ import { GoshAdapterFactory } from 'react-gosh' import { TSystemContract } from '../../types/blockchain.types' import { TGoshCommitTag } from '../types/repository.types' import { GoshRepository } from '../blockchain/repository' -import { EDaoInviteStatus } from '../types/onboarding.types' import { Task } from '../blockchain/task' import { AggregationFn } from '@eversdk/core' import { SystemContract } from '../blockchain/systemcontract' import { appContextAtom, appToastStatusSelector } from '../../store/app.state' +export function usePartnerDaoList(params: { initialize?: boolean } = {}) { + const { initialize } = params + const [data, setData] = useRecoilState(partnerDaoListAtom) + + const getDaoList = useCallback(async () => { + try { + setData((state) => ({ ...state, isFetching: true })) + + const items: TDaoListItem[] = [] + for (const ver of Object.keys(AppConfig.getVersions({ reverse: true }))) { + const sc = AppConfig.goshroot.getSystemContract(ver) + await Promise.all( + PARTNER_DAO_NAMES.map(async (name) => { + const account = (await sc.getDao({ name })) as Dao + if (await account.isDeployed()) { + const members = await account.getMembers({}) + items.push({ + account, + name, + address: account.address, + version: ver, + supply: _.sum(members.map(({ allowance }) => allowance)), + members: members.length, + }) + } + }), + ) + + if (items.length === PARTNER_DAO_NAMES.length) { + break + } + } + + setData((state) => ({ ...state, items })) + } catch (e: any) { + setData((state) => ({ ...state, error: e })) + } finally { + setData((state) => ({ ...state, isFetching: false })) + } + }, []) + + useEffect(() => { + if (initialize && PARTNER_DAO_NAMES.length) { + getDaoList() + } + }, [initialize, getDaoList]) + + return { + ...data, + items: [...data.items].sort((a, b) => (a.name > b.name ? 1 : -1)), + isEmpty: !data.isFetching && !data.items.length, + } +} + export function useCreateDao() { const profile = useProfile() const { user } = useUser() @@ -221,6 +277,7 @@ export function useUserDaoList(params: { count?: number; initialize?: boolean } .from('users') .select(`*, github (updated_at, gosh_url)`) .eq('gosh_username', username) + .not('auth_user', 'is', null) if (error) { throw new GoshError('Get onboarding data', error.message) } @@ -229,8 +286,7 @@ export function useUserDaoList(params: { count?: number; initialize?: boolean } } const imported: { [name: string]: string[] } = {} - const row = data[0] - for (const item of row.github) { + for (const item of data[0].github) { if (item.updated_at) { continue } @@ -1541,7 +1597,7 @@ export function useCreateDaoMember() { }) const daonames = _.flatten(profiles.map(({ daonames }) => daonames)) await member.wallet!.createDaoMember({ members, daonames, comment }) - } else { + } else if (profiles.length > 0) { const memberAddCells = profiles.map(({ profile, daonames }) => ({ type: EDaoEventType.DAO_MEMBER_ADD, params: { diff --git a/web/src/v6.1.0/hooks/oauth.hooks.ts b/web/src/v6.1.0/hooks/oauth.hooks.ts index 3ebf45078..236ec1466 100644 --- a/web/src/v6.1.0/hooks/oauth.hooks.ts +++ b/web/src/v6.1.0/hooks/oauth.hooks.ts @@ -3,15 +3,16 @@ import { useRecoilState, useResetRecoilState } from 'recoil' import { OAuthSessionAtom } from '../store/oauth.state' import { Provider } from '@supabase/supabase-js' import { supabase } from '../../supabase' -import { useEffect } from 'react' +import { useCallback, useEffect } from 'react' -export function useOauth() { +export function useOauth(options?: { initialize?: boolean }) { + const { initialize } = options || {} const location = useLocation() const [oauth, setOAuth] = useRecoilState(OAuthSessionAtom) const resetOAuth = useResetRecoilState(OAuthSessionAtom) - const signin = async (provider: Provider) => { - await supabase.singinOAuth(provider) + const signin = async (provider: Provider, options?: { redirectTo?: string }) => { + await supabase.singinOAuth(provider, options) } const signout = async () => { @@ -19,15 +20,17 @@ export function useOauth() { resetOAuth() } + const getOAuthSession = useCallback(async () => { + setOAuth({ session: null, isLoading: true }) + const { data } = await supabase.client.auth.getSession() + setOAuth({ session: data.session, isLoading: false }) + }, []) + useEffect(() => { - const _getOAuthSession = async () => { - setOAuth({ session: null, isLoading: true }) - const { data } = await supabase.client.auth.getSession() - setOAuth({ session: data.session, isLoading: false }) + if (initialize) { + getOAuthSession() } - - _getOAuthSession() - }, [setOAuth]) + }, [initialize, getOAuthSession]) useEffect(() => { const params = new URLSearchParams(location.search) diff --git a/web/src/v6.1.0/hooks/onboarding.hooks.ts b/web/src/v6.1.0/hooks/onboarding.hooks.ts index ede871c25..90bd226ab 100644 --- a/web/src/v6.1.0/hooks/onboarding.hooks.ts +++ b/web/src/v6.1.0/hooks/onboarding.hooks.ts @@ -1,11 +1,12 @@ +import { useCallback, useEffect } from 'react' import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState, } from 'recoil' +import _ from 'lodash' import { - daoInvitesSelector, octokitSelector, onboardingDataAtom, onboardingStatusDataAtom, @@ -14,11 +15,8 @@ import { repositoriesSelector, } from '../store/onboarding.state' import { supabase } from '../../supabase' -import { useCallback, useEffect } from 'react' import { TOAuthSession } from '../types/oauth.types' import { - EDaoInviteStatus, - TOnboardingInvite, TOnboardingOrganization, TOnboardingRepository, TOnboardingStatusDao, @@ -27,16 +25,22 @@ import { GoshError } from '../../errors' import { AppConfig } from '../../appconfig' import { useUser } from './user.hooks' import { validateOnboardingDao, validateOnboardingRepo } from '../validators' -import { debounce } from 'lodash' import { useOauth } from './oauth.hooks' -import _ from 'lodash' import { appToastStatusSelector } from '../../store/app.state' -export function useOnboardingData(oauth?: TOAuthSession) { +export function useOnboardingData( + oauth?: TOAuthSession, + options?: { initialize?: boolean }, +) { + const { initialize } = options || {} + const [status, setStatus] = useRecoilState( + appToastStatusSelector('__onboardingupload'), + ) + const user = useUser() + const { signout } = useOauth() const [data, setData] = useRecoilState(onboardingDataAtom) const resetData = useResetRecoilState(onboardingDataAtom) const [organizations, setOrganizations] = useRecoilState(organizationsSelector) - const [invites, setInvites] = useRecoilState(daoInvitesSelector) const repositoriesChecked = useRecoilValue(repositoriesCheckedSelector) const octokit = useRecoilValue(octokitSelector) @@ -95,78 +99,170 @@ export function useOnboardingData(oauth?: TOAuthSession) { })) } - const getDaoInvites = async () => { - if (!oauth?.session) { - return + const upload = async (params: { email: string }) => { + const { email } = params + + try { + if (!user.persist.username || !oauth?.session?.user.id) { + throw new GoshError('Value error', 'User undefined') + } + + // Create DB record for current username + let dbUser = await _getDbUser(user.persist.username) + if (!dbUser) { + throw new GoshError( + 'Value error', + 'User is not found in onboarding database', + ) + } + + // Create one more record of db user because of RLS + // TODO: Replace with update when backend service appears + dbUser = await _createDbUser({ + username: dbUser.gosh_username, + pubkey: dbUser.gosh_pubkey, + authid: oauth?.session?.user.id, + email: dbUser.email, + emailextra: email, + }) + + // Save auto clone repositories + const goshAddress = AppConfig.versions[AppConfig.getLatestVersion()] + const goshProtocol = `gosh://${goshAddress}` + for (const item of repositoriesChecked) { + const { daoname, name } = item + await _createDbGithubRecord({ + user_id: dbUser.id, + github_url: `/${daoname}/${name}`, + gosh_url: `${goshProtocol}/${daoname.toLowerCase()}/${name.toLowerCase()}`, + }) + } + + // Validate onboarding data + const validated = await Promise.all( + repositoriesChecked.map(async (item) => { + const daoname = item.daoname.toLowerCase() + const reponame = item.name.toLowerCase() + const daoValidation = await validateOnboardingDao(daoname) + if (!daoValidation.valid) { + return false + } + const repoValidation = await validateOnboardingRepo(daoname, reponame) + if (!repoValidation.valid) { + return false + } + return true + }), + ) + + setData((state) => ({ + ...state, + emailOther: email || oauth?.session?.user.email || '', + step: 'complete', + })) + const valid = validated.every((r) => !!r) + if (valid) { + setData((state) => ({ ...state, redirectTo: '/a/orgs' })) + await signout() + } else { + setData((state) => ({ ...state, redirectTo: '/onboarding/status' })) + } + } catch (e: any) { + setStatus((state) => ({ ...state, type: 'error', data: e })) + throw e } + } - setInvites((state) => ({ ...state, isFetching: true })) + const updateData = (data?: object) => { + setData((state) => ({ ...state, ...data })) + } + + const _getDbUser = async (username: string) => { const { data, error } = await supabase.client - .from('dao_invite') - .select('id, dao_name') - .eq('recipient_email', oauth.session.user.email) - .is('recipient_status', null) + .from('users') + .select() + .eq('gosh_username', username) + .single() + if (error?.code === 'PGRST116') { + return null + } if (error) { - setInvites({ isFetching: false, items: [] }) - throw new GoshError('Get DAO invites', error.message) + throw new GoshError(error.message) } - setInvites((state) => ({ - ...state, - items: data.map((item) => { - const found = state.items.find((i) => i.id === item.id) - if (found) { - return found - } - return { id: item.id, daoname: item.dao_name, accepted: null } - }), - isFetching: false, - })) - - return !!data.length + return data } - const toggleDaoInvite = (status: boolean, item: TOnboardingInvite) => { - setInvites((state) => ({ - ...state, - items: state.items.map((i) => { - if (i.id !== item.id) { - return i - } - return { ...item, accepted: status } - }), - })) + const _createDbUser = async (params: { + username: string + pubkey: string + authid: string + email: string | null + emailextra: string | null + }) => { + const { username, pubkey, authid, email, emailextra } = params + const { data, error } = await supabase.client + .from('users') + .insert({ + gosh_username: username, + gosh_pubkey: `0x${pubkey}`, + auth_user: authid, + email, + email_other: emailextra, + }) + .select() + .single() + if (error) { + throw new GoshError(error.message) + } + return data } - const updateData = (data?: object) => { - setData((state) => ({ ...state, ...data })) + const _createDbGithubRecord = async (item: { + user_id: string + github_url: string + gosh_url: string + }) => { + const { user_id, github_url, gosh_url } = item + const { count, error } = await supabase.client + .from('github') + .select('*', { count: 'exact' }) + .eq('user_id', user_id) + .eq('github_url', github_url) + .eq('gosh_url', gosh_url) + if (error) { + throw new Error(error.message) + } + + if (!count) { + const { error } = await supabase.client.from('github').insert(item) + if (error) { + throw new Error(error.message) + } + } } useEffect(() => { - if (oauth && !data.redirectTo) { - setData((state) => { - const { isLoading, session } = oauth - if (isLoading) { - return { ...state, step: undefined } - } - if (!session) { - return { ...state, step: 'signin' } - } - return { ...state, step: state.step || 'invites' } - }) + if (initialize && !data.redirectTo) { + if (oauth?.isLoading) { + setData((state) => ({ ...state, step: undefined })) + } else if (!oauth?.session) { + setData((state) => ({ ...state, step: 'signin' })) + } else { + setData((state) => ({ ...state, step: state.step || 'organizations' })) + } } - }, [oauth, data.redirectTo, setData]) + }, [initialize, oauth?.isLoading, data.redirectTo]) return { data, - invites, organizations, repositories: { selected: repositoriesChecked, }, getOrganizations, toggleOrganization, - getDaoInvites, - toggleDaoInvite, + upload, + uploadStatus: status, updateData, resetData, } @@ -275,175 +371,6 @@ export function useOnboardingRepositories(organization: TOnboardingOrganization) return { repositories, getRepositories, getNext, toggleRepository } } -export function useOnboardingSignup(oauth: TOAuthSession) { - const data = useRecoilValue(onboardingDataAtom) - const repositories = useRecoilValue(repositoriesCheckedSelector) - const invites = useRecoilValue(daoInvitesSelector) - const { signup: _signup } = useUser() - const [status, setStatus] = useRecoilState( - appToastStatusSelector('__onboardingsignup'), - ) - - const getDbUser = async (username: string) => { - const { data, error } = await supabase.client - .from('users') - .select() - .eq('gosh_username', username) - .single() - if (error?.code === 'PGRST116') return null - if (error) { - throw new GoshError(error.message) - } - return data - } - - const createDbUser = async ( - username: string, - pubkey: string, - authUserId: string, - email: string | null, - emailOther: string | null, - ) => { - const { data, error } = await supabase.client - .from('users') - .insert({ - gosh_username: username, - gosh_pubkey: `0x${pubkey}`, - auth_user: authUserId, - email, - email_other: emailOther, - }) - .select() - .single() - if (error) { - throw new GoshError(error.message) - } - return data - } - - const createDbGithubRecord = async (item: { - user_id: string - github_url: string - gosh_url: string - }) => { - const { user_id, github_url, gosh_url } = item - const { count, error } = await supabase.client - .from('github') - .select('*', { count: 'exact' }) - .eq('user_id', user_id) - .eq('github_url', github_url) - .eq('gosh_url', gosh_url) - if (error) { - throw new Error(error.message) - } - - if (!count) { - const { error } = await supabase.client.from('github').insert(item) - if (error) { - throw new Error(error.message) - } - } - } - - const signup = async (username: string) => { - try { - if (!oauth.session) { - throw new GoshError('OAuth session undefined') - } - - // Prepare data - setStatus((state) => ({ ...state, type: 'pending', data: 'Prepare data' })) - username = username.trim().toLowerCase() - const seed = data.phrase.join(' ') - const keypair = await AppConfig.goshclient.crypto.mnemonic_derive_sign_keys({ - phrase: seed, - }) - - // Deploy GOSH account - setStatus((state) => ({ - ...state, - type: 'pending', - data: 'Create GOSH account', - })) - await _signup({ phrase: seed, username }) - - // Get or create DB user - setStatus((state) => ({ - ...state, - type: 'pending', - data: 'Update onboarding DB', - })) - let dbUser = await getDbUser(username) - if (!dbUser) { - dbUser = await createDbUser( - username, - keypair.public, - oauth.session.user.id, - data.isEmailPublic ? oauth.session.user.email || null : null, - data.emailOther || null, - ) - } - - // Save auto clone repositories - const goshAddress = Object.values(AppConfig.versions).reverse()[0] - const goshProtocol = `gosh://${goshAddress}` - for (const item of repositories) { - const { daoname, name } = item - await createDbGithubRecord({ - user_id: dbUser.id, - github_url: `/${daoname}/${name}`, - gosh_url: `${goshProtocol}/${daoname.toLowerCase()}/${name.toLowerCase()}`, - }) - } - - // Update DAO invites status - for (const invite of invites.items) { - const { error } = await supabase.client - .from('dao_invite') - .update({ - recipient_username: username, - recipient_status: invite.accepted - ? EDaoInviteStatus.ACCEPTED - : EDaoInviteStatus.REJECTED, - token_expired: true, - }) - .eq('id', invite.id) - if (error) { - throw new GoshError(error.message) - } - } - - // Validate onboarding data - const validationResult = await Promise.all( - repositories.map(async (item) => { - const daoname = item.daoname.toLowerCase() - const reponame = item.name.toLowerCase() - - const daoValidation = await validateOnboardingDao(daoname) - if (!daoValidation.valid) { - return false - } - - const repoValidation = await validateOnboardingRepo(daoname, reponame) - if (!repoValidation.valid) { - return false - } - - return true - }), - ) - - setStatus((state) => ({ ...state, type: 'dismiss', data: null })) - return validationResult.every((r) => !!r) - } catch (e: any) { - setStatus((state) => ({ ...state, type: 'error', data: e })) - throw e - } - } - - return { signup, status } -} - export function useOnboardingStatus(oauth?: TOAuthSession) { const { signout: _signout } = useOauth() const [data, setData] = useRecoilState(onboardingStatusDataAtom) @@ -621,7 +548,7 @@ export function useOnboardingStatus(oauth?: TOAuthSession) { } } - const validateDaoNameDebounce = debounce(async (index: number, value: string) => { + const validateDaoNameDebounce = _.debounce(async (index: number, value: string) => { const validated = await validateOnboardingDao(value) setData((state) => ({ ...state, @@ -641,7 +568,7 @@ export function useOnboardingStatus(oauth?: TOAuthSession) { })) }, 500) - const validateRepoNameDebounce = debounce( + const validateRepoNameDebounce = _.debounce( async (id: string, dao: string, value: string) => { const validated = await validateOnboardingRepo(dao, value) setData((state) => ({ diff --git a/web/src/v6.1.0/hooks/user.hooks.ts b/web/src/v6.1.0/hooks/user.hooks.ts index 6f0c46a34..d5cf7b127 100644 --- a/web/src/v6.1.0/hooks/user.hooks.ts +++ b/web/src/v6.1.0/hooks/user.hooks.ts @@ -13,10 +13,15 @@ import { userAtom, userPersistAtom, userProfileSelector } from '../../store/user import { TUserPersist } from '../../types/user.types' import { validatePhrase } from '../../validators' import { EGoshError, GoshError } from '../../errors' -import { validateUsername } from '../validators' +import { validateOnboardingDao, validateUsername } from '../validators' import { getSystemContract } from '../blockchain/helpers' import { userPersistAtom as _userPersistAtom, userAtom as _userAtom } from 'react-gosh' -import { appContextAtom } from '../../store/app.state' +import { appContextAtom, appToastStatusSelector } from '../../store/app.state' +import { userSignupAtom } from '../store/signup.state' +import { useOauth } from './oauth.hooks' +import { useCreateDao } from './dao.hooks' +import { supabase } from '../../supabase' +import { EDaoInviteStatus } from '../types/dao.types' export function useUser() { const [userPersist, setUserPersist] = useRecoilState(userPersistAtom) @@ -115,6 +120,12 @@ export function useUser() { _resetUserPersist() _setUserPersist((state) => ({ ...state, username, profile: profile.address })) // /TODO: for react-gosh; REMOVE after refactor + + return { + username, + profile: profile.address, + keys: derived, + } } const signout = () => { @@ -152,3 +163,244 @@ export function useProfile() { const profile = useRecoilValue(userProfileSelector) return profile } + +export function useUserSignup() { + const { signup: _signup } = useUser() + const { signin } = useOauth() + const { createDao } = useCreateDao() + const [data, setData] = useRecoilState(userSignupAtom) + const [status, setStatus] = useRecoilState(appToastStatusSelector('__signupuser')) + + const setStep = (step: 'username' | 'daoinvite' | 'phrase' | 'complete') => { + setData((state) => ({ ...state, step })) + } + + const setPhrase = (phrase: string[]) => { + setData((state) => ({ ...state, phrase })) + } + + const setDaoInviteStatus = (id: string, status: boolean) => { + setData((state) => ({ + ...state, + daoinvites: state.daoinvites.map((item) => { + return item.id !== id ? item : { ...item, accepted: status } + }), + })) + } + + const submitUsernameStep = async (params: { email: string; username: string }) => { + const email = params.email.toLowerCase() + + try { + // Validate username + const username = params.username.trim().toLowerCase() + const profile = await AppConfig.goshroot.getUserProfile({ username }) + if (await profile.isDeployed()) { + throw new GoshError( + EGoshError.PROFILE_EXISTS, + `GOSH username is already taken`, + ) + } + + // Check for DAO name = username + const { valid } = await validateOnboardingDao(username) + if (!valid) { + throw new GoshError( + 'Value error', + `GOSH username is already taken or incorrect`, + ) + } + + // Get DAO invites, generate random phrase and update state + const daoinvites = await _getDaoInvites(email) + const { phrase } = await AppConfig.goshclient.crypto.mnemonic_from_random({}) + setData((state) => ({ + ...state, + email: params.email.toLowerCase(), + username, + phrase: state.phrase.length ? state.phrase : phrase.split(' '), + daoinvites: daoinvites.map((item) => { + const found = state.daoinvites.find((v) => v.id === item.id) + if (found) { + return found + } + return { id: item.id, daoname: item.dao_name, accepted: null } + }), + step: daoinvites.length ? 'daoinvite' : 'phrase', + })) + } catch (e: any) { + setStatus((state) => ({ ...state, type: 'error', data: e })) + throw e + } + } + + const submitDaoInvitesStep = async () => { + try { + // Update DAO invites status + for (const invite of data.daoinvites) { + const { error } = await supabase.client + .from('dao_invite') + .update({ + recipient_username: data.username, + recipient_status: invite.accepted + ? EDaoInviteStatus.ACCEPTED + : EDaoInviteStatus.REJECTED, + token_expired: invite.accepted === false, + }) + .eq('id', invite.id) + if (error) { + throw new GoshError(error.message) + } + } + + // Update step + setData((state) => ({ ...state, step: 'phrase' })) + } catch (e: any) { + setStatus((state) => ({ ...state, type: 'error', data: e })) + throw e + } + } + + const submitPhraseCreateStep = async (phrase: string[]) => { + try { + const { valid, reason } = await validatePhrase(phrase.join(' ')) + if (!valid) { + throw new GoshError('Value error', { + code: EGoshError.PHRASE_INVALID, + message: reason, + }) + } + setData((state) => ({ ...state, phrase, step: 'phrasecheck' })) + } catch (e: any) { + setStatus((state) => ({ ...state, type: 'error', data: e })) + throw e + } + } + + const submitPhraseCheckStep = async (params: { + words: string[] + numbers: number[] + }) => { + const { words, numbers } = params + try { + // Check random words against phrase + const validated = words.map((w, index) => { + return w === data.phrase[numbers[index]] + }) + if (!validated.every((v) => !!v)) { + throw new GoshError('Value error', 'Words check failed') + } + + // Create GOSH account + setStatus((state) => ({ + ...state, + type: 'pending', + data: 'Create GOSH account', + })) + const { keys } = await _signup({ + phrase: data.phrase.join(' '), + username: data.username, + }) + + // Create DB record for user + setStatus((state) => ({ + ...state, + type: 'pending', + data: 'Update database', + })) + const dbUser = await _getDbUser(data.username) + if (!dbUser) { + await _createDbUser({ + username: data.username, + pubkey: keys.public, + email: data.email, + }) + } + + setStatus((state) => ({ ...state, type: 'dismiss', data: null })) + } catch (e: any) { + setStatus((state) => ({ ...state, type: 'error', data: e })) + throw e + } + } + + const submitCompleteStep = async (params: { provider: 'github' | null }) => { + const { provider } = params + + try { + if (provider) { + await signin(provider, { + redirectTo: `${document.location.origin}/onboarding`, + }) + } else { + await createDao({ + name: data.username, + tags: [], + supply: 20, + isMintOn: true, + }) + } + } catch (e: any) { + setStatus((state) => ({ ...state, type: 'error', data: e })) + throw e + } + } + + const _getDbUser = async (username: string) => { + const { data, error } = await supabase.client + .from('users') + .select() + .eq('gosh_username', username) + .single() + if (error?.code === 'PGRST116') { + return null + } + if (error) { + throw new GoshError(error.message) + } + return data + } + + const _createDbUser = async (params: { + username: string + pubkey: string + email: string + }) => { + const { username, pubkey, email } = params + const { data, error } = await supabase.client + .from('users') + .insert({ gosh_username: username, gosh_pubkey: `0x${pubkey}`, email }) + .select() + .single() + if (error) { + throw new GoshError(error.message) + } + return data + } + + const _getDaoInvites = async (email: string) => { + const { data, error } = await supabase.client + .from('dao_invite') + .select('id, dao_name') + .eq('recipient_email', email) + .is('recipient_status', null) + if (error) { + console.error('Get DAO invites', error.message) + return [] + } + return data + } + + return { + data, + status, + setStep, + setPhrase, + setDaoInviteStatus, + submitUsernameStep, + submitDaoInvitesStep, + submitPhraseCreateStep, + submitPhraseCheckStep, + submitCompleteStep, + } +} diff --git a/web/src/v6.1.0/pages/AccountLayout.tsx b/web/src/v6.1.0/pages/AccountLayout.tsx index 71745e48b..d48532bd6 100644 --- a/web/src/v6.1.0/pages/AccountLayout.tsx +++ b/web/src/v6.1.0/pages/AccountLayout.tsx @@ -1,15 +1,14 @@ -import { useRecoilValue } from 'recoil' -import { onboardingDataAtom } from '../store/onboarding.state' import { withPin, withRouteAnimation } from '../hocs' -import OnboardingComplete from './Onboarding/components/Complete' +import { OnboardingComplete } from './Onboarding/components' import { AnimatedOutlet } from '../components/Outlet' +import { useOnboardingData } from '../hooks/onboarding.hooks' const AccountLayout = () => { - const { step } = useRecoilValue(onboardingDataAtom) + const { data } = useOnboardingData() return (
- {step === 'complete' && } + {data.step === 'complete' && }
) diff --git a/web/src/v6.1.0/pages/DaoMemberList/components/MemberInviteList/ListItem.tsx b/web/src/v6.1.0/pages/DaoMemberList/components/MemberInviteList/ListItem.tsx index 67d01dd9b..638a7b498 100644 --- a/web/src/v6.1.0/pages/DaoMemberList/components/MemberInviteList/ListItem.tsx +++ b/web/src/v6.1.0/pages/DaoMemberList/components/MemberInviteList/ListItem.tsx @@ -1,11 +1,10 @@ import CopyClipboard from '../../../../../components/CopyClipboard' -import { TDaoInviteListItem } from '../../../../types/dao.types' +import { EDaoInviteStatus, TDaoInviteListItem } from '../../../../types/dao.types' import { shortString } from '../../../../../utils' import Skeleton from '../../../../../components/Skeleton' import { useDao, useDaoInviteList } from '../../../../hooks/dao.hooks' import { ToastError } from '../../../../../components/Toast' import { Button } from '../../../../../components/Form' -import { EDaoInviteStatus } from '../../../../types/onboarding.types' import classNames from 'classnames' import { toast } from 'react-toastify' diff --git a/web/src/v6.1.0/pages/Onboarding/Onboarding.tsx b/web/src/v6.1.0/pages/Onboarding/Onboarding.tsx index c3c332d92..900f6c866 100644 --- a/web/src/v6.1.0/pages/Onboarding/Onboarding.tsx +++ b/web/src/v6.1.0/pages/Onboarding/Onboarding.tsx @@ -2,23 +2,18 @@ import { useEffect } from 'react' import { Navigate, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' import { ToastError } from '../../../components/Toast' -import GithubOrganizations from './components/GithubOrganizations' -import GoshPhrase from './components/GoshPhrase' -import GoshPhraseCheck from './components/GoshPhraseCheck' -import OAuthSignin from './components/OAuthSignin' -import GoshUsername from './components/GoshUsername' -import GoshDaoInvites from './components/GoshDaoInvites' -import { useUser } from '../../hooks/user.hooks' -import Loader from '../../../components/Loader/Loader' import { useOnboardingData } from '../../hooks/onboarding.hooks' import { useOauth } from '../../hooks/oauth.hooks' import { withRouteAnimation } from '../../hocs' +import { useUser } from '../../hooks/user.hooks' +import Loader from '../../../components/Loader' +import { GithubOrganizations, OAuthSignin } from './components' const OnboardingPage = () => { const navigate = useNavigate() - const { persist } = useUser() - const { signin, signout, oauth } = useOauth() - const { data } = useOnboardingData(oauth) + const user = useUser() + const { signin, signout, oauth } = useOauth({ initialize: true }) + const { data } = useOnboardingData(oauth, { initialize: true }) const signinOAuth = async () => { try { @@ -41,32 +36,26 @@ const OnboardingPage = () => { useEffect(() => { if (oauth.error) { toast.error() - navigate('/') + navigate('/onboarding') } }, [oauth.error]) + if (!user.persist.phrase) { + return + } + if (data.redirectTo) { return } - if (persist.pin) { - return - } + return (
{oauth.isLoading && Please, wait...} {data.step === 'signin' && } - {data.step === 'invites' && ( - - )} {data.step === 'organizations' && ( )} - {data.step === 'phrase' && } - {data.step === 'phrase-check' && } - {data.step === 'username' && ( - - )}
) } diff --git a/web/src/v6.1.0/pages/Onboarding/components/Complete.tsx b/web/src/v6.1.0/pages/Onboarding/components/Complete.tsx index 9c00e98a5..0d338811e 100644 --- a/web/src/v6.1.0/pages/Onboarding/components/Complete.tsx +++ b/web/src/v6.1.0/pages/Onboarding/components/Complete.tsx @@ -4,10 +4,12 @@ import githubgosh from '../../../../assets/images/githubgosh.svg' import { useEffect } from 'react' import { Button } from '../../../../components/Form' import { useOnboardingData } from '../../../hooks/onboarding.hooks' +import { useUser } from '../../../hooks/user.hooks' const OnboardingComplete = () => { + const { user } = useUser() const { - data: { username, emailOther }, + data: { emailOther }, updateData, resetData, } = useOnboardingData() @@ -27,11 +29,11 @@ const OnboardingComplete = () => { -
-
+
+
Welcome to GOSH,
- {username} + {user.username}

@@ -47,4 +49,4 @@ const OnboardingComplete = () => { ) } -export default OnboardingComplete +export { OnboardingComplete } diff --git a/web/src/v6.1.0/pages/Onboarding/components/GithubOrganizations/GithubOrganizations.tsx b/web/src/v6.1.0/pages/Onboarding/components/GithubOrganizations/GithubOrganizations.tsx index bc2f7de63..079532935 100644 --- a/web/src/v6.1.0/pages/Onboarding/components/GithubOrganizations/GithubOrganizations.tsx +++ b/web/src/v6.1.0/pages/Onboarding/components/GithubOrganizations/GithubOrganizations.tsx @@ -6,7 +6,7 @@ import ListEmpty from '../ListEmpty' import OAuthProfile from '../OAuthProfile' import PreviousStep from '../PreviousStep' import { Formik, Form, Field } from 'formik' -import { FormikCheckbox, FormikInput } from '../../../../../components/Formik' +import { FormikInput } from '../../../../../components/Formik' import yup from '../../../../yup-extended' import { TOAuthSession } from '../../../../types/oauth.types' import { useOnboardingData } from '../../../../hooks/onboarding.hooks' @@ -22,19 +22,19 @@ type TGithubOrganizationsProps = { const GithubOrganizations = (props: TGithubOrganizationsProps) => { const { oauth, signoutOAuth } = props - const { data, invites, organizations, repositories, updateData, getOrganizations } = + const { data, organizations, repositories, updateData, getOrganizations, upload } = useOnboardingData(oauth) const onBackClick = () => { updateData({ step: 'invites' }) } - const onContinueClick = (values: any) => { - updateData({ - step: 'phrase', - isEmailPublic: values.is_email_public, - emailOther: values.email_other, - }) + const onFormSubmit = async (values: { email_other: string }) => { + try { + await upload({ email: values.email_other }) + } catch (e: any) { + console.error(e.message) + } } useEffect(() => { @@ -58,11 +58,7 @@ const GithubOrganizations = (props: TGithubOrganizationsProps) => {

- {!invites.items.length ? ( - - ) : ( - - )} +
@@ -75,33 +71,15 @@ const GithubOrganizations = (props: TGithubOrganizationsProps) => { - {() => ( + {({ isSubmitting }) => (
-
- - Enable other GOSH users to find me by - email {oauth.session?.user.email}{' '} - (optional) -
- ), - }} - /> -
-
{ component={FormikInput} autoComplete="off" placeholder="Email for notifications" + disabled={isSubmitting} help="You can input another email to send notifications to" />
@@ -117,7 +96,10 @@ const GithubOrganizations = (props: TGithubOrganizationsProps) => { @@ -125,21 +107,8 @@ const GithubOrganizations = (props: TGithubOrganizationsProps) => { )} - - {!repositories.selected.length && - !!invites.items.filter((i) => i.accepted === true).length && ( -
- -
- )}
+
-
-
-
-
- -
- - {!invites.isFetching && !invites.items.length && ( - You have no pending invites to DAOs on GOSH - )} - -
- {invites.items.map((item, index) => ( - - ))} -
-
-
- ) -} - -export default GoshDaoInvites diff --git a/web/src/v6.1.0/pages/Onboarding/components/GoshDaoInvites/index.ts b/web/src/v6.1.0/pages/Onboarding/components/GoshDaoInvites/index.ts deleted file mode 100644 index 4654e00e7..000000000 --- a/web/src/v6.1.0/pages/Onboarding/components/GoshDaoInvites/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './GoshDaoInvites' -export { default } from './GoshDaoInvites' diff --git a/web/src/v6.1.0/pages/Onboarding/components/GoshPhrase.tsx b/web/src/v6.1.0/pages/Onboarding/components/GoshPhrase.tsx deleted file mode 100644 index a1e422b04..000000000 --- a/web/src/v6.1.0/pages/Onboarding/components/GoshPhrase.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { Field } from 'formik' -import { useEffect } from 'react' -import { toast } from 'react-toastify' -import { ToastError } from '../../../../components/Toast' -import { FormikCheckbox } from '../../../../components/Formik' -import PreviousStep from './PreviousStep' -import { AppConfig } from '../../../../appconfig' -import { EGoshError, GoshError } from '../../../../errors' -import PhraseForm from '../../../../components/PhraseForm/PhraseForm' -import { TOAuthSession } from '../../../types/oauth.types' -import { useOnboardingData } from '../../../hooks/onboarding.hooks' -import Alert from '../../../../components/Alert/Alert' -import yup from '../../../yup-extended' - -type TGoshPhraseProps = { - oauth: TOAuthSession -} - -const GoshPhrase = (props: TGoshPhraseProps) => { - const { oauth } = props - const { - data: { phrase }, - updateData, - } = useOnboardingData(oauth) - - const onBackClick = () => { - updateData({ step: 'organizations' }) - } - - const onFormSubmit = async (values: { words: string[] }) => { - try { - const { words } = values - const { valid } = await AppConfig.goshclient.crypto.mnemonic_verify({ - phrase: words.join(' '), - }) - if (!valid) { - throw new GoshError(EGoshError.PHRASE_INVALID) - } - updateData({ phrase: words, step: 'phrase-check' }) - } catch (e: any) { - console.error(e.message) - toast.error() - } - } - - useEffect(() => { - const _setRandomPhrase = async () => { - const { phrase } = await AppConfig.goshclient.crypto.mnemonic_from_random({}) - updateData({ phrase: phrase.split(' ') }) - } - - if (!phrase.length) { - _setRandomPhrase() - } - }, [phrase]) - - return ( -
-
-
- -
- -
- Let's set up your GOSH account -
- -
- Write down the seed phrase in a safe place or enter an existing one if - you already have a GOSH account -
-
- -
- updateData({ phrase: words })} - > - -
- GOSH cannot reset this phrase! If you forget it, you might - lose access to your account -
-
- -
- -
-
-
-
- ) -} - -export default GoshPhrase diff --git a/web/src/v6.1.0/pages/Onboarding/components/GoshPhraseCheck.tsx b/web/src/v6.1.0/pages/Onboarding/components/GoshPhraseCheck.tsx deleted file mode 100644 index 0764c85a7..000000000 --- a/web/src/v6.1.0/pages/Onboarding/components/GoshPhraseCheck.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useState } from 'react' -import { toast } from 'react-toastify' -import { ToastError } from '../../../../components/Toast' -import PreviousStep from './PreviousStep' -import { GoshError } from '../../../../errors' -import PhraseForm from '../../../../components/PhraseForm/PhraseForm' -import { TOAuthSession } from '../../../types/oauth.types' -import { useOnboardingData } from '../../../hooks/onboarding.hooks' - -type TGoshPhraseProps = { - oauth: TOAuthSession -} - -const generateRandomWordNumbers = () => { - const min = 0 - const max = 11 - const numbers: number[] = [] - while (true) { - const num = Math.floor(Math.random() * (max - min + 1)) + min - if (numbers.indexOf(num) < 0) { - numbers.push(num) - } - if (numbers.length === 3) { - break - } - } - return numbers.sort((a, b) => a - b) -} - -const GoshPhraseCheck = (props: TGoshPhraseProps) => { - const { oauth } = props - const { - data: { phrase }, - updateData, - } = useOnboardingData(oauth) - const [rndNumbers, setRndNumbers] = useState([]) - - const onBackClick = () => { - updateData({ step: 'phrase' }) - } - - const onFormSubmit = async (values: { words: string[] }) => { - try { - const { words } = values - const validated = words.map((w, index) => { - return w === phrase[rndNumbers[index]] - }) - if (!validated.every((v) => !!v)) { - throw new GoshError('Words check failed') - } - updateData({ step: 'username' }) - } catch (e: any) { - console.error(e.message) - toast.error() - } - } - - useEffect(() => { - setRndNumbers(generateRandomWordNumbers()) - }, []) - - return ( -
-
-
- -
- -
- Let's set up your GOSH account -
- -
- Please input requested words from your phrase to ensure it is written - correctly -
-
- -
-

- Input words{' '} - - {rndNumbers.map((n) => n + 1).join(' - ')} - {' '} - of your phrase -

- -
-
- ) -} - -export default GoshPhraseCheck diff --git a/web/src/v6.1.0/pages/Onboarding/components/GoshUsername.tsx b/web/src/v6.1.0/pages/Onboarding/components/GoshUsername.tsx deleted file mode 100644 index 42b3b95e1..000000000 --- a/web/src/v6.1.0/pages/Onboarding/components/GoshUsername.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { Field, Form, Formik } from 'formik' -import { useSetRecoilState } from 'recoil' -import yup from '../../../yup-extended' -import { PinCodeModal } from '../../../components/Modal' -import { appModalStateAtom } from '../../../../store/app.state' -import { TOAuthSession } from '../../../types/oauth.types' -import PreviousStep from './PreviousStep' -import { FormikInput } from '../../../../components/Formik' -import { Button } from '../../../../components/Form' -import Alert from '../../../../components/Alert' -import { useOnboardingData, useOnboardingSignup } from '../../../hooks/onboarding.hooks' - -type TGoshUsernameProps = { - oauth: TOAuthSession - signoutOAuth(): Promise -} - -const GoshUsername = (props: TGoshUsernameProps) => { - const { oauth, signoutOAuth } = props - const { data, updateData } = useOnboardingData(oauth) - const { signup } = useOnboardingSignup(oauth) - const setModal = useSetRecoilState(appModalStateAtom) - - const onBackClick = () => { - updateData({ step: 'phrase' }) - } - - const onFormSubmit = async (values: { username: string }) => { - try { - // Signup with onboarding - const username = values.username.trim().toLowerCase() - const seed = data.phrase.join(' ') - const isAllValid = await signup(username) - - // Create PIN-code - setModal({ - static: true, - isOpen: true, - element: ( - { - if (isAllValid) { - await signoutOAuth() - } - updateData({ - step: 'complete', - username, - email: oauth.session!.user.email!, - redirectTo: isAllValid ? '/a/orgs' : '/onboarding/status', - }) - }} - /> - ), - }) - } catch (e: any) { - console.error(e.message) - } - } - - return ( -
-
-
- -
-
Choose a short nickname
-
or create a new one
-
- -
-
-
- {oauth.session?.user.user_metadata.user_name} -
-
- your GOSH nickname -
- - - {({ isSubmitting, setFieldValue }) => ( -
-
- - setFieldValue( - 'username', - e.target.value.toLowerCase(), - ) - } - help={ - <> -

GOSH username

-

- Can be changed, if is already taken or - you prefer another -

- - } - /> -
- - -
- This is your unique cryptographic identifier in - Gosh.
- Please note that after creating your username it - will be impossible to change it in the future -
-
- -
- -
-
- )} -
-
-
-
- ) -} - -export default GoshUsername diff --git a/web/src/v6.1.0/pages/Onboarding/components/OAuthSignin.tsx b/web/src/v6.1.0/pages/Onboarding/components/OAuthSignin.tsx index f3af03408..af8515c4c 100644 --- a/web/src/v6.1.0/pages/Onboarding/components/OAuthSignin.tsx +++ b/web/src/v6.1.0/pages/Onboarding/components/OAuthSignin.tsx @@ -27,7 +27,7 @@ const OAuthSignin = (props: TOAuthSigninProps) => {
@@ -35,4 +35,4 @@ const OAuthSignin = (props: TOAuthSigninProps) => { ) } -export default OAuthSignin +export { OAuthSignin } diff --git a/web/src/v6.1.0/pages/Onboarding/components/index.ts b/web/src/v6.1.0/pages/Onboarding/components/index.ts new file mode 100644 index 000000000..fd7f6ed0c --- /dev/null +++ b/web/src/v6.1.0/pages/Onboarding/components/index.ts @@ -0,0 +1,4 @@ +export * from './OAuthSignin' +export * from './GithubOrganizations/GithubOrganizations' +export * from './GithubRepositories' +export * from './Complete' diff --git a/web/src/v6.1.0/pages/Signup/Signup.tsx b/web/src/v6.1.0/pages/Signup/Signup.tsx index 0d92e0a88..86c2120bb 100644 --- a/web/src/v6.1.0/pages/Signup/Signup.tsx +++ b/web/src/v6.1.0/pages/Signup/Signup.tsx @@ -1,7 +1,55 @@ -import { Navigate } from 'react-router-dom' +import { AnimatePresence, motion } from 'framer-motion' +import { useUserSignup } from '../../hooks/user.hooks' +import { + CompleteForm, + DaoInvitesForm, + PhraseCheckForm, + PhraseCreateForm, + UsernameForm, +} from './components' +import { withRouteAnimation } from '../../hocs' + +const motionProps = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + exit: { opacity: 0 }, + transition: { duration: 0.25 }, +} const SignupPage = () => { - return + const { data } = useUserSignup() + + return ( +
+ + {data.step === 'username' && ( + + + + )} + {data.step === 'daoinvite' && ( + + + + )} + {data.step === 'phrase' && ( + + + + )} + {data.step === 'phrasecheck' && ( + + + + )} + {data.step === 'complete' && ( + + + + )} + +
+ ) } -export default SignupPage +export default withRouteAnimation(SignupPage) diff --git a/web/src/v6.1.0/pages/Signup/components/CompleteForm.tsx b/web/src/v6.1.0/pages/Signup/components/CompleteForm.tsx new file mode 100644 index 000000000..61da9b648 --- /dev/null +++ b/web/src/v6.1.0/pages/Signup/components/CompleteForm.tsx @@ -0,0 +1,84 @@ +import { Form, Formik } from 'formik' +import { Button } from '../../../../components/Form' +import { useUserSignup } from '../../../hooks/user.hooks' +import { useNavigate } from 'react-router-dom' +import { useState } from 'react' + +const CompleteForm = () => { + const navigate = useNavigate() + const { submitCompleteStep } = useUserSignup() + const [isAnySubmitting, setIsAnySubmitting] = useState(false) + + const onGithubSubmit = async () => { + try { + setIsAnySubmitting(true) + await submitCompleteStep({ provider: 'github' }) + } catch (e: any) { + console.error(e.message) + } finally { + setIsAnySubmitting(false) + } + } + + const onSkipSubmit = async () => { + try { + setIsAnySubmitting(true) + await submitCompleteStep({ provider: null }) + navigate('/a/orgs') + } catch (e: any) { + console.error(e.message) + } finally { + setIsAnySubmitting(false) + } + } + + return ( +
+
+ Github +
+
+ Do you want to upload your repository from GitHub +
+
+
+ + {({ isSubmitting }) => ( +
+ +
+ )} +
+
+
+ + {({ isSubmitting }) => ( +
+ +
+ )} +
+
+
+
+ ) +} + +export { CompleteForm } diff --git a/web/src/v6.1.0/pages/Signup/components/DaoInvitesForm/DaoInvitesForm.tsx b/web/src/v6.1.0/pages/Signup/components/DaoInvitesForm/DaoInvitesForm.tsx new file mode 100644 index 000000000..1348d246b --- /dev/null +++ b/web/src/v6.1.0/pages/Signup/components/DaoInvitesForm/DaoInvitesForm.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react' +import { Button } from '../../../../../components/Form' +import DaoInviteListItem from './ListItem' +import { useUserSignup } from '../../../../hooks/user.hooks' +import { Form, Formik } from 'formik' +import { PreviousStep } from '../PreviousStep' + +const DaoInvitesForm = () => { + const { data, submitDaoInvitesStep } = useUserSignup() + const [isSubmitting, setIsSubmitting] = useState(false) + + const onFormSubmit = async () => { + try { + setIsSubmitting(true) + await submitDaoInvitesStep() + } catch (e: any) { + console.error(e.message) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+
+ +
+
+ Accept or decline invitations to the DAO +
+ + {({ isSubmitting }) => ( +
+
+ +
+
+ )} +
+
+
+
+ {data.daoinvites.map((item, index) => ( + + ))} +
+
+
+ ) +} + +export { DaoInvitesForm } diff --git a/web/src/v6.1.0/pages/Onboarding/components/GoshDaoInvites/ListItem.tsx b/web/src/v6.1.0/pages/Signup/components/DaoInvitesForm/ListItem.tsx similarity index 85% rename from web/src/v6.1.0/pages/Onboarding/components/GoshDaoInvites/ListItem.tsx rename to web/src/v6.1.0/pages/Signup/components/DaoInvitesForm/ListItem.tsx index 836bc7ac5..6ccdfb2a6 100644 --- a/web/src/v6.1.0/pages/Onboarding/components/GoshDaoInvites/ListItem.tsx +++ b/web/src/v6.1.0/pages/Signup/components/DaoInvitesForm/ListItem.tsx @@ -1,18 +1,17 @@ -import { TOnboardingInvite } from '../../../../types/onboarding.types' -import { TOAuthSession } from '../../../../types/oauth.types' -import { useOnboardingData } from '../../../../hooks/onboarding.hooks' import classNames from 'classnames' import emptylogo from '../../../../../assets/images/emptylogo.svg' import { Button } from '../../../../../components/Form' +import { TDBDaoInvite } from '../../../../types/dao.types' +import { useUserSignup } from '../../../../hooks/user.hooks' type TOrganizationListItemProps = { - oauth: TOAuthSession - item: TOnboardingInvite + item: TDBDaoInvite + disabled?: boolean } const DaoInviteListItem = (props: TOrganizationListItemProps) => { - const { oauth, item } = props - const { toggleDaoInvite } = useOnboardingData(oauth) + const { item, disabled } = props + const { setDaoInviteStatus } = useUserSignup() return (
@@ -40,8 +39,9 @@ const DaoInviteListItem = (props: TOrganizationListItemProps) => { ? '!bg-red-ff3b30 !text-white !border-transparent' : null, )} + disabled={disabled} onClick={() => { - toggleDaoInvite(false, item) + setDaoInviteStatus(item.id, false) }} > Reject @@ -59,8 +59,9 @@ const DaoInviteListItem = (props: TOrganizationListItemProps) => { ? '!bg-green-600 !text-white !border-transparent' : null, )} + disabled={disabled} onClick={() => { - toggleDaoInvite(true, item) + setDaoInviteStatus(item.id, true) }} > Accept diff --git a/web/src/v6.1.0/pages/Signup/components/PhraseCheckForm.tsx b/web/src/v6.1.0/pages/Signup/components/PhraseCheckForm.tsx new file mode 100644 index 000000000..cb07b6d21 --- /dev/null +++ b/web/src/v6.1.0/pages/Signup/components/PhraseCheckForm.tsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from 'react' +import PhraseForm from '../../../../components/PhraseForm' +import { PreviousStep } from './PreviousStep' +import { useUserSignup } from '../../../hooks/user.hooks' +import { useSetRecoilState } from 'recoil' +import { appModalStateAtom } from '../../../../store/app.state' +import { PinCodeModal } from '../../../components/Modal' + +const generateRandomWordNumbers = () => { + const min = 0 + const max = 11 + const numbers: number[] = [] + while (true) { + const num = Math.floor(Math.random() * (max - min + 1)) + min + if (numbers.indexOf(num) < 0) { + numbers.push(num) + } + if (numbers.length === 3) { + break + } + } + return numbers.sort((a, b) => a - b) +} + +const PhraseCheckForm = () => { + const { data, setStep, submitPhraseCheckStep } = useUserSignup() + const setModal = useSetRecoilState(appModalStateAtom) + const [rndNumbers, setRndNumbers] = useState([]) + const [isSubmitting, setIsSubmitting] = useState(false) + + const onFormSubmit = async (values: { words: string[] }) => { + try { + setIsSubmitting(true) + await submitPhraseCheckStep({ words: values.words, numbers: rndNumbers }) + setModal({ + static: true, + isOpen: true, + element: ( + setStep('complete')} + /> + ), + }) + } catch (e: any) { + console.error(e.message) + } finally { + setIsSubmitting(false) + } + } + + useEffect(() => { + setRndNumbers(generateRandomWordNumbers()) + }, []) + + return ( +
+
+
+ +
+
+ Let's set up your GOSH account +
+
+ Please input requested words from your phrase to ensure it is written + correctly +
+
+ +
+
+

+ Input words{' '} + + {rndNumbers.map((n) => n + 1).join(' - ')} + {' '} + of your phrase +

+ +
+
+
+ ) +} + +export { PhraseCheckForm } diff --git a/web/src/v6.1.0/pages/Signup/components/PhraseCreateForm.tsx b/web/src/v6.1.0/pages/Signup/components/PhraseCreateForm.tsx new file mode 100644 index 000000000..2bafb2dcf --- /dev/null +++ b/web/src/v6.1.0/pages/Signup/components/PhraseCreateForm.tsx @@ -0,0 +1,86 @@ +import { Field } from 'formik' +import { FormikCheckbox } from '../../../../components/Formik' +import Alert from '../../../../components/Alert/Alert' +import yup from '../../../yup-extended' +import PhraseForm from '../../../../components/PhraseForm' +import { PreviousStep } from './PreviousStep' +import { useUserSignup } from '../../../hooks/user.hooks' +import { useState } from 'react' + +const PhraseCreateForm = () => { + const { data, setPhrase, submitPhraseCreateStep } = useUserSignup() + const [isSubmitting, setIsSubmitting] = useState(false) + + const onFormSubmit = async (values: { words: string[] }) => { + try { + setIsSubmitting(true) + await submitPhraseCreateStep(values.words) + } catch (e: any) { + console.error(e.message) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+
+ +
+
+ Let's set up your GOSH account +
+
+ Write down the seed phrase in a safe place or enter an existing one if + you already have a GOSH account +
+
+ +
+
+ setPhrase(words)} + > + +
+ GOSH cannot reset this phrase! If you forget it, you might + lose access to your account +
+
+ +
+ +
+
+
+
+
+ ) +} + +export { PhraseCreateForm } diff --git a/web/src/v6.1.0/pages/Signup/components/PreviousStep.tsx b/web/src/v6.1.0/pages/Signup/components/PreviousStep.tsx new file mode 100644 index 000000000..948750404 --- /dev/null +++ b/web/src/v6.1.0/pages/Signup/components/PreviousStep.tsx @@ -0,0 +1,36 @@ +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Button } from '../../../../components/Form' +import { useUserSignup } from '../../../hooks/user.hooks' + +type TPreviousStepProps = { + step: 'username' | 'daoinvite' | 'phrase' + disabled?: boolean +} + +const PreviousStep = (props: TPreviousStepProps) => { + const { step, disabled } = props + const { setStep } = useUserSignup() + + const onButtonClick = () => { + setStep(step) + } + + return ( + + ) +} + +export { PreviousStep } diff --git a/web/src/v6.1.0/pages/Signup/components/UsernameForm.tsx b/web/src/v6.1.0/pages/Signup/components/UsernameForm.tsx new file mode 100644 index 000000000..959a1392d --- /dev/null +++ b/web/src/v6.1.0/pages/Signup/components/UsernameForm.tsx @@ -0,0 +1,99 @@ +import { Field, Form, Formik } from 'formik' +import yup from '../../../yup-extended' +import { FormikInput } from '../../../../components/Formik' +import Alert from '../../../../components/Alert' +import { Button } from '../../../../components/Form' +import { useUserSignup } from '../../../hooks/user.hooks' + +type TFormValues = { + email: string + username: string +} + +const UsernameForm = () => { + const { data, submitUsernameStep } = useUserSignup() + + const onFormSubmit = async (values: TFormValues) => { + try { + await submitUsernameStep(values) + } catch (e: any) { + console.error(e.message) + } + } + + return ( +
+
+
Welcome to Gosh
+
Choose a short Nickname and Email
+
+ +
+
+ + {({ isSubmitting, setFieldValue }) => ( +
+
+ +
+
+ + setFieldValue( + 'username', + e.target.value.toLowerCase(), + ) + } + /> +
+ + +
+ This is your unique cryptographic identifier in + Gosh.
+ Please note that after creating your username it + will be impossible to change it in the future +
+
+ +
+ +
+
+ )} +
+
+
+
+ ) +} + +export { UsernameForm } diff --git a/web/src/v6.1.0/pages/Signup/components/index.ts b/web/src/v6.1.0/pages/Signup/components/index.ts new file mode 100644 index 000000000..64caad8fd --- /dev/null +++ b/web/src/v6.1.0/pages/Signup/components/index.ts @@ -0,0 +1,6 @@ +export * from './PreviousStep' +export * from './UsernameForm' +export * from './DaoInvitesForm/DaoInvitesForm' +export * from './PhraseCreateForm' +export * from './PhraseCheckForm' +export * from './CompleteForm' diff --git a/web/src/v6.1.0/pages/UserDaoList/UserDaoList.tsx b/web/src/v6.1.0/pages/UserDaoList/UserDaoList.tsx index 9532b1c2e..a52083def 100644 --- a/web/src/v6.1.0/pages/UserDaoList/UserDaoList.tsx +++ b/web/src/v6.1.0/pages/UserDaoList/UserDaoList.tsx @@ -1,12 +1,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons' import { ButtonLink, Input } from '../../../components/Form' -import { ListBoundary } from './components' +import { ListBoundaryUser, ListBoundaryPartner } from './components' import Loader from '../../../components/Loader' -import { useUserDaoList } from '../../hooks/dao.hooks' +import { usePartnerDaoList, useUserDaoList } from '../../hooks/dao.hooks' +import { PARTNER_DAO_NAMES } from '../../../constants' const UserDaoListPage = () => { const userDaoList = useUserDaoList() + const partnerDaoList = usePartnerDaoList() return ( <> @@ -39,14 +41,27 @@ const UserDaoListPage = () => {
-
-

Your organizations

- {userDaoList.isFetching && ( - Updating... - )} +
+
+

Your organizations

+ {userDaoList.isFetching && ( + Updating... + )} +
+
- + {!!PARTNER_DAO_NAMES.length && ( +
+
+

Partners

+ {partnerDaoList.isFetching && ( + Updating... + )} +
+ +
+ )} ) } diff --git a/web/src/v6.1.0/pages/UserDaoList/components/ListBoundaryPartner.tsx b/web/src/v6.1.0/pages/UserDaoList/components/ListBoundaryPartner.tsx new file mode 100644 index 000000000..92604f8e4 --- /dev/null +++ b/web/src/v6.1.0/pages/UserDaoList/components/ListBoundaryPartner.tsx @@ -0,0 +1,45 @@ +import { useErrorBoundary, withErrorBoundary } from 'react-error-boundary' +import Alert from '../../../../components/Alert' +import { usePartnerDaoList } from '../../../hooks/dao.hooks' +import { ListItem, ListItemSkeleton } from './ListItem' +import { useEffect } from 'react' + +const ListBoundaryInner = () => { + const partnerDaoList = usePartnerDaoList({ initialize: true }) + const { showBoundary } = useErrorBoundary() + + useEffect(() => { + if (partnerDaoList.error) { + showBoundary(partnerDaoList.error) + } + }, [partnerDaoList.error]) + + return ( +
+
+ {partnerDaoList.isFetching && !partnerDaoList.items.length && ( +
+ +
+ )} + + {partnerDaoList.items.map((item, index) => ( +
+ +
+ ))} +
+
+ ) +} + +const ListBoundaryPartner = withErrorBoundary(ListBoundaryInner, { + fallbackRender: ({ error }) => ( + +

Fetch partner DAO list error

+
{error.message}
+
+ ), +}) + +export { ListBoundaryPartner } diff --git a/web/src/v6.1.0/pages/UserDaoList/components/ListBoundary.tsx b/web/src/v6.1.0/pages/UserDaoList/components/ListBoundaryUser.tsx similarity index 96% rename from web/src/v6.1.0/pages/UserDaoList/components/ListBoundary.tsx rename to web/src/v6.1.0/pages/UserDaoList/components/ListBoundaryUser.tsx index fefcdf64e..d9c5ff898 100644 --- a/web/src/v6.1.0/pages/UserDaoList/components/ListBoundary.tsx +++ b/web/src/v6.1.0/pages/UserDaoList/components/ListBoundaryUser.tsx @@ -67,7 +67,7 @@ const ListBoundaryInner = () => { ) } -const ListBoundary = withErrorBoundary(ListBoundaryInner, { +const ListBoundaryUser = withErrorBoundary(ListBoundaryInner, { fallbackRender: ({ error }) => (

Fetch DAO list error

@@ -76,4 +76,4 @@ const ListBoundary = withErrorBoundary(ListBoundaryInner, { ), }) -export { ListBoundary } +export { ListBoundaryUser } diff --git a/web/src/v6.1.0/pages/UserDaoList/components/index.ts b/web/src/v6.1.0/pages/UserDaoList/components/index.ts index ec586c004..1ddef22df 100644 --- a/web/src/v6.1.0/pages/UserDaoList/components/index.ts +++ b/web/src/v6.1.0/pages/UserDaoList/components/index.ts @@ -1,2 +1,3 @@ -export * from './ListBoundary' +export * from './ListBoundaryUser' +export * from './ListBoundaryPartner' export * from './ListItem' diff --git a/web/src/v6.1.0/store/dao.state.ts b/web/src/v6.1.0/store/dao.state.ts index 9f225f4e4..eeeefe8ce 100644 --- a/web/src/v6.1.0/store/dao.state.ts +++ b/web/src/v6.1.0/store/dao.state.ts @@ -13,6 +13,14 @@ import { TTaskDetails, } from '../types/dao.types' +export const partnerDaoListAtom = atom({ + key: `PartnerDaoListAtom_${contextVersion}`, + default: { + isFetching: false, + items: [], + }, +}) + export const userDaoListAtom = atom({ key: `UserDaoListAtom_${contextVersion}`, default: { diff --git a/web/src/v6.1.0/store/onboarding.state.ts b/web/src/v6.1.0/store/onboarding.state.ts index 412ef8c99..344fcbb35 100644 --- a/web/src/v6.1.0/store/onboarding.state.ts +++ b/web/src/v6.1.0/store/onboarding.state.ts @@ -3,7 +3,6 @@ import { atom, selector, selectorFamily } from 'recoil' import { contextVersion } from '../constants' import { TOnboardingData, - TOnboardingInvite, TOnboardingOrganization, TOnboardingRepository, TOnboardingStatusDao, @@ -13,11 +12,7 @@ import { OAuthSessionAtom } from './oauth.state' export const onboardingDataAtom = atom({ key: `OnboardingDataAtom_${contextVersion}`, default: { - invites: { items: [], isFetching: false }, organizations: { items: [], isFetching: false }, - phrase: [], - isEmailPublic: true, - username: '', emailOther: '', }, }) @@ -97,19 +92,3 @@ export const repositoriesSelector = selectorFamily< })) }, }) - -export const daoInvitesSelector = selector<{ - items: TOnboardingInvite[] - isFetching: boolean -}>({ - key: `DaoInvitesSelector_${contextVersion}`, - get: ({ get }) => { - return get(onboardingDataAtom).invites - }, - set: ({ set }, value) => { - set(onboardingDataAtom, (state) => ({ - ...state, - invites: value as any, - })) - }, -}) diff --git a/web/src/v6.1.0/store/signup.state.ts b/web/src/v6.1.0/store/signup.state.ts new file mode 100644 index 000000000..dbcb544a4 --- /dev/null +++ b/web/src/v6.1.0/store/signup.state.ts @@ -0,0 +1,20 @@ +import { atom } from 'recoil' +import { contextVersion } from '../constants' +import { TDBDaoInvite } from '../types/dao.types' + +export const userSignupAtom = atom<{ + username: string + email: string + phrase: string[] + daoinvites: TDBDaoInvite[] + step: 'username' | 'daoinvite' | 'phrase' | 'phrasecheck' | 'complete' +}>({ + key: `UserSignupAtom_${contextVersion}`, + default: { + username: '', + email: '', + phrase: [], + daoinvites: [], + step: 'username', + }, +}) diff --git a/web/src/v6.1.0/types/dao.types.ts b/web/src/v6.1.0/types/dao.types.ts index 575464374..be6ee8c52 100644 --- a/web/src/v6.1.0/types/dao.types.ts +++ b/web/src/v6.1.0/types/dao.types.ts @@ -12,6 +12,13 @@ export enum EDaoMemberType { User = 'user', } +export enum EDaoInviteStatus { + ACCEPTED = 'accepted', + REJECTED = 'rejected', + REVOKED = 'revoked', + PROPOSAL_CREATED = 'proposal_created', +} + export type TDaoListItem = { account: Dao | null name: string @@ -220,3 +227,9 @@ export type TDaoTaskList = { hasNext?: boolean error?: any } + +export type TDBDaoInvite = { + id: string + daoname: string + accepted: boolean | null +} diff --git a/web/src/v6.1.0/types/onboarding.types.ts b/web/src/v6.1.0/types/onboarding.types.ts index 61b8806ea..418343fb6 100644 --- a/web/src/v6.1.0/types/onboarding.types.ts +++ b/web/src/v6.1.0/types/onboarding.types.ts @@ -1,32 +1,11 @@ import { TValidationResult } from '../../types/validator.types' -export enum EDaoInviteStatus { - ACCEPTED = 'accepted', - REJECTED = 'rejected', - REVOKED = 'revoked', - PROPOSAL_CREATED = 'proposal_created', -} - export type TOnboardingData = { - step?: - | 'signin' - | 'invites' - | 'organizations' - | 'phrase' - | 'phrase-check' - | 'username' - | 'complete' - invites: { - items: TOnboardingInvite[] - isFetching: boolean - } + step?: 'signin' | 'organizations' | 'complete' organizations: { items: TOnboardingOrganization[] isFetching: boolean } - phrase: string[] - isEmailPublic: boolean - username: string emailOther: string redirectTo?: string } @@ -55,12 +34,6 @@ export type TOnboardingRepository = { isSelected: boolean } -export type TOnboardingInvite = { - id: string - daoname: string - accepted: boolean | null -} - export type TOnboardingStatusDao = { name: string repos: TOnboardingStatusRepo[] diff --git a/web/src/v6.1.0/validators.ts b/web/src/v6.1.0/validators.ts index 0ed7f76fe..f83583a1f 100644 --- a/web/src/v6.1.0/validators.ts +++ b/web/src/v6.1.0/validators.ts @@ -116,8 +116,8 @@ export const validateOnboardingDao = async (name: string): Promise