-
Notifications
You must be signed in to change notification settings - Fork 0
Feat - Implement WalletConnect #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b53ee19
f3fda0a
2d1bf75
3b70bae
29f50c0
5dfcbe7
031decf
e9e2761
7deb0f1
00a3443
26723fb
4edb719
12a6d7b
bdaf132
adfaec8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { Link, ModalBody, ModalContent, ModalH1, PageContent } from "../../Commo | |
| import { | ||
| assert, | ||
| getAddressContraction, | ||
| isHydrated, | ||
| getChainExplorerTxUrl, | ||
| getOrderDestChainId, | ||
| getPayment, | ||
|
|
@@ -23,6 +24,7 @@ import { ROZO_INVOICE_URL } from "../../../constants/rozoConfig"; | |
| import { usePayoutPolling } from "../../../hooks/usePayoutPolling"; | ||
| import { usePusherPayout } from "../../../hooks/usePusherPayout"; | ||
| import { useRozoPay } from "../../../hooks/useRozoPay"; | ||
| import { resolveConfirmedPayoutTxHash } from "../../../payment/resolveConfirmedPayoutTxHash"; | ||
| import { | ||
| beginRequestScope, | ||
| cancelRequestScope, | ||
|
|
@@ -91,17 +93,19 @@ const Confirmation: React.FC = () => { | |
| // is known as soon as the pay-in txHash is confirmed. | ||
| const isStellarDirectSameTx = useMemo(() => { | ||
| if (!order) return false; | ||
|
|
||
| const meta = (order as any).metadata as Record<string, unknown> | undefined; | ||
| const meta = order.metadata as Record<string, unknown> | undefined; | ||
| if (meta?.settlementMode !== "stellar_direct") return false; | ||
|
|
||
| // txHash fields only exist on hydrated orders — narrow first. | ||
| const hydrated = isHydrated(order) ? order : null; | ||
| const sourceTx = | ||
| (order as any).sourceStartTxHash ?? (meta?.payinTransactionHash as string | undefined); | ||
|
|
||
| hydrated?.sourceStartTxHash ?? | ||
| (meta?.payinTransactionHash as string | undefined); | ||
| const destTx = | ||
| (order as any).payoutTransactionHash ?? | ||
| (order as any).destFastFinishTxHash ?? | ||
| (order as any).destClaimTxHash; | ||
| (meta?.payoutTransactionHash as string | undefined) ?? | ||
| hydrated?.payoutTransactionHash ?? | ||
| hydrated?.destFastFinishTxHash ?? | ||
| hydrated?.destClaimTxHash; | ||
|
|
||
| return !!sourceTx && !!destTx && sourceTx === destTx; | ||
| }, [order]); | ||
|
Comment on lines
94
to
111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 — test gap on payout-status logic. |
||
|
|
@@ -123,7 +127,13 @@ const Confirmation: React.FC = () => { | |
| } | ||
| } | ||
|
|
||
| if (payParams && (tokenMode === "stellar" || tokenMode === "solana" || tokenMode === "evm")) { | ||
| if ( | ||
| payParams && | ||
| (tokenMode === "stellar" || | ||
| tokenMode === "solana" || | ||
| tokenMode === "evm" || | ||
| tokenMode === "all") | ||
| ) { | ||
| return payParams.showProcessingPayout; | ||
| } | ||
|
|
||
|
|
@@ -140,6 +150,15 @@ const Confirmation: React.FC = () => { | |
| return undefined; | ||
| }, [pusherPayoutTxHash, order]); | ||
|
|
||
| const confirmedPayoutTxHash = payinConfirmed?.payoutTxHash; | ||
| const confirmedPayoutTxHashUrl = useMemo(() => { | ||
| if (!confirmedPayoutTxHash || !order) return undefined; | ||
| return getChainExplorerTxUrl( | ||
| getOrderDestChainId(order), | ||
| confirmedPayoutTxHash, | ||
| ); | ||
| }, [confirmedPayoutTxHash, order]); | ||
|
|
||
| const rozoPaymentId = useMemo(() => { | ||
| const id = order?.externalId || paymentStateContext.rozoPaymentId; | ||
| return id; | ||
|
|
@@ -500,6 +519,7 @@ const Confirmation: React.FC = () => { | |
|
|
||
| // Payout is resolved when either Pusher or polling has found the destination txhash | ||
| const payoutResolved = !!( | ||
| (confirmedPayoutTxHash && confirmedPayoutTxHashUrl) || | ||
| (pusherPayoutTxHash && computedPusherPayoutTxHashUrl) || | ||
| (payoutTxHash && payoutTxHashUrl) | ||
| ); | ||
|
|
@@ -683,28 +703,28 @@ const Confirmation: React.FC = () => { | |
| }, [done, paymentStateContext, rawPayInHash, rozoPaymentId]); | ||
|
|
||
| /** | ||
| * Payout that is settled by the payin itself, so there is nothing to wait | ||
| * for: stellar_direct with source txHash === destination txHash (known from | ||
| * the order, or from the confirmed API response), and deposit-address flows | ||
| * (which previously marked payout completed on payin detection). Separate | ||
| * from the completion effect so its dedupe cannot swallow this event. | ||
| * Complete payout immediately when the payin confirmation response already | ||
| * includes its destination hash. This covers payouts completed before the | ||
| * Confirmation page subscribed to Pusher, plus stellar_direct same-tx flows. | ||
| */ | ||
| useEffect(() => { | ||
| if (!done || !rawPayInHash || !rozoPaymentId) return; | ||
| const sameTx = isStellarDirectSameTx || !!payinConfirmed?.sameTxPayout; | ||
| // Deposit-address flow: only when the API already reports the destination | ||
| // tx. Otherwise leave payoutCompletedRef untouched so the normal Pusher / | ||
| // polling payout wait runs — never report the source tx as the payout. | ||
| const depositPayout = | ||
| !!paymentStateContext.selectedDepositAddressOption && !!payinConfirmed?.payoutTxHash; | ||
| if (!sameTx && !depositPayout) return; | ||
| const payoutHash = sameTx ? rawPayInHash : payinConfirmed!.payoutTxHash!; | ||
| const payoutHash = resolveConfirmedPayoutTxHash( | ||
| rawPayInHash, | ||
| payinConfirmed?.payoutTxHash, | ||
| sameTx, | ||
| ); | ||
| if (!payoutHash) return; | ||
| const payoutKey = `${payoutHash}-${rozoPaymentId}`; | ||
| if (payoutCompletedSent.current === payoutKey) return; | ||
| payoutCompletedSent.current = payoutKey; | ||
| payoutCompletedRef.current = true; | ||
| setPaymentPayoutCompleted(payoutHash, rozoPaymentId); | ||
| context.log("[CONFIRMATION] payout completed directly:", { sameTx, depositPayout, payoutHash }); | ||
| context.log("[CONFIRMATION] payout completed from payin response:", { | ||
| sameTx, | ||
| payoutHash, | ||
| }); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [done, rawPayInHash, rozoPaymentId, isStellarDirectSameTx, payinConfirmed]); | ||
|
|
||
|
|
@@ -854,15 +874,26 @@ const Confirmation: React.FC = () => { | |
| <ModalBody> | ||
| {payoutLoading ? ( | ||
| <LoadingText>Processing payout...</LoadingText> | ||
| ) : (computedPusherPayoutTxHashUrl && pusherPayoutTxHash) || | ||
| ) : (confirmedPayoutTxHashUrl && confirmedPayoutTxHash) || | ||
| (computedPusherPayoutTxHashUrl && pusherPayoutTxHash) || | ||
| (payoutTxHashUrl && payoutTxHash) ? ( | ||
| <Link | ||
| href={computedPusherPayoutTxHashUrl || payoutTxHashUrl || "#"} | ||
| href={ | ||
| confirmedPayoutTxHashUrl || | ||
| computedPusherPayoutTxHashUrl || | ||
| payoutTxHashUrl || | ||
| "#" | ||
| } | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| style={{ fontSize: 14, fontWeight: 400 }} | ||
| > | ||
| {getAddressContraction(pusherPayoutTxHash || payoutTxHash || "")} | ||
| {getAddressContraction( | ||
| confirmedPayoutTxHash || | ||
| pusherPayoutTxHash || | ||
| payoutTxHash || | ||
| "", | ||
| )} | ||
| <ExternalIcon /> | ||
| </Link> | ||
| ) : ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import { useEffect, useState } from "react"; | ||
| import { useAccount } from "wagmi"; | ||
|
|
||
| import { ROUTES } from "../../../constants/routes"; | ||
| import { useConnect } from "../../../hooks/useConnect"; | ||
| import { useConnectors } from "../../../hooks/useConnectors"; | ||
| import { usePayContext } from "../../../hooks/usePayContext"; | ||
| import { useWallet } from "../../../wallets/useWallets"; | ||
| import CopyToClipboard from "../../Common/CopyToClipboard"; | ||
| import CustomQRCode from "../../Common/CustomQRCode"; | ||
| import Alert from "../../Common/Alert"; | ||
| import Button from "../../Common/Button"; | ||
| import { | ||
| ModalBody, | ||
| ModalContent, | ||
| ModalH1, | ||
| PageContent, | ||
| } from "../../Common/Modal/styles"; | ||
|
|
||
| /** | ||
| * Desktop-only WalletConnect connect flow: renders our own QR + copyable URI | ||
| * instead of WalletConnect's bundled modal. The desktop-only connector is | ||
| * configured with showQrModal: false in defaultConnectors.ts. | ||
| * | ||
| * pendingConnectorId may be either the real "walletConnect" connector id, or a | ||
| * no-extension fallback stub id ("wc-fallback-…"). The connector is always the | ||
| * shared walletConnect one; the stub only drives branding ("Scan with | ||
| * MetaMask" + its logo), since any WC wallet can scan the QR. | ||
| * | ||
| * The connector emits a "display_uri" message on its own `emitter` (wagmi | ||
| * gives every connector a dedicated Emitter<ConnectorEventMap>, wired up in | ||
| * createConfig's setup()) once connect() kicks off the pairing handshake; | ||
| * we grab that URI and render it here. | ||
| */ | ||
| const ConnectWalletConnect: React.FC = () => { | ||
| const context = usePayContext(); | ||
| const { pendingConnectorId, setRoute } = context; | ||
| const brandingWallet = useWallet(pendingConnectorId ?? ""); | ||
| const { connect } = useConnect(); | ||
| const { isConnected } = useAccount(); | ||
|
|
||
| // Resolve the real WC connector regardless of which tile the user clicked. | ||
| const connectors = useConnectors(); | ||
| const wcConnector = connectors.find((c) => c.id === "walletConnect"); | ||
|
|
||
| const [uri, setUri] = useState<string | null>(null); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [attempt, setAttempt] = useState(0); | ||
|
|
||
| useEffect(() => { | ||
| if (!wcConnector) return; | ||
|
|
||
| setError(null); | ||
| setUri(null); | ||
|
|
||
| const listener = (event: { type: string; data?: unknown }) => { | ||
| if (event.type === "display_uri" && typeof event.data === "string") { | ||
| setUri(event.data); | ||
| } | ||
| }; | ||
| wcConnector.emitter.on("message", listener); | ||
|
|
||
| connect({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 — connect side effect has no abort/cancel on unmount. Cleanup only removes the Also: if |
||
| connector: wcConnector, | ||
| mutation: { | ||
| onError: (err) => setError(err?.message ?? "Failed to connect"), | ||
| }, | ||
| }); | ||
|
|
||
| return () => { | ||
| wcConnector.emitter.off("message", listener); | ||
| }; | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [wcConnector, attempt]); | ||
|
|
||
| // Error Alert changes content height — poke the modal so it re-measures. | ||
| useEffect(() => { | ||
| if (error) setTimeout(context.triggerResize, 10); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [error]); | ||
|
Comment on lines
+76
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 — redirects even when already connected via a different wallet. This effect navigates to |
||
|
|
||
| useEffect(() => { | ||
| if (!isConnected) return; | ||
| setRoute(ROUTES.SELECT_TOKEN, { event: "walletconnect-connected" }); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [isConnected]); | ||
|
|
||
| if (!wcConnector) return <Alert>Connector not found</Alert>; | ||
|
|
||
| // Branding: fallback stubs carry the wallet's own name/logo; the generic | ||
| // walletConnect tile falls back to WalletConnect branding. | ||
| const title = brandingWallet?.name | ||
| ? `Scan with ${brandingWallet.name}` | ||
| : "Scan with WalletConnect"; | ||
|
|
||
| return ( | ||
| <PageContent> | ||
| <ModalContent style={{ paddingBottom: 8, gap: 14 }}> | ||
| <ModalH1>{title}</ModalH1> | ||
| <ModalBody> | ||
| Scan this QR code with a WalletConnect-compatible wallet, or copy | ||
| the link below. | ||
| </ModalBody> | ||
| <div style={{ width: "60%", margin: "0 auto" }}> | ||
| <CustomQRCode value={uri ?? undefined} image={brandingWallet?.iconConnector} /> | ||
| </div> | ||
| {error && ( | ||
| <> | ||
| <Alert error>{error}</Alert> | ||
| <Button | ||
| onClick={() => setAttempt((n) => n + 1)} | ||
| style={{ margin: "0 auto" }} | ||
| > | ||
| Try again | ||
| </Button> | ||
| </> | ||
| )} | ||
| <div style={{ display: "flex", justifyContent: "center" }}> | ||
| <CopyToClipboard variant="button" string={uri ?? ""}> | ||
| Copy to clipboard | ||
| </CopyToClipboard> | ||
| </div> | ||
| </ModalContent> | ||
| </PageContent> | ||
| ); | ||
| }; | ||
|
|
||
| export default ConnectWalletConnect; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
settlementModevalue is read but no longer checked, so any non-Stellar order whose source and destination hash fields happen to match is classified asisStellarDirectSameTx. WhenshowProcessingPayoutis enabled, this suppresses the payout-processing state and its Pusher/polling path even though the order was not routed throughstellar_direct; restore thesettlementMode === "stellar_direct"guard before comparing hashes.Useful? React with 👍 / 👎.