feat: normalizeTokenAddress, dualChainConnect, useQuery migration#51
Conversation
- pay-common: chain-aware normalizeTokenAddress (EVM=lowercase, Solana/Stellar=pass-through); safe getChainName fallback - pay-common: tokensByChainAddr + getKnownToken use normalizeTokenAddress - connectkit: workspace:* dep + moduleResolution bundler (fixes ox types) - connectkit: migrate useWalletPaymentOptions, useSolanaPaymentOptions, useStellarPaymentOptions, useDepositAddressOptions to @tanstack/react-query; remove refreshUtils.ts - connectkit: useDepositAddressOptions enabled guard usdRequired > 0, appId param removed, fallback on error - connectkit: dualChainConnect state in PayContext/RozoPayProvider; ConnectorList sets flag on mobile dual-chain tap; RozoPayModal guards route to SELECT_METHOD instead of EVM SELECT_TOKEN - connectkit: trimTokenAmount applied in PaymentBreakdown + WaitingDepositAddress for native token display - connectkit: normalizeTokenAddress applied across validatePayoutToken, useTokenOptions, useDepositAddressOptions, useWalletPaymentOptions, Confirmation page token comparisons
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
AI review: 0 P0 / 4 P1 / 3 P2 — no blocking issues foundLabeled P0 (blocking)None. P1 (should fix)
P2 (nice to have)
ai-pr-review · claude-sonnet-5 |
shawnmuggle
left a comment
There was a problem hiding this comment.
AI review found no P0 blockers for a trusted Rozo contributor.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8dfbf278bc
ℹ️ 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".
Connect EVM directly via connect() instead of routing to ROUTES.CONNECT, removing the 'Connect Phantom' intermediate screen. RozoPayModal effect 2 now handles dualChainConnect=true at any route (not just CONNECT page), clearing the flag and routing to SELECT_METHOD to show both wallet tiles.
AI review: 0 P0 / 4 P1 / 3 P2 — no blocking issues foundLabeled P0 (blocking)None. P1 (should fix)
P2 (nice to have)
ai-pr-review · claude-sonnet-5 |
shawnmuggle
left a comment
There was a problem hiding this comment.
AI review found no P0 blockers for a trusted Rozo contributor.
…s loading state - SelectMethod: route directly to MOBILECONNECTORS on mobile (skip Connectors page); eagerly call hydrateOrder() on modal open so deeplink wallets have a payId before user taps - MobileConnectors: show injected wallets (connect directly) + deeplink wallets in one list immediately; pulse animation while order hydrates; taps silently ignored until orderReady - SelectMethod: show 'Pay to address' tile while depositAddressOptions loading (disabled + 'Loading...' subtitle); previously hidden until options arrived
…order - MobileConnectors: fix name/shortName cross-match so injected Phantom hides deeplink Phantom duplicate - MobileConnectors/styles: apply width/height/object-fit to img same as svg so injected wallet icons (Rabby etc) render at correct 60x60 - SelectMethod: exclude depositAddress from disabled-to-bottom sort so loading 'Pay to address' tile stays above Stellar
…ediate page MobileConnectors handleDeeplinkWallet: on iOS (and all platforms), open the wallet deeplink by programmatically clicking an <a> tag inside the direct user gesture context instead of routing to WAITING_WALLET page. Safari/WebKit allows <a> clicks from user gestures even when window.open is blocked. Closes modal immediately so user goes straight to wallet app. Also fix appId resolution from order.metadata to use proper type assertion for metadata Record<string, any> access. chore: version bumps, bundle analysis, lockfile
|
/ai-review |
window.doNotTrack access threw ReferenceError under Next.js SSR, returning HTTP 500 for the entire checkout page on invoice.rozo.ai when telemetry was enabled. Extract isDNTEnabled to a util and gate on typeof window === 'undefined' before touching window.doNotTrack. Adds a node:test regression check that fails on the pre-fix shape. Fixes invoice.rozo.ai SSR incident 2026-07-13.
Fires once per empty-render episode when SelectToken renders its 'No tokens available' state with a connected wallet. Re-arms after refresh or reconnect so a subsequent empty state re-captures. Properties: token_mode, connected_chains, preferred_token_symbols, preferred_token_count — enough to segment funnel drop-off by chain and by whether preferredTokens narrowed the eligible set.
- CHANGELOG: 0.1.36-beta.5 entry for both changes.
- ANALYTICS.md: add payment_no_tokens_available to event catalog.
- PROVIDER_SETUP.md: document the SSR isDNTEnabled fix in Common
Mistakes, note that telemetry={false} workaround is no longer needed
on 0.1.36+.
- 06-wallet-stellar.md: expand section 3 to explain the two safety gates (NEXT_PUBLIC_E2E build flag + window.__E2E_STELLAR_SECRET__ runtime injection), match the real providers.tsx wiring, and note that the provider deletes the secret from window after reading. Add Notes entry pointing at the Playwright webServer command. - .env.e2e.example: clarify that Playwright starts the dev server with NEXT_PUBLIC_E2E=1 automatically \u2014 no manual env var to set.
Review summaryReviewed the full diff (connectkit + pay-common) with two independent passes plus manual verification of the payment-critical paths. No P0 / blocking issues. The token/chain data changes are safe: zero token address/decimals/symbol value changes, zero tokens added or removed, zero exported API removed — the ~3k-line churn in Please confirm (behavior change, no changelog)
P2 (non-blocking)
Verified clean (no bug)
|
shawnmuggle
left a comment
There was a problem hiding this comment.
Approved — no P0/blocking issues. Findings are non-blocking (P2) plus one behavior change to confirm (default-appId payment options gate); details in the review summary comment above.
Previous gate skipped useQuery when appId was absent or equal to
DEFAULT_ROZO_APP_ID ("rozoIntentPay"), so any integration that didn't
pass an appId (or used the default) rendered a connected wallet with
zero payable tokens and fired payment_no_tokens_available immediately.
Restore the pre-migration behavior: fall back to DEFAULT_ROZO_APP_ID
(same fallback paymentEffects/createPaymentPayload/usePaymentState
already use downstream) and drop the DEFAULT-app-id short-circuit
from the enabled clause. Balance queries now run consistently across
wallet/solana/stellar hooks regardless of appId config.
shawnmuggle
left a comment
There was a problem hiding this comment.
Re-approved after incremental review of d9b6566...fbf7c91.
The default-appId gate I flagged is correctly fixed across all three hooks (useWalletPaymentOptions, useSolanaPaymentOptions, useStellarPaymentOptions): the appId !== DEFAULT_ROZO_APP_ID condition is removed from enabled, stableAppId restores the ?? DEFAULT_ROZO_APP_ID fallback, and the non-null assertion on appId in queryFn is dropped — queryKey and request param stay in sync, no stale-cache risk. The new SelectToken PAYMENT_NO_TOKENS_AVAILABLE analytics is self-contained (fires once per empty-render episode, re-arms on state flip), no payment-path side effects. Versions finalized beta→release (intent-pay 0.1.36 / intent-common 0.1.23), example app no longer depends on beta SDKs. No new blocking issues; remaining items are the previously-noted non-blocking P2s.
Summary
Backport and extension of improvements from PR #47 onto master, plus new mobile UX and iOS deeplink improvements.
1. Chain-aware token address normalization (
normalizeTokenAddress)EVM token addresses are case-insensitive (EIP-55 checksummed). Solana (base58) and Stellar (StrKey) are case-sensitive — lowercasing corrupts them. All previous code used
.toLowerCase()everywhere, breaking lookups for native Solana/Stellar tokens.Changes:
pay-common/src/token.ts: newnormalizeTokenAddress(chainId, address)— EVM lowercased, Solana/Stellar pass-throughtokensByChainAddrmap andgetKnownTokenusenormalizeTokenAddressgetChainNamewraps in try/catch, returns"Chain {id}"instead of throwingvalidatePayoutToken,useTokenOptions,useWalletPaymentOptions,useDepositAddressOptions,useSolanaPaymentOptions,useStellarPaymentOptions,ConfirmationpagemoduleResolution: node → bundlerin connectkit tsconfig (resolvesoxexports map, removes false build warning)@rozoai/intent-commondep switched toworkspace:*2.
useQuerymigration for payment option hooksAll four hooks replace a custom
refreshUtilsstate machine with@tanstack/react-queryuseQuery.refreshUtils.tsdeleted.Hooks migrated:
useWalletPaymentOptions,useSolanaPaymentOptions,useStellarPaymentOptions,useDepositAddressOptionsShared config:
staleTime: 30_000,retry: 1,refetchOnWindowFocus: falseKey improvements:
useDepositAddressOptions:enabledguard requiresusdRequired > 0(prevents premature query before order loads);appIdparam removed; error path returnsfallbackOptionsso UI never goes blankuseTokenOptions:refreshPromisestypedPromise<unknown>[]to matchrefetchreturn type3. Dual-chain wallet connect (
dualChainConnect)Phantom mobile injects both EVM and Solana simultaneously. Old code auto-routed to EVM
SELECT_TOKENthe momentisEthConnectedfired — no path to Solana tokens.Changes:
PayContext/RozoPayProvider: newdualChainConnect: booleanstateConnectorList: mobile dual-chain path callsconnect()directly (no intermediateConnectWithInjectorpage) +setDualChainConnect(true)+solanaWallets.select()simultaneouslyRozoPayModal: whenisEthConnectedfires withdualChainConnect=true, routes toSELECT_METHOD(shows both wallet tiles) instead ofSELECT_TOKENResult: Tap Phantom → both chains connect silently → land on
SELECT_METHODwith EVM + Solana tiles.4.
trimTokenAmountStrips trailing zeros from decimal token amount strings (
"0.01600" → "0.016"). Applied in:PaymentBreakdown: fee + total displayWaitingDepositAddress: fee row + "Send Exactly" (replacesformatAmountWithTokenSymbolwhich rounded to 2dp — wrong for native tokens like0.016885 SOL)5. Mobile wallet chooser UX overhaul
Problem: "Pay with wallet" on mobile required 3 steps: tile → Connectors page (pulse skeleton waiting for hydration) → tap "Other" → Choose Wallet page → tap wallet → deeplink.
Changes:
SelectMethod: "Pay with wallet" routes directly toROUTES.MOBILECONNECTORSon mobile (skips Connectors page entirely)SelectMethod: eagerly callshydrateOrder()on modal open (gated onmodalOpen && isMobile) so deeplink wallets have apayIdready before user tapsMobileConnectors: rewritten — shows deeplink wallets alphabetically; renders immediately; pulse animation while order hydrates; taps silently ignored untilorderReadyMobileConnectors: dedup injected vs deeplink wallets by cross-matchingname/shortNameso Phantom only appears onceMobileConnectors/styles:imgelements getwidth: 100%; height: 100%; object-fit: cover(same assvg) so injected wallet icons (Rabby etc) render at correct 60x60SelectMethod: "Pay to address" tile now shows whiledepositAddressOptions.loading(disabled + "Loading..." subtitle) instead of being hidden until options arriveSelectMethod: "Pay to address" stays in position (above Stellar) while loading — excluded from disabled-to-bottom sort6. iOS deeplink — skip WAITING_WALLET intermediate page
Problem: On iOS Safari/WebKit,
window.open()from JavaScript is blocked unless triggered by a direct user gesture. The SDK used to route toWAITING_WALLETpage with a "Continue in Phantom" button — one extra tap.Fix:
MobileConnectorsnow opens the wallet deeplink by programmatically creating and clicking an<a>tag inside the direct user gesture context (the wallet tap). Safari/WebKit allows<a>tag clicks from user gestures even whenwindow.openis blocked. Modal closes immediately — user goes straight to the wallet app.Result (iOS): Tap Phantom → deeplink opens directly, no intermediate page.
Result (Android): Same —
window.openworks, modal closes, user goes to wallet app.Tested
tsc --noEmit: 0 errors (connectkit + pay-common)rollupbuild: clean, no warningspnpm lint: passes