diff --git a/apps/studio/pages/partners/stripe/projects/login.tsx b/apps/studio/pages/partners/stripe/projects/login.tsx index 4f1d396404e..dd19e0ca54a 100644 --- a/apps/studio/pages/partners/stripe/projects/login.tsx +++ b/apps/studio/pages/partners/stripe/projects/login.tsx @@ -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 = () => { + )} @@ -195,6 +205,7 @@ const StripeProjectsLoginPage: NextPageWithLayout = () => { + )} diff --git a/apps/studio/tests/pages/stripe-projects-login.test.tsx b/apps/studio/tests/pages/stripe-projects-login.test.tsx new file mode 100644 index 00000000000..be2c55fdf11 --- /dev/null +++ b/apps/studio/tests/pages/stripe-projects-login.test.tsx @@ -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() + 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() + 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({ message: 'Confirmation failed' }, { status: 500 }), + }) + + customRender() + + 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() +})