Skip to content

feat: new credits topup#9439

Merged
davidejensen merged 7 commits into
devfrom
feat/buy-credits-stripe
Jul 23, 2026
Merged

feat: new credits topup#9439
davidejensen merged 7 commits into
devfrom
feat/buy-credits-stripe

Conversation

@davidejensen

@davidejensen davidejensen commented Jul 20, 2026

Copy link
Copy Markdown
Member

Pull Request Description

What does this PR change?

Fix #9438
This PR adds the credits topup functionality in the client. It will be enabled only in .zone env and it's all behind feature flag.
Credits topup allow users to buy more credits from the + icon in the credits balance or when buying an item and not having enough credits.

Test Instructions

IMPORTANT NOTE: the UI is temporary, being the feature disabled we don't care about the UI aspect at the moment, as it's awaiting the design, do not report UI related bugs.

Test Steps

Check that nothing changes in .org environment

  1. Launch the client
  2. Open the explore panel
  3. Verify that in the top right no credits balance is shown

Check that the new feature works in .zone environment

  1. Launch the client with dclenv=zone
  2. Open the explore panel
  3. In the top right verify that next to the credits balance there is a "+" icon
  4. Click on it
  5. Verify that a popup shows the different credits packs to buy
  6. Select one
  7. Verify it opens a browser page
  8. Complete the transaction by writing random stuff, make sure you use a card with the following number "4242 4242 4242 4242"
  9. Once you complete the transaction go back to the client
  10. Verify that the transaction completed
  11. Verify that the balance updated

Additional Testing Notes

  • Note any edge cases to verify
  • Mention specific areas that need careful testing
  • List known limitations or potential issues

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered
  • For SDK features: Test scene is included

Code Review Reference

Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.

@davidejensen davidejensen self-assigned this Jul 20, 2026
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@davidejensen
davidejensen marked this pull request as ready for review July 22, 2026 07:45
@davidejensen
davidejensen requested review from a team as code owners July 22, 2026 07:45
@github-actions
github-actions Bot requested a review from anicalbano July 22, 2026 07:46
@decentraland-bot
decentraland-bot self-requested a review July 22, 2026 07:46
@davidejensen davidejensen changed the title feat: add credits topup feat: new credits topup Jul 22, 2026

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PR Review: feat: add credits topup

STEP 2 — Root-cause check

PASS. This PR introduces a new feature (credits top-up via Stripe checkout), not a fix. The implementation directly addresses the stated goal: letting users buy credits from the client via the "+" icon in the credits balance or when purchasing an item without enough credits.

STEP 3 — Design & integration

PASS. New units introduced:

  1. CreditsTopUpService — manages the async checkout → poll → credited flow. This is a standalone service (not ECS) created and disposed by CreditPurchasePlugin. The lifecycle it manages (Stripe checkout + order polling) is user-triggered and has no existing owner in the codebase. It does not duplicate any existing lifecycle.

  2. CreditsTopUpModalController — standard MVC controller following ControllerBase<TView, TParams>. Created in CreditPurchasePlugin and registered with mvcManager. Follows the existing pattern (e.g., CreditPurchaseModalController).

  3. CreditsTopUpAnalytics — analytics relay wired via MVCManagerAnalyticsDecorator. Follows the existing pattern (e.g., MarketplaceCreditsAnalytics).

All new units are created at the composition root (CreditPurchasePlugin) and disposed properly. No lifecycle duplication detected.

Teardown trace — all subscriptions matched:

  • StatusChanged += in constructor → -= in Dispose()
  • RetryButton.onClick.AddListener in OnViewShowRemoveListener in OnViewClose
  • BuyButton.onClick.AddListener in BindPackItemsRemoveAllListeners in OnViewClose
  • Analytics events += in CreditsTopUpAnalytics constructor → -= in Dispose()
  • lifeCts created in OnViewShowSafeCancelAndDispose in OnViewClose
  • cts/skipForegroundCts in service → SafeCancelAndDispose in Dispose()

STEP 4 — Member audit

Public members reviewed:

  • ICreditsTopUpService interface — 6 members, all consumed by CreditsTopUpModalController and tests. No single-use concerns.
  • CreditsTopUpStatus factory methods — each maps to a distinct stage. Clean value-type state machine.
  • CreditPackCatalog.PACKS — static catalog, consumed by the controller and tests. Clearly documented as temporary (server-driven in future).
  • Analytics events on the controller (BuyCreditsStarted, RedirectedToStripe, etc.) — each consumed by CreditsTopUpAnalytics. Clean relay pattern.

No absent-≠-false, single-use-merge, or re-derive issues found.

