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',