Compare commits
1 Commits
codex/add-
...
codex/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1ff406d03 |
23
.github/workflows/lint-forced-user-id.yml
vendored
23
.github/workflows/lint-forced-user-id.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Lint Forced User ID Patterns
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint-forced-user-id:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Check for forced/hardcoded user_id patterns
|
||||
run: python backend/scripts/check_forced_user_id_patterns.py
|
||||
@@ -16,11 +16,6 @@ class RouterManager:
|
||||
self.app = app
|
||||
self.included_routers = []
|
||||
self.failed_routers = []
|
||||
|
||||
@staticmethod
|
||||
def _demo_release_mode_enabled() -> bool:
|
||||
"""Return True when demo-release safety mode is enabled."""
|
||||
return os.getenv("ALWRITY_DEMO_RELEASE", "false").lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
def include_router_safely(self, router, router_name: str = None) -> bool:
|
||||
"""Include a router safely with error handling."""
|
||||
@@ -93,27 +88,16 @@ class RouterManager:
|
||||
from routers.seo_tools import router as seo_tools_router
|
||||
self.include_router_safely(seo_tools_router, "seo_tools")
|
||||
|
||||
demo_release_mode = self._demo_release_mode_enabled()
|
||||
|
||||
# Facebook Writer router
|
||||
if demo_release_mode:
|
||||
logger.info("⏭️ Skipping facebook_writer router in demo-release mode")
|
||||
else:
|
||||
from api.facebook_writer.routers import facebook_router
|
||||
self.include_router_safely(facebook_router, "facebook_writer")
|
||||
from api.facebook_writer.routers import facebook_router
|
||||
self.include_router_safely(facebook_router, "facebook_writer")
|
||||
|
||||
# LinkedIn routers
|
||||
if demo_release_mode:
|
||||
logger.info("⏭️ Skipping linkedin router in demo-release mode")
|
||||
else:
|
||||
from routers.linkedin import router as linkedin_router
|
||||
self.include_router_safely(linkedin_router, "linkedin")
|
||||
from routers.linkedin import router as linkedin_router
|
||||
self.include_router_safely(linkedin_router, "linkedin")
|
||||
|
||||
if demo_release_mode:
|
||||
logger.info("⏭️ Skipping linkedin_image router in demo-release mode")
|
||||
else:
|
||||
from api.linkedin_image_generation import router as linkedin_image_router
|
||||
self.include_router_safely(linkedin_image_router, "linkedin_image")
|
||||
from api.linkedin_image_generation import router as linkedin_image_router
|
||||
self.include_router_safely(linkedin_image_router, "linkedin_image")
|
||||
|
||||
# Brainstorm router
|
||||
from api.brainstorm import router as brainstorm_router
|
||||
@@ -217,11 +201,8 @@ class RouterManager:
|
||||
|
||||
# Persona router
|
||||
try:
|
||||
if self._demo_release_mode_enabled():
|
||||
logger.info("⏭️ Skipping persona router in demo-release mode")
|
||||
else:
|
||||
from api.persona_routes import router as persona_router
|
||||
self.include_router_safely(persona_router, "persona")
|
||||
from api.persona_routes import router as persona_router
|
||||
self.include_router_safely(persona_router, "persona")
|
||||
except Exception as e:
|
||||
logger.warning(f"Persona router not mounted: {e}")
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
"""Facebook Post generation service."""
|
||||
|
||||
from typing import Dict, Any
|
||||
@@ -25,7 +24,8 @@ class FacebookPostService(FacebookWriterBaseService):
|
||||
actual_tone = request.custom_tone if request.post_tone.value == "Custom" else request.post_tone.value
|
||||
|
||||
# Get persona data for enhanced content generation
|
||||
user_id = int(os.getenv("ALWRITY_FALLBACK_USER_ID", "0"))
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
user_id = 1
|
||||
persona_data = self._get_persona_data(user_id)
|
||||
|
||||
# Build the prompt
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
"""Remaining Facebook Writer services - placeholder implementations."""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
@@ -17,7 +16,8 @@ class FacebookReelService(FacebookWriterBaseService):
|
||||
actual_style = request.custom_style if request.reel_style.value == "Custom" else request.reel_style.value
|
||||
|
||||
# Get persona data for enhanced content generation
|
||||
user_id = int(os.getenv("ALWRITY_FALLBACK_USER_ID", "0"))
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
user_id = 1
|
||||
persona_data = self._get_persona_data(user_id)
|
||||
|
||||
base_prompt = f"""
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
"""Facebook Story generation service."""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
@@ -31,7 +30,8 @@ class FacebookStoryService(FacebookWriterBaseService):
|
||||
actual_tone = request.custom_tone if request.story_tone.value == "Custom" else request.story_tone.value
|
||||
|
||||
# Get persona data for enhanced content generation
|
||||
user_id = int(os.getenv("ALWRITY_FALLBACK_USER_ID", "0"))
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
user_id = 1
|
||||
persona_data = self._get_persona_data(user_id)
|
||||
|
||||
# Build the prompt
|
||||
|
||||
@@ -94,36 +94,36 @@ async def generate_platform_persona_endpoint(
|
||||
async def update_persona_endpoint(
|
||||
persona_id: int,
|
||||
update_data: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
user_id: int = Query(..., description="User ID")
|
||||
):
|
||||
"""Update an existing persona."""
|
||||
user_id = int(current_user.get("id"))
|
||||
return await update_persona(user_id, persona_id, update_data)
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
return await update_persona(1, persona_id, update_data)
|
||||
|
||||
@router.delete("/{persona_id}")
|
||||
async def delete_persona_endpoint(
|
||||
persona_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
user_id: int = Query(..., description="User ID")
|
||||
):
|
||||
"""Delete a persona."""
|
||||
user_id = int(current_user.get("id"))
|
||||
return await delete_persona(user_id, persona_id)
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
return await delete_persona(1, persona_id)
|
||||
|
||||
@router.get("/check/readiness")
|
||||
async def check_persona_readiness_endpoint(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Check if user has sufficient data for persona generation."""
|
||||
user_id = int(current_user.get("id"))
|
||||
return await validate_persona_generation_readiness(user_id)
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
return await validate_persona_generation_readiness(1)
|
||||
|
||||
@router.get("/preview/generate")
|
||||
async def generate_preview_endpoint(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Generate a preview of the writing persona without saving."""
|
||||
user_id = int(current_user.get("id"))
|
||||
return await generate_persona_preview(user_id)
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
return await generate_persona_preview(1)
|
||||
|
||||
@router.get("/platforms/supported")
|
||||
async def get_supported_platforms_endpoint():
|
||||
@@ -160,12 +160,12 @@ async def optimize_facebook_persona_endpoint(
|
||||
|
||||
@router.post("/generate-content")
|
||||
async def generate_content_with_persona_endpoint(
|
||||
request: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
request: Dict[str, Any]
|
||||
):
|
||||
"""Generate content using persona replication engine."""
|
||||
try:
|
||||
user_id = int(current_user.get("id"))
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
user_id = 1
|
||||
platform = request.get("platform")
|
||||
content_request = request.get("content_request")
|
||||
content_type = request.get("content_type", "post")
|
||||
@@ -189,13 +189,13 @@ async def generate_content_with_persona_endpoint(
|
||||
@router.get("/export/{platform}")
|
||||
async def export_persona_prompt_endpoint(
|
||||
platform: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Export hardened persona prompt for external use."""
|
||||
try:
|
||||
engine = PersonaReplicationEngine()
|
||||
user_id = int(current_user.get("id"))
|
||||
export_package = engine.export_persona_for_external_use(user_id, platform)
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
export_package = engine.export_persona_for_external_use(1, platform)
|
||||
|
||||
if "error" in export_package:
|
||||
raise HTTPException(status_code=400, detail=export_package["error"])
|
||||
@@ -207,12 +207,12 @@ async def export_persona_prompt_endpoint(
|
||||
|
||||
@router.post("/validate-content")
|
||||
async def validate_content_endpoint(
|
||||
request: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
request: Dict[str, Any]
|
||||
):
|
||||
"""Validate content against persona constraints."""
|
||||
try:
|
||||
user_id = int(current_user.get("id"))
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
user_id = 1
|
||||
platform = request.get("platform")
|
||||
content = request.get("content")
|
||||
|
||||
@@ -242,14 +242,14 @@ async def validate_content_endpoint(
|
||||
async def update_platform_persona_endpoint(
|
||||
platform: str,
|
||||
update_data: Dict[str, Any],
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Update platform-specific persona fields for a user.
|
||||
|
||||
Allows editing persona fields in the UI and saving them to the database.
|
||||
"""
|
||||
user_id = int(current_user.get("id"))
|
||||
return await update_platform_persona(user_id, platform, update_data)
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
return await update_platform_persona(1, platform, update_data)
|
||||
|
||||
@router.get("/facebook-persona/check/{user_id}")
|
||||
async def check_facebook_persona_endpoint(
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail CI on forced/hardcoded user_id patterns outside test fixtures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
CHECK_GLOBS = ("**/*.py",)
|
||||
EXCLUDED_SUBSTRINGS = (
|
||||
"/.git/",
|
||||
"/.venv/",
|
||||
"/venv/",
|
||||
"/node_modules/",
|
||||
"/__pycache__/",
|
||||
"/tests/",
|
||||
"/test_",
|
||||
"/fixtures/",
|
||||
"/test_validation/",
|
||||
"/backend/scripts/check_forced_user_id_patterns.py",
|
||||
)
|
||||
|
||||
RULES = [
|
||||
(re.compile(r"\buser_id\s*=\s*1\b"), "hardcoded `user_id = 1`"),
|
||||
(re.compile(r"force\s+user_id", re.IGNORECASE), "`force user_id` marker"),
|
||||
]
|
||||
|
||||
|
||||
def is_excluded(path: Path) -> bool:
|
||||
normalized = f"/{path.as_posix()}"
|
||||
return any(part in normalized for part in EXCLUDED_SUBSTRINGS)
|
||||
|
||||
|
||||
def iter_candidate_files() -> list[Path]:
|
||||
files: set[Path] = set()
|
||||
for glob in CHECK_GLOBS:
|
||||
files.update(REPO_ROOT.glob(glob))
|
||||
return sorted(p for p in files if p.is_file() and not is_excluded(p.relative_to(REPO_ROOT)))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
violations: list[tuple[Path, int, str, str]] = []
|
||||
|
||||
for file_path in iter_candidate_files():
|
||||
rel_path = file_path.relative_to(REPO_ROOT)
|
||||
try:
|
||||
text = file_path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||
for pattern, label in RULES:
|
||||
if pattern.search(line):
|
||||
violations.append((rel_path, line_number, label, line.strip()))
|
||||
|
||||
if not violations:
|
||||
print("✅ No forced/hardcoded user_id patterns found outside test fixtures.")
|
||||
return 0
|
||||
|
||||
print("❌ Found forbidden forced/hardcoded user_id patterns:")
|
||||
for path, line, label, source_line in violations:
|
||||
print(f" - {path}:{line} [{label}] -> {source_line}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -410,7 +410,8 @@ class ContentGenerator:
|
||||
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
|
||||
|
||||
# Build the prompt for grounded generation using persona if available (DB vs session override)
|
||||
user_id = int(getattr(request, "user_id", 0) or 0)
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
user_id = 1
|
||||
persona_data = self._get_cached_persona_data(user_id, 'linkedin')
|
||||
if getattr(request, 'persona_override', None):
|
||||
try:
|
||||
@@ -484,7 +485,8 @@ class ContentGenerator:
|
||||
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
|
||||
|
||||
# Build the prompt for grounded generation using persona if available (DB vs session override)
|
||||
user_id = int(getattr(request, "user_id", 0) or 0)
|
||||
# Beta testing: Force user_id=1 for all requests
|
||||
user_id = 1
|
||||
persona_data = self._get_cached_persona_data(user_id, 'linkedin')
|
||||
if getattr(request, 'persona_override', None):
|
||||
try:
|
||||
|
||||
@@ -23,11 +23,6 @@ class MonitoringDataService:
|
||||
def __init__(self, db_session: Session):
|
||||
self.db = db_session
|
||||
|
||||
|
||||
def _resolve_strategy_user_id(self, strategy_id: int) -> str:
|
||||
strategy = self.db.query(EnhancedContentStrategy).filter(EnhancedContentStrategy.id == strategy_id).first()
|
||||
return str(getattr(strategy, "user_id", "0") or "0")
|
||||
|
||||
async def save_monitoring_data(self, strategy_id: int, monitoring_plan: Dict[str, Any]) -> bool:
|
||||
"""Save monitoring plan and tasks to database."""
|
||||
try:
|
||||
@@ -70,22 +65,19 @@ class MonitoringDataService:
|
||||
|
||||
self.db.add(task)
|
||||
|
||||
strategy_user_id = self._resolve_strategy_user_id(strategy_id)
|
||||
|
||||
# Save activation status
|
||||
activation_status = StrategyActivationStatus(
|
||||
strategy_id=strategy_id,
|
||||
user_id=strategy_user_id,
|
||||
user_id=1, # Default user ID
|
||||
activation_date=datetime.utcnow(),
|
||||
status='active'
|
||||
)
|
||||
self.db.add(activation_status)
|
||||
|
||||
# Save initial performance metrics
|
||||
strategy_user_id = self._resolve_strategy_user_id(strategy_id)
|
||||
performance_metrics = StrategyPerformanceMetrics(
|
||||
strategy_id=strategy_id,
|
||||
user_id=strategy_user_id,
|
||||
user_id=1, # Default user ID
|
||||
metric_date=datetime.utcnow(),
|
||||
data_source='monitoring_plan',
|
||||
confidence_score=85 # High confidence for monitoring plan data
|
||||
@@ -349,11 +341,10 @@ class MonitoringDataService:
|
||||
"""Update performance metrics for a strategy."""
|
||||
try:
|
||||
logger.info(f"Updating performance metrics for strategy {strategy_id}")
|
||||
strategy_user_id = self._resolve_strategy_user_id(strategy_id)
|
||||
|
||||
performance_metrics = StrategyPerformanceMetrics(
|
||||
strategy_id=strategy_id,
|
||||
user_id=strategy_user_id,
|
||||
user_id=1, # Default user ID
|
||||
metric_date=datetime.utcnow(),
|
||||
traffic_growth_percentage=metrics.get('traffic_growth'),
|
||||
engagement_rate_percentage=metrics.get('engagement_rate'),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from typing import Dict, Any, List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from loguru import logger
|
||||
@@ -22,7 +21,7 @@ class StrategyCopilotService:
|
||||
"""Generate data for a specific category."""
|
||||
try:
|
||||
# Get user onboarding data
|
||||
user_id = int(os.getenv("ALWRITY_FALLBACK_USER_ID", "0"))
|
||||
user_id = 1 # TODO: Get from auth context
|
||||
integrated_data = await self.onboarding_integration_service.process_onboarding_data(str(user_id), self.db)
|
||||
onboarding_data = integrated_data.get('canonical_profile', {})
|
||||
|
||||
@@ -82,7 +81,7 @@ class StrategyCopilotService:
|
||||
"""Analyze complete strategy for completeness and coherence."""
|
||||
try:
|
||||
# Get user data for context
|
||||
user_id = int(os.getenv("ALWRITY_FALLBACK_USER_ID", "0"))
|
||||
user_id = 1 # TODO: Get from auth context
|
||||
integrated_data = await self.onboarding_integration_service.process_onboarding_data(str(user_id), self.db)
|
||||
onboarding_data = integrated_data.get('canonical_profile', {})
|
||||
|
||||
@@ -119,7 +118,7 @@ class StrategyCopilotService:
|
||||
field_definition = self._get_field_definition(field_id)
|
||||
|
||||
# Get user data
|
||||
user_id = int(os.getenv("ALWRITY_FALLBACK_USER_ID", "0"))
|
||||
user_id = 1 # TODO: Get from auth context
|
||||
# Use SSOT
|
||||
integrated_data = await self.onboarding_integration_service.process_onboarding_data(str(user_id), self.db)
|
||||
onboarding_data = integrated_data.get('canonical_profile', {})
|
||||
|
||||
@@ -120,15 +120,6 @@ class SIFReleaseReadinessTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertFalse(validation["is_contextual"])
|
||||
self.assertEqual(validation["tasks_below_min_evidence"], 1)
|
||||
|
||||
|
||||
def test_demo_release_flag_guards_sensitive_routers(self):
|
||||
source = Path("backend/alwrity_utils/router_manager.py").read_text()
|
||||
self.assertIn("ALWRITY_DEMO_RELEASE", source)
|
||||
self.assertIn("Skipping facebook_writer router in demo-release mode", source)
|
||||
self.assertIn("Skipping linkedin router in demo-release mode", source)
|
||||
self.assertIn("Skipping linkedin_image router in demo-release mode", source)
|
||||
self.assertIn("Skipping persona router in demo-release mode", source)
|
||||
|
||||
def test_pillar_coverage_guardrail_backfills_missing(self):
|
||||
tasks = [{"pillarId": "plan", "title": "Plan", "description": "d", "priority": "high", "estimatedTime": 10, "actionType": "navigate", "enabled": True}]
|
||||
grounding = {"workflow_config": {"enforce_pillar_coverage": True}}
|
||||
|
||||
@@ -52,6 +52,27 @@ export interface SubscriptionPlan {
|
||||
}
|
||||
|
||||
const PricingPage: React.FC = () => {
|
||||
const pricingMode = (process.env.REACT_APP_PRICING_MODE || 'alpha').toLowerCase();
|
||||
const isAlphaMode = pricingMode === 'alpha';
|
||||
const tierPolicyByMode: Record<string, { visible: string[]; selectable: string[]; unavailableLabels: Record<string, string> }> = {
|
||||
alpha: {
|
||||
visible: ['free', 'basic', 'pro', 'enterprise'],
|
||||
selectable: ['free', 'basic'],
|
||||
unavailableLabels: { pro: 'Coming Soon', enterprise: 'Contact Sales' },
|
||||
},
|
||||
demo: {
|
||||
visible: ['free', 'basic', 'pro', 'enterprise'],
|
||||
selectable: ['free', 'basic', 'pro'],
|
||||
unavailableLabels: { enterprise: 'Contact Sales' },
|
||||
},
|
||||
production: {
|
||||
visible: ['free', 'basic', 'pro', 'enterprise'],
|
||||
selectable: ['free', 'basic', 'pro'],
|
||||
unavailableLabels: { enterprise: 'Contact Sales' },
|
||||
},
|
||||
};
|
||||
const activeTierPolicy = tierPolicyByMode[pricingMode] || tierPolicyByMode.alpha;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -76,9 +97,11 @@ const PricingPage: React.FC = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiClient.get('/api/subscription/plans');
|
||||
// Filter out any alpha plans and ensure we only show the 4 main tiers
|
||||
// Filter out legacy alpha-named plans and enforce tier visibility policy.
|
||||
const filteredPlans = response.data.data.plans.filter(
|
||||
(plan: SubscriptionPlan) => !plan.name.toLowerCase().includes('alpha')
|
||||
(plan: SubscriptionPlan) =>
|
||||
!plan.name.toLowerCase().includes('alpha') &&
|
||||
activeTierPolicy.visible.includes(plan.tier)
|
||||
);
|
||||
setPlans(filteredPlans);
|
||||
} catch (err) {
|
||||
@@ -111,10 +134,13 @@ const PricingPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// For alpha testing, only allow Free and Basic plans (Pro features not ready)
|
||||
if (plan.tier !== 'free' && plan.tier !== 'basic') {
|
||||
if (!activeTierPolicy.selectable.includes(plan.tier)) {
|
||||
console.error('[PricingPage] Plan tier not available:', plan.tier);
|
||||
setError('This plan is not available for alpha testing');
|
||||
setError(
|
||||
isAlphaMode
|
||||
? 'This plan is not available during alpha testing'
|
||||
: 'This plan is currently not available for self-serve checkout'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -351,6 +377,8 @@ const PricingPage: React.FC = () => {
|
||||
yearlyBilling={yearlyBilling}
|
||||
selectedPlanId={selectedPlan}
|
||||
subscribing={subscribing}
|
||||
canSelectPlan={activeTierPolicy.selectable.includes(plan.tier)}
|
||||
unavailableLabel={activeTierPolicy.unavailableLabels[plan.tier]}
|
||||
onSelectPlan={setSelectedPlan}
|
||||
onSubscribe={handleSubscribe}
|
||||
openKnowMoreModal={openKnowMoreModal}
|
||||
@@ -392,42 +420,48 @@ const PricingPage: React.FC = () => {
|
||||
}}>
|
||||
<Typography variant="h6" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Warning sx={{ color: 'warning.main' }} />
|
||||
Alpha Testing Subscription
|
||||
{isAlphaMode ? 'Alpha Testing Subscription' : 'Confirm Subscription'}
|
||||
</Typography>
|
||||
|
||||
{/* Alpha Testing Notice */}
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
⚠️ Alpha Testing Mode - No Payment Required
|
||||
|
||||
{isAlphaMode ? (
|
||||
<>
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
⚠️ Alpha Testing Mode - No Payment Required
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ display: 'block' }}>
|
||||
Payment integration is coming soon. For now, subscriptions are activated without charge.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
Thank you for participating in our alpha testing! We're crediting this plan to your account.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
mb: 3,
|
||||
bgcolor: 'info.lighter',
|
||||
borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'info.light'
|
||||
}}>
|
||||
<Typography variant="body2" color="info.dark">
|
||||
<strong>Coming in Production:</strong>
|
||||
</Typography>
|
||||
<Typography variant="caption" color="info.dark" sx={{ display: 'block', mt: 0.5 }}>
|
||||
• Secure Stripe/PayPal payment processing<br />
|
||||
• Automatic renewal management<br />
|
||||
• Payment verification & receipts<br />
|
||||
• Upgrade/downgrade options
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="body1" sx={{ mb: 3 }}>
|
||||
Please confirm to continue with your selected subscription plan.
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ display: 'block' }}>
|
||||
Payment integration is coming soon. For now, subscriptions are activated without charge.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
Thank you for participating in our alpha testing! We're crediting the Basic plan ($29 value) to your account.
|
||||
</Typography>
|
||||
|
||||
{/* TODO: Payment Integration Notice */}
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
mb: 3,
|
||||
bgcolor: 'info.lighter',
|
||||
borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'info.light'
|
||||
}}>
|
||||
<Typography variant="body2" color="info.dark">
|
||||
<strong>Coming in Production:</strong>
|
||||
</Typography>
|
||||
<Typography variant="caption" color="info.dark" sx={{ display: 'block', mt: 0.5 }}>
|
||||
• Secure Stripe/PayPal payment processing<br />
|
||||
• Automatic renewal management<br />
|
||||
• Payment verification & receipts<br />
|
||||
• Upgrade/downgrade options
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Note: Current behavior allows renewal without payment verification */}
|
||||
{/* This is intentional for alpha testing but will be secured in production */}
|
||||
|
||||
@@ -69,6 +69,8 @@ interface PlanCardProps {
|
||||
yearlyBilling: boolean;
|
||||
selectedPlanId: number | null;
|
||||
subscribing: boolean;
|
||||
canSelectPlan: boolean;
|
||||
unavailableLabel?: string;
|
||||
onSelectPlan: (planId: number) => void;
|
||||
onSubscribe: (planId: number) => void;
|
||||
openKnowMoreModal: (title: string, content: React.ReactNode) => void;
|
||||
@@ -79,6 +81,8 @@ const PlanCard: React.FC<PlanCardProps> = ({
|
||||
yearlyBilling,
|
||||
selectedPlanId,
|
||||
subscribing,
|
||||
canSelectPlan,
|
||||
unavailableLabel,
|
||||
onSelectPlan,
|
||||
onSubscribe,
|
||||
openKnowMoreModal,
|
||||
@@ -909,13 +913,9 @@ const PlanCard: React.FC<PlanCardProps> = ({
|
||||
</CardContent>
|
||||
|
||||
<CardActions sx={{ justifyContent: 'center', pb: 3, flexDirection: 'column', gap: 1 }}>
|
||||
{plan.tier === 'pro' ? (
|
||||
{!canSelectPlan ? (
|
||||
<Button variant="outlined" size="large" fullWidth disabled sx={{ mb: 1 }}>
|
||||
Coming Soon
|
||||
</Button>
|
||||
) : plan.tier === 'enterprise' ? (
|
||||
<Button variant="outlined" size="large" fullWidth disabled sx={{ mb: 1 }}>
|
||||
Contact Sales
|
||||
{unavailableLabel || 'Unavailable'}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
@@ -951,4 +951,3 @@ const PlanCard: React.FC<PlanCardProps> = ({
|
||||
};
|
||||
|
||||
export default PlanCard;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user