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

@@ -123,3 +123,187 @@ async def stripe_webhook(
except Exception as e:
logger.error(f"Error processing webhook: {e}")
raise HTTPException(status_code=500, detail="Webhook processing failed")
@router.get("/verify-checkout/{user_id}")
async def verify_checkout_status(
user_id: str,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user),
request: Request = None
) -> Dict[str, Any]:
"""
Directly query Stripe for user's current subscription status.
Used during post-checkout polling to get fresh data without waiting for webhooks.
Rate limited: 5 requests per minute per user to prevent abuse.
"""
from ..dependencies import verify_user_access
from models.subscription_models import UserSubscription, SubscriptionPlan, SubscriptionTier
from services.subscription import PricingService
from api.subscription.utils import format_plan_limits
from datetime import datetime
verify_user_access(user_id, current_user)
# Rate limiting: 5 requests per minute per user
now = time.time()
window_start = now - 60 # 1 minute window
if user_id not in _checkout_attempts_by_user:
_checkout_attempts_by_user[user_id] = []
attempts = _checkout_attempts_by_user[user_id]
attempts[:] = [ts for ts in attempts if ts >= window_start]
attempts.append(now)
_checkout_attempts_by_user[user_id] = attempts
if len(attempts) > 5:
client_ip = request.client.host if request and request.client else "unknown"
logger.warning(f"Verify-checkout rate limit exceeded for user_id={user_id}, ip={client_ip}")
raise HTTPException(status_code=429, detail="Too many verification requests. Please wait before trying again.")
stripe_service = StripeService(db)
try:
# First, try to find user in local DB
subscription = db.query(UserSubscription).filter(
UserSubscription.user_id == user_id
).first()
stripe_customer_id = subscription.stripe_customer_id if subscription else None
# If no stripe_customer_id in DB, try to find it by email
if not stripe_customer_id:
try:
import stripe
# Get user email from auth context
user_email = current_user.get("email")
if user_email:
customers = stripe.Customer.list(email=user_email, limit=1)
if customers and customers.data:
stripe_customer_id = customers.data[0].id
logger.info(f"Verify-checkout: Found Stripe customer by email for user {user_id}")
# Update DB with found customer ID
if subscription:
subscription.stripe_customer_id = stripe_customer_id
db.commit()
else:
logger.info(f"Verify-checkout: No local subscription record for user {user_id}, will query Stripe directly")
except Exception as email_err:
logger.warning(f"Failed to find Stripe customer by email: {email_err}")
# If user has a Stripe customer ID, query Stripe directly
if stripe_customer_id:
try:
import stripe
stripe_subscriptions = stripe.Subscription.list(
customer=stripe_customer_id,
status="active",
limit=1
)
if stripe_subscriptions and stripe_subscriptions.data:
stripe_sub = stripe_subscriptions.data[0]
price_id = stripe_sub['items']['data'][0]['price']['id']
logger.info(f"Verify-checkout: Found active Stripe subscription for user {user_id}, plan from price {price_id}")
# Update local DB with fresh Stripe data
stripe_service._update_user_subscription(
user_id,
stripe_customer_id=stripe_customer_id,
stripe_subscription_id=stripe_sub.id,
status="active",
price_id=price_id
)
# Clear caches
try:
PricingService.clear_user_cache(user_id)
except Exception:
pass
try:
from api.subscription.cache import clear_dashboard_cache
clear_dashboard_cache(user_id)
except Exception:
pass
db.expire_all()
# Re-query with fresh data
subscription = db.query(UserSubscription).filter(
UserSubscription.user_id == user_id,
UserSubscription.is_active == True
).first()
if subscription:
return {
"success": True,
"data": {
"active": True,
"plan": subscription.plan.tier.value,
"tier": subscription.plan.tier.value,
"can_use_api": True,
"limits": format_plan_limits(subscription.plan),
"source": "stripe_direct"
}
}
except Exception as stripe_err:
logger.warning(f"Failed to query Stripe directly for user {user_id}: {stripe_err}")
# Fallback to local DB status
if subscription and subscription.is_active:
from services.subscription.pricing_service import PricingService
pricing = PricingService(db)
try:
pricing._ensure_subscription_current(subscription)
except Exception:
pass
return {
"success": True,
"data": {
"active": True,
"plan": subscription.plan.tier.value,
"tier": subscription.plan.tier.value,
"can_use_api": True,
"limits": format_plan_limits(subscription.plan),
"source": "local_db"
}
}
# No active subscription - return free tier
free_plan = db.query(SubscriptionPlan).filter(
SubscriptionPlan.tier == SubscriptionTier.FREE,
SubscriptionPlan.is_active == True
).first()
if free_plan:
return {
"success": True,
"data": {
"active": True,
"plan": "free",
"tier": "free",
"can_use_api": True,
"limits": format_plan_limits(free_plan),
"source": "free_tier"
}
}
return {
"success": True,
"data": {
"active": False,
"plan": "none",
"tier": "none",
"can_use_api": False,
"reason": "No active subscription found",
"source": "none"
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error verifying checkout status for user {user_id}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to verify subscription: {str(e)}")