Skip to content

feat: add support for abbandoned status in credits topup - #9768

Merged
davidejensen merged 3 commits into
devfrom
feat/abbandoned-credits-topup-status
Aug 18, 2026
Merged

feat: add support for abbandoned status in credits topup#9768
davidejensen merged 3 commits into
devfrom
feat/abbandoned-credits-topup-status

Conversation

@davidejensen

@davidejensen davidejensen commented Aug 17, 2026

Copy link
Copy Markdown
Member

Pull Request Description

What does this PR change?

Fix #9737
This PR adds support for the abbandoned status of a credits topup transaction. The abbandoned status happens when a user cancels the transaction in the browser.
This DOES NOT cover the case when a user closes the browser tab of the transaction, in that case we cannot do anything to detect it.

Test Instructions

Test Steps

  1. Launch the client
  2. Open the credits topup UI
  3. select any pack
  4. In the browser press on "Back" in the top right of the stripe transaction
  5. Verify that in the client the UI shows that the transaction was cancelled
    This DOES NOT work when closing the browser page and it's normal, we cannot avoid that

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 Aug 17, 2026
@davidejensen
davidejensen requested review from a team as code owners August 17, 2026 11:57
@github-actions
github-actions Bot requested review from anicalbano and dalkia August 17, 2026 11:57
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

New build in progress, come back later!

Lint

Warnings not reduced: 13132 => 13132 — remove at least 1 warning to merge.

Warnings/errors in files changed by this PR (4)
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsOrderStatusResponse.cs:17  InconsistentNaming  Name 'creditsGranted' does not match rule 'members_should_be_pascal_case'. Suggested name is 'CreditsGranted'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsOrderStatusResponse.cs:22  InconsistentNaming  Name 'error' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Error'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsOrderStatusResponse.cs:20  InconsistentNaming  Name 'newBalance' does not match rule 'members_should_be_pascal_case'. Suggested name is 'NewBalance'.
Assets/DCL/MarketplaceCredits/MarketplaceCreditsAPIService/CreditsOrderStatusResponse.cs:14  InconsistentNaming  Name 'status' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Status'.

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 25054 0 13
PlayMode ✅ Passed 236 0 37

@github-actions
github-actions Bot requested a review from lorux0 August 17, 2026 11:57
@decentraland-bot
decentraland-bot self-requested a review August 17, 2026 11:58

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

Review — PR #9768: feat: add support for abandoned status in credits topup

STEP 2 — Root-cause check: ✅ PASS

The problem is that the credits top-up flow had no awareness of the server's "abandoned" status, which occurs when a user cancels the transaction in the browser. The diff adds recognition of this status at every layer (API response → poll outcome → status type → UI controller), correctly addressing the cause.

STEP 3 — Design & integration: ✅ PASS

No new long-lived units are introduced. The change extends existing enums, adds a factory method to an existing readonly struct, and adds switch arms in existing methods. The pattern mirrors the established handling of Failed and Credited statuses. No lifecycle owners to search — no new persistent state or long-lived objects.

STEP 4 — Member audit

New member Consumers Verdict
CreditsTopUpStatus.Abandoned(CreditPack, string) RunTopUpAsync (1) Follows existing factory pattern — PASS
PollOutcome.Abandoned RunTopUpAsync switch + PollOrderAsync return (2) PASS
CreditsTopUpStage.Abandoned AcknowledgeTerminalState, ApplyStatus, MapStage (3) PASS

STEP 5 — Findings

See inline comments for details. Summary:

# Sev File Finding
1 P1 CreditsTopUpModalController.cs Missing analytics event for Abandoned in OnServiceStatusChanged — abandoned purchases are invisible to the analytics funnel
2 P2 CreditsTopUpService.cs:20 Missing trailing comma on PollOutcome.Abandoned
3 P2 CreditsTopUpServiceShould.cs No unit test for the abandoned status transition (compare TransitionToFailedWhenOrderFails)

STEP 6 — Complexity: SIMPLE

Touches 4 files with ~30 lines. Straightforward enum/switch extensions. Does not modify ECS systems, async patterns, containers, or assembly definitions.

STEP 7 — QA: YES

Changes affect runtime UI behavior (the top-up modal shows a different message for abandoned transactions and enables the retry button).

STEP 8 — Non-blocking warnings

None. Main scene not modified.

Cosmetic note: The branch name and PR title use "abbandoned" (double b) — the correct spelling is "abandoned."

Security review: No security issues found.

REVIEW_RESULT: FAIL ❌
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Enum value additions and switch-case extensions in the credits top-up status flow
QA_REQUIRED: YES


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

Comment thread Explorer/Assets/DCL/MarketplaceCredits/Purchase/TopUp/CreditsTopUpService.cs Outdated

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

Review: feat: add support for abandoned status in credits topup

STEP 2 — Root-cause check

