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:
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user