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!"
}