PASS. The PR addresses a genuine gap: the client did not handle the "abandoned" order status returned by the server when a user cancels a Stripe checkout. The diff adds proper handling at every layer (API response constant → service poll/dispatch → domain enum/factory → controller analytics/UI). This is a cause-level fix, not a symptom workaround.

STEP 3 — Design & integration

PASS. No new long-lived units are introduced. The change is purely additive — new enum values, switch cases, and a factory method — wired into the existing CreditsTopUpService and CreditsTopUpModalController lifecycle owners.

  • Pattern conformance: The Abandoned path mirrors the established Credited/Failed patterns at every layer (API const → PollOutcomeCreditsTopUpStageCreditsTopUpStatus factory → controller dispatch → UI).
  • ModalState.Failed reuse: Mapping AbandonedModalState.Failed is correct. ModalState is a UI-layout concern (which container to show), and both states share the same visual container (FailedContainer + FailedReasonText + RetryButton). Domain-level differentiation happens in ApplyStatus via the CreditsTopUpStage switch.
  • OnViewClose handling: Since Abandoned maps to ModalState.Failed, closing the modal calls AcknowledgeTerminalState() (not CancelTopUp()), which is correct — the server already considers the order terminal.
  • Retry flow: Works end-to-end: ApplyStatus enables the retry button → OnRetryClicked guard passes (currentState == ModalState.Failed) → AcknowledgeTerminalState() resets to Idle → user returns to pack selection.
  • Teardown trace: No new subscriptions, event hookups, or resources are added. All new code paths terminate in existing SetStatus() calls.

STEP 4 — Member audit

New member Consumers Verdict
CreditsTopUpStatus.Abandoned(pack, orderId) 1 (RunTopUpAsync line 158) Same pattern as GrantFailed() (also 1 consumer). Acceptable — factory methods exist for type safety and readability.
PollOutcome.Abandoned 2 (poll mapping line 189, dispatch line 157) Same count as Failed. Fine.
CreditsTopUpStage.Abandoned 5 (service, types, controller ×3) Consistent with Failed usage spread. Fine.
ANALYTICS_ABANDONED 1 (line 269) Single-use const for analytics string. Same pattern as ANALYTICS_ERROR_GRANT_FAILED. Fine.

No single-use-merge, absent≠false, or redundant-guard issues.

STEP 5 — Line-level review

No blocking issues (P0/P1) found. Two P2 items noted below.

[P2] Missing test coverage for the new Abandoned flow

Neither CreditsTopUpServiceShould.cs nor CreditsTopUpModalControllerShould.cs was updated. Four tests should be added to cover the new code paths:

  1. Service: STATUS_ABANDONED poll → CreditsTopUpStage.Abandoned transition (parallel to TransitionToFailedWhenOrderFails). Verify no balance refresh is called.
  2. Service: AcknowledgeTerminalState from Abandoned resets to Idle (parallel to ResetTerminalStateToIdleOnAcknowledge).
  3. Controller: Abandoned stage raises BuyCreditsFailed with step "grant" and error code "abandoned" (parallel to RelayGrantFailureWithGrantStep).
  4. Controller: Close while in Abandoned calls AcknowledgeTerminalState, not CancelTopUp (parallel to AcknowledgeWithoutCancellingWhenClosedAfterTerminalState).
Example test outlines (click to expand)

CreditsTopUpServiceShould.cs:

[Test]
public async Task TransitionToAbandonedWhenOrderIsAbandoned()
{
    // Arrange
    creditsApiClient.GetCheckoutOrderAsync(ORDER_ID, Arg.Any<CancellationToken>())
                    .Returns(Order(CreditsOrderStatusResponse.STATUS_ABANDONED));

    // Act
    service.StartTopUp(PACK);
    await WaitForStageAsync(CreditsTopUpStage.Abandoned);

    // Assert
    Assert.AreEqual(ORDER_ID, service.CurrentStatus.OrderId);
    Assert.AreEqual(PACK.Id, service.CurrentStatus.Pack.Id);
    await creditsApiClient.DidNotReceive().GetUserCreditsAsync(
        Arg.Any<string>(), Arg.Any<CancellationToken>());
}

[Test]
public async Task ResetAbandonedStateToIdleOnAcknowledge()
{
    // Arrange
    creditsApiClient.GetCheckoutOrderAsync(ORDER_ID, Arg.Any<CancellationToken>())
                    .Returns(Order(CreditsOrderStatusResponse.STATUS_ABANDONED));

    service.StartTopUp(PACK);
    await WaitForStageAsync(CreditsTopUpStage.Abandoned);

    // Act
    service.AcknowledgeTerminalState();

    // Assert
    Assert.AreEqual(CreditsTopUpStage.Idle, service.CurrentStatus.Stage);
    Assert.IsFalse(service.IsOrderInFlight);
}

CreditsTopUpModalControllerShould.cs:

[Test]
public void RelayAbandonedAsFailureWithGrantStepAndAbandonedCode()
{
    // Act
    RaiseStatus(CreditsTopUpStatus.Abandoned(PACK, ORDER_ID));

    // Assert
    Assert.AreEqual(1, failed.Count);
    Assert.AreEqual("grant", failed[0].step);
    Assert.AreEqual("abandoned", failed[0].errorCode);
}

