Files
Claude 16673b91d3 Add Billing/Stripe integration and Landing Page (Phase 4+5)
Phase 4 - Billing/Stripe:
- src/Billing/StripeService.php: Stripe API wrapper
  - Checkout session creation
  - Customer management
  - Billing portal sessions
  - Webhook signature verification
- src/Billing/SubscriptionManager.php: Subscription logic
  - Plan management (CRUD)
  - Trial handling
  - Feature access checks
  - Invoice storage
- src/Billing/WebhookHandler.php: Stripe webhook processing
  - checkout.session.completed
  - customer.subscription.* events
  - invoice.paid / payment_failed
- api/stripe-webhook.php: Webhook endpoint
- dashboard/billing.php: Billing dashboard
  - Current plan display with features
  - Plan comparison grid
  - Upgrade buttons with Stripe Checkout
  - Invoice history

Phase 5 - Landing Page:
- landing/index.php: Marketing homepage
  - Hero section with CTA
  - Feature grid (6 features)
  - How it works (3 steps)
  - Final CTA section
  - Responsive design
- landing/pricing.php: Pricing page
  - Dynamic plan cards from DB
  - Monthly/yearly toggle (2 months free)
  - Feature comparison
  - FAQ accordion

All features respect saas_features toggles in settings.
2026-01-23 19:16:18 +00:00

