Enterprise plans should not have the upgrade button (#7628)

* Enterprise plans should not have the upgrade button
Fixes #7627

* Move the check to BillingDialog

* Hide home box and change bool check

* Add component tests

* Clean up
This commit is contained in:
Pierre Jacquier
2025-06-28 12:03:41 -04:00
committed by GitHub
parent 7ec11d23c8
commit af658c909d
3 changed files with 130 additions and 62 deletions

View File

@ -125,18 +125,57 @@ test('Shows a loading spinner when uninitialized credit count', async () => {
await expect(queryByTestId('spinner')).toBeVisible()
})
test('Shows the total credits for Unknown subscription', async () => {
const data = {
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 25,
},
subscriptions: {
monthlyPayAsYouGoApiCreditsTotal: 20,
name: "unknown",
}
const unKnownTierData = {
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 25,
},
subscriptions: {
monthlyPayAsYouGoApiCreditsTotal: 20,
name: "unknown",
}
}
const freeTierData = {
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 0,
},
subscriptions: {
monthlyPayAsYouGoApiCreditsTotal: 20,
name: "free",
}
}
const proTierData = {
// These are all ignored
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 0,
},
subscriptions: {
// This should be ignored because it's Pro tier.
monthlyPayAsYouGoApiCreditsTotal: 20,
name: "pro",
}
}
const enterpriseTierData = {
// These are all ignored, user is part of an org.
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 0,
},
subscriptions: {
// This should be ignored because it's Pro tier.
monthlyPayAsYouGoApiCreditsTotal: 20,
// This should be ignored because the user is part of an Org.
name: "free",
}
}
test('Shows the total credits for Unknown subscription', async () => {
const data = unKnownTierData
server.use(
http.get('*/user/payment/balance', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
@ -166,17 +205,7 @@ test('Shows the total credits for Unknown subscription', async () => {
})
test('Progress bar reflects ratio left of Free subscription', async () => {
const data = {
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 0,
},
subscriptions: {
monthlyPayAsYouGoApiCreditsTotal: 20,
name: "free",
}
}
const data = freeTierData
server.use(
http.get('*/user/payment/balance', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
@ -212,19 +241,7 @@ test('Progress bar reflects ratio left of Free subscription', async () => {
})
})
test('Shows infinite credits for Pro subscription', async () => {
const data = {
// These are all ignored
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 0,
},
subscriptions: {
// This should be ignored because it's Pro tier.
monthlyPayAsYouGoApiCreditsTotal: 20,
name: "pro",
}
}
const data = proTierData
server.use(
http.get('*/user/payment/balance', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
@ -255,19 +272,7 @@ test('Shows infinite credits for Pro subscription', async () => {
await expect(queryByTestId('billing-remaining-progress-bar-inline')).toBe(null)
})
test('Shows infinite credits for Enterprise subscription', async () => {
const data = {
// These are all ignored, user is part of an org.
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 0,
},
subscriptions: {
// This should be ignored because it's Pro tier.
monthlyPayAsYouGoApiCreditsTotal: 20,
// This should be ignored because the user is part of an Org.
name: "free",
}
}
const data = enterpriseTierData
server.use(
http.get('*/user/payment/balance', (req, res, ctx) => {
@ -297,3 +302,58 @@ test('Shows infinite credits for Enterprise subscription', async () => {
await expect(queryByTestId('infinity')).toBeVisible()
await expect(queryByTestId('billing-remaining-progress-bar-inline')).toBe(null)
})
test('Show upgrade button if credits are not infinite', async () => {
const data = freeTierData
server.use(
http.get('*/user/payment/balance', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
}),
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentSubscriptionsResponse(data.subscriptions))
}),
http.get('*/org', (req, res, ctx) => {
return new HttpResponse(403)
}),
)
const billingActor = createActor(billingMachine, { input: BILLING_CONTEXT_DEFAULTS }).start()
const { queryByTestId } = render(<BillingDialog
billingActor={billingActor}
/>)
await act(() => {
billingActor.send({ type: BillingTransition.Update, apiToken: "it doesn't matter wtf this is :)" })
})
await expect(queryByTestId('billing-upgrade-button')).toBeVisible()
})
test('Hide upgrade button if credits are infinite', async () => {
const data = enterpriseTierData
server.use(
http.get('*/user/payment/balance', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
}),
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentSubscriptionsResponse(data.subscriptions))
}),
// Ok finally the first use of an org lol
http.get('*/org', (req, res, ctx) => {
return HttpResponse.json(createOrgResponse())
}),
)
const billingActor = createActor(billingMachine, { input: BILLING_CONTEXT_DEFAULTS }).start()
const { queryByTestId } = render(<BillingDialog
billingActor={billingActor}
/>)
await act(() => {
billingActor.send({ type: BillingTransition.Update, apiToken: "it doesn't matter wtf this is :)" })
})
await expect(queryByTestId('billing-upgrade-button')).toBe(null)
})

View File

@ -6,7 +6,7 @@ import {
BillingRemainingMode,
} from '@src/components/BillingRemaining'
import type { BillingActor } from '@src/machines/billingMachine'
import { type BillingActor } from '@src/machines/billingMachine'
export const BillingDialog = (props: { billingActor: BillingActor }) => {
const billingContext = useSelector(
@ -39,15 +39,18 @@ export const BillingDialog = (props: { billingActor: BillingActor }) => {
mode={BillingRemainingMode.ProgressBarStretch}
billingActor={props.billingActor}
/>
<a
className="bg-ml-black text-ml-white rounded-lg text-center p-1 cursor-pointer"
href="https://zoo.dev/design-studio-pricing"
target="_blank"
rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()}
>
Upgrade
</a>
{!hasUnlimited && (
<a
className="bg-ml-black text-ml-white rounded-lg text-center p-1 cursor-pointer"
href="https://zoo.dev/design-studio-pricing"
target="_blank"
rel="noopener noreferrer"
data-testid="billing-upgrade-button"
onClick={openExternalBrowserIfDesktop()}
>
Upgrade
</a>
)}
</div>
</div>
)

View File

@ -66,6 +66,7 @@ import {
defaultLocalStatusBarItems,
defaultGlobalStatusBarItems,
} from '@src/components/StatusBar/defaultStatusBarItems'
import { useSelector } from '@xstate/react'
type ReadWriteProjectState = {
value: boolean
@ -81,6 +82,8 @@ const Home = () => {
const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
const apiToken = useToken()
const networkMachineStatus = useNetworkMachineStatus()
const billingContext = useSelector(billingActor, ({ context }) => context)
const hasUnlimitedCredits = billingContext.credits === Infinity
// Only create the native file menus on desktop
useEffect(() => {
@ -354,11 +357,13 @@ const Home = () => {
</li>
</ul>
<ul className="flex flex-col">
<li className="contents">
<div className="my-2">
<BillingDialog billingActor={billingActor} />
</div>
</li>
{!hasUnlimitedCredits && (
<li className="contents">
<div className="my-2">
<BillingDialog billingActor={billingActor} />
</div>
</li>
)}
<li className="contents">
<ActionButton
Element="externalLink"