From 62cdd1de5f4893ef573011b49f3494cbccbc00b0 Mon Sep 17 00:00:00 2001 From: Klaudiusz Dembler Date: Fri, 13 Oct 2023 14:26:24 +0200 Subject: [PATCH 1/5] show emails modal after member creation --- .../src/common/hooks/useProcessTransaction.ts | 20 ++++++++++-- .../hooks/useQueryNodeTransactionStatus.ts | 32 ++++++++++++++----- .../common/hooks/useSignAndSendTransaction.ts | 4 ++- .../OnBoardingModal/OnBoardingModal.tsx | 27 +++++++++++++--- .../CurrentMember/CurrentMember.tsx | 6 ++-- .../BuyMembershipModal/BuyMembershipModal.tsx | 32 +++++++++++++++++-- .../BuyMembershipSuccessModal.tsx | 19 +++-------- 7 files changed, 103 insertions(+), 37 deletions(-) diff --git a/packages/ui/src/common/hooks/useProcessTransaction.ts b/packages/ui/src/common/hooks/useProcessTransaction.ts index a156a8ff68..437f8c08a9 100644 --- a/packages/ui/src/common/hooks/useProcessTransaction.ts +++ b/packages/ui/src/common/hooks/useProcessTransaction.ts @@ -8,6 +8,8 @@ import { Observable } from 'rxjs' import { ActorRef, Sender } from 'xstate' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' +import { useApi } from '@/api/hooks/useApi' +import { ProxyApi } from '@/proxyApi' import { error, info } from '../logger' import { hasErrorEvent } from '../model/JoystreamNode' @@ -18,12 +20,14 @@ import { useObservable } from './useObservable' import { useTransactionStatus } from './useTransactionStatus' type SetBlockHash = Dispatch> +type SetBlockNumber = Dispatch> interface UseSignAndSendTransactionParams { transaction: SubmittableExtrinsic<'rxjs'> | undefined signer: Address service: ActorRef setBlockHash?: SetBlockHash + setBlockNumber?: SetBlockNumber } const observeTransaction = ( @@ -31,7 +35,9 @@ const observeTransaction = ( send: Sender, fee: BN, nodeRpcEndpoint: string, - setBlockHash?: SetBlockHash + api?: ProxyApi, + setBlockHash?: SetBlockHash, + setBlockNumber?: SetBlockNumber ) => { const statusCallback = (result: ISubmittableResult) => { const { status, events } = result @@ -51,6 +57,12 @@ const observeTransaction = ( setBlockHash && setBlockHash(hash) + if (api && setBlockNumber) { + api.rpc.chain.getHeader(hash).subscribe((header) => { + setBlockNumber(header.number.toNumber()) + }) + } + if (hasErrorEvent(events)) { subscription.unsubscribe() send({ type: 'ERROR', events }) @@ -103,12 +115,14 @@ export const useProcessTransaction = ({ signer, service, setBlockHash, + setBlockNumber, }: UseSignAndSendTransactionParams) => { const [state, send] = useActor(service) const paymentInfo = useObservable(() => transaction?.paymentInfo(signer), [transaction, signer]) const { setService } = useTransactionStatus() const [endpoints] = useNetworkEndpoints() const { allAccounts, wallet } = useMyAccounts() + const { api } = useApi() useEffect(() => { setService(service) @@ -128,7 +142,9 @@ export const useProcessTransaction = ({ send, fee, endpoints.nodeRpcEndpoint, - setBlockHash + api, + setBlockHash, + setBlockNumber ) send('SIGN_EXTERNAL') diff --git a/packages/ui/src/common/hooks/useQueryNodeTransactionStatus.ts b/packages/ui/src/common/hooks/useQueryNodeTransactionStatus.ts index 75be7264d2..d9cb702eb5 100644 --- a/packages/ui/src/common/hooks/useQueryNodeTransactionStatus.ts +++ b/packages/ui/src/common/hooks/useQueryNodeTransactionStatus.ts @@ -1,30 +1,46 @@ import { Hash } from '@polkadot/types/interfaces/runtime' import { useEffect, useState } from 'react' +import { warning } from '../logger' + import { useBlockHash } from './useBlockHash' import { useQueryNodeStateSubscription } from './useQueryNode' type TransactionStatus = 'confirmed' | 'rejected' | 'unknown' -export function useQueryNodeTransactionStatus(isProcessing: boolean, blockHash?: Hash | string, skip?: boolean) { +export function useQueryNodeTransactionStatus( + isProcessing: boolean, + blockHashOrNumber?: Hash | string | number, + skip?: boolean +) { const { queryNodeState } = useQueryNodeStateSubscription({ shouldResubscribe: true, skip }) const [status, setStatus] = useState('unknown') - const queryNodeBlockHash = useBlockHash(queryNodeState?.indexerHead) + const queryNodeIndexerHeadHash = useBlockHash(queryNodeState?.indexerHead) useEffect(() => { - if (!queryNodeState && isProcessing) { + if (isProcessing) { const timeout = setTimeout(() => { + warning('QN sync timeout') setStatus('confirmed') - }, 10_000) + }, 15_000) return () => clearTimeout(timeout) } - }, [!queryNodeState, isProcessing]) + }, [isProcessing]) useEffect(() => { - if (queryNodeState) { - setStatus(blockHash === queryNodeBlockHash ? 'confirmed' : 'rejected') + if (queryNodeState && blockHashOrNumber) { + let isSynced = false + if (typeof blockHashOrNumber === 'number') { + isSynced = parseInt(queryNodeState.lastCompleteBlock) >= blockHashOrNumber + } else { + isSynced = blockHashOrNumber === queryNodeIndexerHeadHash + } + + if (isSynced) { + setStatus('confirmed') + } } - }, [queryNodeState, queryNodeBlockHash]) + }, [queryNodeState, blockHashOrNumber, queryNodeIndexerHeadHash]) return status } diff --git a/packages/ui/src/common/hooks/useSignAndSendTransaction.ts b/packages/ui/src/common/hooks/useSignAndSendTransaction.ts index ff83d7f8cb..1e0e23d62d 100644 --- a/packages/ui/src/common/hooks/useSignAndSendTransaction.ts +++ b/packages/ui/src/common/hooks/useSignAndSendTransaction.ts @@ -38,6 +38,7 @@ export const useSignAndSendTransaction = ({ extraCosts = BN_ZERO, }: Params) => { const [blockHash, setBlockHash] = useState(undefined) + const [blockNumber, setBlockNumber] = useState(undefined) const apolloClient = useApolloClient() const balance = useBalance(signer) const { send, paymentInfo, isReady, isProcessing } = useProcessTransaction({ @@ -45,8 +46,9 @@ export const useSignAndSendTransaction = ({ signer, service, setBlockHash, + setBlockNumber, }) - const queryNodeStatus = useQueryNodeTransactionStatus(isProcessing, blockHash, skipQueryNode) + const queryNodeStatus = useQueryNodeTransactionStatus(isProcessing, blockNumber || blockHash, skipQueryNode) const { wallet } = useMyAccounts() const { api } = useApi() diff --git a/packages/ui/src/common/modals/OnBoardingModal/OnBoardingModal.tsx b/packages/ui/src/common/modals/OnBoardingModal/OnBoardingModal.tsx index 76962ab0d4..c672089934 100644 --- a/packages/ui/src/common/modals/OnBoardingModal/OnBoardingModal.tsx +++ b/packages/ui/src/common/modals/OnBoardingModal/OnBoardingModal.tsx @@ -18,12 +18,14 @@ import { useModal } from '@/common/hooks/useModal' import { useNetworkEndpoints } from '@/common/hooks/useNetworkEndpoints' import { useOnBoarding } from '@/common/hooks/useOnBoarding' import { useQueryNodeTransactionStatus } from '@/common/hooks/useQueryNodeTransactionStatus' +import { warning } from '@/common/logger' import { onBoardingMachine } from '@/common/modals/OnBoardingModal/machine' import { OnBoardingAccount } from '@/common/modals/OnBoardingModal/OnBoardingAccount' import { OnBoardingMembership } from '@/common/modals/OnBoardingModal/OnBoardingMembership' import { OnBoardingPlugin } from '@/common/modals/OnBoardingModal/OnBoardingPlugin' import { OnBoardingStatus, SetMembershipAccount } from '@/common/providers/onboarding/types' import { definedValues } from '@/common/utils' +import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' import { MemberFormFields } from '@/memberships/modals/BuyMembershipModal/BuyMembershipFormModal' import { BuyMembershipSuccessModal } from '@/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal' import { toExternalResources } from '@/memberships/modals/utils' @@ -33,11 +35,12 @@ export const OnBoardingModal = () => { const { status: realStatus, membershipAccount, setMembershipAccount, isLoading } = useOnBoarding() const status = useDebounce(realStatus, 50) const [state, send] = useMachine(onBoardingMachine) - const [membershipData, setMembershipData] = useState<{ id: string; blockHash: string }>() - const transactionStatus = useQueryNodeTransactionStatus(!!membershipData?.blockHash, membershipData?.blockHash) + const [membershipData, setMembershipData] = useState<{ id: string; blockHash: string; blockNumber: number }>() + const transactionStatus = useQueryNodeTransactionStatus(!!membershipData, membershipData?.blockNumber) const apolloClient = useApolloClient() const [endpoints] = useNetworkEndpoints() const statusRef = useRef() + const { setActive: setActiveMember, members } = useMyMemberships() const step = useMemo(() => { switch (status ?? statusRef.current) { @@ -86,12 +89,12 @@ export const OnBoardingModal = () => { body: JSON.stringify(membershipData), }) - const { error, memberId, blockHash } = await response.json() + const { error, memberId, blockHash, block } = await response.json() if (error) { send({ type: 'ERROR' }) } else { - setMembershipData({ id: parseInt(memberId, 16).toString(), blockHash: blockHash }) + setMembershipData({ id: memberId, blockHash: blockHash, blockNumber: block }) } } catch (err) { send({ type: 'ERROR' }) @@ -118,7 +121,21 @@ export const OnBoardingModal = () => { if (state.matches('success')) { const { form } = state.context - return + return ( + { + const newMember = members.find((member) => member.id === membershipData?.id?.toString()) + if (newMember) { + setActiveMember(newMember) + } else { + warning('Could not find new member', membershipData?.id?.toString()) + } + hideModal() + }} + member={form} + memberId={membershipData?.id} + /> + ) } if (state.matches('transaction') && transactionStatus !== 'confirmed') { diff --git a/packages/ui/src/memberships/components/CurrentMember/CurrentMember.tsx b/packages/ui/src/memberships/components/CurrentMember/CurrentMember.tsx index 2f987a8135..c677842459 100644 --- a/packages/ui/src/memberships/components/CurrentMember/CurrentMember.tsx +++ b/packages/ui/src/memberships/components/CurrentMember/CurrentMember.tsx @@ -21,19 +21,19 @@ import { AddMembershipButton } from '../AddMembershipButton' export const CurrentMember = () => { const { wallet } = useMyAccounts() const { members, hasMembers, active } = useMyMemberships() - const { showModal } = useModal() + const { showModal, modal } = useModal() const { activeMemberSettings, activeMemberExistBackendData } = useNotificationSettings() const showSubscriptionModal = active && activeMemberExistBackendData?.memberExist === false && !activeMemberSettings?.hasBeenAskedForEmail useEffect(() => { - if (!emailVerificationToken && showSubscriptionModal) { + if (!emailVerificationToken && !modal && showSubscriptionModal) { showModal({ modal: 'EmailSubscriptionModal', data: {}, }) } - }, [showSubscriptionModal]) + }, [showSubscriptionModal, modal]) const history = useHistory() const routeQuery = useRouteQuery() diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index 615a96f227..dfef3b1005 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -1,9 +1,12 @@ -import React from 'react' +import { useApolloClient } from '@apollo/client' +import React, { useEffect } from 'react' 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 { warning } from '@/common/logger' +import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' import { toMemberTransactionParams } from '@/memberships/modals/utils' import { BuyMembershipFormModal, MemberFormFields } from './BuyMembershipFormModal' @@ -17,6 +20,15 @@ export const BuyMembershipModal = () => { const membershipPrice = useFirstObservableValue(() => api?.query.members.membershipPrice(), [api?.isConnected]) const [state, send] = useMachine(buyMembershipMachine) + const apolloClient = useApolloClient() + const { setActive: setActiveMember, members } = useMyMemberships() + + const isSuccessful = state.matches('success') + // refetch data after successful member creation + useEffect(() => { + if (!isSuccessful) return + apolloClient.refetchQueries({ include: 'active' }) + }, [isSuccessful, apolloClient]) if (state.matches('prepare')) { const onSubmit = (params: MemberFormFields) => send({ type: 'DONE', form: params }) @@ -41,9 +53,23 @@ export const BuyMembershipModal = () => { ) } - if (state.matches('success')) { + if (isSuccessful) { const { form, memberId } = state.context - return + return ( + { + const newMember = members.find((member) => member.id === memberId?.toString()) + if (newMember) { + setActiveMember(newMember) + } else { + warning('Could not find new member', memberId?.toString()) + } + hideModal() + }} + member={form} + memberId={memberId?.toString()} + /> + ) } return null diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal.tsx index aff1171b9b..7163d7f605 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal.tsx @@ -1,14 +1,12 @@ import React from 'react' -import { ButtonPrimary } from '@/common/components/buttons' +import { ButtonGhost } from '@/common/components/buttons' import { SuccessIcon } from '@/common/components/icons' import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' import { TextMedium } from '@/common/components/typography' -import { useModal } from '@/common/hooks/useModal' import { MemberRow } from '@/memberships/modals/components' import { MemberInfo } from '../../components' -import { MemberModalCall } from '../../components/MemberProfile' import { Member } from '../../types' import { MemberFormFields } from './BuyMembershipFormModal' @@ -20,15 +18,6 @@ interface Props { } export const BuyMembershipSuccessModal = ({ onClose, member, memberId }: Props) => { - const { showModal } = useModal() - const viewMember = () => { - onClose() - - if (memberId) { - showModal({ modal: 'Member', data: { id: memberId } }) - } - } - return ( } /> @@ -39,9 +28,9 @@ export const BuyMembershipSuccessModal = ({ onClose, member, memberId }: Props) - - View my profile - + + Done + ) From 54778d5bbc080c62b578614fd0edb1edc493bf6a Mon Sep 17 00:00:00 2001 From: Klaudiusz Dembler Date: Mon, 16 Oct 2023 13:47:31 +0200 Subject: [PATCH 2/5] add tests --- packages/ui/src/app/App.stories.tsx | 87 ++++++++++++++----- .../OnBoardingModal/OnBoardingModal.tsx | 19 +--- .../BuyMembershipModal/BuyMembershipModal.tsx | 19 +--- .../BuyMembershipSuccessModal.tsx | 18 +++- packages/ui/src/mocks/providers/api.tsx | 3 + 5 files changed, 86 insertions(+), 60 deletions(-) diff --git a/packages/ui/src/app/App.stories.tsx b/packages/ui/src/app/App.stories.tsx index d155d041b1..15640aaec3 100644 --- a/packages/ui/src/app/App.stories.tsx +++ b/packages/ui/src/app/App.stories.tsx @@ -46,8 +46,8 @@ const alice = member('alice') const bob = member('bob') const charlie = member('charlie') -const MEMBER_DATA = { - id: '12', +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 handle: 'realbobbybob', metadata: { name: 'BobbyBob', @@ -118,7 +118,7 @@ export default { members: { buyMembership: { event: 'MembershipBought', - data: [MEMBER_DATA.id], + data: [NEW_MEMBER_DATA.id], onSend: args.onBuyMembership, failure: parameters.txFailure, }, @@ -130,7 +130,7 @@ export default { queries: [ { query: GetMemberDocument, - data: { membershipByUniqueInput: { ...bob, ...MEMBER_DATA, invitees: [] } }, + data: { membershipByUniqueInput: { ...bob, ...NEW_MEMBER_DATA, invitees: [] } }, }, { query: GetBackendMemberExistsDocument, @@ -314,10 +314,10 @@ export const FaucetMembership: Story = { expect(modal.getByText('Please fill in all the details below.')) // Check that the CAPTCHA blocks the next step - await userEvent.type(modal.getByLabelText('Member Name'), MEMBER_DATA.metadata.name) - await userEvent.type(modal.getByLabelText('Membership handle'), MEMBER_DATA.handle) - await userEvent.type(modal.getByLabelText('About member'), MEMBER_DATA.metadata.about) - await userEvent.type(modal.getByLabelText('Member Avatar'), MEMBER_DATA.metadata.avatar.avatarUri) + 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) await userEvent.click(modal.getByLabelText(/^I agree to the/)) expect(getButtonByText(modal, 'Create a Membership')).toBeDisabled() }) @@ -330,10 +330,10 @@ export const FaucetMembership: Story = { const fillMembershipForm = async (modal: Container) => { await selectFromDropdown(modal, 'Root account', 'alice') await selectFromDropdown(modal, 'Controller account', 'bob') - await userEvent.type(modal.getByLabelText('Member Name'), MEMBER_DATA.metadata.name) - await userEvent.type(modal.getByLabelText('Membership handle'), MEMBER_DATA.handle) - await userEvent.type(modal.getByLabelText('About member'), MEMBER_DATA.metadata.about) - await userEvent.type(modal.getByLabelText('Member Avatar'), MEMBER_DATA.metadata.avatar.avatarUri) + 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) await userEvent.click(modal.getByLabelText(/^I agree to the/)) } @@ -382,28 +382,71 @@ export const BuyMembershipHappy: 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, externalResources: [{ type: MembershipMetadata.ExternalResource.ResourceType.EMAIL, value: 'bobby@bob.com' }], }), invitingMemberId: undefined, referrerId: undefined, }) - const viewProfileButton = getButtonByText(modal, 'View my profile') - expect(viewProfileButton).toBeEnabled() - userEvent.click(viewProfileButton) + const doneButton = getButtonByText(modal, 'Done') + expect(doneButton).toBeEnabled() + userEvent.click(doneButton) + }) + }, +} + +// in this test, we are testing whether the email subscription modal is shown after the membership is bought +// there's no easy way to change mocked members mid-story so we start with memberships +// this way the BuyMembershipModal can set active member, which should trigger the email subscription modal +export const BuyMembershipEmailSignup: Story = { + args: { hasMemberships: true, isLoggedIn: false, hasRegisteredEmail: false, hasBeenAskedForEmail: false }, + + play: async ({ args, canvasElement, step }) => { + const screen = within(canvasElement) + const modal = withinModal(canvasElement) + + userEvent.click(getButtonByText(screen, 'Select membership')) + const newMemberButton = screen.getByText('New Member') as HTMLElement + expect(newMemberButton).toBeEnabled() + userEvent.click(newMemberButton) + const createButton = getButtonByText(modal, 'Create a Membership') + await fillMembershipForm(modal) + await waitFor(() => expect(createButton).toBeEnabled()) + userEvent.click(createButton) + userEvent.click(await waitFor(() => getButtonByText(modal, 'Sign and create a member'))) + + 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, + }) + + const doneButton = getButtonByText(modal, 'Done') + expect(doneButton).toBeEnabled() + userEvent.click(doneButton) - expect(modal.getByText('Profile')) - expect(modal.getByText(MEMBER_DATA.handle)) + await waitFor(() => modal.findByText('Sign up to email notifications')) }) }, } diff --git a/packages/ui/src/common/modals/OnBoardingModal/OnBoardingModal.tsx b/packages/ui/src/common/modals/OnBoardingModal/OnBoardingModal.tsx index c672089934..1b130b3ace 100644 --- a/packages/ui/src/common/modals/OnBoardingModal/OnBoardingModal.tsx +++ b/packages/ui/src/common/modals/OnBoardingModal/OnBoardingModal.tsx @@ -18,14 +18,12 @@ import { useModal } from '@/common/hooks/useModal' import { useNetworkEndpoints } from '@/common/hooks/useNetworkEndpoints' import { useOnBoarding } from '@/common/hooks/useOnBoarding' import { useQueryNodeTransactionStatus } from '@/common/hooks/useQueryNodeTransactionStatus' -import { warning } from '@/common/logger' import { onBoardingMachine } from '@/common/modals/OnBoardingModal/machine' import { OnBoardingAccount } from '@/common/modals/OnBoardingModal/OnBoardingAccount' import { OnBoardingMembership } from '@/common/modals/OnBoardingModal/OnBoardingMembership' import { OnBoardingPlugin } from '@/common/modals/OnBoardingModal/OnBoardingPlugin' import { OnBoardingStatus, SetMembershipAccount } from '@/common/providers/onboarding/types' import { definedValues } from '@/common/utils' -import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' import { MemberFormFields } from '@/memberships/modals/BuyMembershipModal/BuyMembershipFormModal' import { BuyMembershipSuccessModal } from '@/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal' import { toExternalResources } from '@/memberships/modals/utils' @@ -40,7 +38,6 @@ export const OnBoardingModal = () => { const apolloClient = useApolloClient() const [endpoints] = useNetworkEndpoints() const statusRef = useRef() - const { setActive: setActiveMember, members } = useMyMemberships() const step = useMemo(() => { switch (status ?? statusRef.current) { @@ -121,21 +118,7 @@ export const OnBoardingModal = () => { if (state.matches('success')) { const { form } = state.context - return ( - { - const newMember = members.find((member) => member.id === membershipData?.id?.toString()) - if (newMember) { - setActiveMember(newMember) - } else { - warning('Could not find new member', membershipData?.id?.toString()) - } - hideModal() - }} - member={form} - memberId={membershipData?.id} - /> - ) + return } if (state.matches('transaction') && transactionStatus !== 'confirmed') { diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx index dfef3b1005..c73b25f672 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipModal.tsx @@ -5,8 +5,6 @@ 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 { warning } from '@/common/logger' -import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' import { toMemberTransactionParams } from '@/memberships/modals/utils' import { BuyMembershipFormModal, MemberFormFields } from './BuyMembershipFormModal' @@ -21,7 +19,6 @@ export const BuyMembershipModal = () => { const membershipPrice = useFirstObservableValue(() => api?.query.members.membershipPrice(), [api?.isConnected]) const [state, send] = useMachine(buyMembershipMachine) const apolloClient = useApolloClient() - const { setActive: setActiveMember, members } = useMyMemberships() const isSuccessful = state.matches('success') // refetch data after successful member creation @@ -55,21 +52,7 @@ export const BuyMembershipModal = () => { if (isSuccessful) { const { form, memberId } = state.context - return ( - { - const newMember = members.find((member) => member.id === memberId?.toString()) - if (newMember) { - setActiveMember(newMember) - } else { - warning('Could not find new member', memberId?.toString()) - } - hideModal() - }} - member={form} - memberId={memberId?.toString()} - /> - ) + return } return null diff --git a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal.tsx b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal.tsx index 7163d7f605..8b2f8a6851 100644 --- a/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal.tsx +++ b/packages/ui/src/memberships/modals/BuyMembershipModal/BuyMembershipSuccessModal.tsx @@ -4,6 +4,8 @@ import { ButtonGhost } from '@/common/components/buttons' import { SuccessIcon } from '@/common/components/icons' import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/common/components/Modal' import { TextMedium } from '@/common/components/typography' +import { warning } from '@/common/logger' +import { useMyMemberships } from '@/memberships/hooks/useMyMemberships' import { MemberRow } from '@/memberships/modals/components' import { MemberInfo } from '../../components' @@ -18,9 +20,21 @@ interface Props { } export const BuyMembershipSuccessModal = ({ onClose, member, memberId }: Props) => { + const { setActive: setActiveMember, members } = useMyMemberships() + + const handleClose = () => { + const newMember = members.find((member) => member.id === memberId?.toString()) + if (newMember) { + setActiveMember(newMember) + } else { + warning('Could not find new member', memberId?.toString()) + } + onClose() + } + return ( - } /> + } /> You have just successfully created a new membership @@ -28,7 +42,7 @@ export const BuyMembershipSuccessModal = ({ onClose, member, memberId }: Props) - + Done diff --git a/packages/ui/src/mocks/providers/api.tsx b/packages/ui/src/mocks/providers/api.tsx index 8bddcc7f27..059ad529de 100644 --- a/packages/ui/src/mocks/providers/api.tsx +++ b/packages/ui/src/mocks/providers/api.tsx @@ -42,6 +42,9 @@ export const MockApiProvider: FC = ({ children, chain }) => { // Common mocks: const rpcChain = { getBlockHash: createType('BlockHash', BLOCK_HASH), + getHeader: { + number: BLOCK_HEAD, + }, subscribeNewHeads: { parentHash: BLOCK_HASH, number: BLOCK_HEAD, From 85728d940b71ffb67ec9de9253a8d9baf6a6a8ed Mon Sep 17 00:00:00 2001 From: Klaudiusz Dembler Date: Tue, 17 Oct 2023 12:48:10 +0200 Subject: [PATCH 3/5] fix unit tests --- packages/ui/test/_mocks/transactions.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/ui/test/_mocks/transactions.ts b/packages/ui/test/_mocks/transactions.ts index 3588abe900..aebe969ab3 100644 --- a/packages/ui/test/_mocks/transactions.ts +++ b/packages/ui/test/_mocks/transactions.ts @@ -136,6 +136,14 @@ export const stubApi = () => { from([createType('BlockHash', '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY')]) }) + set(api, 'api.rpc.chain.getHeader', () => + from([ + { + number: createType('BlockNumber', 1337), + }, + ]) + ) + return api } From aa1684e1a9dbbb02905bf4fe6451e02e3bb3f54f Mon Sep 17 00:00:00 2001 From: Klaudiusz Dembler Date: Tue, 17 Oct 2023 14:37:38 +0200 Subject: [PATCH 4/5] simplify getting blockNumber --- .../ui/src/common/hooks/useProcessTransaction.ts | 15 ++------------- .../src/common/hooks/useSignAndSendTransaction.ts | 8 +++++--- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/common/hooks/useProcessTransaction.ts b/packages/ui/src/common/hooks/useProcessTransaction.ts index 437f8c08a9..6e301051cf 100644 --- a/packages/ui/src/common/hooks/useProcessTransaction.ts +++ b/packages/ui/src/common/hooks/useProcessTransaction.ts @@ -20,14 +20,12 @@ import { useObservable } from './useObservable' import { useTransactionStatus } from './useTransactionStatus' type SetBlockHash = Dispatch> -type SetBlockNumber = Dispatch> interface UseSignAndSendTransactionParams { transaction: SubmittableExtrinsic<'rxjs'> | undefined signer: Address service: ActorRef setBlockHash?: SetBlockHash - setBlockNumber?: SetBlockNumber } const observeTransaction = ( @@ -36,8 +34,7 @@ const observeTransaction = ( fee: BN, nodeRpcEndpoint: string, api?: ProxyApi, - setBlockHash?: SetBlockHash, - setBlockNumber?: SetBlockNumber + setBlockHash?: SetBlockHash ) => { const statusCallback = (result: ISubmittableResult) => { const { status, events } = result @@ -57,12 +54,6 @@ const observeTransaction = ( setBlockHash && setBlockHash(hash) - if (api && setBlockNumber) { - api.rpc.chain.getHeader(hash).subscribe((header) => { - setBlockNumber(header.number.toNumber()) - }) - } - if (hasErrorEvent(events)) { subscription.unsubscribe() send({ type: 'ERROR', events }) @@ -115,7 +106,6 @@ export const useProcessTransaction = ({ signer, service, setBlockHash, - setBlockNumber, }: UseSignAndSendTransactionParams) => { const [state, send] = useActor(service) const paymentInfo = useObservable(() => transaction?.paymentInfo(signer), [transaction, signer]) @@ -143,8 +133,7 @@ export const useProcessTransaction = ({ fee, endpoints.nodeRpcEndpoint, api, - setBlockHash, - setBlockNumber + setBlockHash ) send('SIGN_EXTERNAL') diff --git a/packages/ui/src/common/hooks/useSignAndSendTransaction.ts b/packages/ui/src/common/hooks/useSignAndSendTransaction.ts index 1e0e23d62d..81b1398f3c 100644 --- a/packages/ui/src/common/hooks/useSignAndSendTransaction.ts +++ b/packages/ui/src/common/hooks/useSignAndSendTransaction.ts @@ -14,6 +14,7 @@ import { getFeeSpendableBalance } from '@/common/providers/transactionFees/provi import { Address } from '../types' +import { useFirstObservableValue } from './useFirstObservableValue' import { useProcessTransaction } from './useProcessTransaction' import { useQueryNodeTransactionStatus } from './useQueryNodeTransactionStatus' @@ -38,19 +39,20 @@ export const useSignAndSendTransaction = ({ extraCosts = BN_ZERO, }: Params) => { const [blockHash, setBlockHash] = useState(undefined) - const [blockNumber, setBlockNumber] = useState(undefined) const apolloClient = useApolloClient() const balance = useBalance(signer) + const { api } = useApi() const { send, paymentInfo, isReady, isProcessing } = useProcessTransaction({ transaction, signer, service, setBlockHash, - setBlockNumber, }) + const blockNumber = useFirstObservableValue(() => { + if (blockHash) return api?.rpc.chain.getHeader(blockHash) + }, [api?.isConnected, blockHash])?.number.toNumber() const queryNodeStatus = useQueryNodeTransactionStatus(isProcessing, blockNumber || blockHash, skipQueryNode) const { wallet } = useMyAccounts() - const { api } = useApi() const sign = useCallback(() => { if (wallet && api) { From d93340f5da83be4773778ed5bcda93ab44672972 Mon Sep 17 00:00:00 2001 From: Klaudiusz Dembler Date: Wed, 18 Oct 2023 12:54:01 +0200 Subject: [PATCH 5/5] remove unnecessary api from observeTransaction --- packages/ui/src/common/hooks/useProcessTransaction.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/ui/src/common/hooks/useProcessTransaction.ts b/packages/ui/src/common/hooks/useProcessTransaction.ts index 6e301051cf..a156a8ff68 100644 --- a/packages/ui/src/common/hooks/useProcessTransaction.ts +++ b/packages/ui/src/common/hooks/useProcessTransaction.ts @@ -8,8 +8,6 @@ import { Observable } from 'rxjs' import { ActorRef, Sender } from 'xstate' import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' -import { useApi } from '@/api/hooks/useApi' -import { ProxyApi } from '@/proxyApi' import { error, info } from '../logger' import { hasErrorEvent } from '../model/JoystreamNode' @@ -33,7 +31,6 @@ const observeTransaction = ( send: Sender, fee: BN, nodeRpcEndpoint: string, - api?: ProxyApi, setBlockHash?: SetBlockHash ) => { const statusCallback = (result: ISubmittableResult) => { @@ -112,7 +109,6 @@ export const useProcessTransaction = ({ const { setService } = useTransactionStatus() const [endpoints] = useNetworkEndpoints() const { allAccounts, wallet } = useMyAccounts() - const { api } = useApi() useEffect(() => { setService(service) @@ -132,7 +128,6 @@ export const useProcessTransaction = ({ send, fee, endpoints.nodeRpcEndpoint, - api, setBlockHash )