chahinebrini 63fae25531 fix(android-protection): explicit specialUse FGS type — Samsung/Android 16 crash loop
RebreakVpnService.onStartCommand crashed with SecurityException because Android 16's validateForegroundServiceType rejects the implicit 2-arg startForeground(). Now passes FOREGROUND_SERVICE_TYPE_SPECIAL_USE explicitly (Google's documented best practice) and guards the call so a failed foreground promotion stops the service cleanly instead of crashing the app. Verified vs reported Galaxy A54 / Android 16 signature (97% of crash events, 1-user crash loop).

Bundles pending working-tree work across native/marketing/locales/mac + graphify-out rebuild. gitignore: google-services.json + /screenshots/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:33:28 +02:00

309 lines
15 KiB
Vue

<template>
<div class="min-h-screen bg-default font-sans pb-16 md:pb-0">
<!-- Header -->
<div v-motion :initial="{ opacity: 0, y: -30 }" :visible="{ opacity: 1, y: 0, transition: { duration: 600 } }"
class="pt-10 pb-12 text-center px-4">
<div
class="inline-flex items-center gap-2 bg-amber-950/60 border border-amber-700/40 rounded-full px-4 py-1.5 text-sm text-amber-300 mb-4 animate-pulse">
<UIcon name="i-heroicons-fire" class="text-amber-400" />
{{ $t('pricing.founding_banner') }}
</div>
<div
class="inline-flex items-center gap-2 bg-primary-950/60 border border-primary-800/40 rounded-full px-4 py-1.5 text-sm text-primary-300 mb-6">
<UIcon name="i-heroicons-sparkles" class="text-primary-400" />
{{ $t('pricing.title') }}
</div>
<h1 class="text-4xl md:text-5xl font-extrabold text-highlighted mb-4">
{{ $t('pricing.subtitle_start') }}<br />
<span
class="text-transparent bg-clip-text bg-linear-to-r from-primary-400 via-primary-300 to-green-400">
{{ $t('pricing.subtitle_end') }}
</span>
</h1>
<!-- Geräteübergreifender Schutz Device-Hero -->
<div class="mt-8 flex flex-col items-center gap-4">
<p class="text-sm text-muted max-w-md">{{ $t('pricing.cross_device_tagline') }}</p>
<div class="flex items-center justify-center gap-4 sm:gap-6">
<div v-for="d in crossDevices" :key="d.label" class="flex flex-col items-center gap-2">
<div
class="w-14 h-14 rounded-2xl bg-elevated border border-default flex items-center justify-center">
<UIcon :name="d.icon" class="text-primary-300 text-2xl" />
</div>
<span class="text-xs text-muted font-medium">{{ d.label }}</span>
</div>
</div>
</div>
<!-- Billing Cycle Picker -->
<div class="flex items-center justify-center gap-2 mt-8">
<button v-for="opt in billingOptions" :key="opt.value" @click="billing = opt.value"
class="relative px-4 py-2 rounded-full text-sm font-semibold transition-all"
:class="billing === opt.value ? 'bg-primary-700 text-white' : 'bg-muted text-muted hover:text-highlighted'">
{{ opt.label }}
<span v-if="opt.badge"
class="absolute -top-2 -right-2 bg-green-500 text-white text-[9px] font-bold px-1.5 py-0.5 rounded-full">{{
opt.badge }}</span>
</button>
</div>
</div>
<!-- Pricing Plans -->
<div v-motion :initial="{ opacity: 0, y: 40 }"
:visible="{ opacity: 1, y: 0, transition: { duration: 700, delay: 100 } }"
class="px-4 pb-16 max-w-5xl mx-auto">
<UPricingPlans :plans="plans" />
</div>
<!-- Feature Comparison -->
<div v-motion :initial="{ opacity: 0, y: 50 }" :visible="{ opacity: 1, y: 0, transition: { duration: 700 } }"
class="px-4 pb-12 max-w-3xl mx-auto">
<div class="bg-elevated border border-purple-800/30 rounded-2xl p-6 md:p-8">
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-xl bg-purple-800/30 flex items-center justify-center shrink-0 mt-0.5">
<UIcon name="i-heroicons-shield-check" class="text-purple-400 text-xl" />
</div>
<div>
<h3 class="font-bold text-highlighted text-lg mb-2">{{ $t('pricing.pro_meaning_title') }}</h3>
<p class="text-muted text-sm leading-relaxed">
{{ $t('pricing.pro_meaning_desc') }}
</p>
</div>
</div>
</div>
</div>
<!-- Feature Comparison Table -->
<div v-motion :initial="{ opacity: 0, y: 50 }" :visible="{ opacity: 1, y: 0, transition: { duration: 700 } }"
class="px-4 pb-12 max-w-4xl mx-auto">
<h2 class="text-2xl font-extrabold text-highlighted text-center mb-2">
{{ $t('pricing.comparison_title') }}
</h2>
<p class="text-muted text-center text-sm mb-10">
{{ $t('pricing.comparison_subtitle') }}
</p>
<div class="bg-elevated border border-default rounded-2xl overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-default">
<th class="text-left p-4 text-muted font-semibold">{{ $t('pricing.feature') }}</th>
<th class="p-4 text-center font-semibold text-xs text-primary-300">Pro</th>
<th class="p-4 text-center text-purple-400 font-semibold text-xs">Legend</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in comparisonRows" :key="row.label"
:class="i % 2 === 0 ? 'bg-white/2' : ''">
<td class="p-4 text-default font-medium">{{ row.label }}</td>
<td class="p-4 text-center">
<UIcon v-if="row.pro === true" name="i-heroicons-check-circle"
class="text-primary-400 text-lg" />
<span v-else-if="typeof row.pro === 'string'"
class="text-primary-300 text-xs font-semibold">{{ row.pro }}</span>
<UIcon v-else name="i-heroicons-minus" class="text-dimmed" />
</td>
<td class="p-4 text-center">
<UIcon v-if="row.legend === true" name="i-heroicons-check-circle"
class="text-purple-400 text-lg" />
<span v-else-if="typeof row.legend === 'string'"
class="text-purple-300 text-xs font-semibold">{{ row.legend }}</span>
<UIcon v-else name="i-heroicons-minus" class="text-dimmed" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Quotes -->
<div v-motion :initial="{ opacity: 0, y: 40 }" :visible="{ opacity: 1, y: 0, transition: { duration: 700 } }"
class="px-4 pb-24 max-w-4xl mx-auto">
<h2 class="text-2xl font-extrabold text-highlighted text-center mb-2">
{{ $t('pricing.quotes_title') }}
</h2>
<p class="text-muted text-center text-sm mb-10">{{ $t('pricing.quotes_subtitle') }}</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<UPageCard v-for="(q, i) in quotes" :key="q.author" v-motion :initial="{ opacity: 0, y: 30 }"
:visible="{ opacity: 1, y: 0, transition: { duration: 500, delay: i * 120 } }">
<div class="flex items-center gap-3 mb-4">
<UAvatar :src="q.image" :text="q.initials" size="md" class="ring-2 ring-white/10 shrink-0" />
<div>
<div class="text-highlighted text-sm font-semibold leading-tight">{{ q.author }}</div>
<div class="text-muted text-xs">{{ q.role }}</div>
</div>
</div>
<p class="text-default text-sm leading-relaxed italic">&ldquo; {{ q.text }} &rdquo;</p>
</UPageCard>
</div>
</div>
<!-- FAQ -->
<div v-motion :initial="{ opacity: 0, y: 40 }" :visible="{ opacity: 1, y: 0, transition: { duration: 700 } }"
class="px-4 pb-24 max-w-2xl mx-auto">
<h2 class="text-2xl font-extrabold text-highlighted text-center mb-8">{{ $t('pricing.faq_title') }}</h2>
<UAccordion :items="faqItems" />
</div>
<!-- CTA -->
<div v-motion :initial="{ opacity: 0, scale: 0.95 }"
:visible="{ opacity: 1, scale: 1, transition: { duration: 600 } }"
class="px-4 pb-32 max-w-xl mx-auto text-center">
<h2 class="text-3xl font-extrabold text-highlighted mb-3">{{ $t('pricing.cta_title') }}</h2>
<p class="text-muted mb-6">{{ $t('pricing.cta_desc') }}</p>
<a href="https://apps.apple.com/app/rebreak" target="_blank" rel="noopener">
<UButton size="xl" class="font-bold px-10 rounded-full">
{{ $t('pricing.cta_button') }}
<template #trailing>
<UIcon name="i-heroicons-arrow-right" />
</template>
</UButton>
</a>
</div>
</div>
</template>
<script setup lang="ts">
import type { PricingPlanProps } from "@nuxt/ui";
definePageMeta({ layout: "default" });
const { t } = useI18n();
const billing = ref<'monthly' | 'yearly'>('monthly');
const billingOptions = computed(() => [
{ value: 'monthly', label: t('pricing.billing_monthly'), badge: null },
{ value: 'yearly', label: t('pricing.billing_yearly'), badge: t('pricing.billing_save_pct') },
] as const);
// Geräteübergreifend: ein Abo, alle Plattformen (heroicons sind offline gebündelt).
const crossDevices = [
{ icon: 'i-heroicons-device-phone-mobile', label: 'iPhone' },
{ icon: 'i-heroicons-device-phone-mobile', label: 'Android' },
{ icon: 'i-heroicons-computer-desktop', label: 'Mac' },
{ icon: 'i-heroicons-computer-desktop', label: 'Windows' },
];
const proMonthly = 3.99;
const legendMonthly = 7.99;
const proPrice = computed(() => {
if (billing.value === 'yearly') return (29 / 12).toFixed(2);
return proMonthly.toFixed(2);
});
const legendPrice = computed(() => {
if (billing.value === 'yearly') return (59 / 12).toFixed(2);
return legendMonthly.toFixed(2);
});
const billingCycleLabel = computed(() => {
if (billing.value === 'yearly') return t('pricing.billing_per_year');
return t('pricing.billing_per_month');
});
// Marketing: alle Plan-Buttons zeigen auf App-Store
const appStoreUrl = "https://apps.apple.com/app/rebreak";
const plans = computed<PricingPlanProps[]>(() => [
{
title: t('pricing.plan_pro_title'),
description: t('pricing.plan_pro_desc'),
price: `${proPrice.value}`,
billingCycle: billingCycleLabel.value,
scale: true,
badge: t('pricing.plan_recommended'),
features: [
t('pricing.feat_pro_devices'),
t('pricing.feat_blocklist'),
t('pricing.feat_pro_domains'),
t('pricing.feat_pro_mail'),
t('pricing.feat_coach_pro'),
t('pricing.feat_streak'),
t('pricing.feat_urge_stats'),
t('pricing.feat_community_post'),
],
button: {
label: t('pricing.plan_pro_btn'),
to: appStoreUrl,
target: "_blank",
},
},
{
title: t('pricing.plan_legend_title'),
description: t('pricing.plan_legend_desc'),
price: `${legendPrice.value}`,
billingCycle: billingCycleLabel.value,
features: [
t('pricing.feat_all_pro'),
t('pricing.feat_legend_devices'),
t('pricing.feat_legend_domains'),
t('pricing.feat_legend_mail'),
t('pricing.feat_legend_binder'),
t('pricing.feat_legend_add'),
t('pricing.feat_legend_validate'),
t('pricing.feat_legend_groups'),
t('pricing.feat_coach_legend'),
],
button: {
label: t('pricing.plan_legend_btn'),
to: appStoreUrl,
target: "_blank",
color: "neutral" as const,
variant: "subtle" as const,
},
},
]);
const comparisonRows = computed(() => [
{ label: t('pricing.comp_devices'), pro: t('pricing.comp_pro_devices'), legend: t('pricing.comp_legend_devices') },
{ label: t('pricing.comp_domains'), pro: t('pricing.comp_pro_domains'), legend: t('pricing.comp_legend_domains') },
{ label: t('pricing.comp_mail'), pro: t('pricing.comp_pro_mail_val'), legend: t('pricing.comp_legend_mail_val') },
{ label: t('pricing.comp_coach'), pro: t('pricing.comp_pro_coach_val'), legend: t('pricing.comp_legend_coach_val') },
{ label: t('pricing.comp_blocklist'), pro: true, legend: true },
{ label: t('pricing.comp_streak'), pro: true, legend: true },
{ label: t('pricing.comp_urge'), pro: true, legend: true },
{ label: t('pricing.comp_sos'), pro: true, legend: true },
{ label: t('pricing.comp_community'), pro: true, legend: true },
{ label: t('pricing.comp_post'), pro: true, legend: true },
{ label: t('pricing.comp_buddy'), pro: true, legend: true },
{ label: t('pricing.comp_urge_stats'), pro: true, legend: true },
{ label: t('pricing.comp_binder'), pro: false, legend: true },
{ label: t('pricing.comp_add_domain'), pro: false, legend: true },
{ label: t('pricing.comp_validate'), pro: false, legend: true },
{ label: t('pricing.comp_groups'), pro: false, legend: true },
]);
const quotes = [
{
text: "Zwischen Reiz und Reaktion liegt ein Raum. In diesem Raum liegt unsere Macht, unsere Reaktion zu wählen.",
author: "Viktor Frankl",
role: "Psychiater & Logotherapeut",
image: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Viktor_Frankl2.jpg/200px-Viktor_Frankl2.jpg",
initials: "VF",
},
{
text: "Bis du das Unbewusste bewusst machst, wird es dein Leben lenken, und du wirst es Schicksal nennen.",
author: "Carl Gustav Jung",
role: "Psychiater & Begründer der analytischen Psychologie",
image: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/CGJung.jpg/200px-CGJung.jpg",
initials: "CJ",
},
{
text: "Sucht ist keine Charakterschwäche. Sie ist eine Erkrankung des Gehirns, und sie ist behandelbar.",
author: "Nora Volkow",
role: "Neurowissenschaftlerin, Direktorin des NIDA",
image: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9b/Nora_Volkow2.jpg/200px-Nora_Volkow2.jpg",
initials: "NV",
},
];
const faqItems = computed(() => [
{ label: t('pricing.faq1_q'), content: t('pricing.faq1_a') },
{ label: t('pricing.faq2_q'), content: t('pricing.faq2_a') },
{ label: t('pricing.faq3_q'), content: t('pricing.faq3_a') },
{ label: t('pricing.faq4_q'), content: t('pricing.faq4_a') },
{ label: t('pricing.faq5_q'), content: t('pricing.faq5_a') },
{ label: t('pricing.faq6_q'), content: t('pricing.faq6_a') },
]);
</script>