feat(stellar): improve Stellar wallet UX - #58
Conversation
- Always show Freighter and LOBSTR in wallet list (even without extension) - When extension not installed, route through WalletConnect - Add WalletConnect QR page for desktop without extension - Add modal close detection to WalletConnect (rejects on user dismiss) - Update description text for WalletConnect flow - Pre-fetch supported wallets in StellarContextProvider (no loading spinner) - Cache kit singleton synchronously to avoid flash
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 458b0636d3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- Remove dead StellarWalletConnectQR component (unreachable route, signing bug) - Fix MutationObserver leak in walletconnect.module.ts (add finally block) - Add walletsLoaded flag to StellarContextProvider (fix infinite spinner on error) - Collapse duplicate isPlatformWrapper/else branches in ConnectStellar - Use WALLET_CONNECT_ID constant in ConnectorStellar (no hardcoded string) - Check hasWalletConnect before routing to WalletConnect (custom kit safety)
- Auto-detect Stellar→Stellar USDC/EURC same-chain payments - Force intent: stellar_direct when source=destination chain+token - Add settlementMode field to PaymentResponse - Pass intent as top-level field in POST /payments - Handle settlementMode in PayWithStellarToken (direct send) - Add modal close detection to WalletConnect (MutationObserver) - Update workspace deps for local dev mode
Review — no P0, mergeable; 2 P1s worth a look firstReviewed the signing / fund-routing path closely. No blocker. The P1 — recommend before merge
P2 (non-blocking)
Verdict: mergeable. Fund-safety path verified clean. The multi-account |
AI review — incremental (
|
WalletConnectModule created two independent WalletConnect Cores: one
via SignClient.init() for signing, another via createAppKit()'s own
internal UniversalProvider init for the QR modal. manualWCControl only
affected close() behavior, not initialization, so both always fired.
Switch to UniversalProvider.init() (wraps a SignClient at .client, same
API) and pass that single instance into createAppKit({ universalProvider }),
so AppKit reuses it instead of spinning up its own Core.
Also pin @walletconnect/core/sign-client/universal-provider/ethereum-provider
to one version workspace-wide via pnpm overrides — the tree previously
resolved 5 different @walletconnect/core versions across @reown/appkit
and stellar-wallets-kit, each with its own module-scope singleton.
Horizon/Asset/tx-building symbols were statically imported in StellarContextProvider, usePaymentState, PayWithStellarToken, and (via chainAddress.ts) RozoPayButton itself, forcing the ~14M SDK to parse/execute on every mount regardless of chain used. Converts each to dynamic import(), deferred to actual use (wallet connect, quote, tx submit). Adds validateAddressForChainAsync for RozoPayButton's internal validation path; public isValidStellarAddress/ isValidSolanaAddress/validateAddressForChain stay sync for API compat. Note: webpack still inlines stellar-sdk into the main chunk because the SDK's index.ts barrel re-exports the sync isValidStellarAddress from the same entry graph — these changes reduce eager execution work but do not yet achieve real code-splitting. See conversation for the follow-up needed (subpath export or consumer splitChunks config).
…allback Three related races in the payId-mode source-token switch flow: 1. PayWithStellarToken/PayWithSolanaToken: the auto-transfer effect could fire handleTransfer twice in quick succession, and both calls passed the checkoutDoneRef guard before either finished (or Solana had no guard at all) — firing two checkout POSTs for the same payment. Replaced with a synchronously-claimed in-flight promise so the second call always awaits the first's result instead of racing it. 2. usePaymentState's payWithToken (EVM) trusted the closure-captured pay.order instead of a fresh store read. Combined with a memoized caller holding a stale reference, this let needRozoPayment evaluate against outdated order data intermittently. 3. needRozoPayment only compared chainId, so switching source tokens on the same chain (e.g. USDC Base -> USDT ETH) skipped checkout entirely and silently reused the stale order — worse than #2, since it never even created a payment. Now compares chain AND token address. Also fixed a key collision introduced by #1's EVM counterpart: the checkout dedup cache was keyed by payId alone, so switching source tokens mid-flow could return a cached result for the wrong token. Rekeyed to payId:chainId:tokenAddress.
shawnmuggle
left a comment
There was a problem hiding this comment.
Re-approval — incremental (f7211425…7cda1ff2) — no P0
Prior approval was dismissed by the new push. Scoped this pass to what landed since that SHA rather than re-reviewing the whole feat/stellar-wallet-ux diff.
What actually changed: beta → stable pin, nothing else
Six files, and every one is the release cut:
packages/connectkit/package.json—@rozoai/intent-pay0.1.38-beta.16→0.1.38packages/pay-common/package.json—@rozoai/intent-common0.1.26-beta.5→0.1.26examples/nextjs-app/package.json— both deps repinned to the stable versionspnpm-lock.yaml— specifier + resolution updates matching the aboveCHANGELOG.md(+57),bundle-analysis.html(build artifact)
Zero source changes. No .ts/.tsx touched in this range, so nothing in the signing or fund-routing path moved since the last review.
Verified against the registry
Both packages are actually published, and the lockfile isn't pointing at something that doesn't exist:
@rozoai/intent-pay dist-tags → { latest: '0.1.38', beta: '0.1.38-beta.16' }
Integrity hashes in pnpm-lock.yaml match the published tarballs byte for byte:
intent-pay@0.1.38→sha512-sJ6ALwejToIFOHyfXqYkOAIVvgbhk9HHqmoeZ69+BS6ChFWnYV2hCADvCv1FOjpVLTQsGzOQX6OJFtJqFKYc3w==✅intent-common@0.1.26→sha512-vnIGZK8KnFmiyACV856+Ac8yVWy3IDxKGzs1hB0p73bUOY7frFrvcndQPdqXEzEFWSVkmGdtqQvYtTj0b6bGNw==✅
Also worth noting latest now resolves to 0.1.38, so the pin and the published tag agree — no risk of consumers installing a version this repo never built.
On the red review check
review is failing, but not on this code:
The template is not valid. .github/workflows/ai-pr-review-v2.yml (Line: 120, Col: 20):
Error reading JToken from JsonReader. Path '', line 0, position 0.
That's the AI-review workflow failing to parse its own YAML — it dies before it ever looks at the diff. Infra bug in the review tooling, not a signal about this PR. Security scan, release, and Vercel all pass. Worth fixing separately since it will red-flag every PR in the repo until it is.
Verdict
Re-approving. The prior P1s (WalletConnect getAddress() reusing live[0], etc.) are unchanged and still non-blocking — this push didn't touch them and didn't introduce anything new.
Summary
Improve Stellar wallet UX, add Direct Settlement support, and fix a set of payment-race and bundle-size issues found while testing this flow. Approach: keep Stellar wallet selection always visible (extension or WalletConnect fallback), auto-route same-chain Stellar payments through a direct settlement path, and close several races where a stale order or a duplicated init could send a payment down the wrong path.
Changes Made
Stellar Wallet UX
StellarContextProviderand cache the kit synchronously — removes the loading spinner and remount flash on the Connect Stellar pageStellar Direct Settlement
intent: "stellar_direct"intentsent as a top-level field inPOST /payments, separate from the displaytitlesettlementMode: "stellar_direct"in the response: source IS destination, fee "0.00", direct sendintenttoCreateNewPaymentParams/CreatePaymentRequest,settlementModetoPaymentResponseRace Fixes (payId-mode source-token switching)
PayWithStellarToken/PayWithSolanaToken: the auto-transfer effect could firehandleTransfertwice before either finished (Solana had no guard at all), sending duplicate checkout POSTs for the same payment. Replaced with a synchronously-claimed in-flight promise so a second call awaits the first instead of racing it.usePaymentState.payWithToken(EVM) read a closure-capturedpay.orderinstead of a fresh store read, soneedRozoPaymentcould evaluate against stale order data.needRozoPaymentonly comparedchainId, so switching source tokens on the same chain (e.g. USDC Base → USDT ETH) skipped checkout and silently reused the stale order. Now compares chain and token address.payIdalone, so switching source tokens mid-flow could return a cached result for the wrong token. Rekeyed topayId:chainId:tokenAddress.WalletConnect Double-Init
WalletConnectModulewas creating two independent WalletConnect Cores (one viaSignClient.init(), one viacreateAppKit()'s internalUniversalProviderinit) — both always fired regardless ofmanualWCControl, which only affectedclose(). Deduped to a single Core.Bundle Size / Perf
@stellar/stellar-sdk(~14MB) was statically imported inStellarContextProvider,usePaymentState,PayWithStellarToken, and (viachainAddress.ts)RozoPayButtonitself, forcing it to parse/execute on every mount regardless of chain used. Converted to dynamicimport(), deferred to actual use (wallet connect, quote, tx submit). AddedvalidateAddressForChainAsyncforRozoPayButton's internal validation path; publicisValidStellarAddress/isValidSolanaAddress/validateAddressForChainstay sync for API compatibility.Other
RozoPayModal/routes.ts: registered routes for the Stellar connect flowConnectorStellar: useWALLET_CONNECT_IDconstant instead of a magic stringexamples/nextjs-appandpackages/connectkituseworkspace:*for local devcreatePaymentPayload,derivePayIdPreferredTokens,isDNTEnabled,walletconnect.moduleHow to Test
Wallet UX
Direct Settlement
intent: "stellar_direct"is sent and fee shows "0.00"intentsent, normal hub path usedRace fixes
Perf
pnpm buildinpackages/connectkit, confirmstellar-sdkchunk is not in the main render-critical bundleTesting Checklist
createPaymentPayload,derivePayIdPreferredTokens,isDNTEnabled,walletconnect.module)Reviewer Notes
stellar-sdkinto the main chunk because the SDK'sindex.tsbarrel re-exports the syncisValidStellarAddressfrom the same entry graph — this reduces eager execution work but doesn't yet achieve true code-splitting. A follow-up (subpath export or consumersplitChunksconfig) is still needed if full code-splitting is required.pay.order, chain-only comparison) were fixed together since they all surface as variants of the same "switch source token quickly" bug — worth a closer look atusePaymentState.tsand bothPayWithXTokencomponents together rather than in isolation.Related