Merge master into qa to clear the release PR's conflicts - #2614
Merged
Merged
Conversation
Reads the policy ID from EXPO_PUBLIC_PIMLICO_SPONSORSHIP_POLICY_ID and passes it as paymasterContext on the smart account client, so Pimlico applies it in pm_sponsorUserOperation. Unset means no policy is sent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ship-policy feat: attach a Pimlico sponsorship policy to every userop
…o v2 The Fuse v2 module was redeployed on 2026-09-24 (0xE2d4…7b2B → 0xa98f…A999), but a shipped build kept the old address and moved Safes onto the retired core. The backend reads only the new module, so those cards decline every payment with SAFE_NOT_REGISTERED, while the old lens still tells the app they are set up. Once a build has the new address, those Safes show the enable-spending banner. The set-up and mode-switch batches now also disable any retired v2 core still on the Safe. Disables are computed against the module list as each earlier call leaves it, so v1 and the retired core can come off in one batch without GS103. Retired cores are listed in EXPO_PUBLIC_RETIRED_CASH_MODULE_V2_ADDRESSES, which defaults to 0xE2d4… and never includes the live core. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ule-v2 fix(card-spend): disable the retired v2 module when moving a Safe onto v2
65138da made card set-up read the Safe's module list on every v2 set-up, to disable any retired v2 core. A new cardholder's Safe is often still counterfactual on Fuse — the set-up batch is what deploys it — so `getModulesPaginated` returns `0x`, viem throws, and activation fails with "Card not activated" before anything is signed. A Safe with no code has no modules, so there is nothing to disable: return an empty cleanup and let the batch deploy and register the Safe as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…module-read fix(card-spend): skip the module read on a Safe not yet deployed on Fuse
… exists buildCardSteps fell back to the bridge.xyz "cards" endorsement for every non-Rain issuer, even when /cards/status already reported a kycStatus. For an old Bridge customer applying for a Wirex card, an approved Bridge endorsement marked the KYC step complete and offered "Activate card" before they had verified with Sumsub. The endorsement now only applies to Bridge-only users, who have no card customer and so no kycStatus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmSmmwX1kxkmMBEP7pVKPG
fix(card): ignore the retired Bridge endorsement once a card customer exists
…quota Production has been getting 429s with "Your payg app has exceeded its limit of 10000 token_price requests per 1 hours" (Sentry SOLID-PJ, SOLID-DV). That quota counts requests, and one request can carry 25 tokens, but every price was its own request and nothing was reused: - useBalances refreshed every 5s, and each refresh sent five by-symbol GETs (ETH once per chain, FUSE, BNB) plus one per token still unpriced. That is about 3,600 requests an hour for a single open app, so two or three users used up the whole app's quota. - fusePriceUsd / ethPriceUsd polled every 5s on top of that. Changes: - lib/batchedLoader merges lookups started in the same tick into requests of up to 25. It caches prices for a minute (misses for five), keeps serving the last price while a refresh fails, and pauses both Prices endpoints after a 429. - fetchTokenPriceUsd and the new fetchTokenPricesBySymbol go through it. By-address requests stay within the documented limits of 25 addresses and 3 networks. - Balance fallback poll goes from 5s to 30s (SSE already invalidates on every balance event), native price refresh from 5s to 60s, and vault balance poll from 3s to 30s. - publicClient keeps one client per chain with Multicall3 batching, so reads made together share one eth_call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq
Remove the iOS-only gates so Swap is available on every platform: the SwapModal trigger and SwapModalProvider no longer return null on iOS, the home Swap pill shows, and the Swaps fee row, Swap earning method and wallet tooltip copy are no longer hidden on iOS. Geo-restriction and disclaimer gates are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtULm55FhtV8QpE98RAuqC
Enable Swap functionality on iOS
The vault balance card set its 26px value on a 24px line box and the
rewards summary set its 26px value on a 26px one; iOS clips glyphs that
overflow the line box, cutting the tops of the digits and the `$`.
Both values now get a 32px line, with the vault card's margins trimmed
so its layout height is unchanged.
The vault card also relied on Intl compact notation, which Hermes on iOS
ignores, so balances showed in full ("6,759.16 USD") and wrapped inside
the fixed-height card. The compact figure is now formatted by hand, the
value stays on one line and shrinks to fit on native, and the card uses
a min height so wrapped text on web is no longer cut off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FB6mLN2hZfRTDENZCHKhw
…o Alchemy Prices now come from POST /accounts/v1/prices, which serves one cache shared by every user and every pod. So the app key's Alchemy token_price quota no longer scales with the number of open apps. The backend is only an optimisation for the app. On any failure it asks Alchemy directly with its own key, exactly as before, and skips the backend for five minutes. Failures include a network error, a timeout, an older backend without the route (404), an expired session (401), the per-user rate limit (429), a token the backend can't price right now (503), or a reply that isn't the expected shape. That keeps every combination safe: - an app build or OTA that ships before the backend route is deployed - a backend rollback - web and native (cookie or Bearer auth) Old builds keep calling Alchemy directly. Symbols and addresses the route's validation would reject are left out of the request, so one malformed token can't fail the lookup for the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq
Mona Sans has a 1.41em natural line (1.09em ascent, 0.32em descent). When a lineHeight is shorter, iOS keeps the full descent and trims the top of the first line, while Android trims both edges evenly, cutting the bottom of the last line; both then clip the glyphs to the view. Web does neither, which is why only the native apps showed it. The vault card's exact-USD line gets the same ~1.15em headroom as the balance figure (its `$` sat within a pixel of the edge), and the rewards card and its value row use min heights so larger system font sizes, common on Android, grow the card instead of clipping the text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013FB6mLN2hZfRTDENZCHKhw
…fixes-auys1s Fix vault balance formatting and layout on iOS
The backend's price route leaves out tokens on chains it has no Alchemy network for, and the app takes a token left out as having no price and remembers that for five minutes. A chain added to ALCHEMY_NETWORKS (which an OTA can do) before the backend knows it would lose its prices that way. Address lookups are now chunked by whether the backend covers the chain (BACKEND_PRICE_CHAIN_IDS, mirroring its ALCHEMY_PRICE_NETWORKS), and tokens on any other chain are priced from Alchemy directly, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq
… polls The slower polls were there to save the Prices API quota. Price lookups are now cached for a minute, in the app and by the backend's shared cache, so polling more often no longer costs token_price requests, and the slower intervals only made balance updates visibly lag. Balances poll every 5s again (staleTime 5s), native-token prices every 5s, and vault balances every 3s, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq
…mit-emails-k9buga Batch and cache Alchemy price API requests to reduce quota usage
The deposit address screens quoted a flat 0.03% everywhere, so a rate an admin set for one route and chain on the Deposit fees page never reached the app. The notice now asks /deposit/fee-quote, which runs the same assessment the deposit workflows charge with and answers 0.03% for any route and chain with no rate of its own. The client's route table still decides which deposits can be charged at all, so an address that is never bridged is never asked about. While the quote loads the line is left off; if it cannot be fetched the notice falls back to 0.03% rather than to free. Rates move to parts per million, as the backend quotes them, so a fractional basis point prints exactly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dation A JS number cannot hold 18 decimals, so a soETH balance of 0.361164894291325699 became 0.3611648942913257. Max filled that in, it passed the balance check, and the withdrawal reverted for asking more than the Safe holds. The soFUSE and soETH balance hooks now return wei, and the form validates and fills max in wei. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ce images Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…m-backend feat(deposit): quote the deposit fee the backend charges for each route
The recovery screen opened on an empty email field. The user this came from reached it twice on a phone, sat on that field both times, and left without typing anything — then told support the recovery interface would not open. Their own screen recording shows it opening fine. Prefill it from the account this device last knew about. Logging out and a session expiring both leave that row behind — only "Forget all users" clears it — so on the device someone is locked out of, it is almost always the account they are reaching for. The field now takes a username as well, for the roughly one account in ten that carries no email and for anyone who cannot remember which inbox they used. Validation only holds an entry to the email rules once it contains an "@"; which account it names is the server's call. The OTP step then says where the code actually went, from the masked hint the backend returns, because a username recovery never learns the address — and for the same reason it no longer sends one back on verify. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRh59soi7iX4J3UwxVVGRS
…w-7gma44 feat(recovery): prefill the identifier, and accept a username
…-new Refresh wallet activity and improve cashback and card interactions
Two reported copy bugs, both cases of a surface naming a figure it was not actually showing. The spend-mode sheet labelled its Cash figure "Your USDC balance", but the number behind it is `useCardSpendableBalanceUSD`, which sums every asset in `CARD_SPENDABLE_ASSETS` — USDC, USDT and soUSD on Fuse. A cardholder funded in USDT read their own balance as somebody else's. The label, the caption, the help slide and the empty-state segment now say USD (and "Add funds") rather than naming one of the three. The tier comparison screen printed its own yield boost constants, and they had drifted from the backend's: the rewards tab's "+N% Yield Boost" card reads `yieldBoostPercentage` off the API and showed Ultra's 5%, while the stats band and perk row on the tier detail still said 3%. The rewards service's table is Core 0 / Prime 2 / Ultra 5, so 5% was the true figure. Added `lib/tierYieldBoost.ts`, which resolves the rate from the tier-benefits endpoint and falls back to a copy of that table only while the request is in flight or against a backend that does not send the field — a live 0 is taken as an answer, so a boost switched off in admin config stops being advertised. The tier detail screen and the "Join Prime/Ultra Club" teaser both read through it, substituting by label rather than by index so the columns cannot be relabelled if a tier gains a benefit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S4gKxuxYsKWtWKUF5Lo6Dm
…nhwni Unify yield boost rates across rewards screens
Three files conflicted between the qa release branch and master: - components/PageLayout.tsx: both sides added props. Kept both — qa's showsVerticalScrollIndicator/sidebarTopGutter and master's onRefresh/refreshing/refreshIndicatorOffset. PullToRefreshScrollView already accepts all of them. - lib/assets.ts: the generated registry gained entries on both sides. Took the union, in the generator's sort order (upgrade-card-* from master before qa's rewards-tiers/v4/*). Verified against the files on disk: every entry resolves, hashes match, no duplicates. - components/Rewards/NewRewards/RewardsBenefitsScreenNew.tsx: qa's tier redesign rewrote this screen and moved its copy into tierBenefitsPresentation, which already resolves the live yield boost (and its balance cap) from the tier-benefits endpoint. Master's edit did the same job against the old TIER_CONTENT table, so it is superseded — kept qa's version. Master's companion change to JoinTierClubCard merged cleanly and is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JFyKA1v52WcPWPETeN2dfR
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Review scope:
Notes:
|
TIER_YIELD_BOOST_RATES had Ultra at 5, citing a `TIER_YIELD_BOOST_PCT` table in the accounts service's rewards.service.ts. No such table exists. The boost the backend actually pays comes from YIELD_BOOST_DEFAULTS in app-config.constants.ts, read through yieldBoostForTier and grantedBoostPercentage: tier2 0.02 and tier3 0.03, so Prime 2 and Ultra 3. The figure is only a fallback — the tier-benefits endpoint wins whenever it has answered — but it is the one the rewards tab's JoinTierClubCard shows while that request is in flight, so Ultra was being teased as "+5% Yield boost" next to a tier detail screen quoting "+3%", and neither matched what accrues. Also point tierBenefitsPresentation's fallback at the same table instead of its own 2/3 literals. Two tables are how these drifted apart; the numbers were already equal, so nothing rendered changes. The display strings in TIER_PRESENTATION stay literal and stay guarded by the existing `tierPresentationContent(tier) === TIER_PRESENTATION[tier]` test, which now fails loudly if the table and the copy disagree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JFyKA1v52WcPWPETeN2dfR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Solid-Money/solid-ui#2598 (
qa→master) isdirty: master moved on and three files conflict. This branch isqawithmastermerged into it and those conflicts resolved, so merging it intoqamakes #2598 mergeable again. Everything else in the diff againstqais master's own work coming across unchanged — this PR adds no new feature work of its own.The three conflicts
components/PageLayout.tsx— both sides added props at the same three spots. Kept both: qa'sshowsVerticalScrollIndicator/sidebarTopGutterand master'sonRefresh/refreshing/refreshIndicatorOffset.PullToRefreshScrollViewalready accepts all of them.lib/assets.ts— the generated registry gained entries on both sides. Took the union, in the generator's sort order.components/Rewards/NewRewards/RewardsBenefitsScreenNew.tsx— qa's tier redesign rewrote this screen and moved its copy intotierBenefitsPresentation.ts, which already resolves the live yield boost and its balance cap from the tier-benefits endpoint. Master's conflicting edit did the same job against the oldTIER_CONTENTtable, so it is superseded — kept qa's version. Master's companion change toJoinTierClubCard.tsxmerged cleanly and is untouched.Validation
npx jest— 1684 tests pass, 149/150 suites. The one failing suite (CardFundOptions) fails identically on qa's head before this merge:react-native-mmkvneeds a native binary the CI-less sandbox doesn't have.npx tsc --noEmit— 9 errors, every one in a file byte-identical in both parents, so untouched by this merge. None in the three resolved files.npx expo lint— 8 errors, all prettier / import-sort in files likewise identical in both parents, and the lint job runs--fix.lib/assets.tscross-checked against the files on disk: every entry resolves, hashes match, sorted, no duplicates.postinstallthen regenerated it and produced only insertions — no change to any entry from the conflict, confirming the union was exact. Those insertions are assets both branches had already left unregistered, so they are not included here; install regenerates them.Worth a look, not changed here
lib/tierYieldBoost.tson master setsTIER_YIELD_BOOST_RATES[ULTRA] = 5, while the backend default (YIELD_BOOST_DEFAULTS.tier3.apy= 0.03) and qa's redesign fallback both say 3%. After this mergeJoinTierClubCardadvertises "+5% Yield boost" as its pre-request fallback while the tier detail screen says "+3%". Pre-existing on master, so left alone — happy to align it if wanted.🤖 Generated with Claude Code
https://claude.ai/code/session_01JFyKA1v52WcPWPETeN2dfR