STEP 5 — Line-level findings

See inline comments below. All findings are P2 (minor).

STEP 6 — Security review

No security issues found. No hardcoded secrets, no PII in logs or analytics, server-signed requests for checkout, generic user-facing error messages, feature properly gated behind alfa-credits-topup flag.

STEP 7 — Consumer impact

  • UnityAppWebBrowser.OpenUrlMainThreadOnly changed behavior (double-encoding fix) — backward-compatible improvement.
  • CreditsPanelController constructor expanded with new parameters — the only caller (ExplorePanelPlugin) is updated in this diff.
  • MarketplaceCreditsAPIClient.GetUserCreditsAsync made virtual — additive, backward-compatible.
  • New virtual methods on MarketplaceCreditsAPIClient — additive.

No breaking changes.

STEP 8 — Non-blocking warnings

None. Main scene not modified.


REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: New async flow (UniTask polling with CTS management), MVC controller, API endpoints, analytics, plugin wiring, and feature flag integration across 50 files.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

…tsTopUpModalController.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Warnings not reduced: 14668 => 14707 — remove at least one warning to merge.

Warnings/errors in files changed by this PR (91)
File Line Rule Message
Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditsTopUpServiceShould.cs 194 AccessToModifiedClosure Captured variable is modified in the outer scope
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 178 CSharpWarnings::CS0618 CS0618: Constant 'DCL.FeatureFlags.FeatureFlagsStrings.GPUI_ENABLED' is obsolete: 'GPU Instancer Pro terrain is no longer optional so the flag is not needed'
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs 741 CSharpWarnings::CS8618 Non-nullable property 'EventInfoPrefab' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs 739 CSharpWarnings::CS8618 Non-nullable property 'PlaceCategoriesSO' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs 740 CSharpWarnings::CS8618 Non-nullable property 'PlaceDetailPanelPrefab' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs 742 CSharpWarnings::CS8618 Non-nullable property 'QualityPresets' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs 727 CSharpWarnings::CS8618 Non-nullable property 'pointAtMarkerVisibilitySettings' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditsTopUpServiceShould.cs 358 CSharpWarnings::CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 118 InconsistentNaming Name 'AssetBundleFallback' does not match rule 'Enum members'. Suggested name is 'ASSET_BUNDLE_FALLBACK'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 131 InconsistentNaming Name 'AuthCodeValidation' does not match rule 'Enum members'. Suggested name is 'AUTH_CODE_VALIDATION'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 122 InconsistentNaming Name 'CameraReel' does not match rule 'Enum members'. Suggested name is 'CAMERA_REEL'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsCheckoutError.cs 7 InconsistentNaming Name 'Cancelled' does not match rule 'Enum members'. Suggested name is 'CANCELLED'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsCheckoutError.cs 17 InconsistentNaming Name 'Cancelled' does not match rule 'Enum members'. Suggested name is 'CANCELLED'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 119 InconsistentNaming Name 'ChatHistoryLocalStorage' does not match rule 'Enum members'. Suggested name is 'CHAT_HISTORY_LOCAL_STORAGE'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 133 InconsistentNaming Name 'ChatTranslation' does not match rule 'Enum members'. Suggested name is 'CHAT_TRANSLATION'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 121 InconsistentNaming Name 'CommunityVoiceChat' does not match rule 'Enum members'. Suggested name is 'COMMUNITY_VOICE_CHAT'.
Assets/DCL/PluginSystem/Global/CreditPurchasePlugin.cs 94 InconsistentNaming Name 'CreditPurchasePopupPrefab' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'creditPurchasePopupPrefab'.
Assets/DCL/PluginSystem/Global/CreditPurchasePlugin.cs 95 InconsistentNaming Name 'CreditsTopUpPopupPrefab' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'creditsTopUpPopupPrefab'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 109 InconsistentNaming Name 'CsvVariant' does not match rule 'Enum members'. Suggested name is 'CSV_VARIANT'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 107 InconsistentNaming Name 'CustomMapPinsIcons' does not match rule 'Enum members'. Suggested name is 'CUSTOM_MAP_PINS_ICONS'.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs 741 InconsistentNaming Name 'EventInfoPrefab' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'eventInfoPrefab'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsCheckoutError.cs 8 InconsistentNaming Name 'FeatureDisabled' does not match rule 'Enum members'. Suggested name is 'FEATURE_DISABLED'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 123 InconsistentNaming Name 'Friends' does not match rule 'Enum members'. Suggested name is 'FRIENDS'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 125 InconsistentNaming Name 'FriendsOnlineStatus' does not match rule 'Enum members'. Suggested name is 'FRIENDS_ONLINE_STATUS'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 124 InconsistentNaming Name 'FriendsUserBlocking' does not match rule 'Enum members'. Suggested name is 'FRIENDS_USER_BLOCKING'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 116 InconsistentNaming Name 'GenesisStartingParcel' does not match rule 'Enum members'. Suggested name is 'GENESIS_STARTING_PARCEL'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 135 InconsistentNaming Name 'GiftingEnabled' does not match rule 'Enum members'. Suggested name is 'GIFTING_ENABLED'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 104 InconsistentNaming Name 'GlobalPortableExperience' does not match rule 'Enum members'. Suggested name is 'GLOBAL_PORTABLE_EXPERIENCE'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 132 InconsistentNaming Name 'GpuiEnabled' does not match rule 'Enum members'. Suggested name is 'GPUI_ENABLED'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 113 InconsistentNaming Name 'GreetingOnboarding' does not match rule 'Enum members'. Suggested name is 'GREETING_ONBOARDING'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 128 InconsistentNaming Name 'Ktx2Conversion' does not match rule 'Enum members'. Suggested name is 'KTX2_CONVERSION'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 106 InconsistentNaming Name 'MapPins' does not match rule 'Enum members'. Suggested name is 'MAP_PINS'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 129 InconsistentNaming Name 'MarketplaceCredits' does not match rule 'Enum members'. Suggested name is 'MARKETPLACE_CREDITS'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/MarketplaceCreditsAPIClient.cs 15 InconsistentNaming Name 'MarketplaceCreditsAPIClient' does not match rule 'members_should_be_pascal_case'. Suggested name is 'MarketplaceCreditsApiClient'.
Assets/DCL/MarketplaceCredits/Purchase/Tests/MarketplaceCreditsAPIClientCheckoutShould.cs 6 InconsistentNaming Name 'MarketplaceCreditsAPIClientCheckoutShould' does not match rule 'members_should_be_pascal_case'. Suggested name is 'MarketplaceCreditsApiClientCheckoutShould'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 130 InconsistentNaming Name 'MarketplaceCreditsWalletsVariant' does not match rule 'Enum members'. Suggested name is 'MARKETPLACE_CREDITS_WALLETS_VARIANT'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 102 InconsistentNaming Name 'MultiplayerCompressionMac' does not match rule 'Enum members'. Suggested name is 'MULTIPLAYER_COMPRESSION_MAC'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 101 InconsistentNaming Name 'MultiplayerCompressionWin' does not match rule 'Enum members'. Suggested name is 'MULTIPLAYER_COMPRESSION_WIN'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsCheckoutError.cs 12 InconsistentNaming Name 'NetworkError' does not match rule 'Enum members'. Suggested name is 'NETWORK_ERROR'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsCheckoutError.cs 19 InconsistentNaming Name 'NetworkError' does not match rule 'Enum members'. Suggested name is 'NETWORK_ERROR'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 100 InconsistentNaming Name 'None' does not match rule 'Enum members'. Suggested name is 'NONE'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsCheckoutError.cs 18 InconsistentNaming Name 'NotFound' does not match rule 'Enum members'. Suggested name is 'NOT_FOUND'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 112 InconsistentNaming Name 'Onboarding' does not match rule 'Enum members'. Suggested name is 'ONBOARDING'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 114 InconsistentNaming Name 'OnboardingEnabledVariant' does not match rule 'Enum members'. Suggested name is 'ONBOARDING_ENABLED_VARIANT'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 115 InconsistentNaming Name 'OnboardingGreetingsVariant' does not match rule 'Enum members'. Suggested name is 'ONBOARDING_GREETINGS_VARIANT'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 134 InconsistentNaming Name 'OutfitsEnabled' does not match rule 'Enum members'. Suggested name is 'OUTFITS_ENABLED'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsCheckoutError.cs 9 InconsistentNaming Name 'PaymentsUnavailable' does not match rule 'Enum members'. Suggested name is 'PAYMENTS_UNAVAILABLE'.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs 740 InconsistentNaming Name 'PlaceDetailPanelPrefab' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'placeDetailPanelPrefab'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 103 InconsistentNaming Name 'PortableExperience' does not match rule 'Enum members'. Suggested name is 'PORTABLE_EXPERIENCE'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 105 InconsistentNaming Name 'PortableExperienceChatCommands' does not match rule 'Enum members'. Suggested name is 'PORTABLE_EXPERIENCE_CHAT_COMMANDS'.

