show Stripe Projects errors inline (#48472)

## What kind of change does this PR introduce?

Bug fix.

## What is the current behavior?

Stripe Projects confirmation failures only appear in a toast.

The existing **Unable to load authorization** Admonition is a separate
error state shown when the account request itself cannot be loaded.

## What is the new behavior?

Confirmation failures remain visible below the authorisation actions,
clear on retry, and do not also trigger a toast. They use the shared
`InterstitialActionError`.

| Before | After |
| --- | --- |
| <img width="1024" height="759" alt="Authorize Stripe Projects
Supabase"
src="https://github.com/user-attachments/assets/83cb3144-9ddb-46b7-bfce-970497be61e2"
/> | <img width="1024" height="759" alt="Authorize Stripe Projects
Supabase"
src="https://github.com/user-attachments/assets/bccedde9-9935-457b-a980-0c80499f2f27"
/> |

## To test

These instructions visually check the after state on this branch.
Opening an invalid `ar_id` without the hardcodes only exercises the
existing load-error Admonition, which this PR does not change.

### After on this branch

1. In `apps/studio/pages/partners/stripe/projects/login.tsx`, replace
the `confirmationError` assignment with:
   ```tsx
const confirmationError = 'Failed to authorize Stripe Projects: Test
error'
   ```
2. In the same file, replace the block beginning with `const linkedOrg`
and ending with `interstitialDescription` with:
   ```tsx
   const linkedOrg = { name: 'Example Organization' }
   const emailMatches = true
const displayName = primaryEmail ?? username ?? 'reviewer@example.com'
   const isPending = false
   const isConfirmed = false
   const isConfirming = false
   const isError = false
   const showAuthorizationState = true
   const interstitialDescription =
     'This will create an organization on your behalf in Supabase'
   ```
3. While signed in locally, open
`http://localhost:8082/partners/stripe/projects/login?ar_id=test`.
4. Confirm the error appears below **Authorize Stripe Projects** and
**Cancel**. No real Stripe request is required.
5. Revert both temporary edits.

### Before on master (optional)

1. Check out `master`.
2. In `apps/studio/pages/partners/stripe/projects/login.tsx`, replace
the block beginning with `const linkedOrg` and ending with
`interstitialDescription` with the same block from step 2 above. Do not
add `confirmationError`.
3. While signed in locally, open
`http://localhost:8082/partners/stripe/projects/login?ar_id=test`.
4. Click **Authorize Stripe Projects**.
5. Confirm the failed confirmation appears in a toast beginning **Failed
to confirm account request**.
6. Revert the temporary edit before changing branches.

## Additional context

Stacked on #48471.

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

## Summary by CodeRabbit

* **Bug Fixes**
* Authorization errors during Stripe Projects login are now displayed
inline in both authorization flows.
  * Authorization remains available after a failed confirmation attempt.
* Failed authorization requests no longer trigger an additional toast
notification.
  * Previous errors are cleared when retrying authorization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Danny White authored and GitHub committed 2026-08-03 11:48:46 +07:00
1 parent c0f1ef51fb
commit 022b374f2d
2 files changed
+94 -2

No files matched your search

@@ -10,6 +10,7 @@ import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import {
InterstitialAccountRow,
InterstitialActionError,
InterstitialLayout,
LogoPair,
PartnerLogo,
@@ -27,7 +28,7 @@ import type { NextPageWithLayout } from '@/types'
const PAGE_TITLE = buildStudioPageTitle({ section: 'Authorize Stripe Projects', brand: 'Supabase' })
const StripeProjectsLoginPage: NextPageWithLayout = () => {
export const StripeProjectsLoginPage: NextPageWithLayout = () => {
const router = useRouter()
const { ar_id } = useParams()
const signOut = useSignOut()
@@ -48,7 +49,14 @@ const StripeProjectsLoginPage: NextPageWithLayout = () => {
mutate: confirmAccountRequest,
isPending: isConfirmationPending,
isSuccess: isConfirmationSuccess,
} = useConfirmAccountRequestMutation()
error: confirmationMutationError,
reset: resetConfirmationError,
} = useConfirmAccountRequestMutation({
onError: () => undefined,
})
const confirmationError = confirmationMutationError
? `Failed to authorize Stripe Projects: ${confirmationMutationError.message}`
: undefined
useEffect(() => {
if (!router.isReady) return
@@ -60,6 +68,7 @@ const StripeProjectsLoginPage: NextPageWithLayout = () => {
const handleApprove = async () => {
if (!ar_id || isConfirmationPending) return
resetConfirmationError()
confirmAccountRequest({ arId: ar_id })
}
@@ -155,6 +164,7 @@ const StripeProjectsLoginPage: NextPageWithLayout = () => {
<Button variant="text" block onClick={() => router.push('/')}>
Cancel
</Button>
<InterstitialActionError error={confirmationError} />
</div>
</div>
)}
@@ -195,6 +205,7 @@ const StripeProjectsLoginPage: NextPageWithLayout = () => {
<Button variant="text" onClick={() => router.push('/')}>
Cancel
</Button>
<InterstitialActionError error={confirmationError} />
</div>
</div>
)}
@@ -0,0 +1,81 @@
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HttpResponse } from 'msw'
import { toast } from 'sonner'
import { beforeEach, expect, test, vi } from 'vitest'
import { StripeProjectsLoginPage } from '@/pages/partners/stripe/projects/login'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
const mocks = vi.hoisted(() => ({
routerPush: vi.fn(),
}))
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: () => ({
data: {
email: 'alex@example.com',
email_matches: true,
linked_organization: { name: 'Acme', slug: 'acme' },
},
isPending: false,
isSuccess: true,
isError: false,
error: undefined,
}),
}
})
vi.mock('common', async (importOriginal) => {
const actual = await importOriginal<typeof import('common')>()
return { ...actual, useParams: () => ({ ar_id: 'request-id' }) }
})
vi.mock('next/router', () => ({
useRouter: () => ({ isReady: true, push: mocks.routerPush }),
}))
vi.mock('sonner', () => ({
toast: { error: vi.fn() },
}))
vi.mock('@/lib/auth', () => ({
useSignOut: () => vi.fn(),
}))
vi.mock('@/lib/profile', () => ({
useProfileNameAndPicture: () => ({
username: 'alex',
primaryEmail: 'alex@example.com',
avatarUrl: undefined,
}),
}))
beforeEach(() => {
vi.clearAllMocks()
})
test('shows confirmation failures inline and keeps authorization available', async () => {
const user = userEvent.setup()
addAPIMock({
method: 'post',
path: '/platform/stripe/projects/provisioning/account_requests/:id/confirm',
response: () =>
HttpResponse.json<APIErrorBody>({ message: 'Confirmation failed' }, { status: 500 }),
})
customRender(<StripeProjectsLoginPage dehydratedState={undefined} />)
await user.click(screen.getByRole('button', { name: 'Authorize Stripe Projects' }))
const errorMessage = await screen.findByText(
'Failed to authorize Stripe Projects: Confirmation failed'
)
expect(errorMessage).toHaveAttribute('role', 'alert')
expect(toast.error).not.toHaveBeenCalled()
expect(screen.getByRole('button', { name: 'Authorize Stripe Projects' })).toBeEnabled()
})