feat: Brainstorm Topics with GSC + Issue #518 fixes + Blog Editor enhancements

Issue #518 - Subscription not updating after checkout:
- Fix stale closure in SubscriptionContext checkout polling (use subscriptionRef)
- Move checkout success polling from InitialRouteHandler into SubscriptionContext
- Remove redundant polling code from InitialRouteHandler
- Fix plan label: 'Free' instead of 'No Plan', proper capitalization
- Add plan refresh button in UserBadge
- Add 'View Costing Details' to UserBadge dropdown
- Rename 'ALwrity Podcast Maker' to 'Podcast Creator' across UI
- Clean subscription=success URL param after verification

Blog Writer WYSIWYG Editor enhancements:
- Per-section preview toggle (view/edit icons)
- Enhanced hover-based toolbar
- Circular SVG progress stats bar with detailed tooltip
- Research tool chips in stats bar footer
- Per-section TTS with useTextToSpeech hook (browser native)
- Full blog preview modal with print/PDF support
- PlayAllTTSButton: sequential playback with progress bar
- OnThisPageNav: floating sidebar with scroll tracking
- Section data attributes for scroll anchoring

GSC Brainstorm Topics feature:
- Backend: gsc_brainstorm_service.py (rule-based + LLM recommendations)
- Backend: POST /gsc/brainstorm endpoint with 3-word minimum validation
- Frontend: gscBrainstorm.ts API client
- Frontend: useGSCBrainstormConnection hook (popup OAuth, no /onboarding redirect)
- Frontend: useGSCBrainstorm hook (connect check + brainstorm call)
- Frontend: GSCBrainstormModal (3-tab results: Opportunities, Gaps, AI Recs)
- Frontend: BrainstormButton (visible at 3+ words, GSC connect overlay)
- Wire BrainstormButton into ManualResearchForm and ResearchAction
- Add blog_writer to gsc_auth router features for ALWRITY_ENABLED_FEATURES
This commit is contained in:
ajaysi
2026-05-20 22:34:37 +05:30
parent 68190dedb3
commit 644e72d289
98 changed files with 16137 additions and 2501 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { Box, CircularProgress, Typography } from '@mui/material';
import { useOnboarding } from '../../contexts/OnboardingContext';
@@ -8,9 +8,6 @@ import { shouldSkipOnboarding, getDefaultLandingRoute, isFeatureOnlyMode, getSin
import { restoreNavigationState } from '../../utils/navigationState';
import ConnectionErrorPage from '../shared/ConnectionErrorPage';
const CHECKOUT_POLL_INTERVAL_MS = 2000;
const CHECKOUT_POLL_MAX_ATTEMPTS = 10;
const InitialRouteHandler: React.FC = () => {
const navigateAndLog = (to: string) => {
console.log(`InitialRouteHandler: Redirecting to ${to}`);
@@ -27,11 +24,6 @@ const InitialRouteHandler: React.FC = () => {
error: null,
});
// Post-checkout polling state
const [checkoutPolling, setCheckoutPolling] = useState(false);
const checkoutPollAttempts = useRef(0);
// Track whether the initial subscription check has completed
// Prevents premature routing decisions before we know the user's plan
const [initialCheckDone, setInitialCheckDone] = useState(false);
const urlParams = new URLSearchParams(location.search);
@@ -79,48 +71,22 @@ const InitialRouteHandler: React.FC = () => {
return () => clearTimeout(timeoutId);
}, []);
// Handle post-checkout: when Stripe redirects back with ?subscription=success,
// the webhook may not have processed yet. Poll until subscription becomes active.
// Post-checkout: SubscriptionContext handles the verification polling.
// InitialRouteHandler only needs to detect checkout success for routing decisions.
// The actual subscription update now happens via verifyCheckout polling in SubscriptionContext.
useEffect(() => {
if (!isCheckoutSuccess) return;
// If subscription is already active after checkout, clean up URL
if (subscription?.active && subscription.plan !== 'none' && subscription.plan !== 'free') {
// Webhook has processed — subscription is active, stop polling
if (checkoutPolling) {
console.log('InitialRouteHandler: Checkout success — subscription confirmed active, stopping poll');
setCheckoutPolling(false);
}
return;
}
// Start polling if webhook hasn't processed yet
if (!checkoutPolling && checkoutPollAttempts.current === 0) {
console.log('InitialRouteHandler: Checkout success — subscription not yet active, starting poll');
setCheckoutPolling(true);
}
}, [isCheckoutSuccess, subscription, checkoutPolling]);
// Polling effect for post-checkout
useEffect(() => {
if (!checkoutPolling) return;
if (checkoutPollAttempts.current >= CHECKOUT_POLL_MAX_ATTEMPTS) {
console.log('InitialRouteHandler: Checkout polling exhausted — proceeding with current state');
setCheckoutPolling(false);
return;
}
const timer = setTimeout(async () => {
checkoutPollAttempts.current += 1;
console.log(`InitialRouteHandler: Checkout poll attempt ${checkoutPollAttempts.current}/${CHECKOUT_POLL_MAX_ATTEMPTS}`);
console.log('InitialRouteHandler: Checkout success — subscription confirmed:', subscription.plan);
try {
await checkSubscription();
} catch (err) {
console.error('InitialRouteHandler: Checkout poll check failed:', err);
window.history.replaceState({}, document.title, window.location.pathname);
} catch (e) {
// Ignore URL cleanup errors
}
}, CHECKOUT_POLL_INTERVAL_MS);
return () => clearTimeout(timer);
}, [checkoutPolling, checkSubscription]);
}
}, [isCheckoutSuccess, subscription]);
// Initialize onboarding when subscription is confirmed (but not on checkout success — let redirect happen)
useEffect(() => {
@@ -168,28 +134,6 @@ const InitialRouteHandler: React.FC = () => {
);
}
// Show polling spinner during post-checkout webhook wait
if (checkoutPolling) {
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
minHeight="100vh"
gap={2}
>
<CircularProgress size={60} />
<Typography variant="h6" color="textSecondary">
Activating your subscription...
</Typography>
<Typography variant="body2" color="textSecondary">
This may take a few seconds.
</Typography>
</Box>
);
}
// Post-checkout: subscription is now active (or poll exhausted)
if (isCheckoutSuccess && subscription?.active && subscription.plan !== 'none' && subscription.plan !== 'free') {
// Restore navigation state (saved before Stripe redirect)
@@ -232,7 +176,7 @@ const InitialRouteHandler: React.FC = () => {
hasError: false,
error: null,
});
checkSubscription().catch((err) => {
checkSubscription(true).catch((err) => {
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
setConnectionError({
hasError: true,

View File

@@ -3,6 +3,8 @@ import { useAuth } from '@clerk/clerk-react';
import { setAuthTokenGetter, setClerkSignOut } from '../../api/client';
import { setMediaAuthTokenGetter } from '../../utils/fetchMediaBlobUrl';
import { setBillingAuthTokenGetter } from '../../services/billingService';
import { hallucinationDetectorService } from '../../services/hallucinationDetectorService';
import { writingAssistantService } from '../../services/writingAssistantService';
const TokenInstaller: React.FC = () => {
const { getToken, userId, isSignedIn, signOut } = useAuth();
@@ -35,6 +37,8 @@ const TokenInstaller: React.FC = () => {
setAuthTokenGetter(tokenGetter);
setBillingAuthTokenGetter(tokenGetter);
setMediaAuthTokenGetter(tokenGetter);
hallucinationDetectorService.setAuthTokenGetter(tokenGetter);
writingAssistantService.setAuthTokenGetter(tokenGetter);
}, [getToken]);
useEffect(() => {