2
0

🚸 (billing) Improve feedback when subscription is "past_due"

This commit is contained in:
Baptiste Arnaud
2023-09-12 13:17:14 +02:00
parent 6375a2425f
commit 0ccc2efa45
11 changed files with 86 additions and 33 deletions

View File

@@ -61,20 +61,24 @@ export const getSubscription = authenticatedProcedure
})
const subscriptions = await stripe.subscriptions.list({
customer: workspace.stripeId,
limit: 1,
status: 'active',
})
const subscription = subscriptions?.data.shift()
const currentSubscription = subscriptions.data
.filter((sub) => ['past_due', 'active'].includes(sub.status))
.sort((a, b) => a.created - b.created)
.shift()
if (!subscription)
if (!currentSubscription)
return {
subscription: null,
}
return {
subscription: {
isYearly: subscription.items.data.some((item) => {
status: subscriptionSchema.shape.status.parse(
currentSubscription.status
),
isYearly: currentSubscription.items.data.some((item) => {
return (
priceIds.STARTER.chats.yearly === item.price.id ||
priceIds.STARTER.storage.yearly === item.price.id ||
@@ -82,9 +86,9 @@ export const getSubscription = authenticatedProcedure
priceIds.PRO.storage.yearly === item.price.id
)
}),
currency: subscription.currency as 'usd' | 'eur',
cancelDate: subscription.cancel_at
? new Date(subscription.cancel_at * 1000)
currency: currentSubscription.currency as 'usd' | 'eur',
cancelDate: currentSubscription.cancel_at
? new Date(currentSubscription.cancel_at * 1000)
: undefined,
},
}

View File

@@ -1,13 +1,13 @@
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import { useScopedI18n } from '@/locales'
import { Button, Link } from '@chakra-ui/react'
import { Button, ButtonProps, Link } from '@chakra-ui/react'
type Props = {
workspaceId: string
}
} & Pick<ButtonProps, 'colorScheme'>
export const BillingPortalButton = ({ workspaceId }: Props) => {
export const BillingPortalButton = ({ workspaceId, colorScheme }: Props) => {
const scopedT = useScopedI18n('billing')
const { showToast } = useToast()
const { data } = trpc.billing.getBillingPortalUrl.useQuery(
@@ -23,7 +23,12 @@ export const BillingPortalButton = ({ workspaceId }: Props) => {
}
)
return (
<Button as={Link} href={data?.billingPortalUrl} isLoading={!data}>
<Button
as={Link}
href={data?.billingPortalUrl}
isLoading={!data}
colorScheme={colorScheme}
>
{scopedT('billingPortalButton.label')}
</Button>
)

View File

@@ -100,7 +100,11 @@ export const ChangePlanForm = ({ workspace }: Props) => {
}
}
if (data?.subscription?.cancelDate) return null
if (
data?.subscription?.cancelDate ||
data?.subscription?.status === 'past_due'
)
return null
return (
<Stack spacing={6}>

View File

@@ -1,4 +1,11 @@
import { Text, HStack, Stack, Heading } from '@chakra-ui/react'
import {
Text,
HStack,
Stack,
Heading,
Alert,
AlertIcon,
} from '@chakra-ui/react'
import { Plan } from '@typebot.io/prisma'
import React from 'react'
import { PlanTag } from './PlanTag'
@@ -35,8 +42,21 @@ export const CurrentSubscriptionSummary = ({ workspace }: Props) => {
</Text>
)}
</HStack>
{data?.subscription?.status === 'past_due' && (
<Alert fontSize="sm" status="error">
<AlertIcon />
{scopedT('pastDueAlert')}
</Alert>
)}
{isSubscribed && <BillingPortalButton workspaceId={workspace.id} />}
{isSubscribed && (
<BillingPortalButton
workspaceId={workspace.id}
colorScheme={
data?.subscription?.status === 'past_due' ? 'blue' : undefined
}
/>
)}
</Stack>
)
}

View File

