From 0f3f878b6561aee94503ec8eb1399016895022e6 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 5 Jun 2023 21:31:28 +0300 Subject: [PATCH 01/44] Compoenents Created --- .../ui/src/common/components/Modal/Modals.tsx | 7 ++++ .../BuyMembershipFormModal.tsx | 33 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/common/components/Modal/Modals.tsx b/packages/ui/src/common/components/Modal/Modals.tsx index bd2cc2af01..0d879e6962 100644 --- a/packages/ui/src/common/components/Modal/Modals.tsx +++ b/packages/ui/src/common/components/Modal/Modals.tsx @@ -17,6 +17,13 @@ export const Row = styled.div` height: auto; ` +export const RowInline = styled.div` + display: flex; + flex-direction: row; + width: 100%; + height: auto; +` + export const AccountRow = styled.div` display: grid; grid-template-columns: 1fr 1fr; diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 171938e776..f4985dae4b 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -27,6 +27,7 @@ import { ModalFooterGroup, ModalHeader, Row, + RowInline, ScrolledModal, ScrolledModalBody, ScrolledModalContainer, @@ -51,6 +52,8 @@ import { ReferrerSchema, } from '../../model/validation' import { Member } from '../../types' +import { PlusIcon } from '@/common/components/icons/PlusIcon' +import { RowGapBlock } from '@/common/components/page/PageContent' interface BuyMembershipFormModalProps { onClose: () => void @@ -76,6 +79,7 @@ const CreateMemberSchema = Yup.object().shape({ ), hasTerms: Yup.boolean().required().oneOf([true]), isReferred: Yup.boolean(), + isValidator: Yup.boolean(), referrer: ReferrerSchema, externalResources: ExternalResourcesSchema, }) @@ -88,6 +92,7 @@ export interface MemberFormFields { about: string avatarUri: File | string | null isReferred?: boolean + isValidator?: boolean referrer?: Member hasTerms?: boolean invitor?: Member @@ -101,6 +106,7 @@ const formDefaultValues = { about: '', avatarUri: null, isReferred: false, + isValidator: false, referrer: undefined, hasTerms: false, externalResources: {}, @@ -135,7 +141,7 @@ export const BuyMembershipForm = ({ }, }) - const [handle, isReferred, referrer, captchaToken] = form.watch(['handle', 'isReferred', 'referrer', 'captchaToken']) + const [handle, isReferred, isValidator, referrer, captchaToken] = form.watch(['handle', 'isReferred', 'isValidator', 'referrer', 'captchaToken']) useEffect(() => { if (handle) { @@ -234,6 +240,31 @@ export const BuyMembershipForm = ({ + {type === 'general' && ( + + + + + + {!isValidator && ( + + + + + + + + + )} + + )} + {process.env.REACT_APP_CAPTCHA_SITE_KEY && type === 'onBoarding' && ( Date: Mon, 5 Jun 2023 21:54:51 +0300 Subject: [PATCH 02/44] RowInline created --- .../BuyMembershipModal/BuyMembershipFormModal.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index f4985dae4b..0383e0af5c 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -247,20 +247,22 @@ export const BuyMembershipForm = ({ {!isValidator && ( - + +
+
+ - - +
+
)} )} From 66e224ed2689df5f45a2b2cbeefbcb9de3dfd7ed Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Wed, 7 Jun 2023 20:41:01 +0300 Subject: [PATCH 03/44] create profile modal complete --- .../ui/src/common/components/Modal/Modals.tsx | 1 + .../BuyMembershipFormModal.tsx | 154 ++++++++++++++++-- 2 files changed, 138 insertions(+), 17 deletions(-) diff --git a/packages/ui/src/common/components/Modal/Modals.tsx b/packages/ui/src/common/components/Modal/Modals.tsx index 0d879e6962..170c128a1d 100644 --- a/packages/ui/src/common/components/Modal/Modals.tsx +++ b/packages/ui/src/common/components/Modal/Modals.tsx @@ -22,6 +22,7 @@ export const RowInline = styled.div` flex-direction: row; width: 100%; height: auto; + align-items: center; ` export const AccountRow = styled.div` diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 0383e0af5c..5cfe1eb728 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -2,9 +2,10 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { BalanceOf } from '@polkadot/types/interfaces/runtime' import React, { useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' +import styled from 'styled-components' import * as Yup from 'yup' -import { SelectAccount } from '@/accounts/components/SelectAccount' +import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' import { Account } from '@/accounts/types' @@ -20,7 +21,9 @@ import { LabelLink, ToggleCheckbox, } from '@/common/components/forms' -import { Arrow } from '@/common/components/icons' +import { Arrow, CrossIcon } from '@/common/components/icons' +import { PlusIcon } from '@/common/components/icons/PlusIcon' +import { AlertSymbol } from '@/common/components/icons/symbols' import { Loading } from '@/common/components/Loading' import { ModalFooter, @@ -35,7 +38,7 @@ import { } from '@/common/components/Modal' import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' import { TransactionInfo } from '@/common/components/TransactionInfo' -import { TextMedium } from '@/common/components/typography' +import { TextMedium, TextSmall } from '@/common/components/typography' import { definedValues } from '@/common/utils' import { useYupValidationResolver } from '@/common/utils/validation' import { AvatarInput } from '@/memberships/components/AvatarInput' @@ -52,8 +55,7 @@ import { ReferrerSchema, } from '../../model/validation' import { Member } from '../../types' -import { PlusIcon } from '@/common/components/icons/PlusIcon' -import { RowGapBlock } from '@/common/components/page/PageContent' + interface BuyMembershipFormModalProps { onClose: () => void @@ -71,6 +73,7 @@ const isRequired = 'This field is required.' const CreateMemberSchema = Yup.object().shape({ rootAccount: AccountSchema.required(isRequired), controllerAccount: AccountSchema.required(isRequired), + stashAccountSelect: AccountSchema, avatarUri: AvatarURISchema, name: Yup.string().required(isRequired), handle: HandleSchema.required(isRequired).matches( @@ -87,6 +90,8 @@ const CreateMemberSchema = Yup.object().shape({ export interface MemberFormFields { rootAccount?: Account controllerAccount?: Account + stashAccountSelect?: Account + stashAccounts?: Account[] name: string handle: string about: string @@ -127,6 +132,8 @@ export const BuyMembershipForm = ({ }: BuyMembershipFormProps) => { const { allAccounts } = useMyAccounts() const [formHandleMap, setFormHandleMap] = useState('') + const [stashAccounts, setStashAccounts] = useState([{}]) + const [isAccountAdded, setIsAccountAdded] = useState(true) const { isUploading, uploadAvatarAndSubmit } = useUploadAvatarAndSubmit(onSubmit) const { data } = useGetMembersCountQuery({ variables: { where: { handle_eq: formHandleMap } } }) @@ -141,7 +148,7 @@ export const BuyMembershipForm = ({ }, }) - const [handle, isReferred, isValidator, referrer, captchaToken] = form.watch(['handle', 'isReferred', 'isValidator', 'referrer', 'captchaToken']) + const [handle, isReferred, isValidator, referrer, captchaToken, stashAccountSelect] = form.watch(['handle', 'isReferred', 'isValidator', 'referrer', 'captchaToken', 'stashAccountSelect']) useEffect(() => { if (handle) { @@ -155,10 +162,29 @@ export const BuyMembershipForm = ({ } }, [data?.membershipsConnection.totalCount]) - const isFormValid = !isUploading && form.formState.isValid + const isFormValid = !isUploading && form.formState.isValid && ( !isValidator || stashAccounts?.length > 1 ) const isDisabled = type === 'onBoarding' && process.env.REACT_APP_CAPTCHA_SITE_KEY ? !captchaToken || !isFormValid : !isFormValid + const addStashAccount = () => { + const accountSelection = stashAccountSelect as Account; + setStashAccounts((prevStashAccounts)=>[...prevStashAccounts,accountSelection]) + form?.setValue('stashAccountSelect' as keyof MemberFormFields, undefined) + } + + const removeStashAccount = (index:number) => { + setStashAccounts((prevAccounts)=>prevAccounts.filter((account,ind)=>ind!==index)) + } + + useEffect(()=>{ + const accountSelection = stashAccountSelect as Account; + if (stashAccounts.some((account)=>account === accountSelection)) { + setIsAccountAdded(true) + } else { + setIsAccountAdded(false) + } + },[stashAccountSelect]) + return ( <> @@ -241,30 +267,84 @@ export const BuyMembershipForm = ({ {type === 'general' && ( + <> - {!isValidator && ( - + + {isValidator && ( + <> + + -
+
+ + + + + +
+ {isAccountAdded && ( - - - - + + + This stash account is already added to the list. + -
- )} + ) + } + {stashAccounts.length < 2 && ( + + + + You should add at least 1 stash account. + + + ) + } +
+ + {stashAccounts.map((stashAccount, index)=>{ + if(index !== 0){ + return( + + + + + {removeStashAccount(index)} + }> + + + + + ) + } + })} + + + + + + + : {'Unverified'} ! + + + )} + + )} {process.env.REACT_APP_CAPTCHA_SITE_KEY && type === 'onBoarding' && ( @@ -320,6 +400,14 @@ export const BuyMembershipForm = ({ { + stashAccounts.length > 1 && ( + stashAccounts.map((account, index)=>{ + if(index !== 0){ + form?.register('stashAccounts[' + (index - 1) + ']' as keyof MemberFormFields) + form?.setValue('stashAccounts[' + (index - 1) + ']' as keyof MemberFormFields, account as Account) + } + }) + ) const values = form.getValues() uploadAvatarAndSubmit({ ...values, externalResources: { ...definedValues(values.externalResources) } }) }} @@ -341,3 +429,35 @@ export const BuyMembershipFormModal = ({ onClose, onSubmit, membershipPrice }: B ) } + +const InputNotificationIcon = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 12px; + height: 12px; + color: inherit; + padding-right: 2px; + + .blackPart, + .primaryPart { + fill: currentColor; + } +` +interface PaddingProps { + pl?: number + pr?: number + pt?: number + pb?: number + width?: number +} + +const BtnWrapper = styled.div` + padding-right : ${({ pr }) => ( pr ? pr + 'px' : '0px')}; + padding-left : ${({ pl }) => ( pl ? pl + 'px' : '0px')}; + padding-top : ${({ pt }) => ( pt ? pt + 'px' : '0px')}; + padding-bottom: ${({ pb }) => ( pb ? pb + 'px' : '0px')}; + width : ${({ width }) => ( width ? width + 'px' : '0px')}; + display : flex; + justify-content : end; +` From e1d6beed9066502d4e6752672fd11150d69c1ba7 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Wed, 7 Jun 2023 23:01:37 +0300 Subject: [PATCH 04/44] profile modification modal complete --- .../UpdateMembershipFormModal.tsx | 168 +++++++++++++++++- .../modals/UpdateMembershipModal/types.ts | 3 + 2 files changed, 163 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx index 5279349702..01e3f481a1 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx @@ -1,22 +1,30 @@ import React, { useCallback, useEffect, useState } from 'react' import { useForm, FormProvider } from 'react-hook-form' +import styled from 'styled-components' import * as Yup from 'yup' import { AnySchema } from 'yup' -import { filterAccount, SelectAccount } from '@/accounts/components/SelectAccount' +import { filterAccount, SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' -import { InputComponent, InputText, InputTextarea } from '@/common/components/forms' +import { Account } from '@/accounts/types' +import { ButtonPrimary, ButtonGhost } from '@/common/components/buttons' +import { InlineToggleWrap, InputComponent, InputText, InputTextarea, ToggleCheckbox } from '@/common/components/forms' +import { CrossIcon } from '@/common/components/icons' +import { PlusIcon } from '@/common/components/icons/PlusIcon' +import { AlertSymbol } from '@/common/components/icons/symbols' import { Loading } from '@/common/components/Loading' import { ModalHeader, ModalTransactionFooter, Row, + RowInline, ScrolledModal, ScrolledModalBody, ScrolledModalContainer, } from '@/common/components/Modal' -import { TextMedium } from '@/common/components/typography' +import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' +import { Label, TextMedium, TextSmall } from '@/common/components/typography' import { WithNullableValues } from '@/common/types/form' import { definedValues } from '@/common/utils' import { useYupValidationResolver } from '@/common/utils/validation' @@ -31,6 +39,7 @@ import { MemberWithDetails } from '../../types' import { UpdateMemberForm } from './types' import { changedOrNull, hasAnyEdits, membershipExternalResourceToObject } from './utils' + interface Props { onClose: () => void onSubmit: (params: WithNullableValues) => void @@ -69,7 +78,12 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) ) ) ) - + // isValidator, stashAccounts should be read from member + // const isValidator = member.isValidator ?? false + // const stashAccounts = member.stashAccounts ?? [] + const [stashAccounts, setStashAccounts] = useState([{}]) + const [isAccountAdded, setIsAccountAdded] = useState(true) + const form = useForm({ resolver: useYupValidationResolver(UpdateMemberSchema), defaultValues: { @@ -80,8 +94,8 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) context, mode: 'onChange', }) - - const [controllerAccount, rootAccount, handle] = form.watch(['controllerAccount', 'rootAccount', 'handle']) + + const [controllerAccount, rootAccount, handle, stashAccountSelect, isValidator] = form.watch(['controllerAccount', 'rootAccount', 'handle', 'stashAccountSelect', 'isValidator']) useEffect(() => { form.trigger('handle') @@ -94,8 +108,26 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) const filterRoot = useCallback(filterAccount(controllerAccount), [controllerAccount]) const filterController = useCallback(filterAccount(rootAccount), [rootAccount]) - const canUpdate = form.formState.isValid && hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) + const canUpdate = form.formState.isValid && hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) && ( !isValidator || stashAccounts?.length > 1 ) + const addStashAccount = () => { + const accountSelection = stashAccountSelect as Account; + setStashAccounts((prevStashAccounts)=>[...prevStashAccounts,accountSelection]) + form?.setValue('stashAccountSelect' as keyof UpdateMemberForm, undefined) + } + + const removeStashAccount = (index:number) => { + setStashAccounts((prevAccounts)=>prevAccounts.filter((account,ind)=>ind!==index)) + } + + useEffect(()=>{ + const accountSelection = stashAccountSelect as Account; + if (stashAccounts.some((account)=>account === accountSelection)) { + setIsAccountAdded(true) + } else { + setIsAccountAdded(false) + } + },[stashAccountSelect]) return ( @@ -153,6 +185,84 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) member.externalResources ? member.externalResources.map((resource) => resource.source) : [] } /> + + + + + + + + {isValidator && ( + <> + + + + + + + + + + + + {isAccountAdded && ( + + + + This stash account is already added to the list. + + + ) + } + {stashAccounts.length < 2 && ( + + + + You should add at least 1 stash account. + + + ) + } + + + {stashAccounts.map((stashAccount, index)=>{ + if(index !== 0){ + return( + + + + + {removeStashAccount(index)} + }> + + + + + ) + } + })} + + + + + + + : {'Unverified'} ! + + + + )} + +
@@ -160,9 +270,51 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) next={{ disabled: !canUpdate || isUploading, label: isUploading ? : 'Save changes', - onClick: () => uploadAvatarAndSubmit(form.getValues()), + onClick: () => { + stashAccounts.length > 1 && ( + stashAccounts.map((account, index)=>{ + if(index !== 0){ + form?.register('stashAccounts[' + (index - 1) + ']' as keyof UpdateMemberForm) + form?.setValue('stashAccounts[' + (index - 1) + ']' as keyof UpdateMemberForm, account as Account) + } + }) + ) + uploadAvatarAndSubmit(form.getValues()) + }, }} /> ) } + +const InputNotificationIcon = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 12px; + height: 12px; + color: inherit; + padding-right: 2px; + + .blackPart, + .primaryPart { + fill: currentColor; + } +` +interface PaddingProps { + pl?: number + pr?: number + pt?: number + pb?: number + width?: number +} + +const BtnWrapper = styled.div` + padding-right : ${({ pr }) => ( pr ? pr + 'px' : '0px')}; + padding-left : ${({ pl }) => ( pl ? pl + 'px' : '0px')}; + padding-top : ${({ pt }) => ( pt ? pt + 'px' : '0px')}; + padding-bottom: ${({ pb }) => ( pb ? pb + 'px' : '0px')}; + width : ${({ width }) => ( width ? width + 'px' : '0px')}; + display : flex; + justify-content : end; +` diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts index de145adda7..c797125ac8 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts @@ -9,4 +9,7 @@ export interface UpdateMemberForm { rootAccount?: Account controllerAccount?: Account externalResources: Record + isValidator? : boolean + stashAccountSelect? : Account + stashAccounts: Account[] } From f38ad8a598ae30e88d5ea0de8a72da34bc5e72fb Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 12 Jun 2023 07:15:55 -0700 Subject: [PATCH 05/44] change BuyMembershipModal to multiTransaction modal --- .../BuyMembershipModal/BuyMembershipModal.tsx | 46 ++++++++++++++++++- .../BuyMembershipSignModal.tsx | 6 +-- .../modals/BuyMembershipModal/machine.ts | 43 +++++++++++++++-- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index c73b25f672..43af707ea7 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -33,7 +33,7 @@ export const BuyMembershipModal = () => { return } - if (state.matches('transaction') && api) { + if (state.matches('buyMembershipTransaction') && api) { const transaction = api.tx.members.buyMembership(toMemberTransactionParams(state.context.form)) const { form } = state.context const service = state.children.transaction @@ -50,6 +50,50 @@ export const BuyMembershipModal = () => { ) } + // if (state.matches('temp')) { + // const { form } = state.context + // if(form.isValidator) send({ type : 'PASS'}) + // } + + // if (state.matches('bondValidatorAccTransaction') && api) { + // const transaction = api.tx.members.buyMembership(toMemberTransactionParams(state.context.form)) + // const { form } = state.context + // const service = state.children.transaction + + // return ( + // + // ) + // } + + // if (state.matches('temp')) { + // const { form } = state.context + // if(form.isValidator) send({ type : 'PASS'}) + // } + + // if (state.matches('bondValidatorAccTransaction') && api) { + // const transaction = api.tx.members.buyMembership(toMemberTransactionParams(state.context.form)) + // const { form } = state.context + // const service = state.children.transaction + + // return ( + // + // ) + // } + if (isSuccessful) { const { form, memberId } = state.context return diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx index 904d48c5b0..7870b8acd1 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx @@ -70,9 +70,9 @@ export const BuyMembershipSignModal = ({ const signDisabled = !isReady || !hasFunds || !validationInfo return ( - + - You intend to create a new membership. + {formData.isValidator ? "You intend to create a validator membership.":"You intend to create a new membership."} The creation of the new membership costs . @@ -108,7 +108,7 @@ export const BuyMembershipSignModal = ({ } | { value: 'canceled'; context: Required } | { value: 'error'; context: { form: MemberFormFields; transactionEvents: EventRecord[] } } @@ -34,6 +36,7 @@ export type BuyMembershipEvent = | { type: 'DONE'; form: MemberFormFields } | { type: 'SUCCESS'; memberId: BN } | { type: 'ERROR' } + // | { type: 'SKIP' } export const buyMembershipMachine = createMachine({ initial: 'prepare', @@ -41,12 +44,12 @@ export const buyMembershipMachine = createMachine event.form }), }, }, }, - transaction: { + buyMembershipTransaction: { invoke: { id: 'transaction', src: transactionMachine, @@ -70,6 +73,40 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), + // }), + // cond: isTransactionSuccess, + // }, + // { + // target: 'error', + // cond: isTransactionError, + // actions: assign({ transactionEvents: (context, event) => event.data.events }), + // }, + // { + // target: 'canceled', + // cond: isTransactionCanceled, + // }, + // ], + // }, + // }, ...transactionModalFinalStatusesFactory({ metaMessages: { error: 'There was a problem with creating a membership.', From ccbf067a551002e62238d8237d043fd59f34f3d5 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 12 Jun 2023 08:16:05 -0700 Subject: [PATCH 06/44] add Bonding Validator Account Transaction,fix the machine --- .../BuyMembershipModal/BuyMembershipModal.tsx | 55 ++--------- .../BuyMembershipSignModal.tsx | 14 +-- .../modals/BuyMembershipModal/machine.ts | 96 +++++++++++-------- 3 files changed, 74 insertions(+), 91 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index 43af707ea7..b88d4ade23 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -28,12 +28,18 @@ export const BuyMembershipModal = () => { }, [isSuccessful, apolloClient]) if (state.matches('prepare')) { - const onSubmit = (params: MemberFormFields) => send({ type: 'DONE', form: params }) + const onSubmit = (params: MemberFormFields) => + send({ type: params.isValidator ? 'DONEWITHVAL' : 'DONE', form: params }) return } - if (state.matches('buyMembershipTransaction') && api) { + if ( + (state.matches('buyMembershipTx') || + state.matches('buyValidatorMembershipTx') || + state.matches('bondValidatorAccTx')) && + api + ) { const transaction = api.tx.members.buyMembership(toMemberTransactionParams(state.context.form)) const { form } = state.context const service = state.children.transaction @@ -46,54 +52,11 @@ export const BuyMembershipModal = () => { transaction={transaction} initialSigner={form.controllerAccount} service={service} + bondValidatorAcc={state.matches('bondValidatorAccTx')} /> ) } - // if (state.matches('temp')) { - // const { form } = state.context - // if(form.isValidator) send({ type : 'PASS'}) - // } - - // if (state.matches('bondValidatorAccTransaction') && api) { - // const transaction = api.tx.members.buyMembership(toMemberTransactionParams(state.context.form)) - // const { form } = state.context - // const service = state.children.transaction - - // return ( - // - // ) - // } - - // if (state.matches('temp')) { - // const { form } = state.context - // if(form.isValidator) send({ type : 'PASS'}) - // } - - // if (state.matches('bondValidatorAccTransaction') && api) { - // const transaction = api.tx.members.buyMembership(toMemberTransactionParams(state.context.form)) - // const { form } = state.context - // const service = state.children.transaction - - // return ( - // - // ) - // } - if (isSuccessful) { const { form, memberId } = state.context return diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx index 7870b8acd1..a096c2342a 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx @@ -28,6 +28,7 @@ interface SignProps { transaction: SubmittableExtrinsic<'rxjs', ISubmittableResult> | undefined initialSigner?: Account service: ActorRef + bondValidatorAcc?: Boolean } export const BuyMembershipSignModal = ({ @@ -37,6 +38,7 @@ export const BuyMembershipSignModal = ({ transaction, initialSigner, service, + bondValidatorAcc, }: SignProps) => { const { allAccounts } = useMyAccounts() const [from, setFrom] = useState( @@ -70,12 +72,12 @@ export const BuyMembershipSignModal = ({ const signDisabled = !isReady || !hasFunds || !validationInfo return ( - + - {formData.isValidator ? "You intend to create a validator membership.":"You intend to create a new membership."} - + {formData.isValidator ? (bondValidatorAcc? "You are intending to bond your validator account with your membership": "You intend to create a validator membership."):"You intend to create a new membership."} + {!bondValidatorAcc && ( The creation of the new membership costs . - + )} Fees of will be applied to the transaction. @@ -110,11 +112,11 @@ export const BuyMembershipSignModal = ({ transactionFee={paymentInfo?.partialFee.toBn()} next={{ disabled: signDisabled, label:formData.isValidator ? 'Create membership': 'Sign and create a member', onClick: sign }} > - + />)} ) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index ba680d1202..52c6d4f5b8 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -23,9 +23,9 @@ interface BuyMembershipContext { type BuyMembershipState = | { value: 'prepare'; context: EmptyObject } - | { value: 'buyMembershipTransaction'; context: { form: MemberFormFields } } - // | { value: 'temp'; context: { form: MemberFormFields } } - // | { value: 'bondValidatorAccTransaction'; context: { form: MemberFormFields } } + | { value: 'buyMembershipTx'; context: { form: MemberFormFields } } + | { value: 'buyValidatorMembershipTx'; context: { form: MemberFormFields } } + | { value: 'bondValidatorAccTx'; context: { form: MemberFormFields } } | { value: 'success'; context: Required } | { value: 'canceled'; context: Required } | { value: 'error'; context: { form: MemberFormFields; transactionEvents: EventRecord[] } } @@ -34,9 +34,9 @@ export type BuyMembershipEvent = | { type: 'PASS' } | { type: 'FAIL' } | { type: 'DONE'; form: MemberFormFields } + | { type: 'DONEWITHVAL'; form: MemberFormFields } | { type: 'SUCCESS'; memberId: BN } | { type: 'ERROR' } - // | { type: 'SKIP' } export const buyMembershipMachine = createMachine({ initial: 'prepare', @@ -44,12 +44,16 @@ export const buyMembershipMachine = createMachine event.form }), + }, + DONEWITHVAL: { target: 'buyMembershipTransaction', actions: assign({ form: (_, event) => event.form }), }, }, }, - buyMembershipTransaction: { + buyMembershipTx: { invoke: { id: 'transaction', src: transactionMachine, @@ -73,40 +77,54 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), - // }), - // cond: isTransactionSuccess, - // }, - // { - // target: 'error', - // cond: isTransactionError, - // actions: assign({ transactionEvents: (context, event) => event.data.events }), - // }, - // { - // target: 'canceled', - // cond: isTransactionCanceled, - // }, - // ], - // }, - // }, + buyValidatorMembershipTx: { + invoke: { + id: 'transaction', + src: transactionMachine, + onDone: [ + { + target: 'bondValidatorAccTx', + actions: assign({ + memberId: (context, event) => getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), + }), + cond: isTransactionSuccess, + }, + { + target: 'error', + cond: isTransactionError, + actions: assign({ transactionEvents: (context, event) => event.data.events }), + }, + { + target: 'canceled', + cond: isTransactionCanceled, + }, + ], + }, + }, + bondValidatorAccTx: { + invoke: { + id: 'transaction', + src: transactionMachine, + onDone: [ + { + target: 'success', + // actions: assign({ + // memberId: (context, event) => getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), + // }), + cond: isTransactionSuccess, + }, + { + target: 'error', + cond: isTransactionError, + actions: assign({ transactionEvents: (context, event) => event.data.events }), + }, + { + target: 'canceled', + cond: isTransactionCanceled, + }, + ], + }, + }, ...transactionModalFinalStatusesFactory({ metaMessages: { error: 'There was a problem with creating a membership.', From 9feb4986e21282d49e5cbb9ba4e590f683f18984 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 12 Jun 2023 08:20:20 -0700 Subject: [PATCH 07/44] fix the bug in machine --- .../ui/src/memberships/modals/BuyMembershipModal/machine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index 52c6d4f5b8..b3054ac664 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -48,7 +48,7 @@ export const buyMembershipMachine = createMachine event.form }), }, DONEWITHVAL: { - target: 'buyMembershipTransaction', + target: 'buyValidatorMembershipTx', actions: assign({ form: (_, event) => event.form }), }, }, From f0aa60f5234ce635c10e75052c7988ab441b86a2 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 12 Jun 2023 08:45:30 -0700 Subject: [PATCH 08/44] fix the bug --- .../modals/BuyMembershipModal/BuyMembershipSignModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx index a096c2342a..784decb9a4 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx @@ -112,7 +112,7 @@ export const BuyMembershipSignModal = ({ transactionFee={paymentInfo?.partialFee.toBn()} next={{ disabled: signDisabled, label:formData.isValidator ? 'Create membership': 'Sign and create a member', onClick: sign }} > - {shouldInformAboutLock && ( Date: Mon, 12 Jun 2023 09:04:37 -0700 Subject: [PATCH 09/44] complete the UI,but did not implement transaction for binding validator account --- .../BuyMembershipSignModal.tsx | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx index 784decb9a4..9743d8b402 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx @@ -72,12 +72,31 @@ export const BuyMembershipSignModal = ({ const signDisabled = !isReady || !hasFunds || !validationInfo return ( - + - {formData.isValidator ? (bondValidatorAcc? "You are intending to bond your validator account with your membership": "You intend to create a validator membership."):"You intend to create a new membership."} - {!bondValidatorAcc && ( - The creation of the new membership costs . - )} + + {formData.isValidator + ? bondValidatorAcc + ? 'You are intending to bond your validator account with your membership' + : 'You intend to create a validator membership.' + : 'You intend to create a new membership.'} + + {!bondValidatorAcc && ( + + The creation of the new membership costs . + + )} Fees of will be applied to the transaction. @@ -110,13 +129,23 @@ export const BuyMembershipSignModal = ({ - {!bondValidatorAcc && ()} + {!bondValidatorAcc && ( + + )} ) From 26a13a5772f7ab8d5ef2dd8ad128f279d09d6aae Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 12 Jun 2023 17:28:45 -0700 Subject: [PATCH 10/44] lint:fix --- .../BuyMembershipFormModal.tsx | 201 +++++++++--------- .../BuyMembershipSignModal.tsx | 4 +- .../UpdateMembershipFormModal.tsx | 200 +++++++++-------- .../modals/UpdateMembershipModal/types.ts | 4 +- 4 files changed, 216 insertions(+), 193 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 5cfe1eb728..817fef2523 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -56,7 +56,6 @@ import { } from '../../model/validation' import { Member } from '../../types' - interface BuyMembershipFormModalProps { onClose: () => void onSubmit: (params: MemberFormFields) => void @@ -148,7 +147,14 @@ export const BuyMembershipForm = ({ }, }) - const [handle, isReferred, isValidator, referrer, captchaToken, stashAccountSelect] = form.watch(['handle', 'isReferred', 'isValidator', 'referrer', 'captchaToken', 'stashAccountSelect']) + const [handle, isReferred, isValidator, referrer, captchaToken, stashAccountSelect] = form.watch([ + 'handle', + 'isReferred', + 'isValidator', + 'referrer', + 'captchaToken', + 'stashAccountSelect', + ]) useEffect(() => { if (handle) { @@ -162,28 +168,28 @@ export const BuyMembershipForm = ({ } }, [data?.membershipsConnection.totalCount]) - const isFormValid = !isUploading && form.formState.isValid && ( !isValidator || stashAccounts?.length > 1 ) + const isFormValid = !isUploading && form.formState.isValid && (!isValidator || stashAccounts?.length > 1) const isDisabled = type === 'onBoarding' && process.env.REACT_APP_CAPTCHA_SITE_KEY ? !captchaToken || !isFormValid : !isFormValid const addStashAccount = () => { - const accountSelection = stashAccountSelect as Account; - setStashAccounts((prevStashAccounts)=>[...prevStashAccounts,accountSelection]) + const accountSelection = stashAccountSelect as Account + setStashAccounts((prevStashAccounts) => [...prevStashAccounts, accountSelection]) form?.setValue('stashAccountSelect' as keyof MemberFormFields, undefined) } - const removeStashAccount = (index:number) => { - setStashAccounts((prevAccounts)=>prevAccounts.filter((account,ind)=>ind!==index)) + const removeStashAccount = (index: number) => { + setStashAccounts((prevAccounts) => prevAccounts.filter((account, ind) => ind !== index)) } - useEffect(()=>{ - const accountSelection = stashAccountSelect as Account; - if (stashAccounts.some((account)=>account === accountSelection)) { + useEffect(() => { + const accountSelection = stashAccountSelect as Account + if (stashAccounts.some((account) => account === accountSelection)) { setIsAccountAdded(true) } else { setIsAccountAdded(false) } - },[stashAccountSelect]) + }, [stashAccountSelect]) return ( <> @@ -268,82 +274,88 @@ export const BuyMembershipForm = ({ {type === 'general' && ( <> - - - - - - - {isValidator && ( - <> - - - - - - - - - - - - {isAccountAdded && ( - - - - This stash account is already added to the list. - - - ) - } - {stashAccounts.length < 2 && ( - - - - You should add at least 1 stash account. - - - ) - } - - - {stashAccounts.map((stashAccount, index)=>{ - if(index !== 0){ - return( + + + + + + + {isValidator && ( + <> - - - {removeStashAccount(index)} - }> - - + + + + + + + - ) - } - })} - - - - - - - : {'Unverified'} ! - - - - )} + {isAccountAdded && ( + + + + + + + This stash account is already added to the list. + + )} + {stashAccounts.length < 2 && ( + + + + + + + You should add at least 1 stash account. + + )} + + {stashAccounts.map((stashAccount, index) => { + if (index !== 0) { + return ( + + + + + { + removeStashAccount(index) + }} + > + + + + + + ) + } + })} + + + + + + + : {'Unverified'} ! + + + + )} )} @@ -400,14 +412,13 @@ export const BuyMembershipForm = ({ { - stashAccounts.length > 1 && ( - stashAccounts.map((account, index)=>{ - if(index !== 0){ - form?.register('stashAccounts[' + (index - 1) + ']' as keyof MemberFormFields) - form?.setValue('stashAccounts[' + (index - 1) + ']' as keyof MemberFormFields, account as Account) + stashAccounts.length > 1 && + stashAccounts.map((account, index) => { + if (index !== 0) { + form?.register(('stashAccounts[' + (index - 1) + ']') as keyof MemberFormFields) + form?.setValue(('stashAccounts[' + (index - 1) + ']') as keyof MemberFormFields, account as Account) } }) - ) const values = form.getValues() uploadAvatarAndSubmit({ ...values, externalResources: { ...definedValues(values.externalResources) } }) }} @@ -453,11 +464,11 @@ interface PaddingProps { } const BtnWrapper = styled.div` - padding-right : ${({ pr }) => ( pr ? pr + 'px' : '0px')}; - padding-left : ${({ pl }) => ( pl ? pl + 'px' : '0px')}; - padding-top : ${({ pt }) => ( pt ? pt + 'px' : '0px')}; - padding-bottom: ${({ pb }) => ( pb ? pb + 'px' : '0px')}; - width : ${({ width }) => ( width ? width + 'px' : '0px')}; - display : flex; - justify-content : end; + padding-right: ${({ pr }) => (pr ? pr + 'px' : '0px')}; + padding-left: ${({ pl }) => (pl ? pl + 'px' : '0px')}; + padding-top: ${({ pt }) => (pt ? pt + 'px' : '0px')}; + padding-bottom: ${({ pb }) => (pb ? pb + 'px' : '0px')}; + width: ${({ width }) => (width ? width + 'px' : '0px')}; + display: flex; + justify-content: end; ` diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx index 9743d8b402..eb28ed7fa7 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx @@ -28,7 +28,7 @@ interface SignProps { transaction: SubmittableExtrinsic<'rxjs', ISubmittableResult> | undefined initialSigner?: Account service: ActorRef - bondValidatorAcc?: Boolean + bondValidatorAcc?: boolean } export const BuyMembershipSignModal = ({ @@ -130,7 +130,7 @@ export const BuyMembershipSignModal = ({ void onSubmit: (params: WithNullableValues) => void @@ -83,7 +82,7 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) // const stashAccounts = member.stashAccounts ?? [] const [stashAccounts, setStashAccounts] = useState([{}]) const [isAccountAdded, setIsAccountAdded] = useState(true) - + const form = useForm({ resolver: useYupValidationResolver(UpdateMemberSchema), defaultValues: { @@ -94,8 +93,14 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) context, mode: 'onChange', }) - - const [controllerAccount, rootAccount, handle, stashAccountSelect, isValidator] = form.watch(['controllerAccount', 'rootAccount', 'handle', 'stashAccountSelect', 'isValidator']) + + const [controllerAccount, rootAccount, handle, stashAccountSelect, isValidator] = form.watch([ + 'controllerAccount', + 'rootAccount', + 'handle', + 'stashAccountSelect', + 'isValidator', + ]) useEffect(() => { form.trigger('handle') @@ -108,26 +113,29 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) const filterRoot = useCallback(filterAccount(controllerAccount), [controllerAccount]) const filterController = useCallback(filterAccount(rootAccount), [rootAccount]) - const canUpdate = form.formState.isValid && hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) && ( !isValidator || stashAccounts?.length > 1 ) + const canUpdate = + form.formState.isValid && + hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) && + (!isValidator || stashAccounts?.length > 1) const addStashAccount = () => { - const accountSelection = stashAccountSelect as Account; - setStashAccounts((prevStashAccounts)=>[...prevStashAccounts,accountSelection]) + const accountSelection = stashAccountSelect as Account + setStashAccounts((prevStashAccounts) => [...prevStashAccounts, accountSelection]) form?.setValue('stashAccountSelect' as keyof UpdateMemberForm, undefined) } - const removeStashAccount = (index:number) => { - setStashAccounts((prevAccounts)=>prevAccounts.filter((account,ind)=>ind!==index)) + const removeStashAccount = (index: number) => { + setStashAccounts((prevAccounts) => prevAccounts.filter((account, ind) => ind !== index)) } - useEffect(()=>{ - const accountSelection = stashAccountSelect as Account; - if (stashAccounts.some((account)=>account === accountSelection)) { + useEffect(() => { + const accountSelection = stashAccountSelect as Account + if (stashAccounts.some((account) => account === accountSelection)) { setIsAccountAdded(true) } else { setIsAccountAdded(false) } - },[stashAccountSelect]) + }, [stashAccountSelect]) return ( @@ -189,80 +197,85 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) - + {isValidator && ( - <> - - - - - - - - - - - - {isAccountAdded && ( - - - - This stash account is already added to the list. - - - ) - } - {stashAccounts.length < 2 && ( - - - - You should add at least 1 stash account. - - - ) - } - - - {stashAccounts.map((stashAccount, index)=>{ - if(index !== 0){ - return( - + <> + + + + + + + + + + + + {isAccountAdded && ( - - - {removeStashAccount(index)} - }> - - - + + + + + + This stash account is already added to the list. - ) - } - })} - - - - - - - : {'Unverified'} ! - - - - )} - + )} + {stashAccounts.length < 2 && ( + + + + + + + You should add at least 1 stash account. + + )} + + {stashAccounts.map((stashAccount, index) => { + if (index !== 0) { + return ( + + + + + { + removeStashAccount(index) + }} + > + + + + + + ) + } + })} + + + + + + + : {'Unverified'} ! + + + + )} @@ -271,14 +284,13 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) disabled: !canUpdate || isUploading, label: isUploading ? : 'Save changes', onClick: () => { - stashAccounts.length > 1 && ( - stashAccounts.map((account, index)=>{ - if(index !== 0){ - form?.register('stashAccounts[' + (index - 1) + ']' as keyof UpdateMemberForm) - form?.setValue('stashAccounts[' + (index - 1) + ']' as keyof UpdateMemberForm, account as Account) + stashAccounts.length > 1 && + stashAccounts.map((account, index) => { + if (index !== 0) { + form?.register(('stashAccounts[' + (index - 1) + ']') as keyof UpdateMemberForm) + form?.setValue(('stashAccounts[' + (index - 1) + ']') as keyof UpdateMemberForm, account as Account) } }) - ) uploadAvatarAndSubmit(form.getValues()) }, }} @@ -310,11 +322,11 @@ interface PaddingProps { } const BtnWrapper = styled.div` - padding-right : ${({ pr }) => ( pr ? pr + 'px' : '0px')}; - padding-left : ${({ pl }) => ( pl ? pl + 'px' : '0px')}; - padding-top : ${({ pt }) => ( pt ? pt + 'px' : '0px')}; - padding-bottom: ${({ pb }) => ( pb ? pb + 'px' : '0px')}; - width : ${({ width }) => ( width ? width + 'px' : '0px')}; - display : flex; - justify-content : end; + padding-right: ${({ pr }) => (pr ? pr + 'px' : '0px')}; + padding-left: ${({ pl }) => (pl ? pl + 'px' : '0px')}; + padding-top: ${({ pt }) => (pt ? pt + 'px' : '0px')}; + padding-bottom: ${({ pb }) => (pb ? pb + 'px' : '0px')}; + width: ${({ width }) => (width ? width + 'px' : '0px')}; + display: flex; + justify-content: end; ` diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts index c797125ac8..41ad0a88b8 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts @@ -9,7 +9,7 @@ export interface UpdateMemberForm { rootAccount?: Account controllerAccount?: Account externalResources: Record - isValidator? : boolean - stashAccountSelect? : Account + isValidator?: boolean + stashAccountSelect?: Account stashAccounts: Account[] } From bc7cb69bd454e503c85d738b7b022df9b9cf02bc Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Thu, 14 Sep 2023 07:30:41 -0500 Subject: [PATCH 11/44] add bondValidatorAcc tx --- .../ui/src/common/components/Modal/Modals.tsx | 1 + .../ui/src/common/components/forms/Label.tsx | 3 +- .../BondValidatorAccModal.tsx | 89 ++++++++++ .../BuyMembershipFormModal.tsx | 159 +++--------------- .../BuyMembershipModal/BuyMembershipModal.tsx | 20 ++- .../BuyMembershipSignModal.tsx | 18 +- 6 files changed, 136 insertions(+), 154 deletions(-) create mode 100644 packages/ui/src/memberships/modals/BuyMembershipModal/BondValidatorAccModal.tsx diff --git a/packages/ui/src/common/components/Modal/Modals.tsx b/packages/ui/src/common/components/Modal/Modals.tsx index 170c128a1d..cbe97c3dcd 100644 --- a/packages/ui/src/common/components/Modal/Modals.tsx +++ b/packages/ui/src/common/components/Modal/Modals.tsx @@ -23,6 +23,7 @@ export const RowInline = styled.div` width: 100%; height: auto; align-items: center; + gap: 2px; ` export const AccountRow = styled.div` diff --git a/packages/ui/src/common/components/forms/Label.tsx b/packages/ui/src/common/components/forms/Label.tsx index 4870e99308..fb7045d834 100644 --- a/packages/ui/src/common/components/forms/Label.tsx +++ b/packages/ui/src/common/components/forms/Label.tsx @@ -4,6 +4,7 @@ import { Colors } from '../../constants' import { TooltipContainer } from '../Tooltip' interface LabelProps { + noMargin?: boolean isRequired?: boolean className?: string } @@ -13,7 +14,7 @@ export const Label = styled.label` align-items: center; align-content: center; width: fit-content; - margin-bottom: 4px; + margin-bottom: ${({ noMargin }) => (noMargin ? '0px' : '4px')}; font-size: 14px; line-height: 20px; font-weight: 700; diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BondValidatorAccModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BondValidatorAccModal.tsx new file mode 100644 index 0000000000..2a3defd815 --- /dev/null +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BondValidatorAccModal.tsx @@ -0,0 +1,89 @@ +import { SubmittableExtrinsic } from '@polkadot/api/types' +import { ISubmittableResult } from '@polkadot/types/types' +import React, { useMemo, useState } from 'react' +import { ActorRef } from 'xstate' + +import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' +import { useBalance } from '@/accounts/hooks/useBalance' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { accountOrNamed } from '@/accounts/model/accountOrNamed' +import { Account } from '@/accounts/types' +import { InputComponent } from '@/common/components/forms' +import { ModalBody, ModalTransactionFooter, Row } from '@/common/components/Modal' +import { TextMedium, TokenValue } from '@/common/components/typography' +import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' +import { TransactionModal } from '@/common/modals/TransactionModal' +import { getFeeSpendableBalance } from '@/common/providers/transactionFees/provider' + +import { MemberFormFields } from './BuyMembershipFormModal' + +interface SignProps { + onClose: () => void + formData: MemberFormFields + transaction: SubmittableExtrinsic<'rxjs', ISubmittableResult> | undefined + initialSigner?: Account + service: ActorRef +} + +export const BondValidatorAccModal = ({ onClose, formData, transaction, initialSigner, service }: SignProps) => { + const { allAccounts } = useMyAccounts() + const [from, setFrom] = useState( + initialSigner ?? accountOrNamed(allAccounts, formData.invitor?.controllerAccount || '', 'Controller account') + ) + const fromAddress = from.address + const { isReady, paymentInfo, sign } = useSignAndSendTransaction({ + transaction, + signer: fromAddress, + service, + }) + const balance = useBalance(fromAddress) + const validationInfo = balance?.transferable && paymentInfo?.partialFee + + const hasFunds = useMemo(() => { + if (validationInfo) { + return getFeeSpendableBalance(balance).gte(paymentInfo?.partialFee) + } + }, [fromAddress, !balance, !validationInfo]) + + const signDisabled = !isReady || !hasFunds || !validationInfo + + return ( + + + You are intending to bond your validator account with your membership + + Fees of will be applied to the transaction. + + + + {initialSigner ? ( + setFrom(account)} /> + ) : ( + + )} + + + + + + ) +} diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 817fef2523..a784f06fd8 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -2,10 +2,9 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { BalanceOf } from '@polkadot/types/interfaces/runtime' import React, { useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' -import styled from 'styled-components' import * as Yup from 'yup' -import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' +import { SelectAccount } from '@/accounts/components/SelectAccount' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' import { Account } from '@/accounts/types' @@ -21,9 +20,7 @@ import { LabelLink, ToggleCheckbox, } from '@/common/components/forms' -import { Arrow, CrossIcon } from '@/common/components/icons' -import { PlusIcon } from '@/common/components/icons/PlusIcon' -import { AlertSymbol } from '@/common/components/icons/symbols' +import { Arrow } from '@/common/components/icons' import { Loading } from '@/common/components/Loading' import { ModalFooter, @@ -97,6 +94,7 @@ export interface MemberFormFields { avatarUri: File | string | null isReferred?: boolean isValidator?: boolean + validatorAccount?: Account referrer?: Member hasTerms?: boolean invitor?: Member @@ -111,6 +109,7 @@ const formDefaultValues = { avatarUri: null, isReferred: false, isValidator: false, + validatorAccount: undefined, referrer: undefined, hasTerms: false, externalResources: {}, @@ -131,8 +130,6 @@ export const BuyMembershipForm = ({ }: BuyMembershipFormProps) => { const { allAccounts } = useMyAccounts() const [formHandleMap, setFormHandleMap] = useState('') - const [stashAccounts, setStashAccounts] = useState([{}]) - const [isAccountAdded, setIsAccountAdded] = useState(true) const { isUploading, uploadAvatarAndSubmit } = useUploadAvatarAndSubmit(onSubmit) const { data } = useGetMembersCountQuery({ variables: { where: { handle_eq: formHandleMap } } }) @@ -147,13 +144,12 @@ export const BuyMembershipForm = ({ }, }) - const [handle, isReferred, isValidator, referrer, captchaToken, stashAccountSelect] = form.watch([ + const [handle, isReferred, isValidator, referrer, captchaToken] = form.watch([ 'handle', 'isReferred', 'isValidator', 'referrer', 'captchaToken', - 'stashAccountSelect', ]) useEffect(() => { @@ -168,29 +164,10 @@ export const BuyMembershipForm = ({ } }, [data?.membershipsConnection.totalCount]) - const isFormValid = !isUploading && form.formState.isValid && (!isValidator || stashAccounts?.length > 1) + const isFormValid = !isUploading && form.formState.isValid const isDisabled = type === 'onBoarding' && process.env.REACT_APP_CAPTCHA_SITE_KEY ? !captchaToken || !isFormValid : !isFormValid - const addStashAccount = () => { - const accountSelection = stashAccountSelect as Account - setStashAccounts((prevStashAccounts) => [...prevStashAccounts, accountSelection]) - form?.setValue('stashAccountSelect' as keyof MemberFormFields, undefined) - } - - const removeStashAccount = (index: number) => { - setStashAccounts((prevAccounts) => prevAccounts.filter((account, ind) => ind !== index)) - } - - useEffect(() => { - const accountSelection = stashAccountSelect as Account - if (stashAccounts.some((account) => account === accountSelection)) { - setIsAccountAdded(true) - } else { - setIsAccountAdded(false) - } - }, [stashAccountSelect]) - return ( <> @@ -283,77 +260,22 @@ export const BuyMembershipForm = ({ {isValidator && ( <> - - - - - - - - - - - {isAccountAdded && ( - - - - - - - This stash account is already added to the list. - - )} - {stashAccounts.length < 2 && ( - - - - - - - You should add at least 1 stash account. - - )} - - - {stashAccounts.map((stashAccount, index) => { - if (index !== 0) { - return ( - - - - - { - removeStashAccount(index) - }} - > - - - - - - ) - } - })} - - - - - - - : {'Unverified'} ! - + + + + + + + + + : {'Unverified'} ! + )} @@ -412,13 +334,6 @@ export const BuyMembershipForm = ({ { - stashAccounts.length > 1 && - stashAccounts.map((account, index) => { - if (index !== 0) { - form?.register(('stashAccounts[' + (index - 1) + ']') as keyof MemberFormFields) - form?.setValue(('stashAccounts[' + (index - 1) + ']') as keyof MemberFormFields, account as Account) - } - }) const values = form.getValues() uploadAvatarAndSubmit({ ...values, externalResources: { ...definedValues(values.externalResources) } }) }} @@ -440,35 +355,3 @@ export const BuyMembershipFormModal = ({ onClose, onSubmit, membershipPrice }: B ) } - -const InputNotificationIcon = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 12px; - height: 12px; - color: inherit; - padding-right: 2px; - - .blackPart, - .primaryPart { - fill: currentColor; - } -` -interface PaddingProps { - pl?: number - pr?: number - pt?: number - pb?: number - width?: number -} - -const BtnWrapper = styled.div` - padding-right: ${({ pr }) => (pr ? pr + 'px' : '0px')}; - padding-left: ${({ pl }) => (pl ? pl + 'px' : '0px')}; - padding-top: ${({ pt }) => (pt ? pt + 'px' : '0px')}; - padding-bottom: ${({ pb }) => (pb ? pb + 'px' : '0px')}; - width: ${({ width }) => (width ? width + 'px' : '0px')}; - display: flex; - justify-content: end; -` diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index b88d4ade23..3e6d8235ad 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -7,6 +7,7 @@ import { useMachine } from '@/common/hooks/useMachine' import { useModal } from '@/common/hooks/useModal' import { toMemberTransactionParams } from '@/memberships/modals/utils' +import { BondValidatorAccModal } from './BondValidatorAccModal' import { BuyMembershipFormModal, MemberFormFields } from './BuyMembershipFormModal' import { BuyMembershipSignModal } from './BuyMembershipSignModal' import { BuyMembershipSuccessModal } from './BuyMembershipSuccessModal' @@ -52,7 +53,24 @@ export const BuyMembershipModal = () => { transaction={transaction} initialSigner={form.controllerAccount} service={service} - bondValidatorAcc={state.matches('bondValidatorAccTx')} + /> + ) + } + + if (state.matches('bondValidatorAccTx') && api && state.context.memberId && state.context.form.validatorAccount) { + const transaction = api.tx.members.updateProfile(state.context.memberId, state.context.form.handle, { + validatorAccout: state.context.form.validatorAccount, + }) + const { form } = state.context + const service = state.children.transaction + + return ( + ) } diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx index eb28ed7fa7..b5b280c384 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx @@ -28,7 +28,6 @@ interface SignProps { transaction: SubmittableExtrinsic<'rxjs', ISubmittableResult> | undefined initialSigner?: Account service: ActorRef - bondValidatorAcc?: boolean } export const BuyMembershipSignModal = ({ @@ -38,7 +37,6 @@ export const BuyMembershipSignModal = ({ transaction, initialSigner, service, - bondValidatorAcc, }: SignProps) => { const { allAccounts } = useMyAccounts() const [from, setFrom] = useState( @@ -79,7 +77,7 @@ export const BuyMembershipSignModal = ({ formData.isValidator ? { steps: [{ title: 'Create Membership' }, { title: 'Bind validator account' }], - active: bondValidatorAcc ? 1 : 0, + active: 0, } : undefined } @@ -87,16 +85,12 @@ export const BuyMembershipSignModal = ({ {formData.isValidator - ? bondValidatorAcc - ? 'You are intending to bond your validator account with your membership' - : 'You intend to create a validator membership.' + ?'You intend to create a validator membership.' : 'You intend to create a new membership.'} - {!bondValidatorAcc && ( The creation of the new membership costs . - )} Fees of will be applied to the transaction. @@ -130,22 +124,18 @@ export const BuyMembershipSignModal = ({ - {!bondValidatorAcc && ( - )} ) From d74e18b960108c93edc58cc056dd5d82b9ab6ab7 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Thu, 14 Sep 2023 10:05:22 -0500 Subject: [PATCH 12/44] update storybook --- packages/ui/src/app/App.stories.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index b09248ba0a..c8ef5caa6f 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -32,6 +32,7 @@ type Args = { hasAccounts: boolean hasWallet: boolean isRPCNodeConnected: boolean +<<<<<<< HEAD hasRegisteredEmail: boolean hasBeenAskedForEmail: boolean subscribeEmailError: boolean @@ -40,6 +41,11 @@ type Args = { onTransfer: jest.Mock onSubscribeEmail: jest.Mock onConfirmEmail: jest.Mock +======= + onBuyMembership: CallableFunction + onUpdateMembership: CallableFunction + onTransfer: CallableFunction +>>>>>>> 42c8ac40 (update storybook) } type Story = StoryObj> @@ -126,6 +132,12 @@ export default { onSend: args.onBuyMembership, failure: parameters.txFailure, }, + updateProfile: { + event: 'MembershipUpdated', + data: [MEMBER_DATA.id], + onSend: args.onUpdateMembership, + failure: parameters.txFailure, + }, }, }, }, From a621b78dccd5e9c3c456a65f878675a36913ef6e Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Thu, 14 Sep 2023 10:54:36 -0500 Subject: [PATCH 13/44] fix mistake --- .../modals/BuyMembershipModal/BuyMembershipModal.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index 3e6d8235ad..b71ca592ba 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -35,12 +35,7 @@ export const BuyMembershipModal = () => { return } - if ( - (state.matches('buyMembershipTx') || - state.matches('buyValidatorMembershipTx') || - state.matches('bondValidatorAccTx')) && - api - ) { + if ((state.matches('buyMembershipTx') || state.matches('buyValidatorMembershipTx')) && api) { const transaction = api.tx.members.buyMembership(toMemberTransactionParams(state.context.form)) const { form } = state.context const service = state.children.transaction From c8c8f7d4607ed8d781fe267d662aca78210cf9d4 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Thu, 14 Sep 2023 11:12:14 -0500 Subject: [PATCH 14/44] lint fix --- .../BuyMembershipSignModal.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx index b5b280c384..d232b747e8 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx @@ -85,12 +85,12 @@ export const BuyMembershipSignModal = ({ {formData.isValidator - ?'You intend to create a validator membership.' + ? 'You intend to create a validator membership.' : 'You intend to create a new membership.'} - - The creation of the new membership costs . - + + The creation of the new membership costs . + Fees of will be applied to the transaction. @@ -125,17 +125,15 @@ export const BuyMembershipSignModal = ({ transactionFee={paymentInfo?.partialFee.toBn()} next={{ disabled: signDisabled, - label: formData.isValidator - ? 'Create membership' - : 'Sign and create a member', + label: formData.isValidator ? 'Create membership' : 'Sign and create a member', onClick: sign, }} > - + ) From bb0ae938b539ca267248e19b84229a06befeb4b4 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Thu, 14 Sep 2023 14:52:53 -0500 Subject: [PATCH 15/44] fix storybook --- packages/ui/src/app/App.stories.tsx | 2 +- .../ui/src/app/pages/Proposals/CurrentProposals.stories.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index c8ef5caa6f..2f7a6ff4ed 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -506,7 +506,7 @@ export const BuyMembershipTxFailure: Story = { await userEvent.click(getButtonByText(modal, 'Sign and create a member')) - expect(await screen.findByText('Failure')) + expect(await modal.findByText('Failure')) expect(await modal.findByText('Some error message')) }, } diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index c5f4dcc469..0160685d05 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -514,7 +514,7 @@ export const NotEnoughFunds: Story = { /^Unfortunately the account associated with the currently selected membership has insufficient balance/ ) ) - expect(modal.getByText('Move funds')) + expect(getButtonByText(modal, 'Move funds')).toBeEnabled() }, } From 20fb31f3254d71a001fc5f01758a9aa83cac757e Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Fri, 15 Sep 2023 04:47:20 -0500 Subject: [PATCH 16/44] add storybook for update validator membership --- packages/ui/src/app/App.stories.tsx | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 2f7a6ff4ed..1381719895 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -511,6 +511,93 @@ export const BuyMembershipTxFailure: Story = { }, } +const fillMembershipFormWithValidatorAcc = async (modal: Container) => { + await fillMembershipForm(modal) + const validatorChechButton = modal.getAllByText('Yes')[1] + await userEvent.click(validatorChechButton) + expect(await modal.findByText('Validator account')) + await selectFromDropdown(modal, 'Validator account', 'alice') +} + +export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { + args: { hasMemberships: false, isLoggedIn: false }, + + play: async ({ args, canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + expect(screen.queryByText('Become a member')).toBeNull() + + await userEvent.click(getButtonByText(screen, 'Join Now')) + + await step('Form', async () => { + const createButton = getButtonByText(modal, 'Create a Membership') + + await step('Fill', async () => { + expect(createButton).toBeDisabled() + await fillMembershipFormWithValidatorAcc(modal) + await waitFor(() => expect(createButton).toBeEnabled()) + }) + + await userEvent.click(createButton) + }) + + await step('Create membership', async () => { + expect(modal.getByText('You intend to create a validator membership.')) + expect(modal.getByText('Creation fee:')?.nextSibling?.textContent).toBe('20') + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Create membership')) + }) + + await step('Bind validator account with membership', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership')) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Sign and Bond')) + }) + + await step('Confirm', async () => { + expect(await modal.findByText('Success')) + expect(modal.getByText(MEMBER_DATA.handle)) + // alert(JSON.stringify(args.onBuyMembership)) + expect(args.onBuyMembership).toHaveBeenCalledWith({ + rootAccount: alice.controllerAccount, + controllerAccount: bob.controllerAccount, + handle: MEMBER_DATA.handle, + metadata: metadataToBytes(MembershipMetadata, { + name: MEMBER_DATA.metadata.name, + about: MEMBER_DATA.metadata.about, + avatarUri: MEMBER_DATA.metadata.avatar.avatarUri, + }), + invitingMemberId: undefined, + referrerId: undefined, + }) + + // expect(args.onUpdateMembership).toHaveBeenCalledWith([MEMBER_DATA.id, MEMBER_DATA.handle, { + // validatorAccount: alice.controllerAccount, + // }]) + + const viewProfileButton = getButtonByText(modal, 'View my profile') + expect(viewProfileButton).toBeEnabled() + userEvent.click(viewProfileButton) + + expect(modal.getByText('Profile')) + expect(modal.getByText(MEMBER_DATA.handle)) + }) + }, +} + +export const BuyMembershipWithValidatorAccountNotEnoughFunds: Story = {} + +export const BuyMembershipTxWithValidatorAccountFailure: Story = {} + +export const BuyMembershipWithValidatorAccountHappyAndBindNotEnoughFunds: Story = {} + +export const BuyMembershipWithValidatorAccountHappyAndBindTxFailure: Story = {} + // ---------------------------------------------------------------------------- // Test Email Subsciption Modal // ---------------------------------------------------------------------------- From 7c76ee655f4ef736a53e25d68b0cca9e150f5f23 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Fri, 15 Sep 2023 07:11:31 -0500 Subject: [PATCH 17/44] Revert "fix storybook" This reverts commit 489d7f6b662c53de4cd043f29a906bf20738d129. --- packages/ui/src/app/App.stories.tsx | 2 +- .../ui/src/app/pages/Proposals/CurrentProposals.stories.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 1381719895..b68ec36d45 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -506,7 +506,7 @@ export const BuyMembershipTxFailure: Story = { await userEvent.click(getButtonByText(modal, 'Sign and create a member')) - expect(await modal.findByText('Failure')) + expect(await screen.findByText('Failure')) expect(await modal.findByText('Some error message')) }, } diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 0160685d05..c5f4dcc469 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -514,7 +514,7 @@ export const NotEnoughFunds: Story = { /^Unfortunately the account associated with the currently selected membership has insufficient balance/ ) ) - expect(getButtonByText(modal, 'Move funds')).toBeEnabled() + expect(modal.getByText('Move funds')) }, } From 12faeaa35140bc9671c86659d538307dc2726b6b Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Fri, 15 Sep 2023 09:12:32 -0500 Subject: [PATCH 18/44] add some happy and failure case --- packages/ui/src/app/App.stories.tsx | 155 ++++++++++++++++-- .../BuyMembershipModal/BuyMembershipModal.tsx | 4 +- 2 files changed, 146 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index b68ec36d45..43608dd142 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -77,6 +77,7 @@ export default { argTypes: { onBuyMembership: { action: 'BuyMembership' }, onTransfer: { action: 'BalanceTransfer' }, + onUpdateMembership: { action: 'UpdateMembership' }, onSubscribeEmail: { action: 'SubscribeEmail' }, onConfirmEmail: { action: 'ConfirmEmail' }, }, @@ -99,7 +100,7 @@ export default { mocks: ({ args, parameters }: StoryContext): MocksParameters => { const account = (member: Membership) => ({ - balances: args.hasFunds ? parameters.totalBalance : 0, + balances: member !== charlie ? (args.hasFunds ? parameters.totalBalance : 0) : 0, ...(args.hasMemberships ? { member } : { account: { name: member.handle, address: member.controllerAccount } }), }) return { @@ -136,7 +137,7 @@ export default { event: 'MembershipUpdated', data: [MEMBER_DATA.id], onSend: args.onUpdateMembership, - failure: parameters.txFailure, + failure: parameters.updateTxFailure, }, }, }, @@ -506,7 +507,7 @@ export const BuyMembershipTxFailure: Story = { await userEvent.click(getButtonByText(modal, 'Sign and create a member')) - expect(await screen.findByText('Failure')) + expect(await modal.findByText('Failure')) expect(await modal.findByText('Some error message')) }, } @@ -562,7 +563,6 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { await step('Confirm', async () => { expect(await modal.findByText('Success')) expect(modal.getByText(MEMBER_DATA.handle)) - // alert(JSON.stringify(args.onBuyMembership)) expect(args.onBuyMembership).toHaveBeenCalledWith({ rootAccount: alice.controllerAccount, controllerAccount: bob.controllerAccount, @@ -576,9 +576,9 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { referrerId: undefined, }) - // expect(args.onUpdateMembership).toHaveBeenCalledWith([MEMBER_DATA.id, MEMBER_DATA.handle, { - // validatorAccount: alice.controllerAccount, - // }]) + expect(args.onUpdateMembership).toHaveBeenCalledWith(MEMBER_DATA.id, MEMBER_DATA.handle, { + validatorAccount: alice.controllerAccount, + }) const viewProfileButton = getButtonByText(modal, 'View my profile') expect(viewProfileButton).toBeEnabled() @@ -590,13 +590,146 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { }, } -export const BuyMembershipWithValidatorAccountNotEnoughFunds: Story = {} +export const BuyMembershipWithValidatorAccountNotEnoughFunds: Story = { + args: { hasMemberships: false, isLoggedIn: false }, + parameters: { totalBalance: 20 }, + + play: async ({ canvasElement }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + await userEvent.click(getButtonByText(screen, 'Join Now')) -export const BuyMembershipTxWithValidatorAccountFailure: Story = {} + await fillMembershipFormWithValidatorAcc(modal) + const createButton = getButtonByText(modal, 'Create a Membership') + await waitFor(() => expect(createButton).toBeEnabled()) + await userEvent.click(createButton) -export const BuyMembershipWithValidatorAccountHappyAndBindNotEnoughFunds: Story = {} + expect(modal.getByText('Insufficient funds to cover the membership creation.')) + expect(getButtonByText(modal, 'Create membership')).toBeDisabled() + }, +} -export const BuyMembershipWithValidatorAccountHappyAndBindTxFailure: Story = {} +export const BuyMembershipTxWithValidatorAccountFailure: Story = { + args: { hasMemberships: false, isLoggedIn: false }, + parameters: { txFailure: 'Some error message' }, + + play: async ({ canvasElement }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + await userEvent.click(getButtonByText(screen, 'Join Now')) + + await fillMembershipFormWithValidatorAcc(modal) + const createButton = getButtonByText(modal, 'Create a Membership') + await waitFor(() => expect(createButton).toBeEnabled()) + await userEvent.click(createButton) + + await userEvent.click(getButtonByText(modal, 'Create membership')) + + expect(await modal.findByText('Failure')) + expect(await modal.findByText('Some error message')) + }, +} + +export const BuyMembershipWithValidatorAccountHappyAndBindNotEnoughFunds: Story = { + args: { hasMemberships: false, isLoggedIn: false }, + parameters: { totalBalance: 25 }, + + play: async ({ args, canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + expect(screen.queryByText('Become a member')).toBeNull() + + await userEvent.click(getButtonByText(screen, 'Join Now')) + + await step('Form', async () => { + const createButton = getButtonByText(modal, 'Create a Membership') + + await step('Fill', async () => { + expect(createButton).toBeDisabled() + await fillMembershipFormWithValidatorAcc(modal) + await waitFor(() => expect(createButton).toBeEnabled()) + }) + + await userEvent.click(createButton) + }) + + await step('Create membership', async () => { + expect(modal.getByText('You intend to create a validator membership.')) + expect(modal.getByText('Creation fee:')?.nextSibling?.textContent).toBe('20') + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Create membership')) + }) + + await step('Not enough funds for update tx', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership')) + await selectFromDropdown(modal, 'Sending from account', 'charlie') + expect(modal.getByText('Insufficient funds to cover the membership creation.')) + expect(getButtonByText(modal, 'Sign and Bond')).toBeDisabled() + }) + + await step('Confirm', async () => { + expect(args.onBuyMembership).toHaveBeenCalledWith({ + rootAccount: alice.controllerAccount, + controllerAccount: bob.controllerAccount, + handle: MEMBER_DATA.handle, + metadata: metadataToBytes(MembershipMetadata, { + name: MEMBER_DATA.metadata.name, + about: MEMBER_DATA.metadata.about, + avatarUri: MEMBER_DATA.metadata.avatar.avatarUri, + }), + invitingMemberId: undefined, + referrerId: undefined, + }) + }) + }, +} + +export const BuyMembershipWithValidatorAccountHappyAndBindTxFailure: Story = { + args: { hasMemberships: false, isLoggedIn: false }, + parameters: { updateTxFailure: 'Some error message' }, + + play: async ({ canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + expect(screen.queryByText('Become a member')).toBeNull() + + await userEvent.click(getButtonByText(screen, 'Join Now')) + + await step('Form', async () => { + const createButton = getButtonByText(modal, 'Create a Membership') + + await step('Fill', async () => { + expect(createButton).toBeDisabled() + await fillMembershipFormWithValidatorAcc(modal) + await waitFor(() => expect(createButton).toBeEnabled()) + }) + + await userEvent.click(createButton) + }) + + await step('Create membership', async () => { + expect(modal.getByText('You intend to create a validator membership.')) + expect(modal.getByText('Creation fee:')?.nextSibling?.textContent).toBe('20') + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Create membership')) + }) + + await step('Bind validator account Tx failure', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership')) + await userEvent.click(getButtonByText(modal, 'Sign and Bond')) + expect(await modal.findByText('Failure')) + expect(await modal.findByText('Some error message')) + }) + }, +} // ---------------------------------------------------------------------------- // Test Email Subsciption Modal diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index b71ca592ba..7b42d80839 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -53,8 +53,8 @@ export const BuyMembershipModal = () => { } if (state.matches('bondValidatorAccTx') && api && state.context.memberId && state.context.form.validatorAccount) { - const transaction = api.tx.members.updateProfile(state.context.memberId, state.context.form.handle, { - validatorAccout: state.context.form.validatorAccount, + const transaction = api.tx.members.updateProfile(state.context.memberId.toString(), state.context.form.handle, { + validatorAccount: state.context.form.validatorAccount.address, }) const { form } = state.context const service = state.children.transaction From d25976ff9c0b06048d954c9ef470da6ea062d6c1 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 6 Nov 2023 18:08:26 -0500 Subject: [PATCH 19/44] Metadata to BYTES for UpdateProfile Tx --- .../modals/BuyMembershipModal/BuyMembershipModal.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index 7b42d80839..ad7091adfa 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -5,6 +5,7 @@ import { useApi } from '@/api/hooks/useApi' import { useFirstObservableValue } from '@/common/hooks/useFirstObservableValue' import { useMachine } from '@/common/hooks/useMachine' import { useModal } from '@/common/hooks/useModal' +import { metadataToBytes } from '@/common/model/JoystreamNode' import { toMemberTransactionParams } from '@/memberships/modals/utils' import { BondValidatorAccModal } from './BondValidatorAccModal' @@ -53,9 +54,13 @@ export const BuyMembershipModal = () => { } if (state.matches('bondValidatorAccTx') && api && state.context.memberId && state.context.form.validatorAccount) { - const transaction = api.tx.members.updateProfile(state.context.memberId.toString(), state.context.form.handle, { - validatorAccount: state.context.form.validatorAccount.address, - }) + const transaction = api.tx.members.updateProfile( + state.context.memberId.toString(), + state.context.form.handle, + metadataToBytes(MembershipMetadata, { + validatorAccount: state.context.form.validatorAccount.address, + }) + ) const { form } = state.context const service = state.children.transaction From 04cdc4423d3588fd871128c09e76331f2a14f70a Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Tue, 21 Nov 2023 07:36:11 -0500 Subject: [PATCH 20/44] buyValidatorMembership flow draft --- .../AddStakingAccCandidateModal.tsx | 89 +++++++++++++++++++ .../BuyMembershipModal/BuyMembershipModal.tsx | 36 ++++++-- ...ccModal.tsx => ConfirmStakingAccModal.tsx} | 2 +- .../modals/BuyMembershipModal/machine.ts | 28 +++++- 4 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx rename packages/ui/src/memberships/modals/BuyMembershipModal/{BondValidatorAccModal.tsx => ConfirmStakingAccModal.tsx} (96%) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx new file mode 100644 index 0000000000..e99154de0a --- /dev/null +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx @@ -0,0 +1,89 @@ +import { SubmittableExtrinsic } from '@polkadot/api/types' +import { ISubmittableResult } from '@polkadot/types/types' +import React, { useMemo, useState } from 'react' +import { ActorRef } from 'xstate' + +import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' +import { useBalance } from '@/accounts/hooks/useBalance' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { accountOrNamed } from '@/accounts/model/accountOrNamed' +import { Account } from '@/accounts/types' +import { InputComponent } from '@/common/components/forms' +import { ModalBody, ModalTransactionFooter, Row } from '@/common/components/Modal' +import { TextMedium, TokenValue } from '@/common/components/typography' +import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' +import { TransactionModal } from '@/common/modals/TransactionModal' +import { getFeeSpendableBalance } from '@/common/providers/transactionFees/provider' + +import { MemberFormFields } from './BuyMembershipFormModal' + +interface SignProps { + onClose: () => void + formData: MemberFormFields + transaction: SubmittableExtrinsic<'rxjs', ISubmittableResult> | undefined + initialSigner?: Account + service: ActorRef +} + +export const AddStakingAccCandidateModal = ({ onClose, formData, transaction, initialSigner, service }: SignProps) => { + const { allAccounts } = useMyAccounts() + const [from, setFrom] = useState( + initialSigner ?? accountOrNamed(allAccounts, formData.invitor?.controllerAccount || '', 'Controller account') + ) + const fromAddress = from.address + const { isReady, paymentInfo, sign } = useSignAndSendTransaction({ + transaction, + signer: fromAddress, + service, + }) + const balance = useBalance(fromAddress) + const validationInfo = balance?.transferable && paymentInfo?.partialFee + + const hasFunds = useMemo(() => { + if (validationInfo) { + return getFeeSpendableBalance(balance).gte(paymentInfo?.partialFee) + } + }, [fromAddress, !balance, !validationInfo]) + + const signDisabled = !isReady || !hasFunds || !validationInfo + + return ( + + + You are intending to bond your validator account with your membership + + Fees of will be applied to the transaction. + + + + {initialSigner ? ( + setFrom(account)} /> + ) : ( + + )} + + + + + + ) +} diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index ad7091adfa..9d06f6c130 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -5,13 +5,13 @@ import { useApi } from '@/api/hooks/useApi' import { useFirstObservableValue } from '@/common/hooks/useFirstObservableValue' import { useMachine } from '@/common/hooks/useMachine' import { useModal } from '@/common/hooks/useModal' -import { metadataToBytes } from '@/common/model/JoystreamNode' import { toMemberTransactionParams } from '@/memberships/modals/utils' -import { BondValidatorAccModal } from './BondValidatorAccModal' +import { AddStakingAccCandidateModal } from './AddStakingAccCandidateModal' import { BuyMembershipFormModal, MemberFormFields } from './BuyMembershipFormModal' import { BuyMembershipSignModal } from './BuyMembershipSignModal' import { BuyMembershipSuccessModal } from './BuyMembershipSuccessModal' +import { ConfirmStakingAccModal } from './ConfirmStakingAccModal' import { buyMembershipMachine } from './machine' export const BuyMembershipModal = () => { @@ -53,19 +53,37 @@ export const BuyMembershipModal = () => { ) } - if (state.matches('bondValidatorAccTx') && api && state.context.memberId && state.context.form.validatorAccount) { - const transaction = api.tx.members.updateProfile( + if ( + state.matches('addStakingAccCandidateTx') && + api && + state.context.memberId && + state.context.form.validatorAccount + ) { + const transaction = api.tx.members.addStakingAccountCandidate(state.context.memberId.toString()) + const { form } = state.context + const service = state.children.transaction + + return ( + + ) + } + + if (state.matches('confirmStakingAccTx') && api && state.context.memberId && state.context.form.validatorAccount) { + const transaction = api.tx.members.confirmStakingAccount( state.context.memberId.toString(), - state.context.form.handle, - metadataToBytes(MembershipMetadata, { - validatorAccount: state.context.form.validatorAccount.address, - }) + state.context.form.validatorAccount.address ) const { form } = state.context const service = state.children.transaction return ( - } -export const BondValidatorAccModal = ({ onClose, formData, transaction, initialSigner, service }: SignProps) => { +export const ConfirmStakingAccModal = ({ onClose, formData, transaction, initialSigner, service }: SignProps) => { const { allAccounts } = useMyAccounts() const [from, setFrom] = useState( initialSigner ?? accountOrNamed(allAccounts, formData.invitor?.controllerAccount || '', 'Controller account') diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index b3054ac664..46a0f4d934 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -25,7 +25,8 @@ type BuyMembershipState = | { value: 'prepare'; context: EmptyObject } | { value: 'buyMembershipTx'; context: { form: MemberFormFields } } | { value: 'buyValidatorMembershipTx'; context: { form: MemberFormFields } } - | { value: 'bondValidatorAccTx'; context: { form: MemberFormFields } } + | { value: 'addStakingAccCandidateTx'; context: { form: MemberFormFields } } + | { value: 'confirmStakingAccTx'; context: { form: MemberFormFields } } | { value: 'success'; context: Required } | { value: 'canceled'; context: Required } | { value: 'error'; context: { form: MemberFormFields; transactionEvents: EventRecord[] } } @@ -83,7 +84,7 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), }), @@ -101,7 +102,28 @@ export const buyMembershipMachine = createMachine event.data.events }), + }, + { + target: 'canceled', + cond: isTransactionCanceled, + }, + ], + }, + }, + confirmStakingAccTx: { invoke: { id: 'transaction', src: transactionMachine, From 18b468bb2315ce491652b697417e353a867ed24f Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Tue, 21 Nov 2023 11:43:28 -0500 Subject: [PATCH 21/44] update machine, modal flow --- .../AddStakingAccCandidateModal.tsx | 2 +- .../BuyMembershipFormModal.tsx | 2 +- .../BuyMembershipModal/BuyMembershipModal.tsx | 22 +++++++++---------- .../ConfirmStakingAccModal.tsx | 4 ++-- .../modals/BuyMembershipModal/machine.ts | 17 ++++++++++++++ 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx index e99154de0a..30aac23058 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx @@ -52,7 +52,7 @@ export const AddStakingAccCandidateModal = ({ onClose, formData, transaction, in onClose={onClose} service={service} useMultiTransaction={{ - steps: [{ title: 'Create Membership' }, { title: 'Bind validator account' }], + steps: [{ title: 'Create Membership' }, { title: 'Bind Validator account' }, { title: 'Confirm Validator Account' }], active: 1, }} > diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index a784f06fd8..94408a577c 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -94,7 +94,7 @@ export interface MemberFormFields { avatarUri: File | string | null isReferred?: boolean isValidator?: boolean - validatorAccount?: Account + validatorAccounts?: Account[] referrer?: Member hasTerms?: boolean invitor?: Member diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index 9d06f6c130..22106db726 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -57,37 +57,37 @@ export const BuyMembershipModal = () => { state.matches('addStakingAccCandidateTx') && api && state.context.memberId && - state.context.form.validatorAccount + state.context.form.validatorAccounts && + state.context.bindingValidtorAccStep ) { const transaction = api.tx.members.addStakingAccountCandidate(state.context.memberId.toString()) - const { form } = state.context const service = state.children.transaction return ( ) } - if (state.matches('confirmStakingAccTx') && api && state.context.memberId && state.context.form.validatorAccount) { - const transaction = api.tx.members.confirmStakingAccount( - state.context.memberId.toString(), - state.context.form.validatorAccount.address + if (state.matches('confirmStakingAccTx') && api && state.context.memberId && state.context.form.validatorAccounts) { + const transaction = api.tx.utility.batch( + state.context.form.validatorAccounts.map(({ address }) => + api.tx.members.confirmStakingAccount(state.context.memberId?.toString() ?? '', address) + ) ) - const { form } = state.context const service = state.children.transaction return ( ) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx index 7321e5a11e..9de2e1e061 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx @@ -52,8 +52,8 @@ export const ConfirmStakingAccModal = ({ onClose, formData, transaction, initial onClose={onClose} service={service} useMultiTransaction={{ - steps: [{ title: 'Create Membership' }, { title: 'Bind validator account' }], - active: 1, + steps: [{ title: 'Create Membership' }, { title: 'Bind Validator account' }, { title: 'Confirm Validator Account' }], + active: 2, }} > diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index 46a0f4d934..d2526c2624 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -19,6 +19,7 @@ interface BuyMembershipContext { form?: MemberFormFields memberId?: BN transactionEvents?: EventRecord[] + bindingValidtorAccStep?: number } type BuyMembershipState = @@ -87,6 +88,7 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), + bindingValidtorAccStep: 0, }), cond: isTransactionSuccess, }, @@ -107,9 +109,19 @@ export const buyMembershipMachine = createMachine event.data.events, + bindingValidtorAccStep: (context, event) => + context.bindingValidtorAccStep ? 1 : context.bindingValidtorAccStep + 1, + }), + }, { target: 'confirmStakingAccTx', cond: isTransactionSuccess, + actions: assign({ transactionEvents: (context, event) => event.data.events }), }, { target: 'error', @@ -134,6 +146,7 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), // }), cond: isTransactionSuccess, + actions: assign({ transactionEvents: (context, event) => event.data.events }), }, { target: 'error', @@ -154,3 +167,7 @@ export const buyMembershipMachine = createMachine + context.form?.validatorAccounts?.length && + (!context.bindingValidtorAccStep || context.form.validatorAccounts.length > context.bindingValidtorAccStep) From 979f7c41eb03db1d289abadd55bd5093819cea35 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Tue, 21 Nov 2023 12:34:16 -0500 Subject: [PATCH 22/44] fix machine --- .../modals/BuyMembershipModal/machine.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index d2526c2624..235d5d7377 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -40,6 +40,10 @@ export type BuyMembershipEvent = | { type: 'SUCCESS'; memberId: BN } | { type: 'ERROR' } +const isSelfTransition = (context: BuyMembershipContext) => + !!context.form?.validatorAccounts && + (!context.bindingValidtorAccStep || context.form.validatorAccounts.length > context.bindingValidtorAccStep) + export const buyMembershipMachine = createMachine({ initial: 'prepare', states: { @@ -88,7 +92,7 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), - bindingValidtorAccStep: 0, + bindingValidtorAccStep: (context) => 0, }), cond: isTransactionSuccess, }, @@ -114,8 +118,7 @@ export const buyMembershipMachine = createMachine event.data.events, - bindingValidtorAccStep: (context, event) => - context.bindingValidtorAccStep ? 1 : context.bindingValidtorAccStep + 1, + bindingValidtorAccStep: (context) => (context.bindingValidtorAccStep ?? 0) + 1, }), }, { @@ -167,7 +170,3 @@ export const buyMembershipMachine = createMachine - context.form?.validatorAccounts?.length && - (!context.bindingValidtorAccStep || context.form.validatorAccounts.length > context.bindingValidtorAccStep) From 7a2254e724904f58d088f59f710a0d5aa6a66faa Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Tue, 21 Nov 2023 13:12:47 -0500 Subject: [PATCH 23/44] address merge conflicts --- packages/ui/src/app/App.stories.tsx | 29 ++++++++----------- .../common/model/JoystreamNode/errorEvents.ts | 2 +- .../AddStakingAccCandidateModal.tsx | 6 +++- .../ConfirmStakingAccModal.tsx | 6 +++- .../modals/BuyMembershipModal/machine.ts | 4 --- packages/ui/test/_mocks/transactions.ts | 2 +- 6 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 43608dd142..e328f6e3de 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -32,7 +32,6 @@ type Args = { hasAccounts: boolean hasWallet: boolean isRPCNodeConnected: boolean -<<<<<<< HEAD hasRegisteredEmail: boolean hasBeenAskedForEmail: boolean subscribeEmailError: boolean @@ -41,11 +40,7 @@ type Args = { onTransfer: jest.Mock onSubscribeEmail: jest.Mock onConfirmEmail: jest.Mock -======= - onBuyMembership: CallableFunction onUpdateMembership: CallableFunction - onTransfer: CallableFunction ->>>>>>> 42c8ac40 (update storybook) } type Story = StoryObj> @@ -135,7 +130,7 @@ export default { }, updateProfile: { event: 'MembershipUpdated', - data: [MEMBER_DATA.id], + data: [NEW_MEMBER_DATA.id], onSend: args.onUpdateMembership, failure: parameters.updateTxFailure, }, @@ -562,21 +557,21 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { await step('Confirm', async () => { expect(await modal.findByText('Success')) - expect(modal.getByText(MEMBER_DATA.handle)) + expect(modal.getByText(NEW_MEMBER_DATA.handle)) expect(args.onBuyMembership).toHaveBeenCalledWith({ rootAccount: alice.controllerAccount, controllerAccount: bob.controllerAccount, - handle: MEMBER_DATA.handle, + handle: NEW_MEMBER_DATA.handle, metadata: metadataToBytes(MembershipMetadata, { - name: MEMBER_DATA.metadata.name, - about: MEMBER_DATA.metadata.about, - avatarUri: MEMBER_DATA.metadata.avatar.avatarUri, + name: NEW_MEMBER_DATA.metadata.name, + about: NEW_MEMBER_DATA.metadata.about, + avatarUri: NEW_MEMBER_DATA.metadata.avatar.avatarUri, }), invitingMemberId: undefined, referrerId: undefined, }) - expect(args.onUpdateMembership).toHaveBeenCalledWith(MEMBER_DATA.id, MEMBER_DATA.handle, { + expect(args.onUpdateMembership).toHaveBeenCalledWith(NEW_MEMBER_DATA.id, NEW_MEMBER_DATA.handle, { validatorAccount: alice.controllerAccount, }) @@ -585,7 +580,7 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { userEvent.click(viewProfileButton) expect(modal.getByText('Profile')) - expect(modal.getByText(MEMBER_DATA.handle)) + expect(modal.getByText(NEW_MEMBER_DATA.handle)) }) }, } @@ -676,11 +671,11 @@ export const BuyMembershipWithValidatorAccountHappyAndBindNotEnoughFunds: Story expect(args.onBuyMembership).toHaveBeenCalledWith({ rootAccount: alice.controllerAccount, controllerAccount: bob.controllerAccount, - handle: MEMBER_DATA.handle, + handle: NEW_MEMBER_DATA.handle, metadata: metadataToBytes(MembershipMetadata, { - name: MEMBER_DATA.metadata.name, - about: MEMBER_DATA.metadata.about, - avatarUri: MEMBER_DATA.metadata.avatar.avatarUri, + name: NEW_MEMBER_DATA.metadata.name, + about: NEW_MEMBER_DATA.metadata.about, + avatarUri: NEW_MEMBER_DATA.metadata.avatar.avatarUri, }), invitingMemberId: undefined, referrerId: undefined, diff --git a/packages/ui/src/common/model/JoystreamNode/errorEvents.ts b/packages/ui/src/common/model/JoystreamNode/errorEvents.ts index 4263841661..c1d4e9bf2b 100644 --- a/packages/ui/src/common/model/JoystreamNode/errorEvents.ts +++ b/packages/ui/src/common/model/JoystreamNode/errorEvents.ts @@ -1,6 +1,6 @@ +import { RegistryError } from '@polkadot/types-codec/types' import { EventRecord } from '@polkadot/types/interfaces/system' import { SpRuntimeDispatchError } from '@polkadot/types/lookup' -import { RegistryError } from '@polkadot/types-codec/types' import { isModuleEvent } from './isModuleEvent' diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx index 30aac23058..8f89e9a183 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx @@ -52,7 +52,11 @@ export const AddStakingAccCandidateModal = ({ onClose, formData, transaction, in onClose={onClose} service={service} useMultiTransaction={{ - steps: [{ title: 'Create Membership' }, { title: 'Bind Validator account' }, { title: 'Confirm Validator Account' }], + steps: [ + { title: 'Create Membership' }, + { title: 'Bind Validator account' }, + { title: 'Confirm Validator Account' }, + ], active: 1, }} > diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx index 9de2e1e061..42f53cd92a 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx @@ -52,7 +52,11 @@ export const ConfirmStakingAccModal = ({ onClose, formData, transaction, initial onClose={onClose} service={service} useMultiTransaction={{ - steps: [{ title: 'Create Membership' }, { title: 'Bind Validator account' }, { title: 'Confirm Validator Account' }], + steps: [ + { title: 'Create Membership' }, + { title: 'Bind Validator account' }, + { title: 'Confirm Validator Account' }, + ], active: 2, }} > diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index 235d5d7377..58c1db6813 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -92,7 +92,6 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), - bindingValidtorAccStep: (context) => 0, }), cond: isTransactionSuccess, }, @@ -145,9 +144,6 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), - // }), cond: isTransactionSuccess, actions: assign({ transactionEvents: (context, event) => event.data.events }), }, diff --git a/packages/ui/test/_mocks/transactions.ts b/packages/ui/test/_mocks/transactions.ts index 4c92635d56..aebe969ab3 100644 --- a/packages/ui/test/_mocks/transactions.ts +++ b/packages/ui/test/_mocks/transactions.ts @@ -1,5 +1,5 @@ -import { AugmentedEvents } from '@polkadot/api/types' import { DeriveBalancesAll } from '@polkadot/api-derive/types' +import { AugmentedEvents } from '@polkadot/api/types' import { AnyTuple } from '@polkadot/types/types' import BN from 'bn.js' import { set } from 'lodash' From 83a93a1ae956fe5cc41ac41f7b6f457fd432713b Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Tue, 21 Nov 2023 13:23:46 -0500 Subject: [PATCH 24/44] lint --fix --- packages/ui/src/common/model/JoystreamNode/errorEvents.ts | 2 +- packages/ui/test/_mocks/transactions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/common/model/JoystreamNode/errorEvents.ts b/packages/ui/src/common/model/JoystreamNode/errorEvents.ts index c1d4e9bf2b..4263841661 100644 --- a/packages/ui/src/common/model/JoystreamNode/errorEvents.ts +++ b/packages/ui/src/common/model/JoystreamNode/errorEvents.ts @@ -1,6 +1,6 @@ -import { RegistryError } from '@polkadot/types-codec/types' import { EventRecord } from '@polkadot/types/interfaces/system' import { SpRuntimeDispatchError } from '@polkadot/types/lookup' +import { RegistryError } from '@polkadot/types-codec/types' import { isModuleEvent } from './isModuleEvent' diff --git a/packages/ui/test/_mocks/transactions.ts b/packages/ui/test/_mocks/transactions.ts index aebe969ab3..4c92635d56 100644 --- a/packages/ui/test/_mocks/transactions.ts +++ b/packages/ui/test/_mocks/transactions.ts @@ -1,5 +1,5 @@ -import { DeriveBalancesAll } from '@polkadot/api-derive/types' import { AugmentedEvents } from '@polkadot/api/types' +import { DeriveBalancesAll } from '@polkadot/api-derive/types' import { AnyTuple } from '@polkadot/types/types' import BN from 'bn.js' import { set } from 'lodash' From 06d5006b510f819deddd78fca25ceebaf75e3248 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Wed, 22 Nov 2023 04:07:29 -0500 Subject: [PATCH 25/44] update MembershipForm, UpdateMembershipForm --- .../ui/src/common/components/Modal/Modals.tsx | 4 +- .../BuyMembershipFormModal.tsx | 86 ++++++--- .../UpdateMembershipFormModal.tsx | 177 ++++++------------ .../modals/UpdateMembershipModal/types.ts | 4 +- 4 files changed, 121 insertions(+), 150 deletions(-) diff --git a/packages/ui/src/common/components/Modal/Modals.tsx b/packages/ui/src/common/components/Modal/Modals.tsx index cbe97c3dcd..ce652a7199 100644 --- a/packages/ui/src/common/components/Modal/Modals.tsx +++ b/packages/ui/src/common/components/Modal/Modals.tsx @@ -17,13 +17,13 @@ export const Row = styled.div` height: auto; ` -export const RowInline = styled.div` +export const RowInline = styled.div<{ gap?: number }>` display: flex; flex-direction: row; width: 100%; height: auto; align-items: center; - gap: 2px; + gap: ${({ gap }) => gap ?? 16}px; ` export const AccountRow = styled.div` diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 94408a577c..a87a362c65 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import * as Yup from 'yup' -import { SelectAccount } from '@/accounts/components/SelectAccount' +import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' import { Account } from '@/accounts/types' @@ -20,7 +20,9 @@ import { LabelLink, ToggleCheckbox, } from '@/common/components/forms' -import { Arrow } from '@/common/components/icons' +import { Arrow, CrossIcon } from '@/common/components/icons' +import { PlusIcon } from '@/common/components/icons/PlusIcon' +// import { AlertSymbol } from '@/common/components/icons/symbols' import { Loading } from '@/common/components/Loading' import { ModalFooter, @@ -86,14 +88,13 @@ const CreateMemberSchema = Yup.object().shape({ export interface MemberFormFields { rootAccount?: Account controllerAccount?: Account - stashAccountSelect?: Account - stashAccounts?: Account[] name: string handle: string about: string avatarUri: File | string | null isReferred?: boolean isValidator?: boolean + validatorAccountCandidate?: Account validatorAccounts?: Account[] referrer?: Member hasTerms?: boolean @@ -109,7 +110,6 @@ const formDefaultValues = { avatarUri: null, isReferred: false, isValidator: false, - validatorAccount: undefined, referrer: undefined, hasTerms: false, externalResources: {}, @@ -144,14 +144,17 @@ export const BuyMembershipForm = ({ }, }) - const [handle, isReferred, isValidator, referrer, captchaToken] = form.watch([ + const [handle, isReferred, isValidator, referrer, captchaToken, validatorAccountCandidate] = form.watch([ 'handle', 'isReferred', 'isValidator', 'referrer', 'captchaToken', + 'validatorAccountCandidate', ]) + const [validatorAccounts, setValidatorAccounts] = useState([]) + useEffect(() => { if (handle) { setFormHandleMap(handle) @@ -164,10 +167,21 @@ export const BuyMembershipForm = ({ } }, [data?.membershipsConnection.totalCount]) - const isFormValid = !isUploading && form.formState.isValid + const isFormValid = !isUploading && form.formState.isValid && (!isValidator || validatorAccounts?.length) const isDisabled = type === 'onBoarding' && process.env.REACT_APP_CAPTCHA_SITE_KEY ? !captchaToken || !isFormValid : !isFormValid + const addValidatorAccount = () => { + if (validatorAccountCandidate) { + setValidatorAccounts([...new Set([validatorAccountCandidate, ...validatorAccounts])]) + form?.setValue('validatorAccountCandidate' as keyof MemberFormFields, undefined) + } + } + + const removeValidatorAccount = (index: number) => { + setValidatorAccounts([...validatorAccounts.slice(0, index), ...validatorAccounts.slice(index + 1)]) + } + return ( <> @@ -260,22 +274,46 @@ export const BuyMembershipForm = ({ {isValidator && ( <> - - - + + + + + + + + + + + {validatorAccounts.map((account, index) => ( + + + + { + removeValidatorAccount(index) + }} + > + + + + + ))} + + + + + + + : {'Unverified'} ! + - - - - - - : {'Unverified'} ! - )} @@ -334,6 +372,10 @@ export const BuyMembershipForm = ({ { + validatorAccounts?.map((account, index) => { + form?.register(('validatorAccounts[' + index + ']') as keyof MemberFormFields) + form?.setValue(('validatorAccounts[' + index + ']') as keyof MemberFormFields, account) + }) const values = form.getValues() uploadAvatarAndSubmit({ ...values, externalResources: { ...definedValues(values.externalResources) } }) }} diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx index 56560caeaf..ae2535792e 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react' import { useForm, FormProvider } from 'react-hook-form' -import styled from 'styled-components' import * as Yup from 'yup' import { AnySchema } from 'yup' @@ -9,10 +8,16 @@ import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' import { Account } from '@/accounts/types' import { ButtonPrimary, ButtonGhost } from '@/common/components/buttons' -import { InlineToggleWrap, InputComponent, InputText, InputTextarea, ToggleCheckbox } from '@/common/components/forms' +import { + InlineToggleWrap, + InputComponent, + InputText, + InputTextarea, + Label, + ToggleCheckbox, +} from '@/common/components/forms' import { CrossIcon } from '@/common/components/icons' import { PlusIcon } from '@/common/components/icons/PlusIcon' -import { AlertSymbol } from '@/common/components/icons/symbols' import { Loading } from '@/common/components/Loading' import { ModalHeader, @@ -24,7 +29,7 @@ import { ScrolledModalContainer, } from '@/common/components/Modal' import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' -import { Label, TextMedium, TextSmall } from '@/common/components/typography' +import { TextMedium, TextSmall } from '@/common/components/typography' import { WithNullableValues } from '@/common/types/form' import { definedValues } from '@/common/utils' import { useYupValidationResolver } from '@/common/utils/validation' @@ -77,11 +82,6 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) ) ) ) - // isValidator, stashAccounts should be read from member - // const isValidator = member.isValidator ?? false - // const stashAccounts = member.stashAccounts ?? [] - const [stashAccounts, setStashAccounts] = useState([{}]) - const [isAccountAdded, setIsAccountAdded] = useState(true) const form = useForm({ resolver: useYupValidationResolver(UpdateMemberSchema), @@ -94,14 +94,16 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) mode: 'onChange', }) - const [controllerAccount, rootAccount, handle, stashAccountSelect, isValidator] = form.watch([ + const [controllerAccount, rootAccount, handle, isValidator, validatorAccountCandidate] = form.watch([ 'controllerAccount', 'rootAccount', 'handle', - 'stashAccountSelect', 'isValidator', + 'validatorAccountCandidate', ]) + const [validatorAccounts, setValidatorAccounts] = useState([]) + useEffect(() => { form.trigger('handle') }, [JSON.stringify(context)]) @@ -116,26 +118,19 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) const canUpdate = form.formState.isValid && hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) && - (!isValidator || stashAccounts?.length > 1) + (!isValidator || validatorAccounts?.length) - const addStashAccount = () => { - const accountSelection = stashAccountSelect as Account - setStashAccounts((prevStashAccounts) => [...prevStashAccounts, accountSelection]) - form?.setValue('stashAccountSelect' as keyof UpdateMemberForm, undefined) + const addValidatorAccount = () => { + if (validatorAccountCandidate) { + setValidatorAccounts([...new Set([validatorAccountCandidate, ...validatorAccounts])]) + form?.setValue('validatorAccountCandidate' as keyof UpdateMemberForm, undefined) + } } - const removeStashAccount = (index: number) => { - setStashAccounts((prevAccounts) => prevAccounts.filter((account, ind) => ind !== index)) + const removeValidatorAccount = (index: number) => { + setValidatorAccounts([...validatorAccounts.slice(0, index), ...validatorAccounts.slice(index + 1)]) } - useEffect(() => { - const accountSelection = stashAccountSelect as Account - if (stashAccounts.some((account) => account === accountSelection)) { - setIsAccountAdded(true) - } else { - setIsAccountAdded(false) - } - }, [stashAccountSelect]) return ( @@ -204,70 +199,39 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) <> - - + + - - - - - + + + - {isAccountAdded && ( - - - - - - - This stash account is already added to the list. - - )} - {stashAccounts.length < 2 && ( - - - - - - - You should add at least 1 stash account. - - )} - {stashAccounts.map((stashAccount, index) => { - if (index !== 0) { - return ( - - - - - { - removeStashAccount(index) - }} - > - - - - - - ) - } - })} + {validatorAccounts.map((account, index) => ( + + + + { + removeValidatorAccount(index) + }} + > + + + + + ))} - - + + @@ -284,13 +248,10 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) disabled: !canUpdate || isUploading, label: isUploading ? : 'Save changes', onClick: () => { - stashAccounts.length > 1 && - stashAccounts.map((account, index) => { - if (index !== 0) { - form?.register(('stashAccounts[' + (index - 1) + ']') as keyof UpdateMemberForm) - form?.setValue(('stashAccounts[' + (index - 1) + ']') as keyof UpdateMemberForm, account as Account) - } - }) + validatorAccounts?.map((account, index) => { + form?.register(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm) + form?.setValue(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm, account) + }) uploadAvatarAndSubmit(form.getValues()) }, }} @@ -298,35 +259,3 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) ) } - -const InputNotificationIcon = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 12px; - height: 12px; - color: inherit; - padding-right: 2px; - - .blackPart, - .primaryPart { - fill: currentColor; - } -` -interface PaddingProps { - pl?: number - pr?: number - pt?: number - pb?: number - width?: number -} - -const BtnWrapper = styled.div` - padding-right: ${({ pr }) => (pr ? pr + 'px' : '0px')}; - padding-left: ${({ pl }) => (pl ? pl + 'px' : '0px')}; - padding-top: ${({ pt }) => (pt ? pt + 'px' : '0px')}; - padding-bottom: ${({ pb }) => (pb ? pb + 'px' : '0px')}; - width: ${({ width }) => (width ? width + 'px' : '0px')}; - display: flex; - justify-content: end; -` diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts index 41ad0a88b8..271bd8e71a 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts @@ -10,6 +10,6 @@ export interface UpdateMemberForm { controllerAccount?: Account externalResources: Record isValidator?: boolean - stashAccountSelect?: Account - stashAccounts: Account[] + validatorAccountCandidate?: Account + validatorAccounts: Account[] } From e6a1e66d3201047250c6cd7edda8fee2db89b5b4 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Wed, 22 Nov 2023 10:23:06 -0500 Subject: [PATCH 26/44] update interaction test --- packages/ui/src/app/App.stories.tsx | 128 +++++++++++------- .../ui/src/common/components/Modal/Modals.tsx | 3 +- .../AddStakingAccCandidateModal.tsx | 2 +- .../BuyMembershipFormModal.tsx | 34 +++-- .../BuyMembershipModal/BuyMembershipModal.tsx | 5 +- .../ConfirmStakingAccModal.tsx | 4 +- .../modals/BuyMembershipModal/machine.ts | 5 +- 7 files changed, 117 insertions(+), 64 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index e328f6e3de..2c2eb9bd8e 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -40,7 +40,9 @@ type Args = { onTransfer: jest.Mock onSubscribeEmail: jest.Mock onConfirmEmail: jest.Mock - onUpdateMembership: CallableFunction + onAddStakingAccount: jest.Mock + onConfirmStakingAccount: jest.Mock + batchTx: jest.Mock } type Story = StoryObj> @@ -48,6 +50,7 @@ type Story = StoryObj> const alice = member('alice') const bob = member('bob') const charlie = member('charlie') +const dave = member('dave') const NEW_MEMBER_DATA = { id: alice.id, // we set this to alice's ID so that after member is created, member with same ID can be found in MembershipContext @@ -72,9 +75,11 @@ export default { argTypes: { onBuyMembership: { action: 'BuyMembership' }, onTransfer: { action: 'BalanceTransfer' }, - onUpdateMembership: { action: 'UpdateMembership' }, onSubscribeEmail: { action: 'SubscribeEmail' }, onConfirmEmail: { action: 'ConfirmEmail' }, + onAddStakingAccount: { action: 'AddStakingAccount' }, + onConfirmStakingAccount: { action: 'ConfirmStakingAccount' }, + batchTx: { action: 'BatchTx' }, }, args: { @@ -95,13 +100,16 @@ export default { mocks: ({ args, parameters }: StoryContext): MocksParameters => { const account = (member: Membership) => ({ - balances: member !== charlie ? (args.hasFunds ? parameters.totalBalance : 0) : 0, + balances: member !== dave ? (args.hasFunds ? parameters.totalBalance : 0) : 0, ...(args.hasMemberships ? { member } : { account: { name: member.handle, address: member.controllerAccount } }), }) return { accounts: { active: args.isLoggedIn ? 'alice' : undefined, - list: args.hasMemberships || args.hasAccounts ? [account(alice), account(bob), account(charlie)] : [], + list: + args.hasMemberships || args.hasAccounts + ? [account(alice), account(bob), account(charlie), account(dave)] + : [], hasWallet: args.hasWallet, }, @@ -128,11 +136,24 @@ export default { onSend: args.onBuyMembership, failure: parameters.txFailure, }, - updateProfile: { - event: 'MembershipUpdated', + addStakingAccountCandidate: { + event: 'StakingAccountAdded', data: [NEW_MEMBER_DATA.id], - onSend: args.onUpdateMembership, - failure: parameters.updateTxFailure, + onSend: args.onAddStakingAccount, + failure: parameters.addStakingAccountTxFailure, + }, + confirmStakingAccount: { + event: 'StakingAccountConfirmed', + data: [NEW_MEMBER_DATA.id], + onSend: args.onConfirmStakingAccount, + failure: parameters.confirmStakingAccountTxFailure, + }, + }, + utility: { + batch: { + event: 'TxBatch', + onSend: args.batchTx, + failure: parameters.batchTxFailure, }, }, }, @@ -511,8 +532,11 @@ const fillMembershipFormWithValidatorAcc = async (modal: Container) => { await fillMembershipForm(modal) const validatorChechButton = modal.getAllByText('Yes')[1] await userEvent.click(validatorChechButton) - expect(await modal.findByText('Validator account')) - await selectFromDropdown(modal, 'Validator account', 'alice') + expect(await modal.findByText(/^If your validator account/)) + await selectFromDropdown(modal, /^If your validator account/, 'charlie') + // await selectFromDropdown(modal, /^If your validator account/, 'dave') + const addButton = document.getElementsByClassName('add-button')[0] + await userEvent.click(addButton) } export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { @@ -547,14 +571,24 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { await userEvent.click(getButtonByText(modal, 'Create membership')) }) - await step('Bind validator account with membership', async () => { - expect(await modal.findByText('You are intending to bond your validator account with your membership')) + await step('Add validator account', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership.')) expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') - expect(modal.getByRole('heading', { name: 'bob' })) + expect(modal.getByRole('heading', { name: 'charlie' })) await userEvent.click(getButtonByText(modal, 'Sign and Bond')) }) + await step('Confirm validator account', async () => { + expect( + await modal.findByText('You are intending to confirm your validator account to be bound with your membership') + ) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Sign and Confirm')) + }) + await step('Confirm', async () => { expect(await modal.findByText('Success')) expect(modal.getByText(NEW_MEMBER_DATA.handle)) @@ -571,16 +605,13 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { referrerId: undefined, }) - expect(args.onUpdateMembership).toHaveBeenCalledWith(NEW_MEMBER_DATA.id, NEW_MEMBER_DATA.handle, { - validatorAccount: alice.controllerAccount, - }) + expect(args.onAddStakingAccount).toHaveBeenCalledWith(NEW_MEMBER_DATA.id) - const viewProfileButton = getButtonByText(modal, 'View my profile') - expect(viewProfileButton).toBeEnabled() - userEvent.click(viewProfileButton) + expect(args.batchTx).toHaveBeenCalledTimes(1) - expect(modal.getByText('Profile')) - expect(modal.getByText(NEW_MEMBER_DATA.handle)) + const doneButton = getButtonByText(modal, 'Done') + expect(doneButton).toBeEnabled() + userEvent.click(doneButton) }) }, } @@ -627,11 +658,11 @@ export const BuyMembershipTxWithValidatorAccountFailure: Story = { }, } -export const BuyMembershipWithValidatorAccountHappyAndBindNotEnoughFunds: Story = { +export const BuyMembershipHappyAddValidatorAccFailure: Story = { args: { hasMemberships: false, isLoggedIn: false }, - parameters: { totalBalance: 25 }, + parameters: { addStakingAccountTxFailure: 'Some error message' }, - play: async ({ args, canvasElement, step }) => { + play: async ({ canvasElement, step }) => { const screen = within(canvasElement) const modal = withinModal(canvasElement) @@ -660,33 +691,22 @@ export const BuyMembershipWithValidatorAccountHappyAndBindNotEnoughFunds: Story await userEvent.click(getButtonByText(modal, 'Create membership')) }) - await step('Not enough funds for update tx', async () => { - expect(await modal.findByText('You are intending to bond your validator account with your membership')) - await selectFromDropdown(modal, 'Sending from account', 'charlie') - expect(modal.getByText('Insufficient funds to cover the membership creation.')) - expect(getButtonByText(modal, 'Sign and Bond')).toBeDisabled() - }) + await step('Add Validator Account Tx Failure', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership.')) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'charlie' })) - await step('Confirm', async () => { - expect(args.onBuyMembership).toHaveBeenCalledWith({ - rootAccount: alice.controllerAccount, - controllerAccount: bob.controllerAccount, - handle: NEW_MEMBER_DATA.handle, - metadata: metadataToBytes(MembershipMetadata, { - name: NEW_MEMBER_DATA.metadata.name, - about: NEW_MEMBER_DATA.metadata.about, - avatarUri: NEW_MEMBER_DATA.metadata.avatar.avatarUri, - }), - invitingMemberId: undefined, - referrerId: undefined, - }) + await userEvent.click(getButtonByText(modal, 'Sign and Bond')) + + expect(await modal.findByText('Failure')) + expect(await modal.findByText('Some error message')) }) }, } -export const BuyMembershipWithValidatorAccountHappyAndBindTxFailure: Story = { +export const BuyMembershipAddValidatorAccHappyConfirmTxFailure: Story = { args: { hasMemberships: false, isLoggedIn: false }, - parameters: { updateTxFailure: 'Some error message' }, + parameters: { batchTxFailure: 'Some error message' }, play: async ({ canvasElement, step }) => { const screen = within(canvasElement) @@ -717,9 +737,23 @@ export const BuyMembershipWithValidatorAccountHappyAndBindTxFailure: Story = { await userEvent.click(getButtonByText(modal, 'Create membership')) }) - await step('Bind validator account Tx failure', async () => { - expect(await modal.findByText('You are intending to bond your validator account with your membership')) + await step('Add validator account', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership.')) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'charlie' })) + await userEvent.click(getButtonByText(modal, 'Sign and Bond')) + }) + + await step('Confirm validator account', async () => { + expect( + await modal.findByText('You are intending to confirm your validator account to be bound with your membership') + ) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Sign and Confirm')) + expect(await modal.findByText('Failure')) expect(await modal.findByText('Some error message')) }) diff --git a/packages/ui/src/common/components/Modal/Modals.tsx b/packages/ui/src/common/components/Modal/Modals.tsx index ce652a7199..8fb3bb58c1 100644 --- a/packages/ui/src/common/components/Modal/Modals.tsx +++ b/packages/ui/src/common/components/Modal/Modals.tsx @@ -17,13 +17,14 @@ export const Row = styled.div` height: auto; ` -export const RowInline = styled.div<{ gap?: number }>` +export const RowInline = styled.div<{ gap?: number; top?: number }>` display: flex; flex-direction: row; width: 100%; height: auto; align-items: center; gap: ${({ gap }) => gap ?? 16}px; + margin-top: ${({ top }) => top ?? 0}px; ` export const AccountRow = styled.div` diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx index 8f89e9a183..a8bd0c382d 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx @@ -61,7 +61,7 @@ export const AddStakingAccCandidateModal = ({ onClose, formData, transaction, in }} > - You are intending to bond your validator account with your membership + You are intending to bond your validator account with your membership. Fees of will be applied to the transaction. diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index a87a362c65..2ee4e4d8fb 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -2,6 +2,7 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { BalanceOf } from '@polkadot/types/interfaces/runtime' import React, { useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' +import styled from 'styled-components' import * as Yup from 'yup' import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' @@ -265,15 +266,24 @@ export const BuyMembershipForm = ({ {type === 'general' && ( <> - - - - - - + + + + {isValidator && ( <> - + + + + + + + * + + + If your validator account is not in your signer wallet, paste the account address to the field + below: + @@ -283,11 +293,12 @@ export const BuyMembershipForm = ({ size="large" onClick={addValidatorAccount} disabled={!validatorAccountCandidate} + className="add-button" > - + {validatorAccounts.map((account, index) => ( @@ -397,3 +408,10 @@ export const BuyMembershipFormModal = ({ onClose, onSubmit, membershipPrice }: B ) } + +const SelectValidatorAccountWrapper = styled.div` + margin-top: -4px; + display: flex; + flex-direction: column; + gap: 8px; +` diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index 22106db726..dc652f2f23 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -57,8 +57,7 @@ export const BuyMembershipModal = () => { state.matches('addStakingAccCandidateTx') && api && state.context.memberId && - state.context.form.validatorAccounts && - state.context.bindingValidtorAccStep + state.context.form.validatorAccounts ) { const transaction = api.tx.members.addStakingAccountCandidate(state.context.memberId.toString()) const service = state.children.transaction @@ -68,7 +67,7 @@ export const BuyMembershipModal = () => { onClose={hideModal} formData={state.context.form} transaction={transaction} - initialSigner={state.context.form.validatorAccounts[state.context.bindingValidtorAccStep]} + initialSigner={state.context.form.validatorAccounts[state.context.bindingValidtorAccStep ?? 0]} service={service} /> ) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx index 42f53cd92a..f4a26da7e1 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx @@ -61,7 +61,7 @@ export const ConfirmStakingAccModal = ({ onClose, formData, transaction, initial }} > - You are intending to bond your validator account with your membership + You are intending to confirm your validator account to be bound with your membership Fees of will be applied to the transaction. @@ -84,7 +84,7 @@ export const ConfirmStakingAccModal = ({ onClose, formData, transaction, initial transactionFee={paymentInfo?.partialFee.toBn()} next={{ disabled: signDisabled, - label: 'Sign and Bond', + label: 'Sign and Confirm', onClick: sign, }} > diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index 58c1db6813..b685673b67 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -41,8 +41,9 @@ export type BuyMembershipEvent = | { type: 'ERROR' } const isSelfTransition = (context: BuyMembershipContext) => - !!context.form?.validatorAccounts && - (!context.bindingValidtorAccStep || context.form.validatorAccounts.length > context.bindingValidtorAccStep) + context.form?.validatorAccounts && + context.form?.validatorAccounts.length > 1 && + (!context.bindingValidtorAccStep || context.form.validatorAccounts.length - 1 > context.bindingValidtorAccStep) export const buyMembershipMachine = createMachine({ initial: 'prepare', From b3369078fb2d989bde8d07431d80c5e49185b356 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Wed, 22 Nov 2023 10:44:26 -0500 Subject: [PATCH 27/44] fix machine self transition condition, correct PlusIcon import --- packages/ui/src/common/components/icons/index.ts | 1 + .../modals/BuyMembershipModal/BuyMembershipFormModal.tsx | 4 +--- .../ui/src/memberships/modals/BuyMembershipModal/machine.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/common/components/icons/index.ts b/packages/ui/src/common/components/icons/index.ts index 1c0c748405..9c1d757280 100644 --- a/packages/ui/src/common/components/icons/index.ts +++ b/packages/ui/src/common/components/icons/index.ts @@ -33,3 +33,4 @@ export * from './ApplicationIcon' export * from './CouncilMemberIcon' export * from './VerifiedMemberIcon' export * from './MenuIcon' +export * from './PlusIcon' diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 2ee4e4d8fb..62773ee369 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -21,9 +21,7 @@ import { LabelLink, ToggleCheckbox, } from '@/common/components/forms' -import { Arrow, CrossIcon } from '@/common/components/icons' -import { PlusIcon } from '@/common/components/icons/PlusIcon' -// import { AlertSymbol } from '@/common/components/icons/symbols' +import { Arrow, CrossIcon, PlusIcon } from '@/common/components/icons' import { Loading } from '@/common/components/Loading' import { ModalFooter, diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index b685673b67..568ef9ee65 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -41,7 +41,7 @@ export type BuyMembershipEvent = | { type: 'ERROR' } const isSelfTransition = (context: BuyMembershipContext) => - context.form?.validatorAccounts && + !!context.form?.validatorAccounts && context.form?.validatorAccounts.length > 1 && (!context.bindingValidtorAccStep || context.form.validatorAccounts.length - 1 > context.bindingValidtorAccStep) From 1c6cea5332721659cbd921ecd7393707d7470681 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Fri, 8 Dec 2023 13:33:52 -0500 Subject: [PATCH 28/44] fix signer, update test --- packages/ui/src/app/App.stories.tsx | 184 ++++++++++++++++-- .../AddStakingAccCandidateModal.tsx | 81 ++------ .../BuyMembershipFormModal.tsx | 12 +- .../BuyMembershipModal/BuyMembershipModal.tsx | 62 +++--- .../BuyMembershipSignModal.tsx | 6 +- .../ConfirmStakingAccModal.tsx | 81 ++------ .../modals/BuyMembershipModal/machine.ts | 4 +- 7 files changed, 244 insertions(+), 186 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 2c2eb9bd8e..2ef29d99fb 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -100,7 +100,7 @@ export default { mocks: ({ args, parameters }: StoryContext): MocksParameters => { const account = (member: Membership) => ({ - balances: member !== dave ? (args.hasFunds ? parameters.totalBalance : 0) : 0, + balances: args.hasFunds ? parameters.totalBalance : 0, ...(args.hasMemberships ? { member } : { account: { name: member.handle, address: member.controllerAccount } }), }) return { @@ -134,7 +134,7 @@ export default { event: 'MembershipBought', data: [NEW_MEMBER_DATA.id], onSend: args.onBuyMembership, - failure: parameters.txFailure, + failure: parameters.buyMembershipTxFailure, }, addStakingAccountCandidate: { event: 'StakingAccountAdded', @@ -508,7 +508,7 @@ export const BuyMembershipNotEnoughFund: Story = { export const BuyMembershipTxFailure: Story = { args: { hasMemberships: false, isLoggedIn: false }, - parameters: { txFailure: 'Some error message' }, + parameters: { BuyMembershipTxFailure: 'Some error message' }, play: async ({ canvasElement }) => { const screen = within(canvasElement) @@ -528,18 +528,24 @@ export const BuyMembershipTxFailure: Story = { }, } -const fillMembershipFormWithValidatorAcc = async (modal: Container) => { +const fillMembershipFormWithOneValidatorAcc = async (modal: Container) => { await fillMembershipForm(modal) const validatorChechButton = modal.getAllByText('Yes')[1] await userEvent.click(validatorChechButton) expect(await modal.findByText(/^If your validator account/)) await selectFromDropdown(modal, /^If your validator account/, 'charlie') - // await selectFromDropdown(modal, /^If your validator account/, 'dave') const addButton = document.getElementsByClassName('add-button')[0] await userEvent.click(addButton) } -export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { +const fillMembershipFormWithTwoValidatorAcc = async (modal: Container) => { + await fillMembershipFormWithOneValidatorAcc(modal) + await selectFromDropdown(modal, /^If your validator account/, 'dave') + const addButton = document.getElementsByClassName('add-button')[0] + await userEvent.click(addButton) +} + +export const BuyMembershipHappyBindOneValidatorHappy: Story = { args: { hasMemberships: false, isLoggedIn: false }, play: async ({ args, canvasElement, step }) => { @@ -555,7 +561,7 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { await step('Fill', async () => { expect(createButton).toBeDisabled() - await fillMembershipFormWithValidatorAcc(modal) + await fillMembershipFormWithOneValidatorAcc(modal) await waitFor(() => expect(createButton).toBeEnabled()) }) @@ -604,10 +610,91 @@ export const BuyMembershipWithValidatorAccountHappyAndBindHappy: Story = { invitingMemberId: undefined, referrerId: undefined, }) - expect(args.onAddStakingAccount).toHaveBeenCalledWith(NEW_MEMBER_DATA.id) + expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(NEW_MEMBER_DATA.id, charlie.controllerAccount) + + const doneButton = getButtonByText(modal, 'Done') + expect(doneButton).toBeEnabled() + userEvent.click(doneButton) + }) + }, +} + +export const BuyMembershipHappyAddTwoValidatorHappy: Story = { + args: { hasMemberships: false, isLoggedIn: false }, + + play: async ({ args, canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + expect(screen.queryByText('Become a member')).toBeNull() + + await userEvent.click(getButtonByText(screen, 'Join Now')) + + await step('Form', async () => { + const createButton = getButtonByText(modal, 'Create a Membership') + + await step('Fill', async () => { + expect(createButton).toBeDisabled() + await fillMembershipFormWithTwoValidatorAcc(modal) + await waitFor(() => expect(createButton).toBeEnabled()) + }) + + await userEvent.click(createButton) + }) + + await step('Create membership', async () => { + expect(modal.getByText('You intend to create a validator membership.')) + expect(modal.getByText('Creation fee:')?.nextSibling?.textContent).toBe('20') + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Create membership')) + }) + + await step('Add first validator account', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership.')) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'charlie' })) - expect(args.batchTx).toHaveBeenCalledTimes(1) + await userEvent.click(getButtonByText(modal, 'Sign and Bond')) + }) + + await step('Add second validator account', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership.')) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'dave' })) + + await userEvent.click(getButtonByText(modal, 'Sign and Bond')) + }) + + await step('Confirm validator account', async () => { + expect( + await modal.findByText('You are intending to confirm your validator account to be bound with your membership') + ) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Sign and Confirm')) + }) + + await step('Confirm', async () => { + expect(await modal.findByText('Success')) + expect(modal.getByText(NEW_MEMBER_DATA.handle)) + expect(args.onBuyMembership).toHaveBeenCalledWith({ + rootAccount: alice.controllerAccount, + controllerAccount: bob.controllerAccount, + handle: NEW_MEMBER_DATA.handle, + metadata: metadataToBytes(MembershipMetadata, { + name: NEW_MEMBER_DATA.metadata.name, + about: NEW_MEMBER_DATA.metadata.about, + avatarUri: NEW_MEMBER_DATA.metadata.avatar.avatarUri, + }), + invitingMemberId: undefined, + referrerId: undefined, + }) + expect(args.onAddStakingAccount).toHaveBeenCalledTimes(4) // means 2 times, due to the React hook 'useMomo' + expect(args.batchTx).toHaveBeenCalledTimes(2) const doneButton = getButtonByText(modal, 'Done') expect(doneButton).toBeEnabled() @@ -626,7 +713,7 @@ export const BuyMembershipWithValidatorAccountNotEnoughFunds: Story = { await userEvent.click(getButtonByText(screen, 'Join Now')) - await fillMembershipFormWithValidatorAcc(modal) + await fillMembershipFormWithOneValidatorAcc(modal) const createButton = getButtonByText(modal, 'Create a Membership') await waitFor(() => expect(createButton).toBeEnabled()) await userEvent.click(createButton) @@ -636,9 +723,9 @@ export const BuyMembershipWithValidatorAccountNotEnoughFunds: Story = { }, } -export const BuyMembershipTxWithValidatorAccountFailure: Story = { +export const BuyMembershipWithValidatorAccountFailure: Story = { args: { hasMemberships: false, isLoggedIn: false }, - parameters: { txFailure: 'Some error message' }, + parameters: { buyMembershipTxFailure: 'Some error message' }, play: async ({ canvasElement }) => { const screen = within(canvasElement) @@ -646,7 +733,7 @@ export const BuyMembershipTxWithValidatorAccountFailure: Story = { await userEvent.click(getButtonByText(screen, 'Join Now')) - await fillMembershipFormWithValidatorAcc(modal) + await fillMembershipFormWithOneValidatorAcc(modal) const createButton = getButtonByText(modal, 'Create a Membership') await waitFor(() => expect(createButton).toBeEnabled()) await userEvent.click(createButton) @@ -658,7 +745,7 @@ export const BuyMembershipTxWithValidatorAccountFailure: Story = { }, } -export const BuyMembershipHappyAddValidatorAccFailure: Story = { +export const BuyMembershipHappyAddOneValidatorFailure: Story = { args: { hasMemberships: false, isLoggedIn: false }, parameters: { addStakingAccountTxFailure: 'Some error message' }, @@ -675,7 +762,7 @@ export const BuyMembershipHappyAddValidatorAccFailure: Story = { await step('Fill', async () => { expect(createButton).toBeDisabled() - await fillMembershipFormWithValidatorAcc(modal) + await fillMembershipFormWithOneValidatorAcc(modal) await waitFor(() => expect(createButton).toBeEnabled()) }) @@ -706,7 +793,7 @@ export const BuyMembershipHappyAddValidatorAccFailure: Story = { export const BuyMembershipAddValidatorAccHappyConfirmTxFailure: Story = { args: { hasMemberships: false, isLoggedIn: false }, - parameters: { batchTxFailure: 'Some error message' }, + parameters: { confirmStakingAccountTxFailure: 'Some error message' }, play: async ({ canvasElement, step }) => { const screen = within(canvasElement) @@ -721,7 +808,7 @@ export const BuyMembershipAddValidatorAccHappyConfirmTxFailure: Story = { await step('Fill', async () => { expect(createButton).toBeDisabled() - await fillMembershipFormWithValidatorAcc(modal) + await fillMembershipFormWithOneValidatorAcc(modal) await waitFor(() => expect(createButton).toBeEnabled()) }) @@ -760,6 +847,69 @@ export const BuyMembershipAddValidatorAccHappyConfirmTxFailure: Story = { }, } +export const BuyMembershipAddTwoValidatorAccHappyConfirmTxFailure: Story = { + args: { hasMemberships: false, isLoggedIn: false }, + parameters: { batchTxFailure: 'Some error message' }, + + play: async ({ canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + expect(screen.queryByText('Become a member')).toBeNull() + + await userEvent.click(getButtonByText(screen, 'Join Now')) + + await step('Form', async () => { + const createButton = getButtonByText(modal, 'Create a Membership') + + await step('Fill', async () => { + expect(createButton).toBeDisabled() + await fillMembershipFormWithTwoValidatorAcc(modal) + await waitFor(() => expect(createButton).toBeEnabled()) + }) + + await userEvent.click(createButton) + }) + + await step('Create membership', async () => { + expect(modal.getByText('You intend to create a validator membership.')) + expect(modal.getByText('Creation fee:')?.nextSibling?.textContent).toBe('20') + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Create membership')) + }) + + await step('Add first validator account', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership.')) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'charlie' })) + + await userEvent.click(getButtonByText(modal, 'Sign and Bond')) + }) + + await step('Add second validator account', async () => { + expect(await modal.findByText('You are intending to bond your validator account with your membership.')) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'dave' })) + + await userEvent.click(getButtonByText(modal, 'Sign and Bond')) + }) + + await step('Confirm validator account', async () => { + expect( + await modal.findByText('You are intending to confirm your validator account to be bound with your membership') + ) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'bob' })) + + await userEvent.click(getButtonByText(modal, 'Sign and Confirm')) + + expect(await modal.findByText('Failure')) + expect(await modal.findByText('Some error message')) + }) + }, +} // ---------------------------------------------------------------------------- // Test Email Subsciption Modal // ---------------------------------------------------------------------------- diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx index a8bd0c382d..a036a2fc2e 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx @@ -1,93 +1,46 @@ import { SubmittableExtrinsic } from '@polkadot/api/types' import { ISubmittableResult } from '@polkadot/types/types' -import React, { useMemo, useState } from 'react' +import React from 'react' import { ActorRef } from 'xstate' -import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' -import { useBalance } from '@/accounts/hooks/useBalance' -import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' -import { accountOrNamed } from '@/accounts/model/accountOrNamed' import { Account } from '@/accounts/types' -import { InputComponent } from '@/common/components/forms' -import { ModalBody, ModalTransactionFooter, Row } from '@/common/components/Modal' import { TextMedium, TokenValue } from '@/common/components/typography' import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' -import { TransactionModal } from '@/common/modals/TransactionModal' -import { getFeeSpendableBalance } from '@/common/providers/transactionFees/provider' - -import { MemberFormFields } from './BuyMembershipFormModal' +import { SignTransactionModal } from '@/common/modals/SignTransactionModal/SignTransactionModal' interface SignProps { - onClose: () => void - formData: MemberFormFields transaction: SubmittableExtrinsic<'rxjs', ISubmittableResult> | undefined - initialSigner?: Account + signer: Account service: ActorRef } -export const AddStakingAccCandidateModal = ({ onClose, formData, transaction, initialSigner, service }: SignProps) => { - const { allAccounts } = useMyAccounts() - const [from, setFrom] = useState( - initialSigner ?? accountOrNamed(allAccounts, formData.invitor?.controllerAccount || '', 'Controller account') - ) - const fromAddress = from.address - const { isReady, paymentInfo, sign } = useSignAndSendTransaction({ +export const AddStakingAccCandidateModal = ({ transaction, signer, service }: SignProps) => { + const { paymentInfo } = useSignAndSendTransaction({ transaction, - signer: fromAddress, + signer: signer.address, service, }) - const balance = useBalance(fromAddress) - const validationInfo = balance?.transferable && paymentInfo?.partialFee - - const hasFunds = useMemo(() => { - if (validationInfo) { - return getFeeSpendableBalance(balance).gte(paymentInfo?.partialFee) - } - }, [fromAddress, !balance, !validationInfo]) - - const signDisabled = !isReady || !hasFunds || !validationInfo return ( - - - You are intending to bond your validator account with your membership. - - Fees of will be applied to the transaction. - - - - {initialSigner ? ( - setFrom(account)} /> - ) : ( - - )} - - - - - + You are intending to bond your validator account with your membership. + + Fees of will be applied to the transaction. + + ) } diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 62773ee369..fd2ab59e49 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -70,7 +70,6 @@ const isRequired = 'This field is required.' const CreateMemberSchema = Yup.object().shape({ rootAccount: AccountSchema.required(isRequired), controllerAccount: AccountSchema.required(isRequired), - stashAccountSelect: AccountSchema, avatarUri: AvatarURISchema, name: Yup.string().required(isRequired), handle: HandleSchema.required(isRequired).matches( @@ -172,7 +171,7 @@ export const BuyMembershipForm = ({ const addValidatorAccount = () => { if (validatorAccountCandidate) { - setValidatorAccounts([...new Set([validatorAccountCandidate, ...validatorAccounts])]) + setValidatorAccounts([...new Set([...validatorAccounts, validatorAccountCandidate])]) form?.setValue('validatorAccountCandidate' as keyof MemberFormFields, undefined) } } @@ -314,15 +313,6 @@ export const BuyMembershipForm = ({ ))} - - - - - - - : {'Unverified'} ! - - )} diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index dc652f2f23..d310e7478f 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -1,5 +1,5 @@ import { useApolloClient } from '@apollo/client' -import React, { useEffect } from 'react' +import React, { useEffect, useMemo } from 'react' import { useApi } from '@/api/hooks/useApi' import { useFirstObservableValue } from '@/common/hooks/useFirstObservableValue' @@ -29,15 +29,39 @@ export const BuyMembershipModal = () => { apolloClient.refetchQueries({ include: 'active' }) }, [isSuccessful, apolloClient]) + const buyTransaction = useMemo( + () => state.context.form && api?.tx.members.buyMembership(toMemberTransactionParams(state.context.form)), + [api?.isConnected, state.context.form] + ) + const bindTransaction = useMemo( + () => state.context.memberId && api?.tx.members.addStakingAccountCandidate(state.context.memberId.toString()), + [state.context, state.context.memberId] + ) + const conFirmTransaction = useMemo( + () => + state.context.memberId && + state.context.form?.validatorAccounts && + (state.context.form.validatorAccounts.length > 1 + ? api?.tx.utility.batch( + state.context.form.validatorAccounts.map(({ address }) => + api.tx.members.confirmStakingAccount(state.context.memberId?.toString() ?? '', address) + ) + ) + : api?.tx.members.confirmStakingAccount( + state.context.memberId.toString(), + state.context.form.validatorAccounts[0].address + )), + [api?.isConnected, state.context.memberId, state.context.form?.validatorAccounts] + ) + if (state.matches('prepare')) { const onSubmit = (params: MemberFormFields) => - send({ type: params.isValidator ? 'DONEWITHVAL' : 'DONE', form: params }) + send({ type: params.isValidator ? 'DONE_IS_VALIDATOR' : 'DONE', form: params }) return } - if ((state.matches('buyMembershipTx') || state.matches('buyValidatorMembershipTx')) && api) { - const transaction = api.tx.members.buyMembership(toMemberTransactionParams(state.context.form)) + if ((state.matches('buyMembershipTx') || state.matches('buyValidatorMembershipTx')) && buyTransaction) { const { form } = state.context const service = state.children.transaction @@ -46,47 +70,31 @@ export const BuyMembershipModal = () => { onClose={hideModal} membershipPrice={membershipPrice} formData={form} - transaction={transaction} + transaction={buyTransaction} initialSigner={form.controllerAccount} service={service} /> ) } - if ( - state.matches('addStakingAccCandidateTx') && - api && - state.context.memberId && - state.context.form.validatorAccounts - ) { - const transaction = api.tx.members.addStakingAccountCandidate(state.context.memberId.toString()) + if (state.matches('addStakingAccCandidateTx') && bindTransaction && state.context.form.validatorAccounts) { const service = state.children.transaction return ( ) } - if (state.matches('confirmStakingAccTx') && api && state.context.memberId && state.context.form.validatorAccounts) { - const transaction = api.tx.utility.batch( - state.context.form.validatorAccounts.map(({ address }) => - api.tx.members.confirmStakingAccount(state.context.memberId?.toString() ?? '', address) - ) - ) + if (state.matches('confirmStakingAccTx') && conFirmTransaction && state.context.form.controllerAccount) { const service = state.children.transaction - return ( ) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx index d232b747e8..86577ba0ef 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSignModal.tsx @@ -76,7 +76,11 @@ export const BuyMembershipSignModal = ({ useMultiTransaction={ formData.isValidator ? { - steps: [{ title: 'Create Membership' }, { title: 'Bind validator account' }], + steps: [ + { title: 'Create Membership' }, + { title: 'Bind Validator Account' }, + { title: 'Confirm Validator Account' }, + ], active: 0, } : undefined diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx index f4a26da7e1..ed7a19d5bd 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx @@ -1,93 +1,46 @@ import { SubmittableExtrinsic } from '@polkadot/api/types' import { ISubmittableResult } from '@polkadot/types/types' -import React, { useMemo, useState } from 'react' +import React from 'react' import { ActorRef } from 'xstate' -import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' -import { useBalance } from '@/accounts/hooks/useBalance' -import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' -import { accountOrNamed } from '@/accounts/model/accountOrNamed' import { Account } from '@/accounts/types' -import { InputComponent } from '@/common/components/forms' -import { ModalBody, ModalTransactionFooter, Row } from '@/common/components/Modal' import { TextMedium, TokenValue } from '@/common/components/typography' import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' -import { TransactionModal } from '@/common/modals/TransactionModal' -import { getFeeSpendableBalance } from '@/common/providers/transactionFees/provider' - -import { MemberFormFields } from './BuyMembershipFormModal' +import { SignTransactionModal } from '@/common/modals/SignTransactionModal/SignTransactionModal' interface SignProps { - onClose: () => void - formData: MemberFormFields transaction: SubmittableExtrinsic<'rxjs', ISubmittableResult> | undefined - initialSigner?: Account + signer: Account service: ActorRef } -export const ConfirmStakingAccModal = ({ onClose, formData, transaction, initialSigner, service }: SignProps) => { - const { allAccounts } = useMyAccounts() - const [from, setFrom] = useState( - initialSigner ?? accountOrNamed(allAccounts, formData.invitor?.controllerAccount || '', 'Controller account') - ) - const fromAddress = from.address - const { isReady, paymentInfo, sign } = useSignAndSendTransaction({ +export const ConfirmStakingAccModal = ({ transaction, signer, service }: SignProps) => { + const { paymentInfo } = useSignAndSendTransaction({ transaction, - signer: fromAddress, + signer: signer.address, service, }) - const balance = useBalance(fromAddress) - const validationInfo = balance?.transferable && paymentInfo?.partialFee - - const hasFunds = useMemo(() => { - if (validationInfo) { - return getFeeSpendableBalance(balance).gte(paymentInfo?.partialFee) - } - }, [fromAddress, !balance, !validationInfo]) - - const signDisabled = !isReady || !hasFunds || !validationInfo return ( - - - You are intending to confirm your validator account to be bound with your membership - - Fees of will be applied to the transaction. - - - - {initialSigner ? ( - setFrom(account)} /> - ) : ( - - )} - - - - - + You are intending to confirm your validator account to be bound with your membership + + Fees of will be applied to the transaction. + + ) } diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index 568ef9ee65..b53f0a1377 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -36,7 +36,7 @@ export type BuyMembershipEvent = | { type: 'PASS' } | { type: 'FAIL' } | { type: 'DONE'; form: MemberFormFields } - | { type: 'DONEWITHVAL'; form: MemberFormFields } + | { type: 'DONE_IS_VALIDATOR'; form: MemberFormFields } | { type: 'SUCCESS'; memberId: BN } | { type: 'ERROR' } @@ -54,7 +54,7 @@ export const buyMembershipMachine = createMachine event.form }), }, - DONEWITHVAL: { + DONE_IS_VALIDATOR: { target: 'buyValidatorMembershipTx', actions: assign({ form: (_, event) => event.form }), }, From 3346b386f3685be1ea4fa3fd986e6a92b3d6aec3 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Fri, 8 Dec 2023 13:42:34 -0500 Subject: [PATCH 29/44] revert changes on UpdateMembership --- .../UpdateMembershipFormModal.tsx | 105 +----------------- .../modals/UpdateMembershipModal/types.ts | 3 - 2 files changed, 6 insertions(+), 102 deletions(-) diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx index ae2535792e..5279349702 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx @@ -3,33 +3,20 @@ import { useForm, FormProvider } from 'react-hook-form' import * as Yup from 'yup' import { AnySchema } from 'yup' -import { filterAccount, SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' +import { filterAccount, SelectAccount } from '@/accounts/components/SelectAccount' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' -import { Account } from '@/accounts/types' -import { ButtonPrimary, ButtonGhost } from '@/common/components/buttons' -import { - InlineToggleWrap, - InputComponent, - InputText, - InputTextarea, - Label, - ToggleCheckbox, -} from '@/common/components/forms' -import { CrossIcon } from '@/common/components/icons' -import { PlusIcon } from '@/common/components/icons/PlusIcon' +import { InputComponent, InputText, InputTextarea } from '@/common/components/forms' import { Loading } from '@/common/components/Loading' import { ModalHeader, ModalTransactionFooter, Row, - RowInline, ScrolledModal, ScrolledModalBody, ScrolledModalContainer, } from '@/common/components/Modal' -import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' -import { TextMedium, TextSmall } from '@/common/components/typography' +import { TextMedium } from '@/common/components/typography' import { WithNullableValues } from '@/common/types/form' import { definedValues } from '@/common/utils' import { useYupValidationResolver } from '@/common/utils/validation' @@ -94,15 +81,7 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) mode: 'onChange', }) - const [controllerAccount, rootAccount, handle, isValidator, validatorAccountCandidate] = form.watch([ - 'controllerAccount', - 'rootAccount', - 'handle', - 'isValidator', - 'validatorAccountCandidate', - ]) - - const [validatorAccounts, setValidatorAccounts] = useState([]) + const [controllerAccount, rootAccount, handle] = form.watch(['controllerAccount', 'rootAccount', 'handle']) useEffect(() => { form.trigger('handle') @@ -115,21 +94,7 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) const filterRoot = useCallback(filterAccount(controllerAccount), [controllerAccount]) const filterController = useCallback(filterAccount(rootAccount), [rootAccount]) - const canUpdate = - form.formState.isValid && - hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) && - (!isValidator || validatorAccounts?.length) - - const addValidatorAccount = () => { - if (validatorAccountCandidate) { - setValidatorAccounts([...new Set([validatorAccountCandidate, ...validatorAccounts])]) - form?.setValue('validatorAccountCandidate' as keyof UpdateMemberForm, undefined) - } - } - - const removeValidatorAccount = (index: number) => { - setValidatorAccounts([...validatorAccounts.slice(0, index), ...validatorAccounts.slice(index + 1)]) - } + const canUpdate = form.formState.isValid && hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) return ( @@ -188,58 +153,6 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) member.externalResources ? member.externalResources.map((resource) => resource.source) : [] } /> - - - - - - - - {isValidator && ( - <> - - - - - - - - - - - - {validatorAccounts.map((account, index) => ( - - - - { - removeValidatorAccount(index) - }} - > - - - - - ))} - - - - - - - : {'Unverified'} ! - - - - )} @@ -247,13 +160,7 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) next={{ disabled: !canUpdate || isUploading, label: isUploading ? : 'Save changes', - onClick: () => { - validatorAccounts?.map((account, index) => { - form?.register(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm) - form?.setValue(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm, account) - }) - uploadAvatarAndSubmit(form.getValues()) - }, + onClick: () => uploadAvatarAndSubmit(form.getValues()), }} /> diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts index 271bd8e71a..de145adda7 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts @@ -9,7 +9,4 @@ export interface UpdateMemberForm { rootAccount?: Account controllerAccount?: Account externalResources: Record - isValidator?: boolean - validatorAccountCandidate?: Account - validatorAccounts: Account[] } From 8fbb7aa283375fbcd0b1f5dc1965a4a70213c781 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Fri, 8 Dec 2023 13:55:37 -0500 Subject: [PATCH 30/44] fix --- packages/ui/src/app/App.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index d262a9ff4c..474b453baf 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -510,7 +510,7 @@ export const BuyMembershipNotEnoughFund: Story = { export const BuyMembershipTxFailure: Story = { args: { hasMemberships: false, isLoggedIn: false }, - parameters: { BuyMembershipTxFailure: 'Some error message' }, + parameters: { buyMembershipTxFailure: 'Some error message' }, play: async ({ canvasElement }) => { const screen = within(canvasElement) From d3182d216120ca7125c9bf313eea8a5b5f7c6af5 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 18 Dec 2023 11:21:48 -0500 Subject: [PATCH 31/44] update app.stories.tsx with validator provider mocking --- packages/ui/.storybook/preview.tsx | 21 +- packages/ui/src/app/App.stories.tsx | 74 +++- packages/ui/src/app/Providers.tsx | 35 +- .../pages/Profile/MyMemberships.stories.tsx | 321 ++++++++++++++++++ .../Validators/ValidatorList.stories.tsx | 9 +- packages/ui/src/common/components/Warning.tsx | 4 +- .../components/MemberListItem/MyMember.tsx | 2 +- .../BuyMembershipFormModal.tsx | 2 +- .../UpdateMembershipFormModal.tsx | 167 +++++++-- .../modals/UpdateMembershipModal/types.ts | 4 + .../modals/UpdateMembershipModal/utils.ts | 18 +- .../ui/src/validators/hooks/useValidators.ts | 5 + .../validators/hooks/useValidatorsList.tsx | 4 +- .../ui/src/validators/providers/context.tsx | 5 + .../provider.tsx} | 28 +- 15 files changed, 641 insertions(+), 58 deletions(-) create mode 100644 packages/ui/src/app/pages/Profile/MyMemberships.stories.tsx create mode 100644 packages/ui/src/validators/hooks/useValidators.ts create mode 100644 packages/ui/src/validators/providers/context.tsx rename packages/ui/src/validators/{hooks/useValidatorMembers.tsx => providers/provider.tsx} (78%) diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index 810cd97b2c..4e7742838b 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -12,6 +12,7 @@ import { NotificationsHolder } from '../src/common/components/page/SideNotificat import { TransactionStatus } from '../src/common/components/TransactionStatus/TransactionStatus' import { Colors } from '../src/common/constants' import { ModalContextProvider } from '../src/common/providers/modal/provider' +import { ValidatorContextProvider } from '../src/validators/providers/provider' import { TransactionStatusProvider } from '../src/common/providers/transactionStatus/provider' import { MockProvidersDecorator, MockRouterDecorator } from '../src/mocks/providers' import { i18next } from '../src/services/i18n' @@ -54,15 +55,17 @@ const RHFDecorator: Decorator = (Story) => { const ModalDecorator: Decorator = (Story) => ( - - - - - - - - - + + + + + + + + + + + ) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 474b453baf..9d1d17963e 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -10,7 +10,7 @@ import { createGlobalStyle } from 'styled-components' import { Page, Screen } from '@/common/components/page/Page' import { Colors } from '@/common/constants' import { EMAIL_VERIFICATION_TOKEN_SEARCH_PARAM } from '@/memberships/constants' -import { GetMemberDocument } from '@/memberships/queries' +import { GetMemberDocument, GetMembersWithDetailsDocument } from '@/memberships/queries' import { ConfirmBackendEmailDocument, GetBackendMemberExistsDocument, @@ -122,6 +122,72 @@ export default { members: { membershipPrice: joy(20) }, council: { stage: { stage: { isIdle: true }, changedAt: 123 } }, referendum: { stage: {} }, + + staking: { + bonded: { + multi: [ + 'j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D', + 'j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW', + 'j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP', + 'j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz', + 'j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa', + 'j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN', + 'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP', + 'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ', + 'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg', + 'j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt', + 'j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM', + ], + }, + validators: { + entries: [ + [ + { args: ['j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW'] }, + { commission: 0.1 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz'] }, + { commission: 0.15 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa'] }, + { commission: 0.2 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN'] }, + { commission: 0.01 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP'] }, + { commission: 0.03 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + ], + }, + }, }, tx: { @@ -171,6 +237,10 @@ export default { query: GetBackendMemberExistsDocument, data: { memberExist: args.hasRegisteredEmail }, }, + { + query: GetMembersWithDetailsDocument, + data: { memberships: [member('alice'), member('bob'), member('charlie'), member('dave')] }, + }, ], mutations: [ { @@ -695,7 +765,7 @@ export const BuyMembershipHappyAddTwoValidatorHappy: Story = { invitingMemberId: undefined, referrerId: undefined, }) - expect(args.onAddStakingAccount).toHaveBeenCalledTimes(4) // means 2 times, due to the React hook 'useMomo' + expect(args.onAddStakingAccount).toHaveBeenCalledTimes(4) // means 2 times expect(args.batchTx).toHaveBeenCalledTimes(2) const doneButton = getButtonByText(modal, 'Done') diff --git a/packages/ui/src/app/Providers.tsx b/packages/ui/src/app/Providers.tsx index ded0ae48bf..5b2dcdc770 100644 --- a/packages/ui/src/app/Providers.tsx +++ b/packages/ui/src/app/Providers.tsx @@ -13,6 +13,7 @@ import { OnBoardingProvider } from '@/common/providers/onboarding/provider' import { ResponsiveProvider } from '@/common/providers/responsive/provider' import { TransactionStatusProvider } from '@/common/providers/transactionStatus/provider' import { MembershipContextProvider } from '@/memberships/providers/membership/provider' +import { ValidatorContextProvider } from '@/validators/providers/provider' import { BackendProvider } from './providers/backend/provider' import { GlobalStyle } from './providers/GlobalStyle' @@ -31,22 +32,24 @@ export const Providers = ({ children }: Props) => ( - - - - - - - - - {children} - - - - - - - + + + + + + + + + + {children} + + + + + + + + diff --git a/packages/ui/src/app/pages/Profile/MyMemberships.stories.tsx b/packages/ui/src/app/pages/Profile/MyMemberships.stories.tsx new file mode 100644 index 0000000000..cbf804b91f --- /dev/null +++ b/packages/ui/src/app/pages/Profile/MyMemberships.stories.tsx @@ -0,0 +1,321 @@ +import { expect } from '@storybook/jest' +import { Meta, StoryContext, StoryObj } from '@storybook/react' +import { userEvent, waitFor, within } from '@storybook/testing-library' +import { FC } from 'react' + +import { + GetMemberActionDetailsDocument, + GetMemberDocument, + GetMembersCountDocument, + GetMembersWithDetailsDocument, +} from '@/memberships/queries' +import { Membership, member } from '@/mocks/data/members' +import { Container, getButtonByText, joy, selectFromDropdown, withinModal } from '@/mocks/helpers' +import { MocksParameters } from '@/mocks/providers' + +import { MyMemberships } from './MyMemberships' + +const alice = member('alice') +const bob = member('bob') +const charlie = member('charlie') + +const NEW_MEMBER_DATA = { + id: alice.id, + handle: 'realbobbybob', + metadata: { + name: 'BobbyBob', + about: 'Lorem ipsum...', + avatar: { avatarUri: 'https://api.dicebear.com/6.x/bottts-neutral/svg?seed=bob' }, + }, +} + +type Args = { + onUpdateProfile: jest.Mock + onUpdateAccounts: jest.Mock + onAddStakingAccount: jest.Mock + onConfirmStakingAccount: jest.Mock + onRemoveStakingAccount: jest.Mock + batchTx: jest.Mock +} + +export default { + title: 'Pages/MyProfile/MyMemberships', + component: MyMemberships, + + argTypes: { + onUpdateProfile: { action: 'UpdateProfile' }, + onUpdateAccounts: { action: 'UpdateAccounts' }, + onAddStakingAccount: { action: 'AddStakingAccount' }, + onConfirmStakingAccount: { action: 'ConfirmStakingAccount' }, + onRemoveStakingAccount: { action: 'RemoveStakingAccount' }, + batchTx: { action: 'BatchTx' }, + }, + + parameters: { + totalBalance: 100, + router: { + href: '/profile/memberships', + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + mocks: ({ args, parameters }: StoryContext): MocksParameters => { + const account = (member: Membership) => ({ + balances: parameters.totalBalance, + ...{ member }, + }) + return { + accounts: { + active: 'alice', + list: [account(alice), account(bob), account(charlie)], + hasWallet: true, + }, + chain: { + query: { + members: { membershipPrice: joy(20) }, + membershipWorkingGroup: { budget: joy(166666_66) }, + staking: { + bonded: { + multi: [ + 'j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D', + 'j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW', + 'j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP', + 'j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz', + 'j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa', + 'j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN', + 'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP', + 'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ', + 'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg', + 'j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt', + 'j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM', + ], + }, + validators: { + entries: [ + [ + { args: ['j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW'] }, + { commission: 0.1 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz'] }, + { commission: 0.15 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa'] }, + { commission: 0.2 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN'] }, + { commission: 0.01 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP'] }, + { commission: 0.03 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + ], + }, + }, + }, + derive: { + balances: { + all: { + freeBalance: 1000, + reservedBalance: 1000, + availableBalance: 1000, + lockedBalance: 1000, + lockedBreakdown: [], + vestingLocked: 1000, + isVesting: false, + vestedBalance: joy(1666_66), + vestedClaimable: joy(1666_66), + vesting: [], + vestingTotal: joy(1666_66), + additional: [], + namedReserves: [[]], + }, + }, + }, + + tx: { + members: { + updateProfile: { + event: 'MembershipBought', + data: [NEW_MEMBER_DATA.id], + onSend: args.onUpdateProfile, + failure: parameters.buyMembershipTxFailure, + }, + updateAccounts: { + event: 'MembershipBought', + data: [NEW_MEMBER_DATA.id], + onSend: args.onUpdateAccounts, + failure: parameters.buyMembershipTxFailure, + }, + addStakingAccountCandidate: { + event: 'StakingAccountAdded', + data: [NEW_MEMBER_DATA.id], + onSend: args.onAddStakingAccount, + failure: parameters.addStakingAccountTxFailure, + }, + confirmStakingAccount: { + event: 'StakingAccountConfirmed', + data: [NEW_MEMBER_DATA.id], + onSend: args.onConfirmStakingAccount, + failure: parameters.confirmStakingAccountTxFailure, + }, + removeStakingAccount: { + event: 'StakingAccountRemoved', + data: [NEW_MEMBER_DATA.id], + onSend: args.onRemoveStakingAccount, + failure: parameters.removeStakingAccountTxFailure, + }, + }, + utility: { + batch: { + event: 'TxBatch', + onSend: args.batchTx, + failure: parameters.batchTxFailure, + }, + }, + }, + }, + + gql: { + queries: [ + { + query: GetMemberDocument, + data: { membershipByUniqueInput: member('alice') }, + }, + { + query: GetMembersCountDocument, + data: { membershipsConnection: { totalCount: 3 } }, + }, + { + query: GetMembersWithDetailsDocument, + data: { memberships: [member('alice'), member('bob'), member('charlie'), member('dave')] }, + }, + { + query: GetMemberActionDetailsDocument, + data: { + stakeSlashedEventsConnection: { + totalCount: 2, + }, + terminatedLeaderEventsConnection: { + totalCount: 3, + }, + terminatedWorkerEventsConnection: { + totalCount: 4, + }, + memberInvitedEventsConnection: { + totalCount: 0, + }, + }, + }, + ], + }, + } + }, + }, +} satisfies Meta + +type Story = StoryObj> +export const Default: Story = {} + +const fillMembershipForm = async (modal: Container) => { + await selectFromDropdown(modal, modal.getByText('Root account', { selector: 'label' }), 'bob') + await selectFromDropdown(modal, modal.getByText('Controller account', { selector: 'label' }), 'charlie') + await userEvent.type(modal.getByLabelText('Member Name'), NEW_MEMBER_DATA.metadata.name) + await userEvent.type(modal.getByLabelText('Membership handle'), NEW_MEMBER_DATA.handle) + await userEvent.type(modal.getByLabelText('About member'), NEW_MEMBER_DATA.metadata.about) + await userEvent.type(modal.getByLabelText('Member Avatar'), NEW_MEMBER_DATA.metadata.avatar.avatarUri) +} + +export const UpdateMembershipHappy: Story = { + play: async ({ args, canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + await waitFor(() => expect(screen.getByText('alice'))) + const editButton = document.getElementsByClassName('edit-button')[0] + await userEvent.click(editButton) + + await step('Form', async () => { + const saveButton = getButtonByText(modal, 'Save changes') + expect(saveButton).toBeDisabled() + await fillMembershipForm(modal) + await waitFor(() => expect(saveButton).toBeEnabled()) + await userEvent.click(saveButton) + }) + + await step('Sign', async () => { + expect(modal.getByText('Authorize transaction')) + expect(modal.getByText('You intend to update your membership.')) + expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') + expect(modal.getByRole('heading', { name: 'alice' })) + + await userEvent.click(getButtonByText(modal, 'Sign and update a member')) + }) + + await step('Confirm', async () => { + expect(await modal.findByText('Success')) + expect(modal.getByText('alice')) + expect(args.batchTx).toHaveBeenCalledTimes(1) + + const viewProfileButton = getButtonByText(modal, 'View my profile') + expect(viewProfileButton).toBeEnabled() + userEvent.click(viewProfileButton) + expect(modal.getByText('alice')) + }) + }, +} + +export const UpdateMembershipFailure: Story = { + parameters: { batchTxFailure: 'Some error message' }, + play: async ({ canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + await waitFor(() => expect(screen.getByText('alice'))) + const editButton = document.getElementsByClassName('edit-button')[0] + await userEvent.click(editButton) + + await step('Form', async () => { + const saveButton = getButtonByText(modal, 'Save changes') + expect(saveButton).toBeDisabled() + await fillMembershipForm(modal) + await waitFor(() => expect(saveButton).toBeEnabled()) + await userEvent.click(saveButton) + }) + + await step('Sign', async () => { + expect(modal.getByText('Authorize transaction')) + await userEvent.click(getButtonByText(modal, 'Sign and update a member')) + }) + + await step('Confirm', async () => { + expect(await modal.findByText('Failure')) + expect(await modal.findByText('There was a problem updating membership.')) + }) + }, +} diff --git a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx index 5dd561157b..f9f6e28c4c 100644 --- a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx +++ b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx @@ -1,12 +1,14 @@ import { expect } from '@storybook/jest' import { Meta, StoryObj } from '@storybook/react' import { userEvent, waitFor, within } from '@storybook/testing-library' +import React from 'react' import { Address } from '@/common/types' import { GetMembersWithDetailsDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { joy, selectFromDropdown } from '@/mocks/helpers' import { MocksParameters } from '@/mocks/providers' +import { ValidatorContextProvider } from '@/validators/providers/provider' import { ValidatorList } from './ValidatorList' @@ -14,7 +16,6 @@ type Args = object export default { title: 'Pages/Validators/ValidatorList', - component: ValidatorList, parameters: { mocks: (): MocksParameters => { @@ -231,6 +232,12 @@ export default { } }, }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + render: (args) => ( + + + + ), } satisfies Meta type Story = StoryObj diff --git a/packages/ui/src/common/components/Warning.tsx b/packages/ui/src/common/components/Warning.tsx index c18635a392..c4ed73acaf 100644 --- a/packages/ui/src/common/components/Warning.tsx +++ b/packages/ui/src/common/components/Warning.tsx @@ -31,9 +31,9 @@ export const Warning = ({ title, content, isClosable, additionalContent, icon, i {icon === 'alert' && } {icon === 'info' && } - {title &&
{title}
} + {title ?
{title}
: content && {content}}
- {content && {content}} + {title && content && {content}} {additionalContent} ) diff --git a/packages/ui/src/memberships/components/MemberListItem/MyMember.tsx b/packages/ui/src/memberships/components/MemberListItem/MyMember.tsx index a6590b846a..07af94907e 100644 --- a/packages/ui/src/memberships/components/MemberListItem/MyMember.tsx +++ b/packages/ui/src/memberships/components/MemberListItem/MyMember.tsx @@ -47,7 +47,7 @@ export const MyMemberListItem = ({ member }: { member: Member }) => { - + diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index fd2ab59e49..1aef35de05 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -397,7 +397,7 @@ export const BuyMembershipFormModal = ({ onClose, onSubmit, membershipPrice }: B ) } -const SelectValidatorAccountWrapper = styled.div` +export const SelectValidatorAccountWrapper = styled.div` margin-top: -4px; display: flex; flex-direction: column; diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx index 5279349702..6cc0972f13 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx @@ -1,22 +1,28 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useForm, FormProvider } from 'react-hook-form' import * as Yup from 'yup' import { AnySchema } from 'yup' -import { filterAccount, SelectAccount } from '@/accounts/components/SelectAccount' +import { filterAccount, SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' -import { InputComponent, InputText, InputTextarea } from '@/common/components/forms' +import { ButtonGhost, ButtonPrimary } from '@/common/components/buttons' +import { InputComponent, InputText, InputTextarea, Label, ToggleCheckbox } from '@/common/components/forms' +import { CrossIcon, PlusIcon } from '@/common/components/icons' import { Loading } from '@/common/components/Loading' import { ModalHeader, ModalTransactionFooter, Row, + RowInline, ScrolledModal, ScrolledModalBody, ScrolledModalContainer, } from '@/common/components/Modal' -import { TextMedium } from '@/common/components/typography' +import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' +import { TextMedium, TextSmall } from '@/common/components/typography' +import { Warning } from '@/common/components/Warning' +import { Address } from '@/common/types' import { WithNullableValues } from '@/common/types/form' import { definedValues } from '@/common/utils' import { useYupValidationResolver } from '@/common/utils/validation' @@ -24,12 +30,14 @@ import { AvatarInput } from '@/memberships/components/AvatarInput' import { SocialMediaSelector } from '@/memberships/components/SocialMediaSelector/SocialMediaSelector' import { useUploadAvatarAndSubmit } from '@/memberships/hooks/useUploadAvatarAndSubmit' import { useGetMembersCountQuery } from '@/memberships/queries' +import { useValidators } from '@/validators/hooks/useValidators' import { AvatarURISchema, ExternalResourcesSchema, HandleSchema } from '../../model/validation' import { MemberWithDetails } from '../../types' +import { SelectValidatorAccountWrapper } from '../BuyMembershipModal/BuyMembershipFormModal' import { UpdateMemberForm } from './types' -import { changedOrNull, hasAnyEdits, membershipExternalResourceToObject } from './utils' +import { changedOrNull, hasAnyEdits, hasAnyMetadateChanges, membershipExternalResourceToObject } from './utils' interface Props { onClose: () => void @@ -45,19 +53,15 @@ const UpdateMemberSchema = Yup.object().shape({ externalResources: ExternalResourcesSchema, }) -const getUpdateMemberFormInitial = (member: MemberWithDetails) => ({ - id: member.id, - name: member.name || '', - handle: member.handle || '', - about: member.about || '', - avatarUri: process.env.REACT_APP_AVATAR_UPLOAD_URL ? '' : typeof member.avatar === 'string' ? member.avatar : '', - rootAccount: member.rootAccount, - controllerAccount: member.controllerAccount, - externalResources: membershipExternalResourceToObject(member.externalResources) ?? {}, -}) - export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) => { const { allAccounts } = useMyAccounts() + const { allValidators, allValidatorsWithCtrlAcc } = useValidators() + const isValidatorAccount = useCallback( + (address: Address): boolean | undefined => + allValidators?.map(({ address }) => address).includes(address) || allValidatorsWithCtrlAcc?.includes(address), + [allValidators, allValidatorsWithCtrlAcc] + ) + const initialValidatorAccounts = member.boundAccounts.filter((address) => isValidatorAccount(address)) const [handleMap, setHandleMap] = useState(member.handle) const { data } = useGetMembersCountQuery({ variables: { where: { handle_eq: handleMap } } }) const context = { size: data?.membershipsConnection.totalCount, isHandleChanged: handleMap !== member.handle } @@ -65,15 +69,31 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) onSubmit( changedOrNull( { ...fields, externalResources: { ...definedValues(fields.externalResources) } }, - getUpdateMemberFormInitial(member) + updateMemberFormInitial ) ) ) + const updateMemberFormInitial = useMemo( + () => ({ + id: member.id, + name: member.name || '', + handle: member.handle || '', + about: member.about || '', + avatarUri: process.env.REACT_APP_AVATAR_UPLOAD_URL ? '' : typeof member.avatar === 'string' ? member.avatar : '', + rootAccount: member.rootAccount, + controllerAccount: member.controllerAccount, + externalResources: membershipExternalResourceToObject(member.externalResources) ?? {}, + isValidator: !!initialValidatorAccounts.length, + validatorAccounts: initialValidatorAccounts.length ? [...initialValidatorAccounts] : undefined, + }), + [member, initialValidatorAccounts] + ) + const form = useForm({ resolver: useYupValidationResolver(UpdateMemberSchema), defaultValues: { - ...getUpdateMemberFormInitial(member), + ...updateMemberFormInitial, rootAccount: accountOrNamed(allAccounts, member.rootAccount, 'Root Account'), controllerAccount: accountOrNamed(allAccounts, member.controllerAccount, 'Controller Account'), }, @@ -81,7 +101,15 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) mode: 'onChange', }) - const [controllerAccount, rootAccount, handle] = form.watch(['controllerAccount', 'rootAccount', 'handle']) + const [controllerAccount, rootAccount, handle, isValidator, validatorAccountCandidate, validatorAccounts] = + form.watch([ + 'controllerAccount', + 'rootAccount', + 'handle', + 'isValidator', + 'validatorAccountCandidate', + 'validatorAccounts', + ]) useEffect(() => { form.trigger('handle') @@ -91,10 +119,43 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) handle && setHandleMap(handle) }, [handle]) + useEffect(() => { + alert('all form data: ' + JSON.stringify(form.watch())) + }, [validatorAccounts?.length]) + const filterRoot = useCallback(filterAccount(controllerAccount), [controllerAccount]) const filterController = useCallback(filterAccount(rootAccount), [rootAccount]) - const canUpdate = form.formState.isValid && hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) + const canUpdate = + form.formState.isValid && + hasAnyEdits(form.getValues(), updateMemberFormInitial) && + (!isValidator || validatorAccounts?.length) + + const willBecomeUnverifiedValidator = + updateMemberFormInitial.isValidator && hasAnyMetadateChanges(form.getValues(), updateMemberFormInitial) + + const addValidatorAccount = () => { + if (validatorAccountCandidate) { + setValidatorAccounts([...new Set([...(validatorAccounts ?? []), validatorAccountCandidate.address])]) + form?.setValue('validatorAccountCandidate' as keyof UpdateMemberForm, undefined) + } + } + + const removeValidatorAccount = (index: number) => { + validatorAccounts && + setValidatorAccounts([...validatorAccounts.slice(0, index), ...validatorAccounts.slice(index + 1)]) + } + + const setValidatorAccounts = (accounts: Address[]) => { + form?.unregister('validatorAccounts' as keyof UpdateMemberForm) + validatorAccounts?.map((account, index) => { + form?.unregister(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm) + }) + accounts.map((account, index) => { + form?.register(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm) + form?.setValue(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm, account) + }) + } return ( @@ -106,6 +167,15 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) Please fill in all the details below. + {willBecomeUnverifiedValidator && ( + + )} + resource.source) : [] } /> + + + + + + + {isValidator && ( + <> + + + + + + + * + + + If your validator account is not in your signer wallet, paste the account address to the field + below: + + + + + + + + + + + + {validatorAccounts?.map((address, index) => ( + + + + { + removeValidatorAccount(index) + }} + > + + + + + ))} + + )} diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts index de145adda7..d61ace6c53 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts @@ -1,4 +1,5 @@ import { Account } from '@/accounts/types' +import { Address } from '@/common/types' export interface UpdateMemberForm { id: string @@ -9,4 +10,7 @@ export interface UpdateMemberForm { rootAccount?: Account controllerAccount?: Account externalResources: Record + isValidator?: boolean + validatorAccountCandidate?: Account + validatorAccounts?: Address[] } diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/utils.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/utils.ts index 85c8c069ae..94787bfcee 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/utils.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/utils.ts @@ -13,14 +13,30 @@ export const hasAnyEdits = (form: Record, initial: Record, initial: Record) => { + const metadataFields = ['about', 'avatarUri', 'externalResources', 'validatorAccounts'] + return metadataFields.some((key) => { + const initialValue = initial[key === 'avatarUri' ? 'avatar' : key] || '' + const formValue = form[key] || '' + if (initialValue !== formValue) { + if (key === 'externalResources' || key === 'validatorAccounts') { + return JSON.stringify(initialValue) !== JSON.stringify(formValue) + } + return true + } + return false + }) +} + export const getChangedFields = (form: Record, initial: Record) => { const changedFields = [] for (const key of Object.keys(form)) { + if (key === 'validatorCandidate') continue const initialValue = initial[key === 'avatarUri' ? 'avatar' : key] || '' const formValue = form[key]?.address ?? (form[key] || '') if (initialValue !== formValue) { - if (key === 'externalResources') { + if (key === 'externalResources' || key === 'validatorAccounts') { if (JSON.stringify(initialValue) !== JSON.stringify(formValue)) changedFields.push(key) } else { changedFields.push(key) diff --git a/packages/ui/src/validators/hooks/useValidators.ts b/packages/ui/src/validators/hooks/useValidators.ts new file mode 100644 index 0000000000..9ccd686314 --- /dev/null +++ b/packages/ui/src/validators/hooks/useValidators.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react' + +import { ValidatorsContext } from '../providers/context' + +export const useValidators = () => useContext(ValidatorsContext) diff --git a/packages/ui/src/validators/hooks/useValidatorsList.tsx b/packages/ui/src/validators/hooks/useValidatorsList.tsx index 5e9f63e5b5..2a5fd7e3ee 100644 --- a/packages/ui/src/validators/hooks/useValidatorsList.tsx +++ b/packages/ui/src/validators/hooks/useValidatorsList.tsx @@ -11,7 +11,7 @@ import { last } from '@/common/utils' import { Verification, State, ValidatorWithDetails, ValidatorMembership } from '../types' -import { useValidatorMembers } from './useValidatorMembers' +import { useValidators } from './useValidators' export const useValidatorsList = () => { const { api } = useApi() @@ -19,7 +19,7 @@ export const useValidatorsList = () => { const [isVerified, setIsVerified] = useState(null) const [isActive, setIsActive] = useState(null) const [visibleValidators, setVisibleValidators] = useState([]) - const validators = useValidatorMembers() + const { validatorsWithMembership: validators } = useValidators() const getValidatorInfo = (validator: ValidatorMembership, api: Api): Observable => { const { stashAccount: address, commission } = validator diff --git a/packages/ui/src/validators/providers/context.tsx b/packages/ui/src/validators/providers/context.tsx new file mode 100644 index 0000000000..7681c7e6ba --- /dev/null +++ b/packages/ui/src/validators/providers/context.tsx @@ -0,0 +1,5 @@ +import { createContext } from 'react' + +import { UseValidators } from './provider' + +export const ValidatorsContext = createContext({}) diff --git a/packages/ui/src/validators/hooks/useValidatorMembers.tsx b/packages/ui/src/validators/providers/provider.tsx similarity index 78% rename from packages/ui/src/validators/hooks/useValidatorMembers.tsx rename to packages/ui/src/validators/providers/provider.tsx index 2f6730552d..590a867fa2 100644 --- a/packages/ui/src/validators/hooks/useValidatorMembers.tsx +++ b/packages/ui/src/validators/providers/provider.tsx @@ -1,15 +1,31 @@ -import { useMemo } from 'react' +import React, { ReactNode, useMemo } from 'react' import { map } from 'rxjs' import { useApi } from '@/api/hooks/useApi' import { useFirstObservableValue } from '@/common/hooks/useFirstObservableValue' +import { Address } from '@/common/types' import { perbillToPercent } from '@/common/utils' import { useGetMembersWithDetailsQuery } from '@/memberships/queries' import { asMemberWithDetails } from '@/memberships/types' import { ValidatorMembership } from '../types' -export const useValidatorMembers = () => { +import { ValidatorsContext } from './context' + +interface Props { + children: ReactNode +} + +export interface UseValidators { + allValidators?: { + address: Address + commission: number + }[] + allValidatorsWithCtrlAcc?: (string | undefined)[] + validatorsWithMembership?: ValidatorMembership[] +} + +export const ValidatorContextProvider = (props: Props) => { const { api } = useApi() const allValidators = useFirstObservableValue( () => @@ -71,5 +87,11 @@ export const useValidatorMembers = () => { ) }, [data, allValidators, allValidatorsWithCtrlAcc]) - return validatorsWithMembership + const value = { + allValidators, + allValidatorsWithCtrlAcc, + validatorsWithMembership, + } + + return {props.children} } From 1f44b0dc690b382cdcd1ac303f74d3d029c61bd3 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 18 Dec 2023 11:22:29 -0500 Subject: [PATCH 32/44] Revert "update app.stories.tsx with validator provider mocking" This reverts commit d3182d216120ca7125c9bf313eea8a5b5f7c6af5. --- packages/ui/.storybook/preview.tsx | 21 +- packages/ui/src/app/App.stories.tsx | 74 +--- packages/ui/src/app/Providers.tsx | 35 +- .../pages/Profile/MyMemberships.stories.tsx | 321 ------------------ .../Validators/ValidatorList.stories.tsx | 9 +- packages/ui/src/common/components/Warning.tsx | 4 +- .../components/MemberListItem/MyMember.tsx | 2 +- .../BuyMembershipFormModal.tsx | 2 +- .../UpdateMembershipFormModal.tsx | 167 ++------- .../modals/UpdateMembershipModal/types.ts | 4 - .../modals/UpdateMembershipModal/utils.ts | 18 +- .../useValidatorMembers.tsx} | 28 +- .../ui/src/validators/hooks/useValidators.ts | 5 - .../validators/hooks/useValidatorsList.tsx | 4 +- .../ui/src/validators/providers/context.tsx | 5 - 15 files changed, 58 insertions(+), 641 deletions(-) delete mode 100644 packages/ui/src/app/pages/Profile/MyMemberships.stories.tsx rename packages/ui/src/validators/{providers/provider.tsx => hooks/useValidatorMembers.tsx} (78%) delete mode 100644 packages/ui/src/validators/hooks/useValidators.ts delete mode 100644 packages/ui/src/validators/providers/context.tsx diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index 4e7742838b..810cd97b2c 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -12,7 +12,6 @@ import { NotificationsHolder } from '../src/common/components/page/SideNotificat import { TransactionStatus } from '../src/common/components/TransactionStatus/TransactionStatus' import { Colors } from '../src/common/constants' import { ModalContextProvider } from '../src/common/providers/modal/provider' -import { ValidatorContextProvider } from '../src/validators/providers/provider' import { TransactionStatusProvider } from '../src/common/providers/transactionStatus/provider' import { MockProvidersDecorator, MockRouterDecorator } from '../src/mocks/providers' import { i18next } from '../src/services/i18n' @@ -55,17 +54,15 @@ const RHFDecorator: Decorator = (Story) => { const ModalDecorator: Decorator = (Story) => ( - - - - - - - - - - - + + + + + + + + + ) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 9d1d17963e..474b453baf 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -10,7 +10,7 @@ import { createGlobalStyle } from 'styled-components' import { Page, Screen } from '@/common/components/page/Page' import { Colors } from '@/common/constants' import { EMAIL_VERIFICATION_TOKEN_SEARCH_PARAM } from '@/memberships/constants' -import { GetMemberDocument, GetMembersWithDetailsDocument } from '@/memberships/queries' +import { GetMemberDocument } from '@/memberships/queries' import { ConfirmBackendEmailDocument, GetBackendMemberExistsDocument, @@ -122,72 +122,6 @@ export default { members: { membershipPrice: joy(20) }, council: { stage: { stage: { isIdle: true }, changedAt: 123 } }, referendum: { stage: {} }, - - staking: { - bonded: { - multi: [ - 'j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D', - 'j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW', - 'j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP', - 'j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz', - 'j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa', - 'j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN', - 'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP', - 'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ', - 'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg', - 'j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt', - 'j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM', - ], - }, - validators: { - entries: [ - [ - { args: ['j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW'] }, - { commission: 0.1 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz'] }, - { commission: 0.15 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa'] }, - { commission: 0.2 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN'] }, - { commission: 0.01 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP'] }, - { commission: 0.03 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - ], - }, - }, }, tx: { @@ -237,10 +171,6 @@ export default { query: GetBackendMemberExistsDocument, data: { memberExist: args.hasRegisteredEmail }, }, - { - query: GetMembersWithDetailsDocument, - data: { memberships: [member('alice'), member('bob'), member('charlie'), member('dave')] }, - }, ], mutations: [ { @@ -765,7 +695,7 @@ export const BuyMembershipHappyAddTwoValidatorHappy: Story = { invitingMemberId: undefined, referrerId: undefined, }) - expect(args.onAddStakingAccount).toHaveBeenCalledTimes(4) // means 2 times + expect(args.onAddStakingAccount).toHaveBeenCalledTimes(4) // means 2 times, due to the React hook 'useMomo' expect(args.batchTx).toHaveBeenCalledTimes(2) const doneButton = getButtonByText(modal, 'Done') diff --git a/packages/ui/src/app/Providers.tsx b/packages/ui/src/app/Providers.tsx index 5b2dcdc770..ded0ae48bf 100644 --- a/packages/ui/src/app/Providers.tsx +++ b/packages/ui/src/app/Providers.tsx @@ -13,7 +13,6 @@ import { OnBoardingProvider } from '@/common/providers/onboarding/provider' import { ResponsiveProvider } from '@/common/providers/responsive/provider' import { TransactionStatusProvider } from '@/common/providers/transactionStatus/provider' import { MembershipContextProvider } from '@/memberships/providers/membership/provider' -import { ValidatorContextProvider } from '@/validators/providers/provider' import { BackendProvider } from './providers/backend/provider' import { GlobalStyle } from './providers/GlobalStyle' @@ -32,24 +31,22 @@ export const Providers = ({ children }: Props) => ( - - - - - - - - - - {children} - - - - - - - - + + + + + + + + + {children} + + + + + + + diff --git a/packages/ui/src/app/pages/Profile/MyMemberships.stories.tsx b/packages/ui/src/app/pages/Profile/MyMemberships.stories.tsx deleted file mode 100644 index cbf804b91f..0000000000 --- a/packages/ui/src/app/pages/Profile/MyMemberships.stories.tsx +++ /dev/null @@ -1,321 +0,0 @@ -import { expect } from '@storybook/jest' -import { Meta, StoryContext, StoryObj } from '@storybook/react' -import { userEvent, waitFor, within } from '@storybook/testing-library' -import { FC } from 'react' - -import { - GetMemberActionDetailsDocument, - GetMemberDocument, - GetMembersCountDocument, - GetMembersWithDetailsDocument, -} from '@/memberships/queries' -import { Membership, member } from '@/mocks/data/members' -import { Container, getButtonByText, joy, selectFromDropdown, withinModal } from '@/mocks/helpers' -import { MocksParameters } from '@/mocks/providers' - -import { MyMemberships } from './MyMemberships' - -const alice = member('alice') -const bob = member('bob') -const charlie = member('charlie') - -const NEW_MEMBER_DATA = { - id: alice.id, - handle: 'realbobbybob', - metadata: { - name: 'BobbyBob', - about: 'Lorem ipsum...', - avatar: { avatarUri: 'https://api.dicebear.com/6.x/bottts-neutral/svg?seed=bob' }, - }, -} - -type Args = { - onUpdateProfile: jest.Mock - onUpdateAccounts: jest.Mock - onAddStakingAccount: jest.Mock - onConfirmStakingAccount: jest.Mock - onRemoveStakingAccount: jest.Mock - batchTx: jest.Mock -} - -export default { - title: 'Pages/MyProfile/MyMemberships', - component: MyMemberships, - - argTypes: { - onUpdateProfile: { action: 'UpdateProfile' }, - onUpdateAccounts: { action: 'UpdateAccounts' }, - onAddStakingAccount: { action: 'AddStakingAccount' }, - onConfirmStakingAccount: { action: 'ConfirmStakingAccount' }, - onRemoveStakingAccount: { action: 'RemoveStakingAccount' }, - batchTx: { action: 'BatchTx' }, - }, - - parameters: { - totalBalance: 100, - router: { - href: '/profile/memberships', - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - mocks: ({ args, parameters }: StoryContext): MocksParameters => { - const account = (member: Membership) => ({ - balances: parameters.totalBalance, - ...{ member }, - }) - return { - accounts: { - active: 'alice', - list: [account(alice), account(bob), account(charlie)], - hasWallet: true, - }, - chain: { - query: { - members: { membershipPrice: joy(20) }, - membershipWorkingGroup: { budget: joy(166666_66) }, - staking: { - bonded: { - multi: [ - 'j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D', - 'j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW', - 'j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP', - 'j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz', - 'j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa', - 'j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN', - 'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP', - 'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ', - 'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg', - 'j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt', - 'j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM', - ], - }, - validators: { - entries: [ - [ - { args: ['j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW'] }, - { commission: 0.1 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz'] }, - { commission: 0.15 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa'] }, - { commission: 0.2 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN'] }, - { commission: 0.01 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP'] }, - { commission: 0.03 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - [ - { args: ['j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM'] }, - { commission: 0.05 * 10 ** 9, blocked: false }, - ], - ], - }, - }, - }, - derive: { - balances: { - all: { - freeBalance: 1000, - reservedBalance: 1000, - availableBalance: 1000, - lockedBalance: 1000, - lockedBreakdown: [], - vestingLocked: 1000, - isVesting: false, - vestedBalance: joy(1666_66), - vestedClaimable: joy(1666_66), - vesting: [], - vestingTotal: joy(1666_66), - additional: [], - namedReserves: [[]], - }, - }, - }, - - tx: { - members: { - updateProfile: { - event: 'MembershipBought', - data: [NEW_MEMBER_DATA.id], - onSend: args.onUpdateProfile, - failure: parameters.buyMembershipTxFailure, - }, - updateAccounts: { - event: 'MembershipBought', - data: [NEW_MEMBER_DATA.id], - onSend: args.onUpdateAccounts, - failure: parameters.buyMembershipTxFailure, - }, - addStakingAccountCandidate: { - event: 'StakingAccountAdded', - data: [NEW_MEMBER_DATA.id], - onSend: args.onAddStakingAccount, - failure: parameters.addStakingAccountTxFailure, - }, - confirmStakingAccount: { - event: 'StakingAccountConfirmed', - data: [NEW_MEMBER_DATA.id], - onSend: args.onConfirmStakingAccount, - failure: parameters.confirmStakingAccountTxFailure, - }, - removeStakingAccount: { - event: 'StakingAccountRemoved', - data: [NEW_MEMBER_DATA.id], - onSend: args.onRemoveStakingAccount, - failure: parameters.removeStakingAccountTxFailure, - }, - }, - utility: { - batch: { - event: 'TxBatch', - onSend: args.batchTx, - failure: parameters.batchTxFailure, - }, - }, - }, - }, - - gql: { - queries: [ - { - query: GetMemberDocument, - data: { membershipByUniqueInput: member('alice') }, - }, - { - query: GetMembersCountDocument, - data: { membershipsConnection: { totalCount: 3 } }, - }, - { - query: GetMembersWithDetailsDocument, - data: { memberships: [member('alice'), member('bob'), member('charlie'), member('dave')] }, - }, - { - query: GetMemberActionDetailsDocument, - data: { - stakeSlashedEventsConnection: { - totalCount: 2, - }, - terminatedLeaderEventsConnection: { - totalCount: 3, - }, - terminatedWorkerEventsConnection: { - totalCount: 4, - }, - memberInvitedEventsConnection: { - totalCount: 0, - }, - }, - }, - ], - }, - } - }, - }, -} satisfies Meta - -type Story = StoryObj> -export const Default: Story = {} - -const fillMembershipForm = async (modal: Container) => { - await selectFromDropdown(modal, modal.getByText('Root account', { selector: 'label' }), 'bob') - await selectFromDropdown(modal, modal.getByText('Controller account', { selector: 'label' }), 'charlie') - await userEvent.type(modal.getByLabelText('Member Name'), NEW_MEMBER_DATA.metadata.name) - await userEvent.type(modal.getByLabelText('Membership handle'), NEW_MEMBER_DATA.handle) - await userEvent.type(modal.getByLabelText('About member'), NEW_MEMBER_DATA.metadata.about) - await userEvent.type(modal.getByLabelText('Member Avatar'), NEW_MEMBER_DATA.metadata.avatar.avatarUri) -} - -export const UpdateMembershipHappy: Story = { - play: async ({ args, canvasElement, step }) => { - const screen = within(canvasElement) - const modal = withinModal(canvasElement) - - await waitFor(() => expect(screen.getByText('alice'))) - const editButton = document.getElementsByClassName('edit-button')[0] - await userEvent.click(editButton) - - await step('Form', async () => { - const saveButton = getButtonByText(modal, 'Save changes') - expect(saveButton).toBeDisabled() - await fillMembershipForm(modal) - await waitFor(() => expect(saveButton).toBeEnabled()) - await userEvent.click(saveButton) - }) - - await step('Sign', async () => { - expect(modal.getByText('Authorize transaction')) - expect(modal.getByText('You intend to update your membership.')) - expect(modal.getByText('Transaction fee:')?.nextSibling?.textContent).toBe('5') - expect(modal.getByRole('heading', { name: 'alice' })) - - await userEvent.click(getButtonByText(modal, 'Sign and update a member')) - }) - - await step('Confirm', async () => { - expect(await modal.findByText('Success')) - expect(modal.getByText('alice')) - expect(args.batchTx).toHaveBeenCalledTimes(1) - - const viewProfileButton = getButtonByText(modal, 'View my profile') - expect(viewProfileButton).toBeEnabled() - userEvent.click(viewProfileButton) - expect(modal.getByText('alice')) - }) - }, -} - -export const UpdateMembershipFailure: Story = { - parameters: { batchTxFailure: 'Some error message' }, - play: async ({ canvasElement, step }) => { - const screen = within(canvasElement) - const modal = withinModal(canvasElement) - - await waitFor(() => expect(screen.getByText('alice'))) - const editButton = document.getElementsByClassName('edit-button')[0] - await userEvent.click(editButton) - - await step('Form', async () => { - const saveButton = getButtonByText(modal, 'Save changes') - expect(saveButton).toBeDisabled() - await fillMembershipForm(modal) - await waitFor(() => expect(saveButton).toBeEnabled()) - await userEvent.click(saveButton) - }) - - await step('Sign', async () => { - expect(modal.getByText('Authorize transaction')) - await userEvent.click(getButtonByText(modal, 'Sign and update a member')) - }) - - await step('Confirm', async () => { - expect(await modal.findByText('Failure')) - expect(await modal.findByText('There was a problem updating membership.')) - }) - }, -} diff --git a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx index f9f6e28c4c..5dd561157b 100644 --- a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx +++ b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx @@ -1,14 +1,12 @@ import { expect } from '@storybook/jest' import { Meta, StoryObj } from '@storybook/react' import { userEvent, waitFor, within } from '@storybook/testing-library' -import React from 'react' import { Address } from '@/common/types' import { GetMembersWithDetailsDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { joy, selectFromDropdown } from '@/mocks/helpers' import { MocksParameters } from '@/mocks/providers' -import { ValidatorContextProvider } from '@/validators/providers/provider' import { ValidatorList } from './ValidatorList' @@ -16,6 +14,7 @@ type Args = object export default { title: 'Pages/Validators/ValidatorList', + component: ValidatorList, parameters: { mocks: (): MocksParameters => { @@ -232,12 +231,6 @@ export default { } }, }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - render: (args) => ( - - - - ), } satisfies Meta type Story = StoryObj diff --git a/packages/ui/src/common/components/Warning.tsx b/packages/ui/src/common/components/Warning.tsx index c4ed73acaf..c18635a392 100644 --- a/packages/ui/src/common/components/Warning.tsx +++ b/packages/ui/src/common/components/Warning.tsx @@ -31,9 +31,9 @@ export const Warning = ({ title, content, isClosable, additionalContent, icon, i {icon === 'alert' && } {icon === 'info' && } - {title ?
{title}
: content && {content}} + {title &&
{title}
}
- {title && content && {content}} + {content && {content}} {additionalContent} ) diff --git a/packages/ui/src/memberships/components/MemberListItem/MyMember.tsx b/packages/ui/src/memberships/components/MemberListItem/MyMember.tsx index 07af94907e..a6590b846a 100644 --- a/packages/ui/src/memberships/components/MemberListItem/MyMember.tsx +++ b/packages/ui/src/memberships/components/MemberListItem/MyMember.tsx @@ -47,7 +47,7 @@ export const MyMemberListItem = ({ member }: { member: Member }) => { - + diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 1aef35de05..fd2ab59e49 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -397,7 +397,7 @@ export const BuyMembershipFormModal = ({ onClose, onSubmit, membershipPrice }: B ) } -export const SelectValidatorAccountWrapper = styled.div` +const SelectValidatorAccountWrapper = styled.div` margin-top: -4px; display: flex; flex-direction: column; diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx index 6cc0972f13..5279349702 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/UpdateMembershipFormModal.tsx @@ -1,28 +1,22 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useForm, FormProvider } from 'react-hook-form' import * as Yup from 'yup' import { AnySchema } from 'yup' -import { filterAccount, SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' +import { filterAccount, SelectAccount } from '@/accounts/components/SelectAccount' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' -import { ButtonGhost, ButtonPrimary } from '@/common/components/buttons' -import { InputComponent, InputText, InputTextarea, Label, ToggleCheckbox } from '@/common/components/forms' -import { CrossIcon, PlusIcon } from '@/common/components/icons' +import { InputComponent, InputText, InputTextarea } from '@/common/components/forms' import { Loading } from '@/common/components/Loading' import { ModalHeader, ModalTransactionFooter, Row, - RowInline, ScrolledModal, ScrolledModalBody, ScrolledModalContainer, } from '@/common/components/Modal' -import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' -import { TextMedium, TextSmall } from '@/common/components/typography' -import { Warning } from '@/common/components/Warning' -import { Address } from '@/common/types' +import { TextMedium } from '@/common/components/typography' import { WithNullableValues } from '@/common/types/form' import { definedValues } from '@/common/utils' import { useYupValidationResolver } from '@/common/utils/validation' @@ -30,14 +24,12 @@ import { AvatarInput } from '@/memberships/components/AvatarInput' import { SocialMediaSelector } from '@/memberships/components/SocialMediaSelector/SocialMediaSelector' import { useUploadAvatarAndSubmit } from '@/memberships/hooks/useUploadAvatarAndSubmit' import { useGetMembersCountQuery } from '@/memberships/queries' -import { useValidators } from '@/validators/hooks/useValidators' import { AvatarURISchema, ExternalResourcesSchema, HandleSchema } from '../../model/validation' import { MemberWithDetails } from '../../types' -import { SelectValidatorAccountWrapper } from '../BuyMembershipModal/BuyMembershipFormModal' import { UpdateMemberForm } from './types' -import { changedOrNull, hasAnyEdits, hasAnyMetadateChanges, membershipExternalResourceToObject } from './utils' +import { changedOrNull, hasAnyEdits, membershipExternalResourceToObject } from './utils' interface Props { onClose: () => void @@ -53,15 +45,19 @@ const UpdateMemberSchema = Yup.object().shape({ externalResources: ExternalResourcesSchema, }) +const getUpdateMemberFormInitial = (member: MemberWithDetails) => ({ + id: member.id, + name: member.name || '', + handle: member.handle || '', + about: member.about || '', + avatarUri: process.env.REACT_APP_AVATAR_UPLOAD_URL ? '' : typeof member.avatar === 'string' ? member.avatar : '', + rootAccount: member.rootAccount, + controllerAccount: member.controllerAccount, + externalResources: membershipExternalResourceToObject(member.externalResources) ?? {}, +}) + export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) => { const { allAccounts } = useMyAccounts() - const { allValidators, allValidatorsWithCtrlAcc } = useValidators() - const isValidatorAccount = useCallback( - (address: Address): boolean | undefined => - allValidators?.map(({ address }) => address).includes(address) || allValidatorsWithCtrlAcc?.includes(address), - [allValidators, allValidatorsWithCtrlAcc] - ) - const initialValidatorAccounts = member.boundAccounts.filter((address) => isValidatorAccount(address)) const [handleMap, setHandleMap] = useState(member.handle) const { data } = useGetMembersCountQuery({ variables: { where: { handle_eq: handleMap } } }) const context = { size: data?.membershipsConnection.totalCount, isHandleChanged: handleMap !== member.handle } @@ -69,31 +65,15 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) onSubmit( changedOrNull( { ...fields, externalResources: { ...definedValues(fields.externalResources) } }, - updateMemberFormInitial + getUpdateMemberFormInitial(member) ) ) ) - const updateMemberFormInitial = useMemo( - () => ({ - id: member.id, - name: member.name || '', - handle: member.handle || '', - about: member.about || '', - avatarUri: process.env.REACT_APP_AVATAR_UPLOAD_URL ? '' : typeof member.avatar === 'string' ? member.avatar : '', - rootAccount: member.rootAccount, - controllerAccount: member.controllerAccount, - externalResources: membershipExternalResourceToObject(member.externalResources) ?? {}, - isValidator: !!initialValidatorAccounts.length, - validatorAccounts: initialValidatorAccounts.length ? [...initialValidatorAccounts] : undefined, - }), - [member, initialValidatorAccounts] - ) - const form = useForm({ resolver: useYupValidationResolver(UpdateMemberSchema), defaultValues: { - ...updateMemberFormInitial, + ...getUpdateMemberFormInitial(member), rootAccount: accountOrNamed(allAccounts, member.rootAccount, 'Root Account'), controllerAccount: accountOrNamed(allAccounts, member.controllerAccount, 'Controller Account'), }, @@ -101,15 +81,7 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) mode: 'onChange', }) - const [controllerAccount, rootAccount, handle, isValidator, validatorAccountCandidate, validatorAccounts] = - form.watch([ - 'controllerAccount', - 'rootAccount', - 'handle', - 'isValidator', - 'validatorAccountCandidate', - 'validatorAccounts', - ]) + const [controllerAccount, rootAccount, handle] = form.watch(['controllerAccount', 'rootAccount', 'handle']) useEffect(() => { form.trigger('handle') @@ -119,43 +91,10 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) handle && setHandleMap(handle) }, [handle]) - useEffect(() => { - alert('all form data: ' + JSON.stringify(form.watch())) - }, [validatorAccounts?.length]) - const filterRoot = useCallback(filterAccount(controllerAccount), [controllerAccount]) const filterController = useCallback(filterAccount(rootAccount), [rootAccount]) - const canUpdate = - form.formState.isValid && - hasAnyEdits(form.getValues(), updateMemberFormInitial) && - (!isValidator || validatorAccounts?.length) - - const willBecomeUnverifiedValidator = - updateMemberFormInitial.isValidator && hasAnyMetadateChanges(form.getValues(), updateMemberFormInitial) - - const addValidatorAccount = () => { - if (validatorAccountCandidate) { - setValidatorAccounts([...new Set([...(validatorAccounts ?? []), validatorAccountCandidate.address])]) - form?.setValue('validatorAccountCandidate' as keyof UpdateMemberForm, undefined) - } - } - - const removeValidatorAccount = (index: number) => { - validatorAccounts && - setValidatorAccounts([...validatorAccounts.slice(0, index), ...validatorAccounts.slice(index + 1)]) - } - - const setValidatorAccounts = (accounts: Address[]) => { - form?.unregister('validatorAccounts' as keyof UpdateMemberForm) - validatorAccounts?.map((account, index) => { - form?.unregister(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm) - }) - accounts.map((account, index) => { - form?.register(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm) - form?.setValue(('validatorAccounts[' + index + ']') as keyof UpdateMemberForm, account) - }) - } + const canUpdate = form.formState.isValid && hasAnyEdits(form.getValues(), getUpdateMemberFormInitial(member)) return ( @@ -167,15 +106,6 @@ export const UpdateMembershipFormModal = ({ onClose, onSubmit, member }: Props) Please fill in all the details below.
- {willBecomeUnverifiedValidator && ( - - )} - resource.source) : [] } /> - - - - - - - {isValidator && ( - <> - - - - - - - * - - - If your validator account is not in your signer wallet, paste the account address to the field - below: - - - - - - - - - - - - {validatorAccounts?.map((address, index) => ( - - - - { - removeValidatorAccount(index) - }} - > - - - - - ))} - - )} diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts index d61ace6c53..de145adda7 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts @@ -1,5 +1,4 @@ import { Account } from '@/accounts/types' -import { Address } from '@/common/types' export interface UpdateMemberForm { id: string @@ -10,7 +9,4 @@ export interface UpdateMemberForm { rootAccount?: Account controllerAccount?: Account externalResources: Record - isValidator?: boolean - validatorAccountCandidate?: Account - validatorAccounts?: Address[] } diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/utils.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/utils.ts index 94787bfcee..85c8c069ae 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/utils.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/utils.ts @@ -13,30 +13,14 @@ export const hasAnyEdits = (form: Record, initial: Record, initial: Record) => { - const metadataFields = ['about', 'avatarUri', 'externalResources', 'validatorAccounts'] - return metadataFields.some((key) => { - const initialValue = initial[key === 'avatarUri' ? 'avatar' : key] || '' - const formValue = form[key] || '' - if (initialValue !== formValue) { - if (key === 'externalResources' || key === 'validatorAccounts') { - return JSON.stringify(initialValue) !== JSON.stringify(formValue) - } - return true - } - return false - }) -} - export const getChangedFields = (form: Record, initial: Record) => { const changedFields = [] for (const key of Object.keys(form)) { - if (key === 'validatorCandidate') continue const initialValue = initial[key === 'avatarUri' ? 'avatar' : key] || '' const formValue = form[key]?.address ?? (form[key] || '') if (initialValue !== formValue) { - if (key === 'externalResources' || key === 'validatorAccounts') { + if (key === 'externalResources') { if (JSON.stringify(initialValue) !== JSON.stringify(formValue)) changedFields.push(key) } else { changedFields.push(key) diff --git a/packages/ui/src/validators/providers/provider.tsx b/packages/ui/src/validators/hooks/useValidatorMembers.tsx similarity index 78% rename from packages/ui/src/validators/providers/provider.tsx rename to packages/ui/src/validators/hooks/useValidatorMembers.tsx index 590a867fa2..2f6730552d 100644 --- a/packages/ui/src/validators/providers/provider.tsx +++ b/packages/ui/src/validators/hooks/useValidatorMembers.tsx @@ -1,31 +1,15 @@ -import React, { ReactNode, useMemo } from 'react' +import { useMemo } from 'react' import { map } from 'rxjs' import { useApi } from '@/api/hooks/useApi' import { useFirstObservableValue } from '@/common/hooks/useFirstObservableValue' -import { Address } from '@/common/types' import { perbillToPercent } from '@/common/utils' import { useGetMembersWithDetailsQuery } from '@/memberships/queries' import { asMemberWithDetails } from '@/memberships/types' import { ValidatorMembership } from '../types' -import { ValidatorsContext } from './context' - -interface Props { - children: ReactNode -} - -export interface UseValidators { - allValidators?: { - address: Address - commission: number - }[] - allValidatorsWithCtrlAcc?: (string | undefined)[] - validatorsWithMembership?: ValidatorMembership[] -} - -export const ValidatorContextProvider = (props: Props) => { +export const useValidatorMembers = () => { const { api } = useApi() const allValidators = useFirstObservableValue( () => @@ -87,11 +71,5 @@ export const ValidatorContextProvider = (props: Props) => { ) }, [data, allValidators, allValidatorsWithCtrlAcc]) - const value = { - allValidators, - allValidatorsWithCtrlAcc, - validatorsWithMembership, - } - - return {props.children} + return validatorsWithMembership } diff --git a/packages/ui/src/validators/hooks/useValidators.ts b/packages/ui/src/validators/hooks/useValidators.ts deleted file mode 100644 index 9ccd686314..0000000000 --- a/packages/ui/src/validators/hooks/useValidators.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useContext } from 'react' - -import { ValidatorsContext } from '../providers/context' - -export const useValidators = () => useContext(ValidatorsContext) diff --git a/packages/ui/src/validators/hooks/useValidatorsList.tsx b/packages/ui/src/validators/hooks/useValidatorsList.tsx index 2a5fd7e3ee..5e9f63e5b5 100644 --- a/packages/ui/src/validators/hooks/useValidatorsList.tsx +++ b/packages/ui/src/validators/hooks/useValidatorsList.tsx @@ -11,7 +11,7 @@ import { last } from '@/common/utils' import { Verification, State, ValidatorWithDetails, ValidatorMembership } from '../types' -import { useValidators } from './useValidators' +import { useValidatorMembers } from './useValidatorMembers' export const useValidatorsList = () => { const { api } = useApi() @@ -19,7 +19,7 @@ export const useValidatorsList = () => { const [isVerified, setIsVerified] = useState(null) const [isActive, setIsActive] = useState(null) const [visibleValidators, setVisibleValidators] = useState([]) - const { validatorsWithMembership: validators } = useValidators() + const validators = useValidatorMembers() const getValidatorInfo = (validator: ValidatorMembership, api: Api): Observable => { const { stashAccount: address, commission } = validator diff --git a/packages/ui/src/validators/providers/context.tsx b/packages/ui/src/validators/providers/context.tsx deleted file mode 100644 index 7681c7e6ba..0000000000 --- a/packages/ui/src/validators/providers/context.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { createContext } from 'react' - -import { UseValidators } from './provider' - -export const ValidatorsContext = createContext({}) From ff449b63122950fcd8a34c77ad02d9b8dfc8d82c Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 18 Dec 2023 11:36:20 -0500 Subject: [PATCH 33/44] update storybook comment --- packages/ui/src/app/App.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 474b453baf..4afc47a345 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -695,7 +695,7 @@ export const BuyMembershipHappyAddTwoValidatorHappy: Story = { invitingMemberId: undefined, referrerId: undefined, }) - expect(args.onAddStakingAccount).toHaveBeenCalledTimes(4) // means 2 times, due to the React hook 'useMomo' + expect(args.onAddStakingAccount).toHaveBeenCalledTimes(4) // means 2 times: get the fee info and sign the tx expect(args.batchTx).toHaveBeenCalledTimes(2) const doneButton = getButtonByText(modal, 'Done') From 699b6d9259ee9e378af7fad0a586da30517ecd85 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Wed, 20 Dec 2023 06:00:00 -0500 Subject: [PATCH 34/44] update buymembership machine --- .../BuyMembershipModal/BuyMembershipModal.tsx | 11 +++--- .../modals/BuyMembershipModal/machine.ts | 37 ++++--------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index d310e7478f..11e680fb83 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -55,15 +55,14 @@ export const BuyMembershipModal = () => { ) if (state.matches('prepare')) { - const onSubmit = (params: MemberFormFields) => - send({ type: params.isValidator ? 'DONE_IS_VALIDATOR' : 'DONE', form: params }) + const onSubmit = (params: MemberFormFields) => send({ type: 'DONE', form: params }) return } - if ((state.matches('buyMembershipTx') || state.matches('buyValidatorMembershipTx')) && buyTransaction) { + if (state.matches('buyMembershipTx') && buyTransaction) { const { form } = state.context - const service = state.children.transaction + const service = state.children.buyMembership return ( { } if (state.matches('addStakingAccCandidateTx') && bindTransaction && state.context.form.validatorAccounts) { - const service = state.children.transaction + const service = state.children.addStakingAccCandidate return ( { } if (state.matches('confirmStakingAccTx') && conFirmTransaction && state.context.form.controllerAccount) { - const service = state.children.transaction + const service = state.children.confirmStakingAcc return ( } @@ -36,7 +35,6 @@ export type BuyMembershipEvent = | { type: 'PASS' } | { type: 'FAIL' } | { type: 'DONE'; form: MemberFormFields } - | { type: 'DONE_IS_VALIDATOR'; form: MemberFormFields } | { type: 'SUCCESS'; memberId: BN } | { type: 'ERROR' } @@ -54,47 +52,26 @@ export const buyMembershipMachine = createMachine event.form }), }, - DONE_IS_VALIDATOR: { - target: 'buyValidatorMembershipTx', - actions: assign({ form: (_, event) => event.form }), - }, }, }, buyMembershipTx: { invoke: { - id: 'transaction', + id: 'buyMembership', src: transactionMachine, onDone: [ { - target: 'success', + target: 'addStakingAccCandidateTx', actions: assign({ memberId: (context, event) => getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), }), - cond: isTransactionSuccess, - }, - { - target: 'error', - cond: isTransactionError, - actions: assign({ transactionEvents: (context, event) => event.data.events }), + cond: (context, event) => isTransactionSuccess(context, event) && context.form?.isValidator, }, { - target: 'canceled', - cond: isTransactionCanceled, - }, - ], - }, - }, - buyValidatorMembershipTx: { - invoke: { - id: 'transaction', - src: transactionMachine, - onDone: [ - { - target: 'addStakingAccCandidateTx', + target: 'success', actions: assign({ memberId: (context, event) => getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), }), - cond: isTransactionSuccess, + cond: (context, event) => isTransactionSuccess(context, event) && !context.form?.isValidator, }, { target: 'error', @@ -110,7 +87,7 @@ export const buyMembershipMachine = createMachine Date: Wed, 20 Dec 2023 06:15:46 -0500 Subject: [PATCH 35/44] update buyMembership machine --- .../BuyMembershipModal/BuyMembershipModal.tsx | 11 +++--- .../modals/BuyMembershipModal/machine.ts | 37 ++++--------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index d310e7478f..11e680fb83 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -55,15 +55,14 @@ export const BuyMembershipModal = () => { ) if (state.matches('prepare')) { - const onSubmit = (params: MemberFormFields) => - send({ type: params.isValidator ? 'DONE_IS_VALIDATOR' : 'DONE', form: params }) + const onSubmit = (params: MemberFormFields) => send({ type: 'DONE', form: params }) return } - if ((state.matches('buyMembershipTx') || state.matches('buyValidatorMembershipTx')) && buyTransaction) { + if (state.matches('buyMembershipTx') && buyTransaction) { const { form } = state.context - const service = state.children.transaction + const service = state.children.buyMembership return ( { } if (state.matches('addStakingAccCandidateTx') && bindTransaction && state.context.form.validatorAccounts) { - const service = state.children.transaction + const service = state.children.addStakingAccCandidate return ( { } if (state.matches('confirmStakingAccTx') && conFirmTransaction && state.context.form.controllerAccount) { - const service = state.children.transaction + const service = state.children.confirmStakingAcc return ( } @@ -36,7 +35,6 @@ export type BuyMembershipEvent = | { type: 'PASS' } | { type: 'FAIL' } | { type: 'DONE'; form: MemberFormFields } - | { type: 'DONE_IS_VALIDATOR'; form: MemberFormFields } | { type: 'SUCCESS'; memberId: BN } | { type: 'ERROR' } @@ -54,47 +52,26 @@ export const buyMembershipMachine = createMachine event.form }), }, - DONE_IS_VALIDATOR: { - target: 'buyValidatorMembershipTx', - actions: assign({ form: (_, event) => event.form }), - }, }, }, buyMembershipTx: { invoke: { - id: 'transaction', + id: 'buyMembership', src: transactionMachine, onDone: [ { - target: 'success', + target: 'addStakingAccCandidateTx', actions: assign({ memberId: (context, event) => getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), }), - cond: isTransactionSuccess, - }, - { - target: 'error', - cond: isTransactionError, - actions: assign({ transactionEvents: (context, event) => event.data.events }), + cond: (context, event) => isTransactionSuccess(context, event) && context.form?.isValidator, }, { - target: 'canceled', - cond: isTransactionCanceled, - }, - ], - }, - }, - buyValidatorMembershipTx: { - invoke: { - id: 'transaction', - src: transactionMachine, - onDone: [ - { - target: 'addStakingAccCandidateTx', + target: 'success', actions: assign({ memberId: (context, event) => getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), }), - cond: isTransactionSuccess, + cond: (context, event) => isTransactionSuccess(context, event) && !context.form?.isValidator, }, { target: 'error', @@ -110,7 +87,7 @@ export const buyMembershipMachine = createMachine Date: Wed, 20 Dec 2023 06:31:35 -0500 Subject: [PATCH 36/44] fix --- .../ui/src/memberships/modals/BuyMembershipModal/machine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts index fa3377c9e2..44eae36580 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/machine.ts @@ -64,7 +64,7 @@ export const buyMembershipMachine = createMachine getDataFromEvent(event.data.events, 'members', 'MembershipBought', 0), }), - cond: (context, event) => isTransactionSuccess(context, event) && context.form?.isValidator, + cond: (context, event) => isTransactionSuccess(context, event) && !!context.form?.isValidator, }, { target: 'success', From b5ac65c5bdbba86bbd13a87132a4b3c66110e118 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 22 Dec 2023 11:50:19 +0100 Subject: [PATCH 37/44] Fix the duplicated transaction signing --- packages/ui/src/app/App.stories.tsx | 4 +- .../AddStakingAccCandidateModal.tsx | 52 +++++++------------ .../BuyMembershipModal/BuyMembershipModal.tsx | 31 +++++------ .../ConfirmStakingAccModal.tsx | 52 +++++++------------ 4 files changed, 55 insertions(+), 84 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 4afc47a345..479f9154f2 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -695,8 +695,8 @@ export const BuyMembershipHappyAddTwoValidatorHappy: Story = { invitingMemberId: undefined, referrerId: undefined, }) - expect(args.onAddStakingAccount).toHaveBeenCalledTimes(4) // means 2 times: get the fee info and sign the tx - expect(args.batchTx).toHaveBeenCalledTimes(2) + expect(args.onAddStakingAccount).toHaveBeenCalledTimes(2) + expect(args.batchTx).toHaveBeenCalledTimes(1) const doneButton = getButtonByText(modal, 'Done') expect(doneButton).toBeEnabled() diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx index a036a2fc2e..dcec3a0b41 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/AddStakingAccCandidateModal.tsx @@ -4,8 +4,7 @@ import React from 'react' import { ActorRef } from 'xstate' import { Account } from '@/accounts/types' -import { TextMedium, TokenValue } from '@/common/components/typography' -import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' +import { TextMedium } from '@/common/components/typography' import { SignTransactionModal } from '@/common/modals/SignTransactionModal/SignTransactionModal' interface SignProps { @@ -14,33 +13,22 @@ interface SignProps { service: ActorRef } -export const AddStakingAccCandidateModal = ({ transaction, signer, service }: SignProps) => { - const { paymentInfo } = useSignAndSendTransaction({ - transaction, - signer: signer.address, - service, - }) - - return ( - - You are intending to bond your validator account with your membership. - - Fees of will be applied to the transaction. - - - ) -} +export const AddStakingAccCandidateModal = ({ transaction, signer, service }: SignProps) => ( + + You are intending to bond your validator account with your membership. + +) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index 11e680fb83..8ba2588ec6 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -29,30 +29,25 @@ export const BuyMembershipModal = () => { apolloClient.refetchQueries({ include: 'active' }) }, [isSuccessful, apolloClient]) + const memberId = state.context.memberId?.toString() + const buyTransaction = useMemo( () => state.context.form && api?.tx.members.buyMembership(toMemberTransactionParams(state.context.form)), [api?.isConnected, state.context.form] ) const bindTransaction = useMemo( - () => state.context.memberId && api?.tx.members.addStakingAccountCandidate(state.context.memberId.toString()), - [state.context, state.context.memberId] - ) - const conFirmTransaction = useMemo( - () => - state.context.memberId && - state.context.form?.validatorAccounts && - (state.context.form.validatorAccounts.length > 1 - ? api?.tx.utility.batch( - state.context.form.validatorAccounts.map(({ address }) => - api.tx.members.confirmStakingAccount(state.context.memberId?.toString() ?? '', address) - ) - ) - : api?.tx.members.confirmStakingAccount( - state.context.memberId.toString(), - state.context.form.validatorAccounts[0].address - )), - [api?.isConnected, state.context.memberId, state.context.form?.validatorAccounts] + () => memberId && api?.tx.members.addStakingAccountCandidate(memberId), + [api?.isConnected, memberId] ) + const conFirmTransaction = useMemo(() => { + const validatorAccounts = state.context.form?.validatorAccounts + + if (!api || !memberId || !validatorAccounts) return + + const confirmTxs = validatorAccounts.map(({ address }) => api.tx.members.confirmStakingAccount(memberId, address)) + + return confirmTxs.length > 1 ? api.tx.utility.batch(confirmTxs) : confirmTxs[0] + }, [api?.isConnected, memberId, state.context.form?.validatorAccounts]) if (state.matches('prepare')) { const onSubmit = (params: MemberFormFields) => send({ type: 'DONE', form: params }) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx index ed7a19d5bd..5a724e61ef 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/ConfirmStakingAccModal.tsx @@ -4,8 +4,7 @@ import React from 'react' import { ActorRef } from 'xstate' import { Account } from '@/accounts/types' -import { TextMedium, TokenValue } from '@/common/components/typography' -import { useSignAndSendTransaction } from '@/common/hooks/useSignAndSendTransaction' +import { TextMedium } from '@/common/components/typography' import { SignTransactionModal } from '@/common/modals/SignTransactionModal/SignTransactionModal' interface SignProps { @@ -14,33 +13,22 @@ interface SignProps { service: ActorRef } -export const ConfirmStakingAccModal = ({ transaction, signer, service }: SignProps) => { - const { paymentInfo } = useSignAndSendTransaction({ - transaction, - signer: signer.address, - service, - }) - - return ( - - You are intending to confirm your validator account to be bound with your membership - - Fees of will be applied to the transaction. - - - ) -} +export const ConfirmStakingAccModal = ({ transaction, signer, service }: SignProps) => ( + + You are intending to confirm your validator account to be bound with your membership + +) From bae60409d6bd26086ed3c782426908fb4fb0188e Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 22 Dec 2023 12:11:23 +0100 Subject: [PATCH 38/44] Improve binding tests --- packages/ui/src/app/App.stories.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 479f9154f2..218c4b25ce 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -1,5 +1,6 @@ import { metadataToBytes } from '@joystream/js/utils' import { MembershipMetadata } from '@joystream/metadata-protobuf' +import { SubmittableExtrinsic } from '@polkadot/api/types' import { expect } from '@storybook/jest' import { Meta, StoryContext, StoryObj } from '@storybook/react' import { userEvent, waitFor, within } from '@storybook/testing-library' @@ -44,7 +45,6 @@ type Args = { onConfirmEmail: jest.Mock onAddStakingAccount: jest.Mock onConfirmStakingAccount: jest.Mock - batchTx: jest.Mock } type Story = StoryObj> @@ -81,7 +81,6 @@ export default { onConfirmEmail: { action: 'ConfirmEmail' }, onAddStakingAccount: { action: 'AddStakingAccount' }, onConfirmStakingAccount: { action: 'ConfirmStakingAccount' }, - batchTx: { action: 'BatchTx' }, }, args: { @@ -154,7 +153,8 @@ export default { utility: { batch: { event: 'TxBatch', - onSend: args.batchTx, + onSend: (transactions: SubmittableExtrinsic<'rxjs'>[]) => + transactions.forEach((transaction) => transaction.signAndSend('')), failure: parameters.batchTxFailure, }, }, @@ -612,7 +612,9 @@ export const BuyMembershipHappyBindOneValidatorHappy: Story = { invitingMemberId: undefined, referrerId: undefined, }) + expect(args.onAddStakingAccount).toHaveBeenCalledTimes(1) expect(args.onAddStakingAccount).toHaveBeenCalledWith(NEW_MEMBER_DATA.id) + expect(args.onConfirmStakingAccount).toHaveBeenCalledTimes(1) expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(NEW_MEMBER_DATA.id, charlie.controllerAccount) const doneButton = getButtonByText(modal, 'Done') @@ -696,7 +698,10 @@ export const BuyMembershipHappyAddTwoValidatorHappy: Story = { referrerId: undefined, }) expect(args.onAddStakingAccount).toHaveBeenCalledTimes(2) - expect(args.batchTx).toHaveBeenCalledTimes(1) + expect(args.onAddStakingAccount).toHaveBeenCalledWith(NEW_MEMBER_DATA.id) + expect(args.onConfirmStakingAccount).toHaveBeenCalledTimes(2) + expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(NEW_MEMBER_DATA.id, charlie.controllerAccount) + expect(args.onConfirmStakingAccount).toHaveBeenCalledWith(NEW_MEMBER_DATA.id, dave.controllerAccount) const doneButton = getButtonByText(modal, 'Done') expect(doneButton).toBeEnabled() From c1a0b4b44e467928176d20520267c9d76a0a8d0d Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 22 Dec 2023 12:23:56 +0100 Subject: [PATCH 39/44] Change the select validator account function --- packages/ui/src/app/App.stories.tsx | 31 ++++++++++++----------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 218c4b25ce..ee43e37333 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -530,21 +530,16 @@ export const BuyMembershipTxFailure: Story = { }, } -const fillMembershipFormWithOneValidatorAcc = async (modal: Container) => { +const fillMembershipFormValidatorAccounts = async (modal: Container, accounts: string[]) => { await fillMembershipForm(modal) const validatorChechButton = modal.getAllByText('Yes')[1] await userEvent.click(validatorChechButton) expect(await modal.findByText(/^If your validator account/)) - await selectFromDropdown(modal, /^If your validator account/, 'charlie') - const addButton = document.getElementsByClassName('add-button')[0] - await userEvent.click(addButton) -} - -const fillMembershipFormWithTwoValidatorAcc = async (modal: Container) => { - await fillMembershipFormWithOneValidatorAcc(modal) - await selectFromDropdown(modal, /^If your validator account/, 'dave') - const addButton = document.getElementsByClassName('add-button')[0] - await userEvent.click(addButton) + for (const account of accounts) { + await selectFromDropdown(modal, /^If your validator account/, account) + const addButton = document.getElementsByClassName('add-button')[0] + await userEvent.click(addButton) + } } export const BuyMembershipHappyBindOneValidatorHappy: Story = { @@ -563,7 +558,7 @@ export const BuyMembershipHappyBindOneValidatorHappy: Story = { await step('Fill', async () => { expect(createButton).toBeDisabled() - await fillMembershipFormWithOneValidatorAcc(modal) + await fillMembershipFormValidatorAccounts(modal, ['charlie']) await waitFor(() => expect(createButton).toBeEnabled()) }) @@ -640,7 +635,7 @@ export const BuyMembershipHappyAddTwoValidatorHappy: Story = { await step('Fill', async () => { expect(createButton).toBeDisabled() - await fillMembershipFormWithTwoValidatorAcc(modal) + await fillMembershipFormValidatorAccounts(modal, ['charlie', 'dave']) await waitFor(() => expect(createButton).toBeEnabled()) }) @@ -720,7 +715,7 @@ export const BuyMembershipWithValidatorAccountNotEnoughFunds: Story = { await userEvent.click(getButtonByText(screen, 'Join Now')) - await fillMembershipFormWithOneValidatorAcc(modal) + await fillMembershipFormValidatorAccounts(modal, ['charlie']) const createButton = getButtonByText(modal, 'Create a Membership') await waitFor(() => expect(createButton).toBeEnabled()) await userEvent.click(createButton) @@ -740,7 +735,7 @@ export const BuyMembershipWithValidatorAccountFailure: Story = { await userEvent.click(getButtonByText(screen, 'Join Now')) - await fillMembershipFormWithOneValidatorAcc(modal) + await fillMembershipFormValidatorAccounts(modal, ['charlie']) const createButton = getButtonByText(modal, 'Create a Membership') await waitFor(() => expect(createButton).toBeEnabled()) await userEvent.click(createButton) @@ -769,7 +764,7 @@ export const BuyMembershipHappyAddOneValidatorFailure: Story = { await step('Fill', async () => { expect(createButton).toBeDisabled() - await fillMembershipFormWithOneValidatorAcc(modal) + await fillMembershipFormValidatorAccounts(modal, ['charlie']) await waitFor(() => expect(createButton).toBeEnabled()) }) @@ -815,7 +810,7 @@ export const BuyMembershipAddValidatorAccHappyConfirmTxFailure: Story = { await step('Fill', async () => { expect(createButton).toBeDisabled() - await fillMembershipFormWithOneValidatorAcc(modal) + await fillMembershipFormValidatorAccounts(modal, ['charlie']) await waitFor(() => expect(createButton).toBeEnabled()) }) @@ -871,7 +866,7 @@ export const BuyMembershipAddTwoValidatorAccHappyConfirmTxFailure: Story = { await step('Fill', async () => { expect(createButton).toBeDisabled() - await fillMembershipFormWithTwoValidatorAcc(modal) + await fillMembershipFormValidatorAccounts(modal, ['charlie', 'dave']) await waitFor(() => expect(createButton).toBeEnabled()) }) From 732d7dae93e7f09154bbbacb660e0fd9d70c4413 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Mon, 18 Dec 2023 11:21:48 -0500 Subject: [PATCH 40/44] add validatorProvider --- packages/ui/src/app/Providers.tsx | 35 ++++++++++--------- .../Validators/ValidatorList.stories.tsx | 9 ++++- .../modals/UpdateMembershipModal/types.ts | 4 +++ .../ui/src/validators/hooks/useValidators.ts | 5 +++ .../validators/hooks/useValidatorsList.tsx | 4 +-- .../ui/src/validators/providers/context.tsx | 5 +++ .../provider.tsx} | 28 +++++++++++++-- 7 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 packages/ui/src/validators/hooks/useValidators.ts create mode 100644 packages/ui/src/validators/providers/context.tsx rename packages/ui/src/validators/{hooks/useValidatorMembers.tsx => providers/provider.tsx} (78%) diff --git a/packages/ui/src/app/Providers.tsx b/packages/ui/src/app/Providers.tsx index ded0ae48bf..5b2dcdc770 100644 --- a/packages/ui/src/app/Providers.tsx +++ b/packages/ui/src/app/Providers.tsx @@ -13,6 +13,7 @@ import { OnBoardingProvider } from '@/common/providers/onboarding/provider' import { ResponsiveProvider } from '@/common/providers/responsive/provider' import { TransactionStatusProvider } from '@/common/providers/transactionStatus/provider' import { MembershipContextProvider } from '@/memberships/providers/membership/provider' +import { ValidatorContextProvider } from '@/validators/providers/provider' import { BackendProvider } from './providers/backend/provider' import { GlobalStyle } from './providers/GlobalStyle' @@ -31,22 +32,24 @@ export const Providers = ({ children }: Props) => ( - - - - - - - - - {children} - - - - - - - + + + + + + + + + + {children} + + + + + + + + diff --git a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx index e32ce40fb8..401bcfc3c4 100644 --- a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx +++ b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx @@ -1,12 +1,14 @@ import { expect } from '@storybook/jest' import { Meta, StoryObj } from '@storybook/react' import { userEvent, waitFor, within } from '@storybook/testing-library' +import React from 'react' import { Address } from '@/common/types' import { GetMembersWithDetailsDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { joy, selectFromDropdown } from '@/mocks/helpers' import { MocksParameters } from '@/mocks/providers' +import { ValidatorContextProvider } from '@/validators/providers/provider' import { ValidatorList } from './ValidatorList' @@ -14,7 +16,6 @@ type Args = object export default { title: 'Pages/Validators/ValidatorList', - component: ValidatorList, parameters: { mocks: (): MocksParameters => { @@ -438,6 +439,12 @@ export default { } }, }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + render: (args) => ( + + + + ), } satisfies Meta type Story = StoryObj diff --git a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts index de145adda7..d61ace6c53 100644 --- a/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts +++ b/packages/ui/src/memberships/modals/UpdateMembershipModal/types.ts @@ -1,4 +1,5 @@ import { Account } from '@/accounts/types' +import { Address } from '@/common/types' export interface UpdateMemberForm { id: string @@ -9,4 +10,7 @@ export interface UpdateMemberForm { rootAccount?: Account controllerAccount?: Account externalResources: Record + isValidator?: boolean + validatorAccountCandidate?: Account + validatorAccounts?: Address[] } diff --git a/packages/ui/src/validators/hooks/useValidators.ts b/packages/ui/src/validators/hooks/useValidators.ts new file mode 100644 index 0000000000..9ccd686314 --- /dev/null +++ b/packages/ui/src/validators/hooks/useValidators.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react' + +import { ValidatorsContext } from '../providers/context' + +export const useValidators = () => useContext(ValidatorsContext) diff --git a/packages/ui/src/validators/hooks/useValidatorsList.tsx b/packages/ui/src/validators/hooks/useValidatorsList.tsx index 06f7a3e6aa..24ffaa8598 100644 --- a/packages/ui/src/validators/hooks/useValidatorsList.tsx +++ b/packages/ui/src/validators/hooks/useValidatorsList.tsx @@ -12,7 +12,7 @@ import { last } from '@/common/utils' import { Verification, State, ValidatorWithDetails, ValidatorMembership } from '../types' -import { useValidatorMembers } from './useValidatorMembers' +import { useValidators } from './useValidators' export const useValidatorsList = () => { const { api } = useApi() @@ -20,7 +20,7 @@ export const useValidatorsList = () => { const [isVerified, setIsVerified] = useState(null) const [isActive, setIsActive] = useState(null) const [visibleValidators, setVisibleValidators] = useState([]) - const validators = useValidatorMembers() + const { validatorsWithMembership: validators } = useValidators() const validatorRewardPointsHistory = useFirstObservableValue( () => api?.query.staking.erasRewardPoints.entries(), diff --git a/packages/ui/src/validators/providers/context.tsx b/packages/ui/src/validators/providers/context.tsx new file mode 100644 index 0000000000..7681c7e6ba --- /dev/null +++ b/packages/ui/src/validators/providers/context.tsx @@ -0,0 +1,5 @@ +import { createContext } from 'react' + +import { UseValidators } from './provider' + +export const ValidatorsContext = createContext({}) diff --git a/packages/ui/src/validators/hooks/useValidatorMembers.tsx b/packages/ui/src/validators/providers/provider.tsx similarity index 78% rename from packages/ui/src/validators/hooks/useValidatorMembers.tsx rename to packages/ui/src/validators/providers/provider.tsx index 2f6730552d..590a867fa2 100644 --- a/packages/ui/src/validators/hooks/useValidatorMembers.tsx +++ b/packages/ui/src/validators/providers/provider.tsx @@ -1,15 +1,31 @@ -import { useMemo } from 'react' +import React, { ReactNode, useMemo } from 'react' import { map } from 'rxjs' import { useApi } from '@/api/hooks/useApi' import { useFirstObservableValue } from '@/common/hooks/useFirstObservableValue' +import { Address } from '@/common/types' import { perbillToPercent } from '@/common/utils' import { useGetMembersWithDetailsQuery } from '@/memberships/queries' import { asMemberWithDetails } from '@/memberships/types' import { ValidatorMembership } from '../types' -export const useValidatorMembers = () => { +import { ValidatorsContext } from './context' + +interface Props { + children: ReactNode +} + +export interface UseValidators { + allValidators?: { + address: Address + commission: number + }[] + allValidatorsWithCtrlAcc?: (string | undefined)[] + validatorsWithMembership?: ValidatorMembership[] +} + +export const ValidatorContextProvider = (props: Props) => { const { api } = useApi() const allValidators = useFirstObservableValue( () => @@ -71,5 +87,11 @@ export const useValidatorMembers = () => { ) }, [data, allValidators, allValidatorsWithCtrlAcc]) - return validatorsWithMembership + const value = { + allValidators, + allValidatorsWithCtrlAcc, + validatorsWithMembership, + } + + return {props.children} } From afa815c5b3130685c285e876209cbbc9a3faee84 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Sun, 24 Dec 2023 11:23:08 -0500 Subject: [PATCH 41/44] check validator account --- packages/ui/.storybook/preview.tsx | 13 +- packages/ui/src/app/App.stories.tsx | 119 +++++++++++++++++- .../BuyMembershipFormModal.tsx | 53 +++++++- .../validators/hooks/useValidatorsList.tsx | 6 +- .../ui/src/validators/providers/context.tsx | 2 +- .../ui/src/validators/providers/provider.tsx | 31 ++--- 6 files changed, 199 insertions(+), 25 deletions(-) diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index 810cd97b2c..8ba958bc6b 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -16,6 +16,7 @@ import { TransactionStatusProvider } from '../src/common/providers/transactionSt import { MockProvidersDecorator, MockRouterDecorator } from '../src/mocks/providers' import { i18next } from '../src/services/i18n' import { KeyringContext } from '../src/common/providers/keyring/context' +import { ValidatorContextProvider } from '../src/validators/providers/provider' import { Keyring } from '@polkadot/ui-keyring' configure({ testIdAttribute: 'id' }) @@ -56,11 +57,13 @@ const ModalDecorator: Decorator = (Story) => ( - - - - - + + + + + + + diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index ffe5dfb8a8..0d7e06ff78 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -10,7 +10,12 @@ import { createGlobalStyle } from 'styled-components' import { Page, Screen } from '@/common/components/page/Page' import { Colors } from '@/common/constants' import { EMAIL_VERIFICATION_TOKEN_SEARCH_PARAM } from '@/memberships/constants' -import { GetMemberDocument } from '@/memberships/queries' +import { + GetMemberActionDetailsDocument, + GetMemberDocument, + GetMembersCountDocument, + GetMembersWithDetailsDocument, +} from '@/memberships/queries' import { ConfirmBackendEmailDocument, GetBackendMemberExistsDocument, @@ -119,6 +124,71 @@ export default { members: { membershipPrice: joy(20) }, council: { stage: { stage: { isIdle: true }, changedAt: 123 } }, referendum: { stage: {} }, + staking: { + bonded: { + multi: [ + 'j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D', + 'j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW', + 'j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP', + 'j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz', + 'j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa', + 'j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN', + 'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP', + 'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ', + 'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg', + 'j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt', + 'j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM', + ], + }, + validators: { + entries: [ + [ + { args: ['5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy'] }, + { commission: 0.1 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz'] }, + { commission: 0.15 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa'] }, + { commission: 0.2 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN'] }, + { commission: 0.01 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP'] }, + { commission: 0.03 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + [ + { args: ['j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM'] }, + { commission: 0.05 * 10 ** 9, blocked: false }, + ], + ], + }, + }, }, tx: { @@ -169,6 +239,35 @@ export default { query: GetBackendMemberExistsDocument, data: { memberExist: args.hasRegisteredEmail }, }, + { + query: GetMemberDocument, + data: { membershipByUniqueInput: member('alice') }, + }, + { + query: GetMembersCountDocument, + data: { membershipsConnection: { totalCount: 0 } }, + }, + { + query: GetMembersWithDetailsDocument, + data: { memberships: [member('alice'), member('bob'), member('charlie'), member('dave')] }, + }, + { + query: GetMemberActionDetailsDocument, + data: { + stakeSlashedEventsConnection: { + totalCount: 2, + }, + terminatedLeaderEventsConnection: { + totalCount: 3, + }, + terminatedWorkerEventsConnection: { + totalCount: 4, + }, + memberInvitedEventsConnection: { + totalCount: 0, + }, + }, + }, ], mutations: [ { @@ -703,6 +802,24 @@ export const BuyMembershipHappyAddTwoValidatorHappy: Story = { }, } +export const InvalidValidatorAccountInput: Story = { + args: { hasMemberships: false, isLoggedIn: false }, + parameters: { totalBalance: 20 }, + + play: async ({ canvasElement }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + await userEvent.click(getButtonByText(screen, 'Join Now')) + + await fillMembershipFormValidatorAccounts(modal, ['alice']) + + expect(modal.getByText('This account is neither a validator controller account nor a validator stash account.')) + const addButton = document.getElementsByClassName('add-button')[0] + expect(addButton).toBeDisabled() + }, +} + export const BuyMembershipWithValidatorAccountNotEnoughFunds: Story = { args: { hasMemberships: false, isLoggedIn: false }, parameters: { totalBalance: 20 }, diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index fd2ab59e49..5bf5d26539 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -1,6 +1,6 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { BalanceOf } from '@polkadot/types/interfaces/runtime' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import styled from 'styled-components' import * as Yup from 'yup' @@ -8,6 +8,7 @@ import * as Yup from 'yup' import { SelectAccount, SelectedAccount } from '@/accounts/components/SelectAccount' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { accountOrNamed } from '@/accounts/model/accountOrNamed' +import { encodeAddress } from '@/accounts/model/encodeAddress' import { Account } from '@/accounts/types' import { TermsRoutes } from '@/app/constants/routes' import { ButtonGhost, ButtonPrimary } from '@/common/components/buttons' @@ -22,6 +23,7 @@ import { ToggleCheckbox, } from '@/common/components/forms' import { Arrow, CrossIcon, PlusIcon } from '@/common/components/icons' +import { AlertSymbol } from '@/common/components/icons/symbols' import { Loading } from '@/common/components/Loading' import { ModalFooter, @@ -43,6 +45,7 @@ import { AvatarInput } from '@/memberships/components/AvatarInput' import { SocialMediaSelector } from '@/memberships/components/SocialMediaSelector/SocialMediaSelector' import { useUploadAvatarAndSubmit } from '@/memberships/hooks/useUploadAvatarAndSubmit' import { useGetMembersCountQuery } from '@/memberships/queries' +import { useValidators } from '@/validators/hooks/useValidators' import { SelectMember } from '../../components/SelectMember' import { @@ -130,6 +133,11 @@ export const BuyMembershipForm = ({ const [formHandleMap, setFormHandleMap] = useState('') const { isUploading, uploadAvatarAndSubmit } = useUploadAvatarAndSubmit(onSubmit) const { data } = useGetMembersCountQuery({ variables: { where: { handle_eq: formHandleMap } } }) + const { fetchValidators, allValidators, allValidatorsWithCtrlAcc } = useValidators() + + useEffect(() => { + fetchValidators(true) + }, []) const form = useForm({ resolver: useYupValidationResolver(CreateMemberSchema), @@ -152,6 +160,18 @@ export const BuyMembershipForm = ({ ]) const [validatorAccounts, setValidatorAccounts] = useState([]) + const isValidValidatorAccount = useMemo( + () => + validatorAccountCandidate && + ( + allValidatorsWithCtrlAcc + ?.concat(allValidators?.map(({ address }) => address)) + .filter((element) => !!element) as string[] + ) + .map(encodeAddress) + .includes(encodeAddress(validatorAccountCandidate.address)), + [allValidators, allValidatorsWithCtrlAcc, validatorAccountCandidate] + ) useEffect(() => { if (handle) { @@ -170,7 +190,7 @@ export const BuyMembershipForm = ({ type === 'onBoarding' && process.env.REACT_APP_CAPTCHA_SITE_KEY ? !captchaToken || !isFormValid : !isFormValid const addValidatorAccount = () => { - if (validatorAccountCandidate) { + if (validatorAccountCandidate && isValidValidatorAccount) { setValidatorAccounts([...new Set([...validatorAccounts, validatorAccountCandidate])]) form?.setValue('validatorAccountCandidate' as keyof MemberFormFields, undefined) } @@ -289,12 +309,24 @@ export const BuyMembershipForm = ({ square size="large" onClick={addValidatorAccount} - disabled={!validatorAccountCandidate} + disabled={!isValidValidatorAccount} className="add-button" > + {validatorAccountCandidate && !isValidValidatorAccount && ( + + + + + + + + This account is neither a validator controller account nor a validator stash account. + + + )} {validatorAccounts.map((account, index) => ( @@ -403,3 +435,18 @@ const SelectValidatorAccountWrapper = styled.div` flex-direction: column; gap: 8px; ` + +const InputNotificationIcon = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 12px; + height: 12px; + color: inherit; + padding-right: 2px; + + .blackPart, + .primaryPart { + fill: currentColor; + } +` diff --git a/packages/ui/src/validators/hooks/useValidatorsList.tsx b/packages/ui/src/validators/hooks/useValidatorsList.tsx index 24ffaa8598..a2662cfa88 100644 --- a/packages/ui/src/validators/hooks/useValidatorsList.tsx +++ b/packages/ui/src/validators/hooks/useValidatorsList.tsx @@ -20,7 +20,11 @@ export const useValidatorsList = () => { const [isVerified, setIsVerified] = useState(null) const [isActive, setIsActive] = useState(null) const [visibleValidators, setVisibleValidators] = useState([]) - const { validatorsWithMembership: validators } = useValidators() + const { fetchValidators, validatorsWithMembership: validators } = useValidators() + + useEffect(() => { + fetchValidators(true) + }, []) const validatorRewardPointsHistory = useFirstObservableValue( () => api?.query.staking.erasRewardPoints.entries(), diff --git a/packages/ui/src/validators/providers/context.tsx b/packages/ui/src/validators/providers/context.tsx index 7681c7e6ba..c3d724d9ee 100644 --- a/packages/ui/src/validators/providers/context.tsx +++ b/packages/ui/src/validators/providers/context.tsx @@ -2,4 +2,4 @@ import { createContext } from 'react' import { UseValidators } from './provider' -export const ValidatorsContext = createContext({}) +export const ValidatorsContext = createContext({ fetchValidators: () => {} }) diff --git a/packages/ui/src/validators/providers/provider.tsx b/packages/ui/src/validators/providers/provider.tsx index 590a867fa2..3365f4f6fe 100644 --- a/packages/ui/src/validators/providers/provider.tsx +++ b/packages/ui/src/validators/providers/provider.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useMemo } from 'react' +import React, { ReactNode, useMemo, useState } from 'react' import { map } from 'rxjs' import { useApi } from '@/api/hooks/useApi' @@ -17,6 +17,7 @@ interface Props { } export interface UseValidators { + fetchValidators: (fetchValidators: boolean) => void allValidators?: { address: Address commission: number @@ -27,18 +28,19 @@ export interface UseValidators { export const ValidatorContextProvider = (props: Props) => { const { api } = useApi() - const allValidators = useFirstObservableValue( - () => - api?.query.staking.validators.entries().pipe( - map((entries) => - entries.map((entry) => ({ - address: entry[0].args[0].toString(), - commission: perbillToPercent(entry[1].commission.toBn()), - })) - ) - ), - [api?.isConnected] - ) + const [validatorRelatedPage, fetchValidators] = useState(false) + + const allValidators = useFirstObservableValue(() => { + if (!validatorRelatedPage) return undefined + return api?.query.staking.validators.entries().pipe( + map((entries) => + entries.map((entry) => ({ + address: entry[0].args[0].toString(), + commission: perbillToPercent(entry[1].commission.toBn()), + })) + ) + ) + }, [api?.isConnected, validatorRelatedPage]) const allValidatorsWithCtrlAcc = useFirstObservableValue( () => @@ -55,7 +57,7 @@ export const ValidatorContextProvider = (props: Props) => { boundAccounts_containsAny: (allValidatorsWithCtrlAcc ?.concat(allValidators?.map(({ address }) => address)) - .filter((element) => !element) as string[]) ?? [], + .filter((element) => !!element) as string[]) ?? [], }, } @@ -88,6 +90,7 @@ export const ValidatorContextProvider = (props: Props) => { }, [data, allValidators, allValidatorsWithCtrlAcc]) const value = { + fetchValidators, allValidators, allValidatorsWithCtrlAcc, validatorsWithMembership, From 64b6ed00ec52b6c338e706c5df751ad84b9c00e9 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Sun, 24 Dec 2023 12:23:19 -0500 Subject: [PATCH 42/44] Count the membership's root/controller account into the validator membership --- .../ui/src/validators/providers/provider.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/validators/providers/provider.tsx b/packages/ui/src/validators/providers/provider.tsx index 3365f4f6fe..877bbcb7c7 100644 --- a/packages/ui/src/validators/providers/provider.tsx +++ b/packages/ui/src/validators/providers/provider.tsx @@ -54,14 +54,30 @@ export const ValidatorContextProvider = (props: Props) => { const variables = { where: { - boundAccounts_containsAny: - (allValidatorsWithCtrlAcc - ?.concat(allValidators?.map(({ address }) => address)) - .filter((element) => !!element) as string[]) ?? [], + OR: [ + { + rootAccount_in: + (allValidatorsWithCtrlAcc + ?.concat(allValidators?.map(({ address }) => address)) + .filter((element) => !!element) as string[]) ?? [], + }, + { + controllerAccount_in: + (allValidatorsWithCtrlAcc + ?.concat(allValidators?.map(({ address }) => address)) + .filter((element) => !!element) as string[]) ?? [], + }, + { + boundAccounts_containsAny: + (allValidatorsWithCtrlAcc + ?.concat(allValidators?.map(({ address }) => address)) + .filter((element) => !!element) as string[]) ?? [], + }, + ], }, } - const { data } = useGetMembersWithDetailsQuery({ variables, skip: !!allValidatorsWithCtrlAcc }) + const { data } = useGetMembersWithDetailsQuery({ variables, skip: !allValidatorsWithCtrlAcc }) const memberships = data?.memberships?.map((rawMembership) => ({ membership: asMemberWithDetails(rawMembership), @@ -81,6 +97,10 @@ export const ValidatorContextProvider = (props: Props) => { commission, ...memberships.find( ({ membership }) => + membership.rootAccount === address || + membership.rootAccount === controllerAccount || + membership.controllerAccount === address || + membership.controllerAccount === controllerAccount || membership.boundAccounts.includes(address) || (controllerAccount && membership.boundAccounts.includes(controllerAccount)) ), From ac96134306161ab91e7be5c9c05c28fbf1e538b9 Mon Sep 17 00:00:00 2001 From: eshark9312 Date: Wed, 27 Dec 2023 09:44:27 -0500 Subject: [PATCH 43/44] fix --- packages/ui/src/app/App.stories.tsx | 15 ++++++--- .../Validators/ValidatorList.stories.tsx | 9 +---- .../BuyMembershipFormModal.tsx | 33 ++++++++++--------- .../ui/src/validators/hooks/useValidators.ts | 13 ++++++-- .../validators/hooks/useValidatorsList.tsx | 6 +--- .../ui/src/validators/providers/context.tsx | 2 +- .../ui/src/validators/providers/provider.tsx | 11 ++++--- 7 files changed, 48 insertions(+), 41 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index 0d7e06ff78..3aaa81a2d7 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -629,8 +629,8 @@ export const BuyMembershipTxFailure: Story = { const fillMembershipFormValidatorAccounts = async (modal: Container, accounts: string[]) => { await fillMembershipForm(modal) - const validatorChechButton = modal.getAllByText('Yes')[1] - await userEvent.click(validatorChechButton) + const validatorCheckButton = modal.getAllByText('Yes')[1] + await userEvent.click(validatorCheckButton) expect(await modal.findByText(/^If your validator account/)) for (const account of accounts) { await selectFromDropdown(modal, /^If your validator account/, account) @@ -811,8 +811,15 @@ export const InvalidValidatorAccountInput: Story = { const modal = withinModal(canvasElement) await userEvent.click(getButtonByText(screen, 'Join Now')) - - await fillMembershipFormValidatorAccounts(modal, ['alice']) + await fillMembershipForm(modal) + const validatorCheckButton = modal.getAllByText('Yes')[1] + await userEvent.click(validatorCheckButton) + const validatorAddressInputElement = document.getElementById('select-validatorAccount-input') + expect(validatorAddressInputElement).not.toBeNull() + await userEvent.paste( + validatorAddressInputElement as HTMLElement, + '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' + ) expect(modal.getByText('This account is neither a validator controller account nor a validator stash account.')) const addButton = document.getElementsByClassName('add-button')[0] diff --git a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx index 401bcfc3c4..e32ce40fb8 100644 --- a/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx +++ b/packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx @@ -1,14 +1,12 @@ import { expect } from '@storybook/jest' import { Meta, StoryObj } from '@storybook/react' import { userEvent, waitFor, within } from '@storybook/testing-library' -import React from 'react' import { Address } from '@/common/types' import { GetMembersWithDetailsDocument } from '@/memberships/queries' import { member } from '@/mocks/data/members' import { joy, selectFromDropdown } from '@/mocks/helpers' import { MocksParameters } from '@/mocks/providers' -import { ValidatorContextProvider } from '@/validators/providers/provider' import { ValidatorList } from './ValidatorList' @@ -16,6 +14,7 @@ type Args = object export default { title: 'Pages/Validators/ValidatorList', + component: ValidatorList, parameters: { mocks: (): MocksParameters => { @@ -439,12 +438,6 @@ export default { } }, }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - render: (args) => ( - - - - ), } satisfies Meta type Story = StoryObj diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index 5bf5d26539..e1317e0eeb 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -133,11 +133,7 @@ export const BuyMembershipForm = ({ const [formHandleMap, setFormHandleMap] = useState('') const { isUploading, uploadAvatarAndSubmit } = useUploadAvatarAndSubmit(onSubmit) const { data } = useGetMembersCountQuery({ variables: { where: { handle_eq: formHandleMap } } }) - const { fetchValidators, allValidators, allValidatorsWithCtrlAcc } = useValidators() - - useEffect(() => { - fetchValidators(true) - }, []) + const { allValidators, allValidatorsWithCtrlAcc } = useValidators() const form = useForm({ resolver: useYupValidationResolver(CreateMemberSchema), @@ -160,17 +156,18 @@ export const BuyMembershipForm = ({ ]) const [validatorAccounts, setValidatorAccounts] = useState([]) + const validatorAddresses = useMemo(() => { + if (!allValidatorsWithCtrlAcc || !allValidators) return + return ( + [...allValidatorsWithCtrlAcc, ...allValidators.map(({ address }) => address)].filter( + (element) => !!element + ) as string[] + ).map(encodeAddress) + }, [allValidators, allValidatorsWithCtrlAcc]) + const isValidValidatorAccount = useMemo( - () => - validatorAccountCandidate && - ( - allValidatorsWithCtrlAcc - ?.concat(allValidators?.map(({ address }) => address)) - .filter((element) => !!element) as string[] - ) - .map(encodeAddress) - .includes(encodeAddress(validatorAccountCandidate.address)), - [allValidators, allValidatorsWithCtrlAcc, validatorAccountCandidate] + () => validatorAccountCandidate && validatorAddresses?.includes(encodeAddress(validatorAccountCandidate.address)), + [allValidators, allValidatorsWithCtrlAcc, validatorAddresses, validatorAccountCandidate] ) useEffect(() => { @@ -303,7 +300,11 @@ export const BuyMembershipForm = ({ - + !!validatorAddresses?.includes(encodeAddress(account.address))} + /> useContext(ValidatorsContext) +export const useValidators = () => { + const { setShouldFetchValidators, allValidators, allValidatorsWithCtrlAcc, validatorsWithMembership } = + useContext(ValidatorsContext) + + useEffect(() => { + setShouldFetchValidators(true) + }, []) + + return { allValidators, allValidatorsWithCtrlAcc, validatorsWithMembership } +} diff --git a/packages/ui/src/validators/hooks/useValidatorsList.tsx b/packages/ui/src/validators/hooks/useValidatorsList.tsx index a2662cfa88..24ffaa8598 100644 --- a/packages/ui/src/validators/hooks/useValidatorsList.tsx +++ b/packages/ui/src/validators/hooks/useValidatorsList.tsx @@ -20,11 +20,7 @@ export const useValidatorsList = () => { const [isVerified, setIsVerified] = useState(null) const [isActive, setIsActive] = useState(null) const [visibleValidators, setVisibleValidators] = useState([]) - const { fetchValidators, validatorsWithMembership: validators } = useValidators() - - useEffect(() => { - fetchValidators(true) - }, []) + const { validatorsWithMembership: validators } = useValidators() const validatorRewardPointsHistory = useFirstObservableValue( () => api?.query.staking.erasRewardPoints.entries(), diff --git a/packages/ui/src/validators/providers/context.tsx b/packages/ui/src/validators/providers/context.tsx index c3d724d9ee..37d0aaf735 100644 --- a/packages/ui/src/validators/providers/context.tsx +++ b/packages/ui/src/validators/providers/context.tsx @@ -2,4 +2,4 @@ import { createContext } from 'react' import { UseValidators } from './provider' -export const ValidatorsContext = createContext({ fetchValidators: () => {} }) +export const ValidatorsContext = createContext({ setShouldFetchValidators: () => {} }) diff --git a/packages/ui/src/validators/providers/provider.tsx b/packages/ui/src/validators/providers/provider.tsx index 877bbcb7c7..6993589c72 100644 --- a/packages/ui/src/validators/providers/provider.tsx +++ b/packages/ui/src/validators/providers/provider.tsx @@ -17,7 +17,7 @@ interface Props { } export interface UseValidators { - fetchValidators: (fetchValidators: boolean) => void + setShouldFetchValidators: (fetchValidators: boolean) => void allValidators?: { address: Address commission: number @@ -28,10 +28,11 @@ export interface UseValidators { export const ValidatorContextProvider = (props: Props) => { const { api } = useApi() - const [validatorRelatedPage, fetchValidators] = useState(false) + + const [shouldFetchValidators, setShouldFetchValidators] = useState(false) const allValidators = useFirstObservableValue(() => { - if (!validatorRelatedPage) return undefined + if (!shouldFetchValidators) return undefined return api?.query.staking.validators.entries().pipe( map((entries) => entries.map((entry) => ({ @@ -40,7 +41,7 @@ export const ValidatorContextProvider = (props: Props) => { })) ) ) - }, [api?.isConnected, validatorRelatedPage]) + }, [api?.isConnected, shouldFetchValidators]) const allValidatorsWithCtrlAcc = useFirstObservableValue( () => @@ -110,7 +111,7 @@ export const ValidatorContextProvider = (props: Props) => { }, [data, allValidators, allValidatorsWithCtrlAcc]) const value = { - fetchValidators, + setShouldFetchValidators, allValidators, allValidatorsWithCtrlAcc, validatorsWithMembership, From 6e750c4950a6a3299988896036ac623249ce583d Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Thu, 28 Dec 2023 17:15:58 +0100 Subject: [PATCH 44/44] Skip validator query until it's needed --- .../modals/BuyMembershipModal/BuyMembershipFormModal.tsx | 2 +- packages/ui/src/validators/hooks/useValidators.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx index e1317e0eeb..8a1f7f2dd1 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipFormModal.tsx @@ -133,7 +133,6 @@ export const BuyMembershipForm = ({ const [formHandleMap, setFormHandleMap] = useState('') const { isUploading, uploadAvatarAndSubmit } = useUploadAvatarAndSubmit(onSubmit) const { data } = useGetMembersCountQuery({ variables: { where: { handle_eq: formHandleMap } } }) - const { allValidators, allValidatorsWithCtrlAcc } = useValidators() const form = useForm({ resolver: useYupValidationResolver(CreateMemberSchema), @@ -155,6 +154,7 @@ export const BuyMembershipForm = ({ 'validatorAccountCandidate', ]) + const { allValidators, allValidatorsWithCtrlAcc } = useValidators({ skip: isValidator ?? true }) const [validatorAccounts, setValidatorAccounts] = useState([]) const validatorAddresses = useMemo(() => { if (!allValidatorsWithCtrlAcc || !allValidators) return diff --git a/packages/ui/src/validators/hooks/useValidators.ts b/packages/ui/src/validators/hooks/useValidators.ts index 6e266be1e8..cbbdcdcad7 100644 --- a/packages/ui/src/validators/hooks/useValidators.ts +++ b/packages/ui/src/validators/hooks/useValidators.ts @@ -2,12 +2,14 @@ import { useContext, useEffect } from 'react' import { ValidatorsContext } from '../providers/context' -export const useValidators = () => { +type Props = { skip?: boolean } + +export const useValidators = ({ skip = false }: Props = {}) => { const { setShouldFetchValidators, allValidators, allValidatorsWithCtrlAcc, validatorsWithMembership } = useContext(ValidatorsContext) useEffect(() => { - setShouldFetchValidators(true) + if (!skip) setShouldFetchValidators(true) }, []) return { allValidators, allValidatorsWithCtrlAcc, validatorsWithMembership }