Files
supabase/apps/studio/components/ui/PartnerManagedResource.test.tsx
Danny White 9e3a10d557 feat(studio): payment method states for Stripe Projects orgs (#44965)
## What kind of change does this PR introduce?

UI changes for Stripe-managed billing surfaces.

- Resolves DEPR-537
- Related to DEPR-538

## What is the current behaviour?

Stripe-connected organisations still look too self-serve in Studio.

- Payment Methods still reads mostly like ordinary Supabase card
management, even though billing is handled through a Shared Payment
Token via Stripe Projects
- invoice messaging still implies support is the path to changing
payment methods, even for Stripe-managed orgs
- the Subscription Plan flow still needs Stripe-specific guardrails so
users are redirected to the correct upgrade path rather than trying to
self-serve everything in Studio
- the base branch now correctly separates `integration_source` from
`billing_partner`, but this stacked work still needs to carry that split
through the Stripe billing-token surfaces

## What is the new behaviour?

This PR makes the Stripe-managed billing surfaces behave like
Stripe-managed billing surfaces, while leaving AWS and Vercel on the
existing `billing_partner` path.

- Payment Methods now keeps the familiar saved-card row, but augments
Stripe-managed rows with Shared Payment Token context, token status, and
Stripe Projects affordances
- Stripe-managed invoice messaging now points users to Stripe Projects
rather than to support for payment-method changes
- the Subscription Plan flow keeps the existing managed-billing shape,
with Stripe-specific guardrails layered in where plan changes should be
handled outside Studio
- AWS and Vercel continue to use the existing partner-managed alerts and
CTAs driven by `billing_partner` / `billing_via_partner`

| Subscription plan sheet |
| --- |
| <img width="1780" height="448" alt="CleanShot 2026-04-24 at 17 21
43@2x"
src="https://github.com/user-attachments/assets/34c0f3ba-fc42-4d07-97a2-0e4f4cefc55e"
/> |
| _Upgrade instructions_ |
| <img width="1786" height="460" alt="CleanShot 2026-04-24 at 17 20
12@2x"
src="https://github.com/user-attachments/assets/bb67c835-b9b2-4648-b0e1-9c2f8d2317d3"
/> |
| _Downgrade instructions_ | 

> [!NOTE]
> The below screenshots are outdated. The _Shared Payment Token_
terminology has been removed in favour of more generic copy such as
_Stripe Projects token_.

| Stripe payment method states |
| --- |
| <img width="1436" height="234" alt="CleanShot 2026-04-23 at 19 03
49@2x"
src="https://github.com/user-attachments/assets/52ed7a00-dfba-4b66-9a07-a6346692d3c8"
/> |
| _Healthy_ |
| <img width="1434" height="224" alt="CleanShot 2026-04-23 at 19 04
50@2x"
src="https://github.com/user-attachments/assets/94efd943-b7bf-4da2-9e1b-1828aae97126"
/> |
| _Card expiring soon_ |
| <img width="1436" height="236" alt="CleanShot 2026-04-23 at 19 06
51@2x"
src="https://github.com/user-attachments/assets/272cb707-c724-4629-890e-853972e53a18"
/> |
| _Card expired_ |
| <img width="1308" height="238" alt="CleanShot 2026-04-23 at 19 07
21@2x"
src="https://github.com/user-attachments/assets/3eadd2a9-def3-4f43-850e-7d82adfb0b57"
/> |
| _Token expired_ |

## Dependencies

This PR is stacked on:

- #44328

It also depends on the private platform work that exposes Stripe project
connection state and SPT details:

- https://github.com/supabase/platform/pull/31874
- https://github.com/supabase/platform/pull/31940

## Platform dependency status

Most of the remaining platform work for this stack is now covered by the
private dependency below:

- https://github.com/supabase/platform/pull/31940

That PR is expected to provide the SPT details and paid-flow fixes this
Studio work depends on. In practice, the main caveat here is less
“Studio still needs a bunch of new platform work” and more “do not merge
this until `platform#31940` has landed and the end-to-end Stripe-managed
flow has been rechecked”.

## Local testing

Use the same local Stripe setup as the base branch, with
`integration_source: 'stripe_projects'` returned consistently for:

- `/platform/organizations`
- `/platform/organizations/:slug/projects`
- `/platform/projects/:ref`

For payment method demos, the temporary local mock currently lives in
private `platform` on:

- `/platform/organizations/:slug/payments`

That mock can be flipped between:

- healthy token + healthy underlying card
- healthy token + card expiring soon
- healthy token + expired card
- expired token

Then verify:

- the org and project connection affordances from #44328 still render
correctly
- Payment Methods shows Stripe-managed token context rather than
implying ordinary self-serve card management
- regression test ordinary non-Stripe payment methods too, to confirm
the standard saved-card row still renders with the existing `Expires:`
copy and no Shared Payment Token affordances
- invoice messaging points Stripe-managed orgs to Stripe Projects rather
than support
- Subscription Plan keeps the managed-billing guardrails for Stripe
- AWS and Vercel orgs still show the existing partner-managed messaging
rather than the Stripe-specific notices

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Stripe-managed organizations show Stripe Projects billing guidance,
replace in-app payment management with Stripe links, and adjust billing
copy.
* Payment methods support Shared Payment Tokens (SPTs): token
expiry/status badges with tooltips, “Handled via Stripe Projects”
indicator, token last4/expiry display, and disabled local update/delete
actions for SPTs.

* **API**
* Payments response now includes optional shared payment token details
for payment methods.

* **Documentation**
  * Added links to Stripe Projects billing docs in relevant flows.

* **Tests**
  * Updated and added tests covering Stripe-managed and SPT behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Raúl Barroso <code@raulb.dev>
2026-04-28 12:17:29 +10:00

111 lines
3.4 KiB
TypeScript

import { screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PartnerManagedResource from './PartnerManagedResource'
import { MANAGED_BY } from '@/lib/constants/infrastructure'
import { render } from '@/tests/helpers'
const { mockUseAwsRedirectQuery, mockUseVercelRedirectQuery } = vi.hoisted(() => ({
mockUseAwsRedirectQuery: vi.fn(),
mockUseVercelRedirectQuery: vi.fn(),
}))
vi.mock('@/data/integrations/vercel-redirect-query', () => ({
useVercelRedirectQuery: mockUseVercelRedirectQuery,
}))
vi.mock('@/data/integrations/aws-redirect-query', () => ({
useAwsRedirectQuery: mockUseAwsRedirectQuery,
}))
vi.mock('./PartnerIcon', () => ({
default: () => <div data-testid="partner-icon" />,
}))
describe('PartnerManagedResource', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseVercelRedirectQuery.mockReturnValue({
data: undefined,
isLoading: false,
isError: false,
})
mockUseAwsRedirectQuery.mockReturnValue({
data: undefined,
isLoading: false,
isError: false,
})
})
it('renders Stripe connected copy and never shows CTA even when cta prop exists', () => {
render(
<PartnerManagedResource
managedBy={MANAGED_BY.STRIPE_PROJECTS}
resource="Payment Methods"
details={
<>
Run <code>stripe projects upgrade supabase/free</code>
</>
}
cta={{ installationId: 'vercel-installation-id', organizationSlug: 'aws-org' }}
/>
)
expect(screen.getByText('Payment Methods are connected to Stripe')).toBeInTheDocument()
expect(screen.getByText('stripe projects upgrade supabase/free')).toBeInTheDocument()
expect(screen.queryByRole('link')).toBeNull()
expect(mockUseVercelRedirectQuery).toHaveBeenCalledWith(
{ installationId: 'vercel-installation-id' },
expect.objectContaining({ enabled: false })
)
expect(mockUseAwsRedirectQuery).toHaveBeenCalledWith(
{ organizationSlug: 'aws-org' },
expect.objectContaining({ enabled: false })
)
})
it('renders AWS CTA only when a redirect URL is available', () => {
mockUseAwsRedirectQuery.mockReturnValue({
data: { url: 'https://console.aws.amazon.com/billing/home#/' },
isLoading: false,
isError: false,
})
render(
<PartnerManagedResource
managedBy={MANAGED_BY.AWS_MARKETPLACE}
resource="Invoices"
cta={{ organizationSlug: 'aws-org', path: 'bills' }}
/>
)
expect(screen.getByText('Invoices are managed by AWS Marketplace')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'View Invoices on AWS Marketplace' })).toHaveAttribute(
'href',
'https://console.aws.amazon.com/billing/home#/bills'
)
})
it('does not render Vercel CTA when redirect URL is unavailable', () => {
mockUseVercelRedirectQuery.mockReturnValue({
data: undefined,
isLoading: false,
isError: false,
})
render(
<PartnerManagedResource
managedBy={MANAGED_BY.VERCEL_MARKETPLACE}
resource="Organization plans"
cta={{ installationId: 'vercel-installation-id', path: '/settings' }}
/>
)
expect(
screen.getByText('Organization plans are managed by Vercel Marketplace')
).toBeInTheDocument()
expect(screen.queryByRole('link', { name: /view organization plans/i })).toBeNull()
})
})