Skip to content

feat: pay collection publishing fee with shop credits - #3436

Open
decentraland-bot wants to merge 8 commits into
masterfrom
feat/shop-credits-publish-fee
Open

feat: pay collection publishing fee with shop credits#3436
decentraland-bot wants to merge 8 commits into
masterfrom
feat/shop-credits-publish-fee

Conversation

@decentraland-bot

@decentraland-bot decentraland-bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds SHOP_CREDITS as a new PaymentMethod enum value alongside MANA and FIAT
  • New saga branch in handlePublishCollectionRequest that calls useShopCreditsCollectionManager from decentraland-dapps (PR fix: scene not working properly when adding some specific builder assets. #809), converting USD WEI to usdPriceCents and obtaining identity for ADR-44 signed-fetch
  • "Pay with Shop Credits" button in the payment step UI, gated by shop-credits-for-collections-fee feature flag, showing credit cost and user balance (from credits?.usd?.credits), with top-up CTA linking to the shop /credits page when balance is insufficient
  • Shop credits bypass MANA authorization entirely — no allowance approval step needed
  • totalPriceUSD threaded through action payload (Option A) for the saga's WEI-to-cents conversion

Additional fix

  • Fixes an issue where refreshing the collection detail page led to an empty collection items list. The root cause was undefined query parameter values being serialized as the literal string "undefined" in builder.ts, causing the API to return no results. The fix filters out undefined values before passing params to the request.

Dependencies

Shop credits research findings applied

  • Credits-to-USD ratio: USD_CENTS_PER_CREDIT = 10 (1 credit = $0.10), consistent with credits-server and shop repos
  • Balance from existing GET /users/:address/credits endpoint — usd.credits field (no new endpoint needed)
  • Top-up links to MARKETPLACE_WEB_URL/credits (shop's Stripe checkout page)

Test plan

  • Verify feature flag shop-credits-for-collections-fee gates the "Pay with Shop Credits" button
  • With sufficient shop credits: button enabled, clicking triggers SHOP_CREDITS saga branch
  • With insufficient shop credits: button disabled, "Top up" link visible and points to credits page
  • Marketplace credits toggle still works independently for MANA path
  • Third-party collections do not show the shop credits button
  • MANA authorization step is skipped when paying with shop credits
  • Saga correctly converts USD WEI to usdPriceCents and passes to useShopCreditsCollectionManager
  • Refreshing the collection detail page still shows the collection items (builder.ts fix)

Screenshots

Pay with shop credits:

Screen.Recording.2026-07-28.at.2.11.38.PM.mp4
image

Pay with MANA (insufficient balance):
image

Pay with shop credits (insufficient balance):
image

Pay with MANA + Marketplace credits FF enabled:
image

Pay with card + Marketplace credits FF enabled:
image

Shop credits FF disabled:
image

🤖 Generated with Claude Code

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>
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
builder Error Error Jul 28, 2026 5:31pm

Request Review

@coveralls

coveralls commented Jul 24, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 30379952735

Coverage decreased (-0.05%) to 52.354%

Details

  • Coverage decreased (-0.05%) from the base build.
  • Patch coverage: 13 uncovered changes across 3 files (5 of 18 lines covered, 27.78%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
src/modules/collection/sagas.ts 11 3 27.27%
src/modules/features/selectors.ts 4 1 25.0%
src/lib/api/builder.ts 2 0 0.0%
Total (4 files) 18 5 27.78%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
src/lib/api/builder.ts 1 15.98%

Coverage Stats

Coverage Status
Relevant Lines: 12733
Covered Lines: 7281
Line Coverage: 57.18%
Relevant Branches: 5577
Covered Branches: 2305
Branch Coverage: 41.33%
Branches in Coverage %: Yes
Coverage Strength: 34.38 hits per line

💛 - Coveralls

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts and PayPublicationFeeStep.tsx both 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() in modules/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', '') and const getCreditsUrl = ... execute at import time. If config isn't initialized yet, they return empty strings permanently. Move inside the component or use useMemo. Also, getManaUrl / getCreditsUrl use a get prefix suggesting a function, but they're string constants — consider renaming to manaUrl / 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 to PaymentMethod.MANA (the original default) or adding a "Select a payment method" hint.

5. [Dead code] Unused translation keys

  • pay_shop_credits_with_count is defined in en.json, es.json, and zh.json but 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.ts query-string undefined-filtering fix and es.json typo corrections ("collección" → "colección") are good fixes but unrelated to shop credits. Consider extracting to separate commits for clean git 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 useShopCreditsCollectionManager fails (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 because PublishWizardCollectionModal.tsx checks paymentMethod === PaymentMethod.SHOP_CREDITS before 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.ts undefined-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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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', '')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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')
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants