Add re-trial card to the group Billing page
What does this MR do and why?
This MR adds a dismissible card near the top of the group Billing page, telling Free namespace Owners that they can start a GitLab Ultimate trial again.
Previously, the only prompt shown to a re-trial-eligible namespace was a small "Try for free" button low down in the plan-comparison cards, a location owners rarely reached. This card surfaces the same offer higher on the page.
Scoping and behaviour:
- Renders only for top-level Free namespaces eligible to start a trial again (previous trial expired 90+ days ago).
- Visible to Owners only, gated on the
:edit_billingability, which excludes auditors who can read the billing page. - Does not render for paid namespaces, namespaces with an active trial, or namespaces that never trialled.
- Dismissible per owner, via a new
Users::GroupCalloutenum valuebilling_retrial_card: 42. Dismissal is per user, so one owner dismissing the card does not hide it for other owners of the same namespace. No migration is needed sincefeature_nameis an existing integer column. - The CTA links to the existing trial flow (
new_trial_path).
Implementation notes for reviewers:
- Eligibility is exposed to the frontend as
eligibleForRetrial, computed inee/app/helpers/billing_plans_helper.rbaseligible_for_trial? && !never_had_trial?. This is logically equivalent to callingNamespace#eligible_for_retrial?, but reuses a value the page already computes, avoiding a second CustomersDot eligibility lookup (Redis-cached for 8 hours) on every page render. ee/app/assets/javascripts/pages/groups/billings/index.jsnow passeswithApolloProvider: true. The billing app previously had no Apollo provider; the callout dismisser needs one for its mutation. Without it, dismissal fails silently.- No new feature flag. The card is gated by the existing
saas_ultimate_retrialflag, whichNamespace#eligible_for_retrial?already checks. - This also corrects the gap above the plan cards from 40px to the 24px specified in the design. Because adjacent CSS margins collapse to the larger value, that gap is governed by the plan-cards row's own margin, so the spacing also changes for namespaces that do not see the card. The Figma block is an auto-layout column with a uniform 24px gap throughout, so 24px appears to be the intended value everywhere, but this is worth a designer's confirmation.
Tracking: two internal events, click_start_trial_on_retrial_card_group_billing and dismiss_retrial_card_group_billing, matching the issue's requirement to track the CTA click and the dismissal. Both event definitions use product_group: acquisition. This was chosen because no event definition in the repo uses monetization, and the analogous existing billing-page trial event (click_start_trial_cta_group_billing) uses acquisition. Worth confirming with reviewers.
Tests:
- A Jest component spec covering every condition under which the card must not render.
- A Capybara feature spec (
ee/spec/features/groups/owner_views_retrial_card_spec.rb) using:capture_snowplow_eventsto assert both events are delivered through real browser clicks, and that dismissal persists across page loads. - Updates to the billing plans helper spec.
No changelog trailer is included, since the card is behind saas_ultimate_retrial, whose YAML has default_enabled: false.
References
- Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/625037
- Epic: https://gitlab.com/groups/gitlab-org/-/work_items/22825
- UX issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/603646
- Figma: https://www.figma.com/design/TutSB4e30Dx3GSYenGQDT4/Re-Activation-of-Ultimate-Trials?node-id=28-1109
- Related, not yet built: https://gitlab.com/gitlab-org/gitlab/-/issues/625030 (post-trial usage card) and https://gitlab.com/gitlab-org/gitlab/-/issues/625034 (post-trial widget duration)
Screenshots or screen recordings
| Before | After |
|---|---|
![]() |
![]() |
How to set up and validate locally
-
Ensure the GDK is running in SaaS mode (
GITLAB_SIMULATE_SAAS=1). -
Open a
rails consoleand, for a top-level group you own, run:Feature.enable(:saas_ultimate_retrial) group = Group.find_by_full_path('your-group') group.gitlab_subscription.update!( hosted_plan: Plan.find_by(name: Plan::FREE), trial: true, trial_starts_on: 200.days.ago, trial_ends_on: 170.days.ago ) Rails.cache.write("namespaces:eligible_trials:#{group.id}", ['ultimate_with_dap'], expires_in: 8.hours)The
Rails.cache.writeline stands in for the CustomersDot eligibility response, so a local CustomersDot instance is not required. After this,group.eligible_for_retrial?should returntrue. -
Visit the group's Billing page. The card should appear above the current-plan cards.
-
Note: the trial dates must be well in the past. A trial that expired within the last 10 days triggers the separate end-of-trial modal, which renders over the card.
-
To run the feature spec locally, prefix the
rspeccommand withENABLE_RSPACK=true, otherwise the asset manifest lookup fails.
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
- I have evaluated the MR acceptance checklist for this MR.

