feat: pay collection publishing fee with shop credits - #3436
feat: pay collection publishing fee with shop credits#3436decentraland-bot wants to merge 8 commits into
Conversation
Add SHOP_CREDITS payment method alongside existing MANA and FIAT options. The saga calls useShopCreditsCollectionManager from decentraland-dapps (Phase 2) which handles the authorize-publication flow with the credits-server. UI shows a "Pay with Shop Credits" button gated by feature flag, with top-up CTA when balance is insufficient. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Coverage Report for CI Build 30379952735Coverage decreased (-0.05%) to 52.354%Details
Uncovered Changes
Coverage Regressions1 previously-covered line in 1 file lost coverage.
Coverage Stats💛 - Coveralls |
decentraland-bot
left a comment
There was a problem hiding this comment.
Code Review: feat: pay collection publishing fee with shop credits
PR: #3436
Branch: feat/shop-credits-publish-fee
Files changed: 18 (+652 / -170)
Git conventions (ADR-6): ✅ PR title and branch name follow semantic commit format.
Findings Summary
- P0 — Blocker: 0
- P1 — Major: 0
- P2 — Minor: 8
Verdict: APPROVE — no blocking issues found. The P2 suggestions below are improvements to consider but not merge blockers.
(Note: unable to submit formal approval since the PR was authored by the same bot account. This review carries an approve recommendation.)
P2 — Minor
1. [DRY] Duplicated WEI-to-cents conversion
sagas.tsandPayPublicationFeeStep.tsxboth contain:Math.ceil(Number(ethers.utils.formatEther(totalPriceUSD)) * 100)- If this formula ever changes in one place but not the other, the UI will show a different credit cost than what the saga actually charges. Consider extracting a shared utility (e.g.
weiToUsdCents()inmodules/collection/utils.ts).
2. [Coupling] Hardcoded USD_CENTS_PER_CREDIT = 10
- File:
PayPublicationFeeStep.tsx:20 - This business-critical constant determines how much real money a user spends. If the credits-server changes the ratio, the frontend will silently show wrong prices. Consider fetching it from the credits API response or moving to environment config.
3. [Side-effect] Module-scope config.get() calls
- File:
PayPublicationFeeStep.tsx:22-23 const getManaUrl = config.get('ACCOUNT_URL', '')andconst getCreditsUrl = ...execute at import time. If config isn't initialized yet, they return empty strings permanently. Move inside the component or useuseMemo. Also,getManaUrl/getCreditsUrluse agetprefix suggesting a function, but they're string constants — consider renaming tomanaUrl/creditsUrl.
4. [UX] No default payment method selected
- File:
PayPublicationFeeStep.tsx:60 useState<PaymentMethod | null>(null)means the Confirm button is disabled on first render with no visual prompt explaining why. Consider defaulting toPaymentMethod.MANA(the original default) or adding a "Select a payment method" hint.
5. [Dead code] Unused translation keys
pay_shop_credits_with_countis defined inen.json,es.json, andzh.jsonbut is not referenced in any component code in this diff. Remove if unused, or add a comment if it will be used by a dependent PR.
6. [Scope] Unrelated changes bundled
builder.tsquery-string undefined-filtering fix andes.jsontypo corrections ("collección" → "colección") are good fixes but unrelated to shop credits. Consider extracting to separate commits for cleangit blame.
7. [Robustness] Shop credits saga error handling
- File:
sagas.ts:493-515 - The MANA branch handles specific error codes (4001 for user rejection, etc.) and the FIAT branch has Wert-specific handling. The shop credits branch relies only on the outer generic catch. If
useShopCreditsCollectionManagerfails (credits-server rejects, identity expired), users get a generic error toast. Consider adding specific error handling for common failure modes.
8. [Clarity] Magic '0' value in handleBuyWithShopCredits
- File:
PayPublicationFeeStep.tsx:218 onNextStep(PaymentMethod.SHOP_CREDITS, '0')— the'0'is a sentinel meaning "no MANA needed." This works becausePublishWizardCollectionModal.tsxcheckspaymentMethod === PaymentMethod.SHOP_CREDITSbefore checking the amount. A brief inline comment would clarify the intent.
Security Review
No security issues found.
- ✅ No hardcoded secrets or credentials
- ✅ Identity handled via ADR-44 signed-fetch (server-side validation)
- ✅ Balance check is client-side for UX only; actual credit deduction is server-side via
useShopCreditsCollectionManager - ✅ Feature flag properly gates the new payment method
- ✅ URLs come from environment config, no user input in URL construction
- ✅ No XSS vectors — all dynamic content is React-rendered (auto-escaped)
CI Status
- ✅ Tests: passing
- ✅ Audit: passing
- ❌ Vercel deployment: failed — investigate before merge (
npx vercel inspect dpl_6xMWCUj2uQMae3jpkGxTUnkNbE5V --logs)
Consumer Impact
Changes are internal to the Builder frontend. No public API surface, exported packages, or shared schemas are modified. PaymentMethod enum and publishCollectionRequest action are Builder-internal. No downstream consumer impact.
What's done well
- Clean feature-flag gating separates rollout risk from code risk
- Reusing the existing credits API response (
usd.credits) avoids a new endpoint - Marketplace credits toggle correctly restricted to MANA/Card (not Shop Credits)
- Proper MANA authorization bypass for shop credits flow
- Good i18n coverage across EN, ES, ZH
- Spanish typo fixes are good housekeeping
builder.tsundefined-param fix prevents real query-string bugs
Reviewed by Jarvis 🤖 · Requested by Rocío Corral Mena (<@U09E2DJQADU>) via Slack
| import { getBackgroundStyle } from 'modules/item/utils' | ||
| import creditsIcon from 'icons/credits.svg' | ||
|
|
||
| const USD_CENTS_PER_CREDIT = 10 |
There was a problem hiding this comment.
[P2] USD_CENTS_PER_CREDIT = 10 is a business-critical constant. If the credits-server ever changes the ratio, this will silently show wrong prices. Consider fetching from the credits API response or moving to environment config.
|
|
||
| const USD_CENTS_PER_CREDIT = 10 | ||
|
|
||
| const getManaUrl = config.get('ACCOUNT_URL', '') |
There was a problem hiding this comment.
[P2] getManaUrl and getCreditsUrl execute config.get() at module load time (import-time side effect). If config isn't initialized yet, these are empty strings forever. Also, the get prefix suggests functions but these are string constants — consider renaming to manaUrl / creditsUrl and moving inside the component or into a useMemo.
| if (!creditsServerUrl) { | ||
| throw new Error('Missing CREDITS_SERVER_URL configuration') | ||
| } | ||
|
|
There was a problem hiding this comment.
[P2] This WEI-to-cents conversion (Math.ceil(Number(ethers.utils.formatEther(totalPriceUSD)) * 100)) is duplicated in PayPublicationFeeStep.tsx for the shopCreditsNeeded calculation. If the formula changes in one place but not the other, the UI will show a different credit cost than what the saga charges. Consider extracting a shared weiToUsdCents() utility.
- Extract shared weiToUsdCents() and shopCreditsNeededForPrice() into collection/utils.ts to eliminate duplication between saga and component - Move config.get() calls from module scope into component via useMemo, rename getManaUrl/getCreditsUrl to manaUrl/creditsUrl - Add specific error handling for user rejection (code 4001) in shop credits saga branch - Remove unused pay_shop_credits_with_count translation key from en/es/zh - Add comment explaining the '0' sentinel in handleBuyWithShopCredits Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
SHOP_CREDITSas a newPaymentMethodenum value alongsideMANAandFIAThandlePublishCollectionRequestthat callsuseShopCreditsCollectionManagerfromdecentraland-dapps(PR fix: scene not working properly when adding some specific builder assets. #809), converting USD WEI tousdPriceCentsand obtaining identity for ADR-44 signed-fetchshop-credits-for-collections-feefeature flag, showing credit cost and user balance (fromcredits?.usd?.credits), with top-up CTA linking to the shop/creditspage when balance is insufficienttotalPriceUSDthreaded through action payload (Option A) for the saga's WEI-to-cents conversionAdditional fix
undefinedquery parameter values being serialized as the literal string"undefined"inbuilder.ts, causing the API to return no results. The fix filters outundefinedvalues before passing params to the request.Dependencies
decentraland-dappsPR fix: scene not working properly when adding some specific builder assets. #809 (Phase 2 —useShopCreditsCollectionManager)credits-serverPR fix: close adblock modal after continuing #526 (Phase 1 —POST /credits/authorize-publication)shop-credits-for-collections-feein the Builder applicationShop credits research findings applied
USD_CENTS_PER_CREDIT = 10(1 credit = $0.10), consistent withcredits-serverandshopreposGET /users/:address/creditsendpoint —usd.creditsfield (no new endpoint needed)MARKETPLACE_WEB_URL/credits(shop's Stripe checkout page)Test plan
shop-credits-for-collections-feegates the "Pay with Shop Credits" buttonSHOP_CREDITSsaga branchusdPriceCentsand passes touseShopCreditsCollectionManagerScreenshots
Pay with shop credits:
Screen.Recording.2026-07-28.at.2.11.38.PM.mp4
Pay with MANA (insufficient balance):

Pay with shop credits (insufficient balance):

Pay with MANA + Marketplace credits FF enabled:

Pay with card + Marketplace credits FF enabled:

Shop credits FF disabled:

🤖 Generated with Claude Code