Authorizing will redirect you to {redirectUrl}
diff --git a/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx b/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx
index 621e49ea9d7..6d1ab488c80 100644
--- a/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx
+++ b/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx
@@ -142,38 +142,57 @@ export function ApiAuthorizationValidScreen({
} = useApiAuthorizationQuery({ id: auth_id })
const isApproved = (requester?.approved_at ?? null) !== null
- const { mutate: approveRequest } = useApiAuthorizationApproveMutation({
+ const {
+ mutate: approveRequest,
+ error: approveError,
+ reset: resetApproveError,
+ } = useApiAuthorizationApproveMutation({
onSuccess: (res) => {
window.location.href = res.url
},
+ onError: () => {
+ setApprovalState('indeterminate')
+ },
})
- const { mutate: declineRequest } = useApiAuthorizationDeclineMutation({
+ const {
+ mutate: declineRequest,
+ error: declineError,
+ reset: resetDeclineError,
+ } = useApiAuthorizationDeclineMutation({
onSuccess: () => {
toast.success('Declined API authorization request')
navigate('/organizations')
},
+ onError: () => {
+ setApprovalState('indeterminate')
+ },
})
+ const actionError = approveError
+ ? `Failed to authorize request: ${approveError.message}`
+ : declineError
+ ? `Failed to cancel authorization request: ${declineError.message}`
+ : undefined
+ const resetActionError = () => {
+ resetApproveError()
+ resetDeclineError()
+ }
const onApproveRequest = form.handleSubmit((values) => {
if (approvalState !== 'indeterminate') {
return
}
+ resetActionError()
setApprovalState('approving')
- approveRequest(
- { id: auth_id, slug: values.selectedOrgSlug },
- { onError: () => setApprovalState('indeterminate') }
- )
+ approveRequest({ id: auth_id, slug: values.selectedOrgSlug })
})
const onDeclineRequest = form.handleSubmit((values) => {
if (approvalState !== 'indeterminate') {
return
}
+ resetActionError()
setApprovalState('declining')
- declineRequest(
- { id: auth_id, slug: values.selectedOrgSlug },
- { onError: () => setApprovalState('indeterminate') }
- )
+ declineRequest({ id: auth_id, slug: values.selectedOrgSlug })
})
if (isLoading) {
@@ -224,6 +243,8 @@ export function ApiAuthorizationValidScreen({
requester={effectiveRequester}
requestedOrganizationSlug={effectiveOrganizationSlug}
organizations={effectiveOrganizationsState}
+ actionError={actionError}
+ onOrganizationChange={resetActionError}
onApprove={onApproveRequest}
onDecline={onDeclineRequest}
/>
diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.tsx
index 1dd3511cd3d..42fababc170 100644
--- a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.tsx
+++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.tsx
@@ -1,6 +1,5 @@
import Link from 'next/link'
-import { useEffect, useMemo, useState } from 'react'
-import { toast } from 'sonner'
+import { useEffect, useEffectEvent, useMemo, useState } from 'react'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
@@ -20,7 +19,10 @@ import {
type CloudMarketplaceOnboardingInfo,
} from '@/components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query'
import { NewAwsMarketplaceOrgModal } from '@/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal'
-import { InterstitialAccountRow } from '@/components/layouts/InterstitialLayout'
+import {
+ InterstitialAccountRow,
+ InterstitialActionError,
+} from '@/components/layouts/InterstitialLayout'
import { InlineLink } from '@/components/ui/InlineLink'
import { useOrganizationLinkAwsMarketplaceMutation } from '@/data/organizations/organization-link-aws-marketplace-mutation'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
@@ -37,12 +39,6 @@ export const AwsMarketplaceOnboardingScreen = ({ buyerId }: { buyerId?: string }
const [linkedOrgSlug, setLinkedOrgSlug] = useState(null)
const [showOrgCreationDialog, setShowOrgCreationDialog] = useState(false)
- useEffect(() => {
- setSelectedOrgSlug(null)
- setLinkedOrgSlug(null)
- setShowOrgCreationDialog(false)
- }, [buyerId])
-
const {
data: organizations,
error: organizationsError,
@@ -72,15 +68,20 @@ export const AwsMarketplaceOnboardingScreen = ({ buyerId }: { buyerId?: string }
{ enabled: !!shouldLoadOnboardingInfo }
)
- const { mutate: linkOrganization, isPending: isLinkingOrganization } =
- useOrganizationLinkAwsMarketplaceMutation({
- onSuccess: (_, variables) => {
- setLinkedOrgSlug(variables.slug)
- },
- onError: (error) => {
- toast.error(error.message, { duration: 7_000 })
- },
- })
+ const {
+ mutate: linkOrganization,
+ isPending: isLinkingOrganization,
+ error: linkOrganizationError,
+ reset: resetLinkOrganizationError,
+ } = useOrganizationLinkAwsMarketplaceMutation({
+ onSuccess: (_, variables) => {
+ setLinkedOrgSlug(variables.slug)
+ },
+ onError: () => undefined,
+ })
+ const linkError = linkOrganizationError
+ ? `Failed to link organization: ${linkOrganizationError.message}`
+ : undefined
const effectiveOrganizations = useMemo(
() => organizations ?? EMPTY_ORGANIZATIONS,
@@ -142,6 +143,18 @@ export const AwsMarketplaceOnboardingScreen = ({ buyerId }: { buyerId?: string }
}
}, [onboardingInfo, effectiveOrganizations])
+ const resetOnBuyerChange = useEffectEvent(() => {
+ setSelectedOrgSlug(null)
+ setLinkedOrgSlug(null)
+ setShowOrgCreationDialog(false)
+ resetLinkOrganizationError()
+ })
+
+ useEffect(() => {
+ resetOnBuyerChange()
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- useEffectEvent fn intentionally not a dep (eslint-plugin-react-hooks v5 doesn't recognize stable useEffectEvent yet)
+ }, [buyerId])
+
if (!buyerId) {
return (
@@ -249,6 +262,7 @@ export const AwsMarketplaceOnboardingScreen = ({ buyerId }: { buyerId?: string }
const primaryAction = hasLinkableOrganizations
? () => {
if (!selectedOrgSlug || !buyerId) return
+ resetLinkOrganizationError()
linkOrganization({ slug: selectedOrgSlug, buyerId })
}
: () => setShowOrgCreationDialog(true)
@@ -278,7 +292,10 @@ export const AwsMarketplaceOnboardingScreen = ({ buyerId }: { buyerId?: string }
}
selectedSlug={selectedOrgSlug}
disabled={isLinking}
- onSelect={setSelectedOrgSlug}
+ onSelect={(slug) => {
+ setSelectedOrgSlug(slug)
+ resetLinkOrganizationError()
+ }}
createLabel={hasLinkableOrganizations ? 'Create new organization' : undefined}
onCreate={hasLinkableOrganizations ? () => setShowOrgCreationDialog(true) : undefined}
/>
@@ -292,21 +309,26 @@ export const AwsMarketplaceOnboardingScreen = ({ buyerId }: { buyerId?: string }
)}
-
- {primaryLabel}
-
-
-
- Learn more
- {' '}
- about billing through AWS.
-
+
+
+ {primaryLabel}
+
+
+
+ {!linkError && (
+
+
+ Learn more
+ {' '}
+ about billing through AWS.
+
+ )}
diff --git a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
index 7e62eb43785..bdd1c39c301 100644
--- a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
+++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
@@ -13,6 +13,7 @@ import {
import { OrganizationInviteError } from './OrganizationInviteError'
import {
InterstitialAccountRow,
+ InterstitialActionError,
InterstitialLayout,
SupabaseLogo,
} from '@/components/layouts/InterstitialLayout'
@@ -197,13 +198,9 @@ export const OrganizationInvite = () => {