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() await expect(queryByTestId('spinner')).toBeVisible()
}) })
test('Shows the total credits for Unknown subscription', async () => { const unKnownTierData = {
const data = { balance: {
balance: { monthlyApiCreditsRemaining: 10,
monthlyApiCreditsRemaining: 10, stableApiCreditsRemaining: 25,
stableApiCreditsRemaining: 25, },
}, subscriptions: {
subscriptions: { monthlyPayAsYouGoApiCreditsTotal: 20,
monthlyPayAsYouGoApiCreditsTotal: 20, name: "unknown",
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( server.use(
http.get('*/user/payment/balance', (req, res, ctx) => { http.get('*/user/payment/balance', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance)) 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 () => { test('Progress bar reflects ratio left of Free subscription', async () => {
const data = { const data = freeTierData
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 0,
},
subscriptions: {
monthlyPayAsYouGoApiCreditsTotal: 20,
name: "free",
}
}
server.use( server.use(
http.get('*/user/payment/balance', (req, res, ctx) => { http.get('*/user/payment/balance', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance)) 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 () => { test('Shows infinite credits for Pro subscription', async () => {
const data = { const data = proTierData
// These are all ignored
balance: {
monthlyApiCreditsRemaining: 10,
stableApiCreditsRemaining: 0,
},
subscriptions: {
// This should be ignored because it's Pro tier.
monthlyPayAsYouGoApiCreditsTotal: 20,
name: "pro",
}
}
server.use( server.use(
http.get('*/user/payment/balance', (req, res, ctx) => { http.get('*/user/payment/balance', (req, res, ctx) => {
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance)) 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) await expect(queryByTestId('billing-remaining-progress-bar-inline')).toBe(null)
}) })
test('Shows infinite credits for Enterprise subscription', async () => { test('Shows infinite credits for Enterprise subscription', async () => {
const data = { const data = 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",
}
}
server.use( server.use(
http.get('*/user/payment/balance', (req, res, ctx) => { 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('infinity')).toBeVisible()
await expect(queryByTestId('billing-remaining-progress-bar-inline')).toBe(null) 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, BillingRemainingMode,
} from '@src/components/BillingRemaining' } from '@src/components/BillingRemaining'
import type { BillingActor } from '@src/machines/billingMachine' import { type BillingActor } from '@src/machines/billingMachine'
export const BillingDialog = (props: { billingActor: BillingActor }) => { export const BillingDialog = (props: { billingActor: BillingActor }) => {
const billingContext = useSelector( const billingContext = useSelector(
@ -39,15 +39,18 @@ export const BillingDialog = (props: { billingActor: BillingActor }) => {
mode={BillingRemainingMode.ProgressBarStretch} mode={BillingRemainingMode.ProgressBarStretch}
billingActor={props.billingActor} billingActor={props.billingActor}
/> />
<a {!hasUnlimited && (
className="bg-ml-black text-ml-white rounded-lg text-center p-1 cursor-pointer" <a
href="https://zoo.dev/design-studio-pricing" className="bg-ml-black text-ml-white rounded-lg text-center p-1 cursor-pointer"
target="_blank" href="https://zoo.dev/design-studio-pricing"
rel="noopener noreferrer" target="_blank"
onClick={openExternalBrowserIfDesktop()} rel="noopener noreferrer"
> data-testid="billing-upgrade-button"
Upgrade onClick={openExternalBrowserIfDesktop()}
</a> >
Upgrade
</a>
)}
</div> </div>
</div> </div>
) )

View File

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