498 lines
15 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Landing Page - Preise
*/
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/SettingsManager.php';
if (file_exists(dirname(__DIR__) . '/src/bootstrap.php')) {
require_once dirname(__DIR__) . '/src/bootstrap.php';
}
use AuroraLivecam\Billing\SubscriptionManager;
$settingsManager = new SettingsManager();
// Pläne laden
$plans = [];
try {
$subscriptions = new SubscriptionManager();
$plans = $subscriptions->getPlans();
} catch (\Exception $e) {
// Fallback-Pläne
$plans = [
['name' => 'Free', 'slug' => 'free', 'price_monthly' => 0, 'features' => ['max_viewers' => 10, 'weather_widget' => true]],
['name' => 'Basic', 'slug' => 'basic', 'price_monthly' => 19, 'features' => ['max_viewers' => 50, 'weather_widget' => true, 'timelapse' => true, 'analytics' => true]],
['name' => 'Professional', 'slug' => 'professional', 'price_monthly' => 49, 'features' => ['max_viewers' => 200, 'custom_domain' => true, 'weather_widget' => true, 'timelapse' => true, 'analytics' => true, 'branding' => true]],
['name' => 'Enterprise', 'slug' => 'enterprise', 'price_monthly' => 149, 'features' => ['max_viewers' => -1, 'custom_domain' => true, 'weather_widget' => true, 'timelapse' => true, 'analytics' => true, 'branding' => true, 'priority_support' => true]],
];
}
$trialDays = $settingsManager->getTrialDays();
// Feature-Labels
$featureLabels = [
'max_viewers' => 'Gleichzeitige Zuschauer',
'storage_gb' => 'Speicherplatz',
'custom_domain' => 'Eigene Domain',
'weather_widget' => 'Wetter-Widget',
'timelapse' => 'Timelapse',
'analytics' => 'Analytics & Statistiken',
'branding' => 'Custom Branding',
'priority_support' => 'Priority Support',
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preise - Aurora Livecam</title>
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
<style>
:root {
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #1a202c;
background: #f7fafc;
}
.header {
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 1rem 2rem;
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-decoration: none;
}
.nav-links a {
color: #4a5568;
text-decoration: none;
margin-left: 1.5rem;
}
.page-header {
text-align: center;
padding: 4rem 2rem;
background: var(--gradient);
color: white;
}
.page-header h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.page-header p {
font-size: 1.1rem;
opacity: 0.9;
}
.pricing-toggle {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
align-items: center;
}
.pricing-toggle span {
font-size: 0.9rem;
}
.pricing-toggle .active {
font-weight: 600;
}
.toggle-switch {
width: 60px;
height: 30px;
background: rgba(255,255,255,0.3);
border-radius: 15px;
position: relative;
cursor: pointer;
}
.toggle-switch::after {
content: '';
position: absolute;
width: 26px;
height: 26px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: 0.3s;
}
.toggle-switch.yearly::after {
left: 32px;
}
.save-badge {
background: #48bb78;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
}
.pricing-container {
max-width: 1200px;
margin: -3rem auto 4rem;
padding: 0 2rem;
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.pricing-card {
background: white;
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
position: relative;
display: flex;
flex-direction: column;
}
.pricing-card.featured {
border: 2px solid #667eea;
transform: scale(1.05);
}
.pricing-card.featured::before {
content: 'Beliebt';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--gradient);
color: white;
padding: 0.25rem 1rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 600;
}
.pricing-card h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.pricing-card .price {
font-size: 3rem;
font-weight: 800;
margin: 1rem 0;
}
.pricing-card .price span {
font-size: 1rem;
font-weight: 400;
color: #718096;
}
.pricing-card .price-yearly {
display: none;
}
.yearly-mode .price-monthly { display: none; }
.yearly-mode .price-yearly { display: block; }
.pricing-card ul {
list-style: none;
flex: 1;
margin: 1.5rem 0;
}
.pricing-card li {
padding: 0.5rem 0;
color: #4a5568;
display: flex;
align-items: center;
gap: 0.5rem;
}
.pricing-card li.included::before {
content: '✓';
color: #48bb78;
font-weight: bold;
}
.pricing-card li.not-included {
color: #a0aec0;
text-decoration: line-through;
}
.pricing-card li.not-included::before {
content: '✗';
color: #e53e3e;
}
.pricing-card .btn {
width: 100%;
padding: 1rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
text-decoration: none;
text-align: center;
transition: all 0.2s;
}
.pricing-card .btn-primary {
background: var(--gradient);
color: white;
}
.pricing-card .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.pricing-card .btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.faq {
max-width: 800px;
margin: 0 auto 4rem;
padding: 0 2rem;
}
.faq h2 {
text-align: center;
margin-bottom: 2rem;
}
.faq-item {
background: white;
border-radius: 0.5rem;
margin-bottom: 1rem;
overflow: hidden;
}
.faq-question {
padding: 1.25rem;
font-weight: 600;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.faq-answer {
padding: 0 1.25rem 1.25rem;
color: #718096;
display: none;
}
.faq-item.open .faq-answer {
display: block;
}
.footer {
background: #1a202c;
color: #a0aec0;
padding: 2rem;
text-align: center;
}
@media (max-width: 768px) {
.pricing-card.featured {
transform: none;
}
.pricing-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-inner">
<a href="/landing/" class="logo">Aurora Livecam</a>
<nav class="nav-links">
<a href="/landing/">Home</a>
<a href="/dashboard/login.php">Login</a>
<a href="/onboarding/register.php" class="btn btn-primary btn-sm">Kostenlos starten</a>
</nav>
</div>
</header>
<section class="page-header">
<h1>Einfache, transparente Preise</h1>
<p><?php echo $trialDays; ?> Tage kostenlos testen - jederzeit kündbar</p>
<div class="pricing-toggle">
<span class="monthly-label active">Monatlich</span>
<div class="toggle-switch" id="billing-toggle"></div>
<span class="yearly-label">Jährlich</span>
<span class="save-badge">2 Monate gratis</span>
</div>
</section>
<div class="pricing-container" id="pricing-container">
<div class="pricing-grid">
<?php foreach ($plans as $index => $plan): ?>
<?php $isFeatured = $plan['slug'] === 'professional'; ?>
<div class="pricing-card <?php echo $isFeatured ? 'featured' : ''; ?>">
<h3><?php echo htmlspecialchars($plan['name']); ?></h3>
<div class="price price-monthly">
<?php if ($plan['price_monthly'] > 0): ?>
CHF <?php echo number_format($plan['price_monthly'], 0); ?><span>/Monat</span>
<?php else: ?>
Kostenlos
<?php endif; ?>
</div>
<div class="price price-yearly">
<?php if (isset($plan['price_yearly']) && $plan['price_yearly'] > 0): ?>
CHF <?php echo number_format($plan['price_yearly'] / 12, 0); ?><span>/Monat</span>
<div style="font-size: 0.875rem; color: #718096;">
CHF <?php echo number_format($plan['price_yearly'], 0); ?> jährlich
</div>
<?php elseif ($plan['price_monthly'] > 0): ?>
CHF <?php echo number_format($plan['price_monthly'] * 10 / 12, 0); ?><span>/Monat</span>
<div style="font-size: 0.875rem; color: #718096;">
CHF <?php echo number_format($plan['price_monthly'] * 10, 0); ?> jährlich
</div>
<?php else: ?>
Kostenlos
<?php endif; ?>
</div>
<ul>
<?php
$features = is_array($plan['features']) ? $plan['features'] : json_decode($plan['features'], true) ?? [];
$allFeatures = ['max_viewers', 'weather_widget', 'timelapse', 'analytics', 'custom_domain', 'branding', 'priority_support'];
foreach ($allFeatures as $feature):
$hasFeature = !empty($features[$feature]);
$value = $features[$feature] ?? null;
?>
<li class="<?php echo $hasFeature ? 'included' : 'not-included'; ?>">
<?php
if ($feature === 'max_viewers' && $value) {
echo $value === -1 ? 'Unbegrenzte Zuschauer' : "Bis $value Zuschauer";
} elseif ($feature === 'storage_gb' && $value) {
echo "$value GB Speicher";
} else {
echo $featureLabels[$feature] ?? ucfirst(str_replace('_', ' ', $feature));
}
?>
</li>
<?php endforeach; ?>
</ul>
<a href="/onboarding/register.php?plan=<?php echo $plan['slug']; ?>"
class="btn <?php echo $isFeatured || $plan['price_monthly'] > 0 ? 'btn-primary' : 'btn-secondary'; ?>">
<?php echo $plan['price_monthly'] > 0 ? 'Jetzt starten' : 'Kostenlos starten'; ?>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- FAQ -->
<section class="faq">
<h2>Häufige Fragen</h2>
<div class="faq-item">
<div class="faq-question">
Kann ich jederzeit wechseln oder kündigen?
<span>+</span>
</div>
<div class="faq-answer">
Ja! Sie können Ihren Plan jederzeit upgraden oder downgraden. Bei einer Kündigung bleibt Ihr Zugang bis zum Ende der Abrechnungsperiode aktiv.
</div>
</div>
<div class="faq-item">
<div class="faq-question">
Was passiert nach dem Trial?
<span>+</span>
</div>
<div class="faq-answer">
Nach Ablauf der <?php echo $trialDays; ?> Tage werden Sie automatisch auf den kostenlosen Plan umgestellt, sofern Sie kein Abo abschliessen. Keine Sorge, Ihre Daten bleiben erhalten.
</div>
</div>
<div class="faq-item">
<div class="faq-question">
Welche Zahlungsmethoden werden akzeptiert?
<span>+</span>
</div>
<div class="faq-answer">
Wir akzeptieren alle gängigen Kreditkarten (Visa, Mastercard, American Express) sowie TWINT und Banküberweisung bei Jahresabos.
</div>
</div>
<div class="faq-item">
<div class="faq-question">
Brauche ich technisches Wissen?
<span>+</span>
</div>
<div class="faq-answer">
Nein! Unser Onboarding-Wizard führt Sie Schritt für Schritt durch die Einrichtung. Sie benötigen lediglich eine Stream-URL (HLS/m3u8) von Ihrem Kamera-Anbieter.
</div>
</div>
</section>
<footer class="footer">
© <?php echo date('Y'); ?> Aurora Livecam. Alle Rechte vorbehalten.
</footer>
<script>
// Billing toggle
const toggle = document.getElementById('billing-toggle');
const container = document.getElementById('pricing-container');
toggle.addEventListener('click', () => {
toggle.classList.toggle('yearly');
container.classList.toggle('yearly-mode');
document.querySelector('.monthly-label').classList.toggle('active');
document.querySelector('.yearly-label').classList.toggle('active');
});
// FAQ accordion
document.querySelectorAll('.faq-question').forEach(q => {
q.addEventListener('click', () => {
q.parentElement.classList.toggle('open');
q.querySelector('span').textContent = q.parentElement.classList.contains('open') ? '' : '+';
});
});
</script>
</body>
</html>