[Test]
public void AcknowledgeWithoutCancellingWhenClosedAfterAbandoned()
{
    // Arrange
    topUpService.CurrentStatus.Returns(CreditsTopUpStatus.Abandoned(PACK, ORDER_ID));
    controller.Show();
    RaiseStatus(CreditsTopUpStatus.Abandoned(PACK, ORDER_ID));

    // Act
    controller.Close();

    // Assert
    topUpService.Received(1).AcknowledgeTerminalState();
    topUpService.DidNotReceive().CancelTopUp();
    Assert.AreEqual(0, cancelled.Count);
}

[P2] Analytics classification note

The Abandoned status routes through BuyCreditsFailed with step="grant" (line 269). This is consistent with the existing pattern (post-checkout terminal states use BuyCreditsFailed), but worth confirming with the analytics consumer: "abandoned" is semantically "user chose not to pay" rather than "payment system failed." If dashboards use BuyCreditsFailed as an error signal, abandoned checkouts could inflate failure metrics. The distinct error_code="abandoned" does allow filtering — just make sure downstream dashboards are aware.

STEP 6 — Complexity

SIMPLE. Four files, ~24 lines of meaningful changes. Straightforward addition of a new enum value and its handling through existing layers. No ECS, async, container, or architectural changes.

STEP 7 — QA assessment

QA_REQUIRED: YES. The change modifies runtime UI behavior — what the user sees when a Stripe payment is abandoned. The PR description includes clear manual test steps.

STEP 8 — Non-blocking warnings

No warnings. Main.unity is not modified.

Security review

No security issues found. All new strings are hardcoded constants. No server-supplied data is interpolated into the abandoned-state UI (unlike the Failed path which can surface order.error). No new API calls, auth changes, or input handling. The trust model (client takes server status at face value) is unchanged.


REVIEW_RESULT: PASS ✅
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Additive enum values and switch cases in the MarketplaceCredits top-up service and controller — no ECS, async, or architectural changes.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by davidejensen via GitHub

@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9768, run #32032427094

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions per run — the average number of exceptions in a run's log; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2701 (×3) 2372 (×3)
CPU average 33.2 ms (33.2–34.6) 37.7 ms (37.6–37.8) 4.6 ms 🔴 14% slower
CPU 1% worst 34.3 ms (33.5–184.5) 291.1 ms (281.2–307.6) 256.9 ms 🔴 750% slower
CPU 0.1% worst 41.4 ms (33.7–332.0) 301.8 ms (291.6–331.0) 260.4 ms ⚪ within noise
GPU average 9.3 ms (9.2–9.4) 9.4 ms (9.3–9.6) 0.2 ms ⚪ within noise
GPU 1% worst 20.7 ms (19.8–26.9) 32.1 ms (30.3–32.8) 11.4 ms 🔴 55% slower
GPU 0.1% worst 36.3 ms (31.6–37.7) 38.0 ms (37.9–39.9) 1.7 ms ⚪ within noise
Exceptions per run 66 66 0 ⚪ none new
Exception breakdown
Exception Baseline (3 runs) Change (3 runs)
[UI] DllNotFoundException 192 192
[ENGINE] NullReferenceException 3 3
[ENGINE] ObjectDisposedException 3 3

Apple M1

Metric Baseline Change Δ Result
Samples 4368 (×3) 3977 (×3)
CPU average 20.5 ms (20.3–21.6) 22.4 ms (22.4–22.9) 1.9 ms 🔴 9% slower
CPU 1% worst 34.7 ms (33.9–34.7) 232.6 ms (227.9–234.3) 197.9 ms 🔴 570% slower
CPU 0.1% worst 34.9 ms (34.9–35.3) 237.4 ms (237.0–237.5) 202.5 ms 🔴 579% slower
GPU average 1.0 ms (0.1–1.6) 3.4 ms (2.9–7.1) 2.5 ms ⚪ within noise
GPU 1% worst 34.2 ms (7.7–34.8) 35.4 ms (35.2–35.7) 1.2 ms ⚪ within noise
GPU 0.1% worst 35.9 ms (35.1–37.2) 36.8 ms (36.5–37.7) 1.0 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

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

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

Evidence:

Screen.Recording.2026-08-17.at.14.33.43.mov
20260817-1347-53.4377469.mp4

@lorenzo-ranciaffi lorenzo-ranciaffi 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.

LGTM

@davidejensen
davidejensen enabled auto-merge (squash) August 18, 2026 08:53
@davidejensen
davidejensen merged commit 2e4c9c8 into dev Aug 18, 2026
23 of 25 checks passed
@davidejensen
davidejensen deleted the feat/abbandoned-credits-topup-status branch August 18, 2026 09:30
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.

Credits top-up: Explorer stays stuck on checkout spinner when browser tab is closed or purchase is cancelled

4 participants