From ec0bb157d75b5bb21a7652f37c556d837aa26a0a Mon Sep 17 00:00:00 2001 From: Malik Zulqurnain Date: Wed, 8 Apr 2026 19:44:40 +0500 Subject: [PATCH] feat(pm): add in-modal success verification state for trades Keep prediction market trade modal open after successful transactions and show a success state with transaction details, explorer link, and copy actions so users can verify on-chain activity before closing. Made-with: Cursor --- .../PredictionMarketTradeModal.scss | 77 ++ .../PredictionMarketTradeModal.tsx | 946 ++++++++++-------- .../ThreadPredictionMarketCard.tsx | 1 - 3 files changed, 631 insertions(+), 393 deletions(-) diff --git a/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.scss b/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.scss index 99afd244279..daecd6f5650 100644 --- a/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.scss +++ b/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.scss @@ -354,6 +354,83 @@ margin-bottom: 12px; } + .success-state-panel { + display: flex; + flex-direction: column; + gap: 12px; + + .success-emoji { + font-size: 28px; + line-height: 1; + } + + .success-description { + color: colors.$neutral-600; + margin: 0; + } + + .success-details { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border: 1px solid colors.$neutral-200; + border-radius: 10px; + background: colors.$neutral-75; + } + + .success-detail-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + } + + .success-tx-row { + display: flex; + flex-direction: column; + gap: 8px; + padding-top: 8px; + border-top: 1px solid colors.$neutral-150; + } + + .success-tx-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; + } + + .success-tx-copy { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border: 1px solid colors.$neutral-200; + border-radius: 6px; + background: colors.$white; + cursor: pointer; + max-width: 100%; + min-width: 0; + font: inherit; + color: inherit; + + .tx-hash-text { + font-size: 12px; + color: colors.$neutral-700; + } + + .copy-icon { + color: colors.$neutral-500; + } + + &:hover { + border-color: colors.$primary-300; + } + } + } + .PredictionMarketTradeModal-tab-content { display: flex; flex-direction: column; diff --git a/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.tsx b/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.tsx index 4c9836170c0..d197fb3b8bc 100644 --- a/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.tsx @@ -109,6 +109,14 @@ type PredictionMarketTradeModalProps = { }; type TabId = 'mint' | 'swap' | 'merge' | 'redeem'; +type SuccessAction = 'mint' | 'swap' | 'merge' | 'redeem'; + +type SuccessState = { + action: SuccessAction; + txHash: string; + submittedBy: string; + completedAt: string; +}; function getTradeParams( market: Market, @@ -187,6 +195,7 @@ export const PredictionMarketTradeModal = ({ const [swapQuoteOutWei, setSwapQuoteOutWei] = useState(null); const [swapQuoteError, setSwapQuoteError] = useState(null); const [swapQuoteLoading, setSwapQuoteLoading] = useState(false); + const [successState, setSuccessState] = useState(null); const swapQuoteDebounceRef = useRef | null>( null, ); @@ -202,6 +211,12 @@ export const PredictionMarketTradeModal = ({ const ethChainId = (community as { ChainNode?: { eth_chain_id?: number } } | undefined) ?.ChainNode?.eth_chain_id ?? 0; + const chainLabel = + (community as { ChainNode?: { name?: string } } | undefined)?.ChainNode + ?.name ?? 'Current network'; + const blockExplorerBaseUrl = + (community as { ChainNode?: { block_explorer?: string } } | undefined) + ?.ChainNode?.block_explorer ?? 'https://basescan.org/'; const { data: positions = [], refetch: refetchPositions } = useGetPredictionMarketPositionsQuery({ @@ -486,7 +501,7 @@ export const PredictionMarketTradeModal = ({ notifyError('Wallet not found for this address.'); return; } - await mintTokens({ + const { transactionHash } = await mintTokens({ ...getTradeParams( effectiveMarket, chainRpc, @@ -507,7 +522,12 @@ export const PredictionMarketTradeModal = ({ ).then((info) => setCollateralInfo(info)); } onSuccess?.(); - onClose(); + setSuccessState({ + action: 'mint', + txHash: transactionHash, + submittedBy: activeAddress, + completedAt: new Date().toLocaleString(), + }); } catch (err) { const msg = err instanceof Error ? err.message : 'Mint failed.'; setErrorMessage(msg); @@ -575,7 +595,7 @@ export const PredictionMarketTradeModal = ({ notifyError(qMsg); return; } - await swapTokens({ + const { transactionHash } = await swapTokens({ ...getTradeParams( effectiveMarket, chainRpc, @@ -600,7 +620,12 @@ export const PredictionMarketTradeModal = ({ ); } onSuccess?.(); - onClose(); + setSuccessState({ + action: 'swap', + txHash: transactionHash, + submittedBy: activeAddress, + completedAt: new Date().toLocaleString(), + }); } catch (err) { const msg = err instanceof Error ? err.message : 'Swap failed.'; setErrorMessage(msg); @@ -638,7 +663,7 @@ export const PredictionMarketTradeModal = ({ notifyError('Wallet not found for this address.'); return; } - await mergeTokens({ + const { transactionHash } = await mergeTokens({ ...getTradeParams( effectiveMarket, chainRpc, @@ -669,7 +694,12 @@ export const PredictionMarketTradeModal = ({ ); } onSuccess?.(); - onClose(); + setSuccessState({ + action: 'merge', + txHash: transactionHash, + submittedBy: activeAddress, + completedAt: new Date().toLocaleString(), + }); } catch (err) { const msg = err instanceof Error ? err.message : 'Merge failed.'; setErrorMessage(msg); @@ -707,7 +737,7 @@ export const PredictionMarketTradeModal = ({ notifyError('Wallet not found for this address.'); return; } - await redeemTokens({ + const { transactionHash } = await redeemTokens({ ...getTradeParams( effectiveMarket, chainRpc, @@ -722,7 +752,12 @@ export const PredictionMarketTradeModal = ({ await refetchPositions(); await refetchEthBalance(); onSuccess?.(); - onClose(); + setSuccessState({ + action: 'redeem', + txHash: transactionHash, + submittedBy: activeAddress, + completedAt: new Date().toLocaleString(), + }); } catch (err) { const msg = err instanceof Error ? err.message : 'Redeem failed.'; setErrorMessage(msg); @@ -1079,6 +1114,26 @@ export const PredictionMarketTradeModal = ({ ); }; + const getSuccessActionLabel = (action: SuccessAction) => { + if (action === 'mint') return 'Deposit and Mint'; + if (action === 'swap') return 'Swap'; + if (action === 'merge') return 'Merge'; + return 'Redeem'; + }; + + const truncateTxHash = (txHash: string) => { + if (!txHash) return ''; + if (txHash.length <= 16) return txHash; + return `${txHash.slice(0, 10)}...${txHash.slice(-8)}`; + }; + + const buildTxExplorerLink = (txHash: string) => + `${blockExplorerBaseUrl}tx/${txHash}`; + const successPanelState: SuccessState | null = successState; + const handleClose = () => { + onClose(); + }; + if (!hasAddresses) { return (
@@ -1169,102 +1224,63 @@ export const PredictionMarketTradeModal = ({ return (
-
- - CustomAddressOption({ - originalProps, - selectedAddressValue: activeAddress, - }), - }} - noOptionsMessage={() => 'No available address'} - value={convertAddressToDropdownOption(activeAddress)} - formatOptionLabel={(option) => ( - - )} - isClearable={false} - isSearchable={false} - options={uniqueAddresses.map(convertAddressToDropdownOption)} - onChange={(option) => - option?.value && setSelectedAddress(option.value) - } - /> -
-
- - {!detailsCollapsed && ( -
- {effectiveMarket.vault_address && market.collateral_address && ( -
- - Vault - +
+
+ Network + + {chainLabel} + +
+
+ Submitted by + + {formatAddressShort(successPanelState.submittedBy, 8, 6)} + +
+
+ Completed + + {successPanelState.completedAt} + +
+
+ Transaction ID +
-
- )} - {market.collateral_address && ( -
- - Collateral - - + />
- )} - {effectiveMarket.router_address && ( -
- - Router - - +
+
+
+ ) : ( + <> +
+ + CustomAddressOption({ + originalProps, + selectedAddressValue: activeAddress, + }), + }} + noOptionsMessage={() => 'No available address'} + value={convertAddressToDropdownOption(activeAddress)} + formatOptionLabel={(option) => ( + + )} + isClearable={false} + isSearchable={false} + options={uniqueAddresses.map(convertAddressToDropdownOption)} + onChange={(option) => + option?.value && setSelectedAddress(option.value) + } + /> +
+
+ + {detailsCollapsed ? ( + + ) : ( + + )} + + {!detailsCollapsed && ( +
+ {effectiveMarket.vault_address && + market.collateral_address && ( +
+ + Vault + + +
+ )} + {market.collateral_address && ( +
+ + Collateral + + +
+ )} + {effectiveMarket.router_address && ( +
+ + Router + + +
+ )} + {market.strategy_address && ( +
+ + Strategy + + +
+ )} + {market.governor_address && ( +
+ + Governor + + +
+ )}
)} - {market.governor_address && ( -
- - Governor +
+
+
+
+ + + PASS BALANCE - + {effectiveMarket.p_token_address && ( +
+ + {formatAddressShort( + effectiveMarket.p_token_address, + 6, + 4, + )} + + +
+ )}
- )} -
- )} -
-
-
-
- - - PASS BALANCE - - {effectiveMarket.p_token_address && ( -
- - {formatAddressShort(effectiveMarket.p_token_address, 6, 4)} +
+ + +  PASS -
- )} -
-
- - -  PASS - -
-
-
-
- - - FAIL BALANCE - - {effectiveMarket.f_token_address && ( -
- - {formatAddressShort(effectiveMarket.f_token_address, 6, 4)} +
+
+
+ + + FAIL BALANCE - + {effectiveMarket.f_token_address && ( +
+ + {formatAddressShort( + effectiveMarket.f_token_address, + 6, + 4, + )} + + +
+ )}
- )} -
-
- - -  FAIL - -
-
-
- - {isResolved ? ( - ( -
- {}} - isDisabled +
+ + +  FAIL +
+
+
+ + {isResolved ? ( + ( +
+ {}} + isDisabled + /> +
+ )} + /> + ) : ( + setActiveTab('mint')} + /> )} - /> - ) : ( - setActiveTab('mint')} - /> - )} - {isResolved ? ( - ( -
- {}} - isDisabled - /> -
+ {isResolved ? ( + ( +
+ {}} + isDisabled + /> +
+ )} + /> + ) : ( + setActiveTab('swap')} + isDisabled={swapDisabled} + /> )} - /> - ) : ( - setActiveTab('swap')} - isDisabled={swapDisabled} - /> - )} - {isResolved ? ( - ( -
- {}} - isDisabled - /> -
+ {isResolved ? ( + ( +
+ {}} + isDisabled + /> +
+ )} + /> + ) : ( + setActiveTab('merge')} + /> )} - /> - ) : ( - setActiveTab('merge')} - /> - )} - setActiveTab('redeem')} - /> -
- {errorMessage && ( -
- - {errorMessage} -
- )} - {isLoading && ( -
- -
+ setActiveTab('redeem')} + /> + + {errorMessage && ( +
+ + {errorMessage} +
+ )} + {isLoading && ( +
+ +
+ )} + {!isLoading && renderTabContent()} + )} - {!isLoading && renderTabContent()} - - + {successPanelState ? ( + <> + + + ) : ( + <> + + + + )}
); diff --git a/packages/commonwealth/client/scripts/views/pages/view_thread/ThreadPredictionMarketCard.tsx b/packages/commonwealth/client/scripts/views/pages/view_thread/ThreadPredictionMarketCard.tsx index 76d7221d491..6cb07f8e6a5 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_thread/ThreadPredictionMarketCard.tsx +++ b/packages/commonwealth/client/scripts/views/pages/view_thread/ThreadPredictionMarketCard.tsx @@ -436,7 +436,6 @@ export const ThreadPredictionMarketCard = ({ const handleTradeModalSuccess = () => { void (async () => { await refreshTradeData(); - setIsTradeModalOpen(false); })(); };