…and 41 more (see the csharp-lint-reports artifact).

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

STEP 2 — Root-cause check

This PR introduces a new credits top-up feature (buy credits via Stripe). The diff addresses the stated goal directly — it is net-new functionality, not a symptom fix. PASS.

STEP 3 — Design & integration

New long-lived units introduced:

  1. CreditsTopUpService — manages the checkout→poll→credit lifecycle for a single purchase.

    • Lifecycle managed: Stripe checkout session creation + order status polling + balance refresh.
    • Searched for existing owners: CreditPurchasePlugin (owns the credit purchase modal lifecycle but has no checkout-poll concept), MarketplaceCreditsAPIClient (HTTP calls only, no lifecycle management), CreditsPanelController (read-only balance display). None manage a checkout-poll lifecycle. The new unit is justified.
  2. CreditsTopUpModalController — MVC controller for the top-up popup.

    • Follows the established ControllerBase<TView, TInputData> pattern. Justified.

Teardown / consumption trace:

  • topUpService.StatusChanged += OnServiceStatusChanged in ctor → -= in Dispose(). ✓
  • viewInstance.RetryButton.onClick.AddListener(OnRetryClicked) in OnViewShowRemoveListener in OnViewClose. ✓
  • packItem.BuyButton.onClick.AddListener(…) in BindPackItemsRemoveAllListeners() in OnViewClose. ✓
  • CreditsTopUpAnalytics: all 5 += subscriptions → matched by 5 -= in Dispose(). ✓
  • CreditsPanelController.view.GetCreditsButton.onClick.AddListenerRemoveListener in Dispose. ✓
  • CancellationTokenSource ctsSafeCancelAndDispose() in Dispose(). ✓
  • CancellationTokenSource skipForegroundCtsSafeCancelAndDispose() in Dispose() and after foreground poll. ✓

