diff --git a/packages/backend/src/helpers/billing/webhooks.ee.ts b/packages/backend/src/helpers/billing/webhooks.ee.ts index 056c754f..b9556d42 100644 --- a/packages/backend/src/helpers/billing/webhooks.ee.ts +++ b/packages/backend/src/helpers/billing/webhooks.ee.ts @@ -21,12 +21,11 @@ const handleSubscriptionCancelled = async (request: IRequest) => { .query() .findOne({ paddle_subscription_id: request.body.subscription_id, - }) - .patchAndFetch(formatSubscription(request)); + }); - if (request.body.status === 'deleted') { - await subscription.$query().delete(); - } + await subscription.$query().patchAndFetch(formatSubscription(request)); + + await subscription.$query().delete(); }; const handleSubscriptionPaymentSucceeded = async (request: IRequest) => { diff --git a/packages/web/src/components/CheckoutCompletedAlert/index.ee.tsx b/packages/web/src/components/CheckoutCompletedAlert/index.ee.tsx new file mode 100644 index 00000000..c2f41f64 --- /dev/null +++ b/packages/web/src/components/CheckoutCompletedAlert/index.ee.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { useLocation } from 'react-router-dom'; +import Alert from '@mui/material/Alert'; +import Typography from '@mui/material/Typography'; + +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function CheckoutCompletedAlert() { + const formatMessage = useFormatMessage(); + const location = useLocation(); + const state = location.state as { checkoutCompleted: boolean }; + + const checkoutCompleted = state?.checkoutCompleted; + + if (!checkoutCompleted) return ; + + return ( + + + {formatMessage('checkoutCompletedAlert.text')} + + + ); +} diff --git a/packages/web/src/components/UsageDataInformation/index.ee.tsx b/packages/web/src/components/UsageDataInformation/index.ee.tsx index 7ff84a5d..275c8cc6 100644 --- a/packages/web/src/components/UsageDataInformation/index.ee.tsx +++ b/packages/web/src/components/UsageDataInformation/index.ee.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; -import Alert from '@mui/material/Alert'; import Stack from '@mui/material/Stack'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -14,6 +13,7 @@ import Typography from '@mui/material/Typography'; import { TBillingCardAction } from '@automatisch/types'; import TrialOverAlert from 'components/TrialOverAlert/index.ee'; +import CheckoutCompletedAlert from 'components/CheckoutCompletedAlert/index.ee'; import * as URLS from 'config/urls'; import useBillingAndUsageData from 'hooks/useBillingAndUsageData.ee'; import useFormatMessage from 'hooks/useFormatMessage'; @@ -98,6 +98,8 @@ export default function UsageDataInformation() { + + diff --git a/packages/web/src/contexts/Paddle.ee.tsx b/packages/web/src/contexts/Paddle.ee.tsx index 7eab8fc9..32deb155 100644 --- a/packages/web/src/contexts/Paddle.ee.tsx +++ b/packages/web/src/contexts/Paddle.ee.tsx @@ -1,7 +1,10 @@ import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import * as URLS from 'config/urls'; import useCloud from 'hooks/useCloud'; import usePaddleInfo from 'hooks/usePaddleInfo.ee'; +import apolloClient from 'graphql/client'; declare global { interface Window { @@ -27,9 +30,32 @@ export const PaddleProvider = ( ): React.ReactElement => { const { children } = props; const isCloud = useCloud(); + const navigate = useNavigate(); const { sandbox, vendorId } = usePaddleInfo(); const [loaded, setLoaded] = React.useState(false); + const paddleEventHandler = React.useCallback(async (payload) => { + const { event, eventData } = payload; + if (event === 'Checkout.Close') { + const completed = eventData.checkout?.completed; + + if (completed) { + // Paddle has side effects in the background, + // so we need to refetch the relevant queries + await apolloClient.refetchQueries({ + include: ['GetTrialStatus', 'GetBillingAndUsage'], + }); + + navigate( + URLS.SETTINGS_BILLING_AND_USAGE, + { + state: { checkoutCompleted: true } + } + ); + } + } + }, [navigate]); + const value = React.useMemo(() => { return { loaded, @@ -69,8 +95,11 @@ export const PaddleProvider = ( window.Paddle.Environment.set('sandbox'); } - window.Paddle.Setup({ vendor: vendorId }); - }, [loaded, sandbox, vendorId]) + window.Paddle.Setup({ + vendor: vendorId, + eventCallback: paddleEventHandler, + }); + }, [loaded, sandbox, vendorId, paddleEventHandler]) return ( diff --git a/packages/web/src/hooks/useBillingAndUsageData.ee.ts b/packages/web/src/hooks/useBillingAndUsageData.ee.ts index 22dbb810..15ff6545 100644 --- a/packages/web/src/hooks/useBillingAndUsageData.ee.ts +++ b/packages/web/src/hooks/useBillingAndUsageData.ee.ts @@ -1,4 +1,6 @@ +import * as React from 'react'; import { useQuery } from '@apollo/client'; +import { useLocation } from 'react-router-dom'; import { DateTime } from 'luxon'; import { TSubscription } from '@automatisch/types'; @@ -30,7 +32,23 @@ type UseBillingAndUsageDataReturn = { } | null; export default function useBillingAndUsageData(): UseBillingAndUsageDataReturn { - const { data, loading } = useQuery(GET_BILLING_AND_USAGE); + const location = useLocation(); + const state = location.state as { checkoutCompleted: boolean }; + const { data, loading, startPolling, stopPolling } = useQuery(GET_BILLING_AND_USAGE); + const checkoutCompleted = state?.checkoutCompleted; + const hasSubscription = !!data?.getBillingAndUsage?.subscription?.status; + + React.useEffect(function pollDataUntilSubscriptionIsCreated() { + if (checkoutCompleted && !hasSubscription) { + startPolling(1000); + } + }, [checkoutCompleted, hasSubscription, startPolling]); + + React.useEffect(function stopPollingWhenSubscriptionIsCreated() { + if (checkoutCompleted && hasSubscription) { + stopPolling(); + } + }, [checkoutCompleted, hasSubscription, stopPolling]); if (loading) return null; diff --git a/packages/web/src/hooks/useTrialStatus.ee.ts b/packages/web/src/hooks/useTrialStatus.ee.ts index ece79ddd..edf67a96 100644 --- a/packages/web/src/hooks/useTrialStatus.ee.ts +++ b/packages/web/src/hooks/useTrialStatus.ee.ts @@ -1,4 +1,6 @@ +import * as React from 'react'; import { useQuery } from '@apollo/client'; +import { useLocation } from 'react-router-dom'; import { DateTime } from 'luxon'; import { GET_TRIAL_STATUS } from 'graphql/queries/get-trial-status.ee'; @@ -48,7 +50,23 @@ function getFeedbackPayload(date: DateTime) { export default function useTrialStatus(): UseTrialStatusReturn { const formatMessage = useFormatMessage(); - const { data, loading } = useQuery(GET_TRIAL_STATUS); + const location = useLocation(); + const state = location.state as { checkoutCompleted: boolean }; + const checkoutCompleted = state?.checkoutCompleted; + const { data, loading, startPolling, stopPolling } = useQuery(GET_TRIAL_STATUS); + const hasTrial = !!data?.getTrialStatus?.expireAt; + + React.useEffect(function pollDataUntilTrialEnds() { + if (checkoutCompleted && hasTrial) { + startPolling(1000); + } + }, [checkoutCompleted, hasTrial, startPolling]); + + React.useEffect(function stopPollingWhenTrialEnds() { + if (checkoutCompleted && !hasTrial) { + stopPolling(); + } + }, [checkoutCompleted, hasTrial, stopPolling]); if (loading || !data.getTrialStatus) return null; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index b4b7437e..22f19f9e 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -156,5 +156,6 @@ "trialBadge.xDaysLeft": "{remainingDays} trial {remainingDays, plural, one {day} other {days}} left", "trialBadge.endsToday": "Trial ends today", "trialBadge.over": "Trial is over", - "trialOverAlert.text": "Your free trial is over. Please upgrade your plan to continue using Automatisch." + "trialOverAlert.text": "Your free trial is over. Please upgrade your plan to continue using Automatisch.", + "checkoutCompletedAlert.text": "Thank you for upgrading your subscription and supporting our self-funded business!" }