@@ -121,6 +121,8 @@ export default {
'billing.currentSubscription.heading': 'Abonnement',
'billing.currentSubscription.subheading': 'Aktuelles Workspace-Abonnement:',
'billing.currentSubscription.cancelDate': 'Wird storniert am',
'billing.currentSubscription.pastDueAlert':
'Die letzte Zahlung ist fehlgeschlagen. Gehen Sie zum Abrechnungsportal, um fortzufahren und eine Kündigung Ihres Abonnements zu vermeiden.',
'billing.invoices.heading': 'Rechnungen',
'billing.invoices.empty': 'Keine Rechnungen für diesen Workspace gefunden.',
'billing.invoices.paidAt': 'Bezahlt am',

View File

@@ -116,6 +116,8 @@ export default {
'billing.currentSubscription.heading': 'Subscription',
'billing.currentSubscription.subheading': 'Current workspace subscription:',
'billing.currentSubscription.cancelDate': 'Will be cancelled on',
'billing.currentSubscription.pastDueAlert':
'The latest payment failed. Head over to the billing portal to proceed and avoid having your subscription canceled.',
'billing.invoices.heading': 'Invoices',
'billing.invoices.empty': 'No invoices found for this workspace.',
'billing.invoices.paidAt': 'Paid at',

View File

@@ -118,6 +118,8 @@ export default {
'billing.currentSubscription.heading': 'Abonnement',
'billing.currentSubscription.subheading': 'Abonnement actuel du workspace :',
'billing.currentSubscription.cancelDate': 'Sera annulé le',
'billing.currentSubscription.pastDueAlert':
"Le dernier paiement a échoué. Rendez-vous sur le portail de facturation pour effectuer la procédure et éviter l'annulation de votre abonnement.",
'billing.invoices.heading': 'Factures',
'billing.invoices.empty': 'Aucune facture trouvée pour ce workspace.',
'billing.invoices.paidAt': 'Payé le',

View File

@@ -120,6 +120,8 @@ export default {
'billing.currentSubscription.subheading':
'Assinatura atual do espaço de trabalho:',
'billing.currentSubscription.cancelDate': 'Será cancelado em',
'billing.currentSubscription.pastDueAlert':
'O último pagamento falhou. Acesse o portal de faturamento para prosseguir e evitar o cancelamento da sua assinatura.',
'billing.invoices.heading': 'Faturas',
'billing.invoices.empty':
'Nenhuma fatura encontrada para este espaço de trabalho.',

View File

@@ -121,6 +121,8 @@ export default {
'billing.currentSubscription.subheading':
'Subscrição actual do espaço de trabalho:',
'billing.currentSubscription.cancelDate': 'Será cancelado em',
'billing.currentSubscription.pastDueAlert':
'O último pagamento falhou. Acesse o portal de faturamento para continuar e evitar o cancelamento da sua assinatura.',
'billing.invoices.heading': 'Facturas',
'billing.invoices.empty':
'Nenhuma factura encontrada para este espaço de trabalho.',

View File

@@ -30689,11 +30689,19 @@
"cancelDate": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"enum": [
"active",
"past_due"
]
}
},
"required": [
"isYearly",
"currency"
"currency",
"status"
],
"additionalProperties": false
},
@@ -32760,7 +32768,7 @@
}
}
},
"/typebots/{typebotId}/blocks/{blockId}/openai/models": {
"/openai/models": {
"get": {
"operationId": "openAI-listModels",
"summary": "List OpenAI models",
@@ -32773,22 +32781,6 @@
}
],
"parameters": [
{
"name": "typebotId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "blockId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "credentialsId",
"in": "query",
@@ -32804,6 +32796,23 @@
"schema": {
"type": "string"
}
},
{
"name": "baseUrl",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "https://api.openai.com/v1"
}
},
{
"name": "apiVersion",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {

View File

@@ -4,6 +4,7 @@ export const subscriptionSchema = z.object({
isYearly: z.boolean(),
currency: z.enum(['eur', 'usd']),
cancelDate: z.date().optional(),
status: z.enum(['active', 'past_due']),
})
export type Subscription = z.infer<typeof subscriptionSchema>