diff --git a/src/components/AssetManagementPanel/AssetManagementApplication/index.tsx b/src/components/AssetManagementPanel/AssetManagementApplication/index.tsx index d07b971..5515a75 100644 --- a/src/components/AssetManagementPanel/AssetManagementApplication/index.tsx +++ b/src/components/AssetManagementPanel/AssetManagementApplication/index.tsx @@ -94,7 +94,10 @@ export const AssetManagementApplication: FunctionComponent< // reject transfer owner holder rejectTransferOwnerHolder, rejectTransferOwnerHolderState, + // reset providers resetProviders, + //errorMessage + errorMessage, } = useTokenInformationContext() const [assetManagementAction, setAssetManagementAction] = useState(AssetManagementActions.None) @@ -193,6 +196,7 @@ export const AssetManagementApplication: FunctionComponent< onRestoreToken={onRestoreToken} restoreTokenState={restoreTokenState} isExpired={isExpired} + errorMessage={errorMessage} /> ) : ( isExpired && ( diff --git a/src/components/AssetManagementPanel/AssetManagementForm/AssetManagementForm.test.tsx b/src/components/AssetManagementPanel/AssetManagementForm/AssetManagementForm.test.tsx index 997db58..b418acb 100644 --- a/src/components/AssetManagementPanel/AssetManagementForm/AssetManagementForm.test.tsx +++ b/src/components/AssetManagementPanel/AssetManagementForm/AssetManagementForm.test.tsx @@ -98,10 +98,29 @@ describe('AssetManagementForm', () => { ) }) - it('enables reject holdership only for token registry v5 after holder transfer context', () => { + it('disables reject holdership when holder and beneficiary are the same account (isHolderAndBeneficiary)', () => { mockUseTokenRegistryVersion.mockReturnValue(TokenRegistryVersions.V5) + // baseProps has beneficiary === holder === account, so isHolderAndBeneficiary=true render() + expect(mockActionSelectionForm).toHaveBeenCalledWith( + expect.objectContaining({ + canRejectHolderTransfer: false, + }) + ) + }) + + it('enables reject holdership for token registry v5 when holder differs from beneficiary', () => { + mockUseTokenRegistryVersion.mockReturnValue(TokenRegistryVersions.V5) + // Different beneficiary means isHolderAndBeneficiary=false, so canRejectHolderTransfer can be true + render( + + ) + expect(mockActionSelectionForm).toHaveBeenCalledWith( expect.objectContaining({ canRejectHolderTransfer: true, diff --git a/src/components/AssetManagementPanel/AssetManagementForm/AssetManagementForm.tsx b/src/components/AssetManagementPanel/AssetManagementForm/AssetManagementForm.tsx index c5eb0b7..df11d81 100644 --- a/src/components/AssetManagementPanel/AssetManagementForm/AssetManagementForm.tsx +++ b/src/components/AssetManagementPanel/AssetManagementForm/AssetManagementForm.tsx @@ -89,6 +89,7 @@ interface AssetManagementFormProps onSetFormAction: (nextFormAction: AssetManagementActions) => void setShowEndorsementChain: (payload: boolean) => void refreshEndorsementChain?: () => void + errorMessage?: string } export const AssetManagementForm: FunctionComponent< @@ -133,6 +134,7 @@ export const AssetManagementForm: FunctionComponent< destroyTokenState, onRestoreToken, restoreTokenState, + errorMessage, }) => { const tokenRegistryVersion = useTokenRegistryVersion() const isTokenRegistryV5 = tokenRegistryVersion === TokenRegistryVersions.V5 @@ -170,8 +172,8 @@ export const AssetManagementForm: FunctionComponent< hasPreviousHolder && hasPreviousBeneficiary && canRejectAfterTransferOwners - const canRejectHolderTransfer = // Allow reject after holder is transferred back to original owner - (isHolderAndBeneficiary ? !hasPreviousBeneficiary : true) && + const canRejectHolderTransfer = + !isHolderAndBeneficiary && isTokenRegistryV5 && isActiveTitleEscrow && isHolder && @@ -179,6 +181,7 @@ export const AssetManagementForm: FunctionComponent< !(isBeneficiary && hasPreviousBeneficiary) const canRejectOwnerTransfer = !isHolderAndBeneficiary && + isTokenRegistryV5 && isActiveTitleEscrow && isBeneficiary && hasPreviousBeneficiary && @@ -296,6 +299,8 @@ export const AssetManagementForm: FunctionComponent< // reject return to issuer handleRestoreToken={onRestoreToken} restoreTokenState={restoreTokenState} + //error message + errorMessage={errorMessage} /> )} diff --git a/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/ActionForm.test.tsx b/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/ActionForm.test.tsx index 040815f..d8f321c 100644 --- a/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/ActionForm.test.tsx +++ b/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/ActionForm.test.tsx @@ -1854,3 +1854,167 @@ describe('ActionForm - EndorseBeneficiary', () => { }) }) }) + +describe('ActionForm - errorMessage prop passthrough', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('passes errorMessage to overlay when TransferHolder fails', async () => { + const mockHandleTransfer = vi.fn() + + renderWithOverlay( + + ) + + await waitFor(() => { + expect(mockShowOverlay).toHaveBeenCalled() + }) + + const overlayNode = mockShowOverlay.mock.calls[0][0] as any + expect(overlayNode.props.title).toBe('Transfer Holder Failed') + expect(overlayNode.props.isSuccess).toBe(false) + expect(overlayNode.props.errorMessage).toBe('User Rejected Transaction') + }) + + it('passes errorMessage to overlay when TransferOwnerHolder fails', async () => { + const mockHandleTransferOwnerHolder = vi.fn() + + renderWithOverlay( + + ) + + await waitFor(() => { + expect(mockShowOverlay).toHaveBeenCalled() + }) + + const overlayNode = mockShowOverlay.mock.calls[0][0] as any + expect(overlayNode.props.title).toBe('Transfer Ownership/Holdership Failed') + expect(overlayNode.props.errorMessage).toBe('Insufficient Funds') + }) + + it('passes errorMessage to overlay when NominateBeneficiary fails', async () => { + const mockHandleNomination = vi.fn() + + renderWithOverlay( + + ) + + await waitFor(() => { + expect(mockShowOverlay).toHaveBeenCalled() + }) + + const overlayNode = mockShowOverlay.mock.calls[0][0] as any + expect(overlayNode.props.title).toBe('Nomination Failed') + expect(overlayNode.props.errorMessage).toBe('Network Error') + }) + + it('passes errorMessage to overlay when RejectTransferOwnerHolder fails', async () => { + const mockHandleRejectTransferOwnerHolder = vi.fn() + + renderWithOverlay( + + ) + + await waitFor(() => { + expect(mockShowOverlay).toHaveBeenCalled() + }) + + const overlayNode = mockShowOverlay.mock.calls[0][0] as any + expect(overlayNode.props.title).toBe( + 'Holdership/Ownership Rejection Failed' + ) + expect(overlayNode.props.errorMessage).toBe('Transaction Rejected') + }) + + it('passes errorMessage to overlay when ReturnToIssuer fails', async () => { + const mockHandleReturnToIssuer = vi.fn() + + renderWithOverlay( + + ) + + await waitFor(() => { + expect(mockShowOverlay).toHaveBeenCalled() + }) + + const overlayNode = mockShowOverlay.mock.calls[0][0] as any + expect(overlayNode.props.title).toBe('Return of ETR Failed') + expect(overlayNode.props.errorMessage).toBe('Contract Call Failed') + }) + + it('passes errorMessage to overlay on successful TransferOwner', async () => { + const mockHandleBeneficiaryTransfer = vi.fn() + + renderWithOverlay( + + ) + + await waitFor(() => { + expect(mockShowOverlay).toHaveBeenCalled() + }) + + const overlayNode = mockShowOverlay.mock.calls[0][0] as any + expect(overlayNode.props.title).toBe('Transfer Owner Success') + expect(overlayNode.props.isSuccess).toBe(true) + expect(overlayNode.props.errorMessage).toBe('Some Error') + }) + + it('passes undefined errorMessage when prop is not provided', async () => { + const mockHandleTransfer = vi.fn() + + renderWithOverlay( + + ) + + await waitFor(() => { + expect(mockShowOverlay).toHaveBeenCalled() + }) + + const overlayNode = mockShowOverlay.mock.calls[0][0] as any + expect(overlayNode.props.errorMessage).toBeUndefined() + }) +}) diff --git a/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/ActionForm.tsx b/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/ActionForm.tsx index 5f4c6d9..64a28cb 100644 --- a/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/ActionForm.tsx +++ b/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/ActionForm.tsx @@ -48,6 +48,7 @@ export const ActionForm: FunctionComponent = props => { setFormActionNone, setShowEndorsementChain, refreshEndorsementChain, + errorMessage, } = props const [remark, setRemark] = useState('') const { showOverlay } = useOverlayContext() @@ -64,7 +65,7 @@ export const ActionForm: FunctionComponent = props => { const { holderTransferringState } = props const isConfirmed = holderTransferringState === FormState.CONFIRMED const isFailed = holderTransferringState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.TRANSFER_HOLDER_SUCCESS : MessageTitle.TRANSFER_HOLDER_FAILED @@ -74,12 +75,13 @@ export const ActionForm: FunctionComponent = props => { } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, holderAddress: isConfirmed ? newHolder : holder, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -90,7 +92,7 @@ export const ActionForm: FunctionComponent = props => { const { transferOwnerHoldersState } = props const isConfirmed = transferOwnerHoldersState === FormState.CONFIRMED const isFailed = transferOwnerHoldersState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.TRANSFER_OWNER_HOLDER_SUCCESS : MessageTitle.TRANSFER_OWNER_HOLDER_FAILED const beneficiaryAddress = isConfirmed ? newOwner : beneficiary @@ -102,13 +104,14 @@ export const ActionForm: FunctionComponent = props => { } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, beneficiaryAddress, holderAddress, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -119,7 +122,7 @@ export const ActionForm: FunctionComponent = props => { const { rejectTransferOwnerHolderState } = props const isConfirmed = rejectTransferOwnerHolderState === FormState.CONFIRMED const isFailed = rejectTransferOwnerHolderState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.REJECT_TRANSFER_OWNER_HOLDER_SUCCESS : MessageTitle.REJECT_TRANSFER_OWNER_HOLDER_FAILED const beneficiaryAddress = isConfirmed ? prevBeneficiary : beneficiary @@ -131,13 +134,14 @@ export const ActionForm: FunctionComponent = props => { } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, beneficiaryAddress, holderAddress, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -148,23 +152,23 @@ export const ActionForm: FunctionComponent = props => { const { rejectTransferHolderState } = props const isConfirmed = rejectTransferHolderState === FormState.CONFIRMED const isFailed = rejectTransferHolderState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.REJECT_TRANSFER_HOLDER_SUCCESS : MessageTitle.REJECT_TRANSFER_HOLDER_FAILED const holderAddress = isConfirmed ? prevHolder : holder - if (isConfirmed || isFailed) { if (refreshEndorsementChain && isConfirmed) { refreshEndorsementChain() } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, holderAddress, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -175,7 +179,7 @@ export const ActionForm: FunctionComponent = props => { const { rejectTransferOwnerState } = props const isConfirmed = rejectTransferOwnerState === FormState.CONFIRMED const isFailed = rejectTransferOwnerState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.REJECT_TRANSFER_OWNER_SUCCESS : MessageTitle.REJECT_TRANSFER_OWNER_FAILED const beneficiaryAddress = isConfirmed ? prevBeneficiary : beneficiary @@ -186,12 +190,13 @@ export const ActionForm: FunctionComponent = props => { } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, beneficiaryAddress, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -202,18 +207,19 @@ export const ActionForm: FunctionComponent = props => { const { nominationState } = props const isConfirmed = nominationState === FormState.CONFIRMED const isFailed = nominationState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.NOMINATE_BENEFICIARY_SUCCESS : MessageTitle.NOMINATE_BENEFICIARY_FAILED if (isConfirmed || isFailed) { showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -224,7 +230,7 @@ export const ActionForm: FunctionComponent = props => { const { nominee, endorseBeneficiaryState } = props const isConfirmed = endorseBeneficiaryState === FormState.CONFIRMED const isFailed = endorseBeneficiaryState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.ENDORSE_BENEFICIARY_SUCCESS : MessageTitle.ENDORSE_BENEFICIARY_FAILED const beneficiaryAddress = isConfirmed ? nominee : beneficiary @@ -235,12 +241,13 @@ export const ActionForm: FunctionComponent = props => { } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, beneficiaryAddress, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -251,7 +258,7 @@ export const ActionForm: FunctionComponent = props => { const { transferOwnersState } = props const isConfirmed = transferOwnersState === FormState.CONFIRMED const isFailed = transferOwnersState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.TRANSFER_OWNER_SUCCESS : MessageTitle.TRANSFER_OWNER_FAILED const beneficiaryAddress = isConfirmed ? newOwner : beneficiary @@ -262,12 +269,13 @@ export const ActionForm: FunctionComponent = props => { } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, beneficiaryAddress, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -278,7 +286,7 @@ export const ActionForm: FunctionComponent = props => { const { returnToIssuerState } = props const isConfirmed = returnToIssuerState === FormState.CONFIRMED const isFailed = returnToIssuerState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.RETURN_TO_ISSUER_DOCUMENT_SUCCESS : MessageTitle.RETURN_TO_ISSUER_DOCUMENT_FAILED const beneficiaryAddress = isConfirmed ? '' : beneficiary @@ -291,13 +299,14 @@ export const ActionForm: FunctionComponent = props => { showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, beneficiaryAddress, holderAddress, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -308,7 +317,7 @@ export const ActionForm: FunctionComponent = props => { const { restoreTokenState } = props const isConfirmed = restoreTokenState === FormState.CONFIRMED const isFailed = restoreTokenState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.REJECT_RETURN_TO_ISSUER_DOCUMENT_SUCCESS : MessageTitle.REJECT_RETURN_TO_ISSUER_DOCUMENT_FAILED const beneficiaryAddress = isConfirmed ? beneficiary : '' @@ -320,13 +329,14 @@ export const ActionForm: FunctionComponent = props => { } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed, beneficiaryAddress, holderAddress, }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() @@ -337,7 +347,7 @@ export const ActionForm: FunctionComponent = props => { const { destroyTokenState } = props const isConfirmed = destroyTokenState === FormState.CONFIRMED const isFailed = destroyTokenState === FormState.ERROR - const msg = isConfirmed + const title = isConfirmed ? MessageTitle.ACCEPT_RETURN_TO_ISSUER_DOCUMENT_SUCCESS : MessageTitle.ACCEPT_RETURN_TO_ISSUER_DOCUMENT_FAILED @@ -347,9 +357,10 @@ export const ActionForm: FunctionComponent = props => { } showOverlay( showDocumentTransferMessage( - msg, + title, { isSuccess: isConfirmed }, - setShowEndorsementChain + setShowEndorsementChain, + errorMessage ) ) setFormActionNone() diff --git a/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/types.ts b/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/types.ts index 5de69d8..628dcf8 100644 --- a/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/types.ts +++ b/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionForm/types.ts @@ -11,6 +11,7 @@ export interface BaseActionFormProps { setFormActionNone: () => void setShowEndorsementChain: (payload: boolean) => void refreshEndorsementChain?: () => void + errorMessage?: string } // Props for TransferHolderForm diff --git a/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionSelectionForm/ActionSelectionForm.tsx b/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionSelectionForm/ActionSelectionForm.tsx index 04500fc..baa4e58 100644 --- a/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionSelectionForm/ActionSelectionForm.tsx +++ b/src/components/AssetManagementPanel/AssetManagementForm/FormVariants/ActionSelectionForm/ActionSelectionForm.tsx @@ -173,35 +173,33 @@ export const ActionSelectionForm: FunctionComponent<
)} -
- {(isReturnedToIssuer || isTokenBurnt || isExpired) && ( -
- {(isReturnedToIssuer || isTokenBurnt) && ( - -

- {isReturnedToIssuer - ? 'ETR Returned to Issuer' - : 'ETR Taken Out of Circulation'} -

-
- )} - {isExpired && ( - -

ETR Expired

-
- )} -
-
- )} +
+
+ {(isReturnedToIssuer || isTokenBurnt) && ( + +

+ {isReturnedToIssuer + ? 'ETR Returned to Issuer' + : 'ETR Taken Out of Circulation'} +

+
+ )} + {isExpired && ( + +

ETR Expired

+
+ )} +
+
{!isTokenBurnt && ( -
-
+
+
{account ? ( <> {canManage ? ( diff --git a/src/components/common/Button/Button.tsx b/src/components/common/Button/Button.tsx index 2e0a191..428a1ec 100644 --- a/src/components/common/Button/Button.tsx +++ b/src/components/common/Button/Button.tsx @@ -154,3 +154,33 @@ export const LabelButton: FunctionComponent = ({ ) } + +interface TextButtonProps { + className?: string + onClick?: () => void + children: ReactNode + disabled?: boolean +} + +export const TextButton: FunctionComponent = ({ + className = '', + onClick, + children, + disabled = false, +}) => { + return ( + + ) +} diff --git a/src/components/common/Navbar/Navbar.tsx b/src/components/common/Navbar/Navbar.tsx index 755ea4c..158ba04 100644 --- a/src/components/common/Navbar/Navbar.tsx +++ b/src/components/common/Navbar/Navbar.tsx @@ -344,7 +344,7 @@ const Navbar = ({ isDarkMode, setIsDarkMode: _setIsDarkMode }: NavbarProps) => { /> ({ + mockCloseOverlay: vi.fn(), +})) + +vi.mock('../../contexts/OverlayContext', async importOriginal => { + const actual = + await importOriginal() + return { + ...actual, + useOverlayContext: () => ({ + closeOverlay: mockCloseOverlay, + showOverlay: vi.fn(), + }), + } +}) + +vi.mock('./MessageAddressResolver', () => ({ + MessageAddressResolver: ({ address }: { address: string }) => ( + {address} + ), +})) + +vi.mock('@/components/icons/Success', () => ({ + default: () =>
, +})) + +vi.mock('@/components/icons/Error', () => ({ + default: () =>
, +})) + +const mockSetShowEndorsementChain = vi.fn() + +const renderWithOverlay = (ui: React.ReactElement) => + render({ui}) + +describe('DocumentTransferMessage', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('errorMessage prop', () => { + it('renders errorMessage text instead of children when errorMessage is provided', () => { + renderWithOverlay( + + default child content + + ) + + expect(screen.getByText('User Rejected Transaction')).toBeInTheDocument() + expect( + screen.queryByText('default child content') + ).not.toBeInTheDocument() + }) + + it('renders children when errorMessage is undefined', () => { + renderWithOverlay( + + child content visible + + ) + + expect(screen.getByText('child content visible')).toBeInTheDocument() + }) + + it('renders children when errorMessage is empty string', () => { + renderWithOverlay( + + child content visible + + ) + + expect(screen.getByText('child content visible')).toBeInTheDocument() + }) + + it('renders the errorMessage inside a paragraph element', () => { + const { container } = renderWithOverlay( + + child + + ) + + const paragraph = container.querySelector('p.mt-3') + expect(paragraph?.textContent).toBe('Insufficient Funds') + }) + }) + + describe('title and icon rendering', () => { + it('renders the title text', () => { + renderWithOverlay( + + content + + ) + + expect(screen.getByText('Transfer Holder Success')).toBeInTheDocument() + }) + + it('renders success icon when isSuccess is true', () => { + renderWithOverlay( + + content + + ) + + expect(screen.getByTestId('success-icon')).toBeInTheDocument() + expect(screen.queryByTestId('error-icon')).not.toBeInTheDocument() + }) + + it('renders error icon when isSuccess is false', () => { + renderWithOverlay( + + content + + ) + + expect(screen.getByTestId('error-icon')).toBeInTheDocument() + expect(screen.queryByTestId('success-icon')).not.toBeInTheDocument() + }) + }) + + describe('buttons', () => { + it('calls closeOverlay when Dismiss button is clicked', () => { + renderWithOverlay( + + content + + ) + + fireEvent.click(screen.getByRole('button', { name: 'Dismiss' })) + expect(mockCloseOverlay).toHaveBeenCalled() + }) + + it('calls setShowEndorsementChain and closeOverlay when View Endorsement Chain is clicked', () => { + renderWithOverlay( + + content + + ) + + fireEvent.click( + screen.getByRole('button', { name: 'View Endorsement Chain' }) + ) + + expect(mockSetShowEndorsementChain).toHaveBeenCalledWith(true) + expect(mockCloseOverlay).toHaveBeenCalled() + }) + }) +}) + +describe('showDocumentTransferMessage', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('passes errorMessage to DocumentTransferMessage', () => { + const node = showDocumentTransferMessage( + MessageTitle.TRANSFER_HOLDER_FAILED, + { isSuccess: false }, + mockSetShowEndorsementChain, + 'User Rejected Transaction' + ) as React.ReactElement + + expect(node.props.errorMessage).toBe('User Rejected Transaction') + }) + + it('passes undefined errorMessage when not provided', () => { + const node = showDocumentTransferMessage( + MessageTitle.TRANSFER_HOLDER_SUCCESS, + { isSuccess: true }, + mockSetShowEndorsementChain + ) as React.ReactElement + + expect(node.props.errorMessage).toBeUndefined() + }) + + it('passes isSuccess from option to DocumentTransferMessage', () => { + const node = showDocumentTransferMessage( + MessageTitle.TRANSFER_HOLDER_SUCCESS, + { isSuccess: true }, + mockSetShowEndorsementChain + ) as React.ReactElement + + expect(node.props.isSuccess).toBe(true) + }) + + it('passes the title to DocumentTransferMessage', () => { + const node = showDocumentTransferMessage( + MessageTitle.NOMINATE_BENEFICIARY_FAILED, + { isSuccess: false }, + mockSetShowEndorsementChain, + 'Network Error' + ) as React.ReactElement + + expect(node.props.title).toBe(MessageTitle.NOMINATE_BENEFICIARY_FAILED) + expect(node.props.errorMessage).toBe('Network Error') + }) + + it('renders TRANSFER_HOLDER children when no errorMessage', () => { + const node = showDocumentTransferMessage( + MessageTitle.TRANSFER_HOLDER_FAILED, + { isSuccess: false, holderAddress: '0xabc' }, + mockSetShowEndorsementChain + ) as React.ReactElement + + const { getByText } = renderWithOverlay(node) + expect(getByText('Transfer Holder Failed')).toBeInTheDocument() + }) + + it('renders errorMessage text in overlay when errorMessage set', () => { + const node = showDocumentTransferMessage( + MessageTitle.TRANSFER_HOLDER_FAILED, + { isSuccess: false }, + mockSetShowEndorsementChain, + 'Insufficient Funds' + ) as React.ReactElement + + const { getByText } = renderWithOverlay(node) + expect(getByText('Insufficient Funds')).toBeInTheDocument() + }) +}) diff --git a/src/components/common/Overlay/OverlayContent/DocumentTransferMessage.tsx b/src/components/common/Overlay/OverlayContent/DocumentTransferMessage.tsx index 32c2c69..c1e3581 100644 --- a/src/components/common/Overlay/OverlayContent/DocumentTransferMessage.tsx +++ b/src/components/common/Overlay/OverlayContent/DocumentTransferMessage.tsx @@ -68,11 +68,12 @@ interface DocumentTransferMessageProps { isConfirmationMessage?: boolean onConfirmationAction?: () => void setShowEndorsementChain: (payload: boolean) => void + errorMessage?: string } export const DocumentTransferMessage: FunctionComponent< DocumentTransferMessageProps -> = ({ title, isSuccess, setShowEndorsementChain, children }) => { +> = ({ title, isSuccess, setShowEndorsementChain, children, errorMessage }) => { const { closeOverlay } = useOverlayContext() const handleViewEndorsementChain = () => { setShowEndorsementChain(true) @@ -94,7 +95,7 @@ export const DocumentTransferMessage: FunctionComponent< {/* Content */}
- {children} + {errorMessage ?

{errorMessage}

: children}
{/* Footer Buttons */} @@ -303,7 +304,8 @@ interface ShowDocumentTransferMessageOptionProps { export const showDocumentTransferMessage = ( title: string, option: ShowDocumentTransferMessageOptionProps, - setShowEndorsementChain: (payload: boolean) => void + setShowEndorsementChain: (payload: boolean) => void, + errorMessage?: string ): ReactNode => { return ( {title === MessageTitle.NO_METAMASK && } {title === MessageTitle.NO_MANAGE_ACCESS && } @@ -345,7 +348,9 @@ export const showDocumentTransferMessage = ( {(title === MessageTitle.ENDORSE_BENEFICIARY_SUCCESS || title === MessageTitle.TRANSFER_OWNER_SUCCESS || title === MessageTitle.TRANSFER_OWNER_FAILED || - title === MessageTitle.ENDORSE_BENEFICIARY_FAILED) && ( + title === MessageTitle.ENDORSE_BENEFICIARY_FAILED || + title === MessageTitle.REJECT_TRANSFER_OWNER_SUCCESS || + title === MessageTitle.REJECT_TRANSFER_OWNER_FAILED) && ( )} {(title === MessageTitle.NOMINATE_BENEFICIARY_SUCCESS || @@ -353,11 +358,15 @@ export const showDocumentTransferMessage = ( )} {(title === MessageTitle.TRANSFER_HOLDER_SUCCESS || - title === MessageTitle.TRANSFER_HOLDER_FAILED) && ( + title === MessageTitle.TRANSFER_HOLDER_FAILED || + title === MessageTitle.REJECT_TRANSFER_HOLDER_SUCCESS || + title === MessageTitle.REJECT_TRANSFER_HOLDER_FAILED) && ( )} {(title === MessageTitle.TRANSFER_OWNER_HOLDER_SUCCESS || - title === MessageTitle.TRANSFER_OWNER_HOLDER_FAILED) && ( + title === MessageTitle.TRANSFER_OWNER_HOLDER_FAILED || + title === MessageTitle.REJECT_TRANSFER_OWNER_HOLDER_SUCCESS || + title === MessageTitle.REJECT_TRANSFER_OWNER_HOLDER_FAILED) && ( o.value === value) ?? options[0] const [isOpen, setIsOpen] = useState(false) const optionItems = options.slice(1) + const hasError = Boolean(error) + + const triggerClassName = clsx( + 'select-field-trigger', + isDarkMode ? 'select-field-trigger--dark' : 'select-field-trigger--light', + hasError && 'select-field-trigger--error' + ) + + const selectedTextClassName = clsx( + 'select-field-value', + selected.value === '' + ? isDarkMode + ? 'select-field-value--placeholder-dark' + : 'select-field-value--placeholder-light' + : isDarkMode + ? 'select-field-value--selected-dark' + : 'select-field-value--selected-light' + ) + + const menuClassName = clsx( + 'select-field-menu', + isDarkMode ? 'select-field-menu--dark' : 'select-field-menu--light' + ) const handleTriggerKeyDown: React.KeyboardEventHandler< HTMLButtonElement @@ -98,27 +122,11 @@ const SelectField = ({ aria-expanded={isOpen} aria-invalid={!!error} aria-describedby={error ? `${id}-error` : undefined} - className={`w-full min-h-[48px] sm:min-h-[40px] rounded-lg border px-3 pr-10 py-3 sm:py-2 text-left text-base sm:text-sm font-gilroy flex items-center justify-between cursor-pointer transition-colors ${ - error - ? 'border-red-500' - : isDarkMode - ? 'bg-black/10 border-white/15' - : 'bg-white/90 border-black/15' - }`} + className={triggerClassName} onClick={() => setIsOpen(open => !open)} onKeyDown={handleTriggerKeyDown} > - - {selected.label} - + {selected.label} {isOpen && ( -
+
{optionItems.map((option, index) => ( diff --git a/src/components/common/Tag/Tag.tsx b/src/components/common/Tag/Tag.tsx index 4b2dc55..8f432f4 100644 --- a/src/components/common/Tag/Tag.tsx +++ b/src/components/common/Tag/Tag.tsx @@ -14,10 +14,7 @@ export const Tag: FunctionComponent = ({ ...props }) => { return ( -
+
{children}
) diff --git a/src/components/common/contexts/TokenInformationContext/TokenInformationContext.tsx b/src/components/common/contexts/TokenInformationContext/TokenInformationContext.tsx index e68d633..f094e04 100644 --- a/src/components/common/contexts/TokenInformationContext/TokenInformationContext.tsx +++ b/src/components/common/contexts/TokenInformationContext/TokenInformationContext.tsx @@ -65,6 +65,7 @@ interface ITokenInformationContext { restoreToken: (...args: any[]) => Promise restoreTokenState: ContractFunctionState resetProviders: () => void + errorMessage?: string } const contractFunctionStub: any = () => { @@ -185,6 +186,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: changeHolder, state: changeHolderState, reset: resetChangeHolder, + errorMessage: changeHolderErrorMessage, } = useContractFunctionHook( titleEscrow, 'transferHolder', @@ -196,6 +198,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: destroyToken, state: destroyTokenState, reset: resetDestroyingTokenState, + errorMessage: destroyTokenErrorMessage, } = useContractFunctionHook( tokenRegistry, 'acceptReturned', @@ -207,6 +210,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: endorseBeneficiary, state: endorseBeneficiaryState, reset: resetEndorseBeneficiary, + errorMessage: endorseBeneficiaryErrorMessage, } = useContractFunctionHook( titleEscrow, 'transferBeneficiary', @@ -218,6 +222,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: nominate, state: nominateState, reset: resetNominate, + errorMessage: nominateErrorMessage, } = useContractFunctionHook( titleEscrow, 'nominate', @@ -229,6 +234,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: rejectTransferHolder, state: rejectTransferHolderState, reset: resetRejectTransferHolder, + errorMessage: rejectTransferHolderErrorMessage, } = useContractFunctionHook( titleEscrow, 'rejectTransferHolder', @@ -240,6 +246,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: rejectTransferOwner, state: rejectTransferOwnerState, reset: resetRejectTransferOwner, + errorMessage: rejectTransferOwnerErrorMessage, } = useContractFunctionHook( titleEscrow, 'rejectTransferBeneficiary', @@ -251,6 +258,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: rejectTransferOwnerHolder, state: rejectTransferOwnerHolderState, reset: resetRejectTransferOwnerHolder, + errorMessage: rejectTransferOwnerHolderErrorMessage, } = useContractFunctionHook( titleEscrow, 'rejectTransferOwners', @@ -262,6 +270,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: restoreToken, // restoreToken function does not return any value state: restoreTokenState, reset: resetRestoreTokenState, + errorMessage: restoreTokenErrorMessage, } = useContractFunctionHook( tokenRegistry, 'rejectReturned', @@ -273,6 +282,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: returnToIssuer, state: returnToIssuerState, reset: resetReturnToIssuer, + errorMessage: returnToIssuerErrorMessage, } = useContractFunctionHook( titleEscrow, 'returnToIssuer', @@ -284,6 +294,7 @@ export const TokenInformationContextProvider: FunctionComponent< send: transferOwners, state: transferOwnerHoldersState, reset: resetTransferOwners, + errorMessage: transferOwnersErrorMessage, } = useContractFunctionHook( titleEscrow, 'transferOwners', @@ -400,6 +411,19 @@ export const TokenInformationContextProvider: FunctionComponent< // Reset states for all write functions when provider changes to allow methods to be called again without refreshing useEffect(resetProviders, [resetProviders, providerOrSigner]) + + const errorMessage = + changeHolderErrorMessage || + destroyTokenErrorMessage || + endorseBeneficiaryErrorMessage || + transferOwnersErrorMessage || + nominateErrorMessage || + rejectTransferOwnerErrorMessage || + rejectTransferHolderErrorMessage || + rejectTransferOwnerHolderErrorMessage || + restoreTokenErrorMessage || + returnToIssuerErrorMessage + return ( {children} diff --git a/src/components/common/contexts/providerContext.tsx b/src/components/common/contexts/providerContext.tsx index 8323800..ab5a269 100644 --- a/src/components/common/contexts/providerContext.tsx +++ b/src/components/common/contexts/providerContext.tsx @@ -1,4 +1,4 @@ -import { ProviderDetails, utils, CHAIN_ID, chainInfo } from '@trustvc/trustvc' +import { CHAIN_ID, chainInfo } from '@trustvc/trustvc' import { ethers, providers } from 'ethers' import { Magic } from 'magic-sdk' import React, { @@ -10,7 +10,7 @@ import React, { useRef, useState, } from 'react' -import { INFURA_API_KEY, NETWORK_NAME } from '../../../configs/chain-config' +import { NETWORK_NAME } from '../../../configs/chain-config' import { ChainInfo } from '../../../utils/chain-info' // import { UnsupportedNetworkError } from '../errors' import { @@ -35,34 +35,32 @@ export enum SIGNER_TYPE { } const createProvider = (chainId: CHAIN_ID) => { - const url = ChainInfo[chainId]?.rpcUrl - if (url) { - const chainMeta = getChainInfo(chainId) - return new providers.StaticJsonRpcProvider(url, { - chainId: Number(chainId), - name: chainMeta?.name ?? `chain-${chainId}`, - }) - } - const chainMeta = getChainInfo(chainId) - const opts: ProviderDetails = { - network: chainMeta?.name ?? `chain-${chainId}`, - providerType: 'infura', - apiKey: INFURA_API_KEY, + // First try to get RPC URL from env (VITE_RPC_URL_*), then fall back to chain defaults + const url = getRpcUrl(String(chainId)) ?? ChainInfo[chainId]?.rpcUrl + + if (!url) { + throw new Error(`No RPC URL configured for chain ${chainId}`) } - return utils.generateProvider(opts) + + return new providers.JsonRpcProvider(url) } // Utility function for use in non-react components that cannot get through hooks let currentProvider: providers.Provider | undefined -try { - currentProvider = createProvider(getChainInfoFromNetworkName(NETWORK_NAME).id) -} catch (e) { - console.error('Invalid NETWORK_NAME; provider init failed', e) - currentProvider = undefined -} -export const getCurrentProvider = (): providers.Provider | undefined => - currentProvider +export const getCurrentProvider = (): providers.Provider | undefined => { + if (!currentProvider) { + try { + currentProvider = createProvider( + getChainInfoFromNetworkName(NETWORK_NAME).id + ) + } catch (e) { + console.error('Invalid NETWORK_NAME; provider init failed', e) + currentProvider = undefined + } + } + return currentProvider +} export interface ProviderContextProps { providerType: SIGNER_TYPE @@ -88,8 +86,8 @@ export const ProviderContext = createContext({ reloadNetwork: async () => {}, supportedChainInfoObjects: [], currentChainId: undefined, - provider: currentProvider, - providerOrSigner: currentProvider, + provider: undefined, + providerOrSigner: undefined, account: undefined, networkChangeLoading: false, setNetworkChangeLoading: () => {}, diff --git a/src/components/home/EndorsementChain/EndorsementChain.tsx b/src/components/home/EndorsementChain/EndorsementChain.tsx index 0d310a8..f5f4eb9 100644 --- a/src/components/home/EndorsementChain/EndorsementChain.tsx +++ b/src/components/home/EndorsementChain/EndorsementChain.tsx @@ -291,7 +291,7 @@ const EndorsementChainLayout: React.FC = ({
{status === 'loading' && (
- Loading + Loading Endorsement Chain..
)} diff --git a/src/components/home/VerifySection/DocumentRenderer.tsx b/src/components/home/VerifySection/DocumentRenderer.tsx index 76eb9ba..be14283 100644 --- a/src/components/home/VerifySection/DocumentRenderer.tsx +++ b/src/components/home/VerifySection/DocumentRenderer.tsx @@ -328,8 +328,7 @@ const DocumentRenderer: React.FC = ({ {!isRendererReady && selectedTemplate !== 'attachmentTab' && (
- Loading document - preview.. + Loading document preview..
)} diff --git a/src/components/home/VerifySection/VerifyError.tsx b/src/components/home/VerifySection/VerifyError.tsx index 000c67e..6f34d68 100644 --- a/src/components/home/VerifySection/VerifyError.tsx +++ b/src/components/home/VerifySection/VerifyError.tsx @@ -86,7 +86,7 @@ const VerifyError: React.FC = ({ errorType, onReset }) => { {/* What Should I Do? link */}
)} diff --git a/src/components/home/VerifySection/VerifySection.test.tsx b/src/components/home/VerifySection/VerifySection.test.tsx index 5bcfefb..2c63640 100644 --- a/src/components/home/VerifySection/VerifySection.test.tsx +++ b/src/components/home/VerifySection/VerifySection.test.tsx @@ -289,13 +289,19 @@ describe('VerifySection', () => { ).toBeInTheDocument() }) - it('navigates to root when Visit Document Gallery is clicked', () => { + it('opens the document gallery in a new tab when clicked', () => { + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null) render() const ctaButton = screen .getByText(/Visit Document Gallery/i) .closest('.cta-button') fireEvent.click(ctaButton as HTMLElement) - expect(mockNavigate).toHaveBeenCalledWith('/') + expect(openSpy).toHaveBeenCalledWith( + 'https://gallery.tradetrust.io', + '_blank', + 'noopener,noreferrer' + ) + openSpy.mockRestore() }) }) }) diff --git a/src/components/home/VerifySection/VerifySection.tsx b/src/components/home/VerifySection/VerifySection.tsx index f45540f..741b37a 100644 --- a/src/components/home/VerifySection/VerifySection.tsx +++ b/src/components/home/VerifySection/VerifySection.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { useNavigate } from 'react-router-dom' import { useVerify } from './useVerify' import NetworkModal from './NetworkModal' import VerifyResult from './VerifyResult' @@ -91,8 +90,6 @@ const VerifySection: React.FC = ({ isDarkMode }) => { ? (CHAIN_NAMES[verifiedChainId] ?? `Chain ${verifiedChainId}`) : undefined - const navigate = useNavigate() - const renderDropzone = () => (
= ({ isDarkMode }) => {
- + Verifying {fileName}...
@@ -148,7 +145,9 @@ const VerifySection: React.FC = ({ isDarkMode }) => { ) return ( -
+
{showEndorsementChain && ( = ({ isDarkMode }) => {