No subscription or resource leaks found. PASS.

STEP 4 — Member audit

Member Consumers Verdict
IsOrderInFlight StartTopUp, OnPackClicked 2 consumers, re-entry guard — justified
CurrentStatus Service internals, controller ApplyStatus, tests Multiple — justified
StatusChanged event Controller relay, tests Multiple — justified
BuyCreditsStarted/Redirected/Completed/Pending/Failed events CreditsTopUpAnalytics (1 each) Event-based analytics pattern — justified

No single-use intermediaries, absent-vs-false conflation, or redundant guards. PASS.

STEP 5 — Line-level review

See inline comments below. All findings are P2 (DTO convention compliance per CLAUDE.md § serialized JSON DTOs).

Security review

  • ✅ No hardcoded secrets or credentials
  • ✅ API calls use signed fetch (authenticated)
  • ✅ No injection vectors — pack IDs come from hardcoded CreditPackCatalog, not user input
  • ✅ Checkout URL originates from first-party server via signed fetch; opened in system browser
  • ✅ No insecure deserialization (JsonUtility)
  • ✅ State machine prevents concurrent checkout race conditions
  • ✅ Feature gated behind alfa-credits-topup feature flag
  • ✅ No PII/sensitive data in logging

No security issues found.

STEP 6 — Complexity assessment

COMPLEX — async checkout→poll lifecycle with cancellation, MVC controller state machine, plugin/container wiring, analytics event hookups, and feature flag integration across multiple assemblies.

STEP 7 — QA assessment

QA_REQUIRED: YES — new user-facing popup and HUD button, runtime code changes, payment flow (Stripe checkout redirect + order status polling).

STEP 8 — Non-blocking warnings

None. Main.unity is not in the changed file list.

STEP 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Introduces async checkout-poll lifecycle with cancellation, MVC controller with state machine, plugin wiring, and analytics across multiple assemblies.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by davidejensen via GitHub

@pravusjif pravusjif left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM nice job!

@DafGreco DafGreco left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✔️ PR reviewed and approved by QA on both platforms following instructions playing both happy and un-happy path

Regressions for this ticket had been performed in order to verify that the normal flow is working as expected:

  • [✔️ ] Log In/Log Out
  • [✔️ ] Backpack and wearables in world
  • [✔️ ] Emotes in world and in backpack
  • [ ✔️ ] Teleport with map/coordinates/Jump In
  • [ ✔️ ] Chat and multiplayer
  • [ ✔️ ] Profile card
  • [✔️ ] Settings

Evidence:

20260723-0856-39.5123728.mp4

@davidejensen
davidejensen merged commit 2dfd5b8 into dev Jul 23, 2026
28 of 33 checks passed
@davidejensen
davidejensen deleted the feat/buy-credits-stripe branch July 23, 2026 09:07
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.

Integrate credits topup in client

4 participants