Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 2e6eb862cc fix: add retry polling for subscription status on payment success page
https://sonarly.com/issue/18955?type=bug

After completing Stripe checkout, the PaymentSuccess page throws an error when the Stripe webhook hasn't yet created the subscription record. The user sees a cryptic error instead of automatic retry.

Fix: Replaced the single-attempt subscription check with a retry polling loop (5 attempts, 2-second intervals) in `PaymentSuccess.tsx`.

**What changed:**
1. Added retry loop: instead of making one `getCurrentUser` query and throwing on failure, the code now polls up to 5 times with 2-second delays between attempts, giving the Stripe webhook up to ~10 seconds to process
2. Replaced `throw new Error(...)` with `enqueueErrorSnackBar(...)`: on exhausted retries, shows a non-blocking snackbar notification instead of throwing an unhandled error that crashes to the error boundary and fires in Sentry
3. Changed `catch` to `finally` for `setIsLoading(false)`: the loading state is always reset, whether we succeed, exhaust retries, or encounter an unexpected error

Uses existing utilities (`sleep` from `~/utils/sleep`, `useSnackBar` from the snack bar manager) already used in other onboarding pages (CreateWorkspace, InviteTeam).
2026-03-27 09:28:27 +00:00
@@ -3,6 +3,7 @@ import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingModalCircularIcon } from '@/onboarding/components/OnboardingModalCircularIcon';
import { ModalContent } from 'twenty-ui/layout';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
@@ -17,6 +18,10 @@ import { AnimatedEaseIn } from 'twenty-ui/utilities';
import { useLazyQuery } from '@apollo/client/react';
import { GetCurrentUserDocument } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { sleep } from '~/utils/sleep';
const SUBSCRIPTION_CHECK_RETRY_DELAY_MS = 2000;
const SUBSCRIPTION_CHECK_MAX_RETRIES = 5;
const StyledTitleContainer = styled.div`
align-items: center;
@@ -33,6 +38,7 @@ export const PaymentSuccess = () => {
});
const setCurrentUser = useSetAtomState(currentUserState);
const [isLoading, setIsLoading] = useState(false);
const { enqueueErrorSnackBar } = useSnackBar();
const navigateWithSubscriptionCheck = async () => {
if (isLoading) return;
@@ -44,23 +50,32 @@ export const PaymentSuccess = () => {
return;
}
const result = await getCurrentUser();
const currentUser = result.data?.currentUser;
const refreshedSubscriptionStatus =
currentUser?.currentWorkspace?.currentBillingSubscription?.status;
for (
let attempt = 0;
attempt < SUBSCRIPTION_CHECK_MAX_RETRIES;
attempt++
) {
const result = await getCurrentUser();
const currentUser = result.data?.currentUser;
const refreshedSubscriptionStatus =
currentUser?.currentWorkspace?.currentBillingSubscription?.status;
if (isDefined(currentUser) && isDefined(refreshedSubscriptionStatus)) {
setCurrentUser(currentUser);
navigate(AppPath.CreateWorkspace);
return;
if (isDefined(currentUser) && isDefined(refreshedSubscriptionStatus)) {
setCurrentUser(currentUser);
navigate(AppPath.CreateWorkspace);
return;
}
if (attempt < SUBSCRIPTION_CHECK_MAX_RETRIES - 1) {
await sleep(SUBSCRIPTION_CHECK_RETRY_DELAY_MS);
}
}
throw new Error(
t`We're waiting for a confirmation from our payment provider (Stripe).\nPlease try again in a few seconds, sorry.`,
enqueueErrorSnackBar(
t`We're waiting for a confirmation from our payment provider (Stripe). Please try again in a few seconds.`,
);
} catch (error) {
} finally {
setIsLoading(false);
throw error;
}
};