Testflight

This commit is contained in:
2026-07-06 15:33:53 +02:00
parent 673ead2e3a
commit 99cd885833
19 changed files with 5288 additions and 5288 deletions

View File

@@ -1,107 +1,107 @@
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useColors } from '../constants/Colors';
type ColorsType = ReturnType<typeof useColors>;
export type QuestionOption = { id: string; emoji: string; label: string; subtitle?: string };
type Props = {
colors: ColorsType;
isDarkMode: boolean;
step: number; // 1-based
totalSteps: number;
title: string;
subtitle: string;
options: QuestionOption[];
selectedId: string | null;
onSelect: (id: string) => void;
onContinue: () => void;
onBack?: () => void;
continueLabel: string;
skipLabel?: string;
onSkip?: () => void;
};
export function OnboardingQuestion({
colors, isDarkMode, step, totalSteps, title, subtitle, options,
selectedId, onSelect, onContinue, onBack, continueLabel, skipLabel, onSkip,
}: Props) {
return (
<SafeAreaView style={[styles.safe, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]} edges={['top', 'left', 'right', 'bottom']}>
<View style={styles.topBar}>
{onBack ? (
<TouchableOpacity onPress={onBack} style={[styles.backBtn, { backgroundColor: colors.surface }]}>
<Ionicons name="arrow-back" size={20} color={colors.primary} />
</TouchableOpacity>
) : <View style={styles.backBtn} />}
<View style={[styles.progressTrack, { backgroundColor: colors.primarySoft }]}>
<View style={[styles.progressFill, { backgroundColor: colors.primary, width: `${Math.round((step / totalSteps) * 100)}%` }]} />
</View>
<View style={styles.backBtn} />
</View>
<Text style={[styles.title, { color: colors.text }]}>{title}</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{subtitle}</Text>
<View style={styles.options}>
{options.map((option) => {
const active = selectedId === option.id;
return (
<TouchableOpacity
key={option.id}
onPress={() => onSelect(option.id)}
activeOpacity={0.85}
style={[styles.card, {
backgroundColor: active ? colors.primarySoft : colors.surface,
borderColor: active ? colors.primary : 'transparent',
}]}
>
<Text style={styles.emoji}>{option.emoji}</Text>
<View style={styles.cardCopy}>
<Text style={[styles.cardLabel, { color: active ? colors.primary : colors.text }]}>{option.label}</Text>
{option.subtitle ? <Text style={[styles.cardSubtitle, { color: colors.textMuted }]}>{option.subtitle}</Text> : null}
</View>
</TouchableOpacity>
);
})}
</View>
<View style={styles.footer}>
{skipLabel && onSkip ? (
<TouchableOpacity onPress={onSkip} style={styles.skipBtn}>
<Text style={[styles.skipText, { color: colors.textMuted }]}>{skipLabel}</Text>
</TouchableOpacity>
) : null}
<TouchableOpacity
onPress={onContinue}
disabled={!selectedId}
activeOpacity={0.86}
style={[styles.cta, { backgroundColor: selectedId ? colors.primary : colors.surfaceMuted }]}
>
<Text style={[styles.ctaText, { color: selectedId ? colors.onPrimary : colors.textMuted }]}>{continueLabel}</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, paddingHorizontal: 22 },
topBar: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingVertical: 10 },
backBtn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
progressTrack: { flex: 1, height: 6, borderRadius: 3, overflow: 'hidden' },
progressFill: { height: 6, borderRadius: 3 },
title: { fontSize: 30, lineHeight: 35, fontWeight: '900', textAlign: 'center', marginTop: 18, marginBottom: 8 },
subtitle: { fontSize: 15, lineHeight: 20, textAlign: 'center', marginBottom: 22 },
options: { gap: 12, flex: 1 },
card: { flexDirection: 'row', alignItems: 'center', gap: 14, borderRadius: 16, borderWidth: 2, paddingHorizontal: 16, paddingVertical: 18 },
emoji: { fontSize: 26 },
cardCopy: { flex: 1, gap: 2 },
cardLabel: { fontSize: 17, fontWeight: '800' },
cardSubtitle: { fontSize: 12.5, lineHeight: 16 },
footer: { gap: 8, paddingBottom: 6 },
skipBtn: { alignItems: 'center', paddingVertical: 6 },
skipText: { fontSize: 14, fontWeight: '700' },
cta: { height: 56, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
ctaText: { fontSize: 17, fontWeight: '800' },
});
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useColors } from '../constants/Colors';
type ColorsType = ReturnType<typeof useColors>;
export type QuestionOption = { id: string; emoji: string; label: string; subtitle?: string };
type Props = {
colors: ColorsType;
isDarkMode: boolean;
step: number; // 1-based
totalSteps: number;
title: string;
subtitle: string;
options: QuestionOption[];
selectedId: string | null;
onSelect: (id: string) => void;
onContinue: () => void;
onBack?: () => void;
continueLabel: string;
skipLabel?: string;
onSkip?: () => void;
};
export function OnboardingQuestion({
colors, isDarkMode, step, totalSteps, title, subtitle, options,
selectedId, onSelect, onContinue, onBack, continueLabel, skipLabel, onSkip,
}: Props) {
return (
<SafeAreaView style={[styles.safe, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]} edges={['top', 'left', 'right', 'bottom']}>
<View style={styles.topBar}>
{onBack ? (
<TouchableOpacity onPress={onBack} style={[styles.backBtn, { backgroundColor: colors.surface }]}>
<Ionicons name="arrow-back" size={20} color={colors.primary} />
</TouchableOpacity>
) : <View style={styles.backBtn} />}
<View style={[styles.progressTrack, { backgroundColor: colors.primarySoft }]}>
<View style={[styles.progressFill, { backgroundColor: colors.primary, width: `${Math.round((step / totalSteps) * 100)}%` }]} />
</View>
<View style={styles.backBtn} />
</View>
<Text style={[styles.title, { color: colors.text }]}>{title}</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{subtitle}</Text>
<View style={styles.options}>
{options.map((option) => {
const active = selectedId === option.id;
return (
<TouchableOpacity
key={option.id}
onPress={() => onSelect(option.id)}
activeOpacity={0.85}
style={[styles.card, {
backgroundColor: active ? colors.primarySoft : colors.surface,
borderColor: active ? colors.primary : 'transparent',
}]}
>
<Text style={styles.emoji}>{option.emoji}</Text>
<View style={styles.cardCopy}>
<Text style={[styles.cardLabel, { color: active ? colors.primary : colors.text }]}>{option.label}</Text>
{option.subtitle ? <Text style={[styles.cardSubtitle, { color: colors.textMuted }]}>{option.subtitle}</Text> : null}
</View>
</TouchableOpacity>
);
})}
</View>
<View style={styles.footer}>
{skipLabel && onSkip ? (
<TouchableOpacity onPress={onSkip} style={styles.skipBtn}>
<Text style={[styles.skipText, { color: colors.textMuted }]}>{skipLabel}</Text>
</TouchableOpacity>
) : null}
<TouchableOpacity
onPress={onContinue}
disabled={!selectedId}
activeOpacity={0.86}
style={[styles.cta, { backgroundColor: selectedId ? colors.primary : colors.surfaceMuted }]}
>
<Text style={[styles.ctaText, { color: selectedId ? colors.onPrimary : colors.textMuted }]}>{continueLabel}</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, paddingHorizontal: 22 },
topBar: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingVertical: 10 },
backBtn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
progressTrack: { flex: 1, height: 6, borderRadius: 3, overflow: 'hidden' },
progressFill: { height: 6, borderRadius: 3 },
title: { fontSize: 30, lineHeight: 35, fontWeight: '900', textAlign: 'center', marginTop: 18, marginBottom: 8 },
subtitle: { fontSize: 15, lineHeight: 20, textAlign: 'center', marginBottom: 22 },
options: { gap: 12, flex: 1 },
card: { flexDirection: 'row', alignItems: 'center', gap: 14, borderRadius: 16, borderWidth: 2, paddingHorizontal: 16, paddingVertical: 18 },
emoji: { fontSize: 26 },
cardCopy: { flex: 1, gap: 2 },
cardLabel: { fontSize: 17, fontWeight: '800' },
cardSubtitle: { fontSize: 12.5, lineHeight: 16 },
footer: { gap: 8, paddingBottom: 6 },
skipBtn: { alignItems: 'center', paddingVertical: 6 },
skipText: { fontSize: 14, fontWeight: '700' },
cta: { height: 56, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
ctaText: { fontSize: 17, fontWeight: '800' },
});

View File

@@ -1,167 +1,167 @@
import React from 'react';
import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Language } from '../types';
import { useColors } from '../constants/Colors';
type ColorsType = ReturnType<typeof useColors>;
const getCopy = (language: Language, isPro: boolean) => {
if (language === 'de') {
return {
title: isPro ? 'Deine Credits sind aufgebraucht' : 'Deine Gratis-Scans sind aufgebraucht',
body: (date: string) => (isPro
? `Deine Credits erneuern sich am ${date}. Kauf Credits nach, um weiterzuscannen.`
: `Deine 3 Gratis-Scans erneuern sich am ${date}. Hol dir Pro für unbegrenztes Scannen.`),
bodyNoDate: isPro
? 'Kauf Credits nach, um weiterzuscannen.'
: 'Hol dir Pro für unbegrenztes Scannen und deinen 7-Tage-Rettungsplan.',
cta: 'Pro-Pläne ansehen',
topupsLabel: 'Oder einzelne Credits kaufen',
later: 'Vielleicht später',
best: 'BESTE WAHL',
credits: 'Credits',
};
}
if (language === 'es') {
return {
title: isPro ? 'Se acabaron tus créditos' : 'Se acabaron tus escaneos gratis',
body: (date: string) => (isPro
? `Tus créditos se renuevan el ${date}. Compra créditos para seguir escaneando.`
: `Tus 3 escaneos gratis se renuevan el ${date}. Pásate a Pro para escanear sin límites.`),
bodyNoDate: isPro
? 'Compra créditos para seguir escaneando.'
: 'Pásate a Pro para escanear sin límites.',
cta: 'Ver planes Pro',
topupsLabel: 'O compra créditos sueltos',
later: 'Quizás más tarde',
best: 'MEJOR OPCIÓN',
credits: 'créditos',
};
}
return {
title: isPro ? "You're out of credits" : "You're out of free scans",
body: (date: string) => (isPro
? `Your credits renew on ${date}. Top up to keep scanning.`
: `Your 3 free scans renew on ${date}. Upgrade to Pro for unlimited scanning.`),
bodyNoDate: isPro
? 'Top up to keep scanning.'
: 'Upgrade to Pro for unlimited scanning.',
cta: 'See Pro Plans',
topupsLabel: 'Or buy single credits',
later: 'Maybe later',
best: 'BEST',
credits: 'credits',
};
};
const formatRenewalDate = (iso: string | null | undefined, language: Language): string | null => {
if (!iso) return null;
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return null;
const locale = language === 'de' ? 'de-DE' : language === 'es' ? 'es-ES' : 'en-US';
return date.toLocaleDateString(locale, { day: 'numeric', month: 'long' });
};
type Props = {
visible: boolean;
language: Language;
colors: ColorsType;
isPro?: boolean;
renewsAtIso?: string | null;
onSeePlans: () => void;
onTopup: (productId: 'topup_small' | 'topup_medium' | 'topup_large') => void;
onDismiss: () => void;
};
const TOPUPS = [
{ id: 'topup_small' as const, amount: 30, best: false },
{ id: 'topup_medium' as const, amount: 100, best: false },
{ id: 'topup_large' as const, amount: 250, best: true },
];
export function OutOfCreditsSheet({ visible, language, colors, isPro = false, renewsAtIso, onSeePlans, onTopup, onDismiss }: Props) {
const copy = getCopy(language, isPro);
const renewalDate = formatRenewalDate(renewsAtIso, language);
return (
<Modal visible={visible} transparent animationType="slide" onRequestClose={onDismiss}>
<View style={styles.backdrop}>
<TouchableOpacity
style={styles.backdropTouchable}
activeOpacity={1}
onPress={onDismiss}
accessibilityRole="button"
accessibilityLabel={copy.later}
/>
<View style={[styles.sheet, { backgroundColor: colors.surface }]} accessibilityViewIsModal>
<View style={[styles.handle, { backgroundColor: colors.border }]} />
<View style={styles.iconWrap}>
<View style={[styles.iconCircle, { backgroundColor: colors.primarySoft }]}>
<Ionicons name="leaf-outline" size={34} color={colors.primary} />
</View>
<View style={[styles.zeroBadge, { backgroundColor: colors.danger }]}>
<Text style={styles.zeroBadgeText}>0</Text>
</View>
</View>
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
<Text style={[styles.body, { color: colors.textSecondary }]}>
{renewalDate ? copy.body(renewalDate) : copy.bodyNoDate}
</Text>
{!isPro && (
<TouchableOpacity style={[styles.cta, { backgroundColor: colors.primary }]} onPress={onSeePlans} activeOpacity={0.86}>
<Ionicons name="ribbon-outline" size={19} color={colors.onPrimary} />
<Text style={[styles.ctaText, { color: colors.onPrimary }]}>{copy.cta}</Text>
</TouchableOpacity>
)}
<Text style={[styles.topupsLabel, { color: colors.textMuted }]}>{copy.topupsLabel.toUpperCase()}</Text>
<View style={styles.topupRow}>
{TOPUPS.map((topup) => (
<TouchableOpacity
key={topup.id}
style={[styles.topupChip, { borderColor: topup.best ? colors.primary : colors.border, backgroundColor: colors.surfaceMuted }]}
onPress={() => onTopup(topup.id)}
activeOpacity={0.85}
>
{topup.best && (
<View style={[styles.bestBadge, { backgroundColor: colors.primary }]}>
<Text style={[styles.bestBadgeText, { color: colors.onPrimary }]}>{copy.best}</Text>
</View>
)}
<Text style={[styles.topupAmount, { color: colors.primary }]}>+{topup.amount}</Text>
<Text style={[styles.topupUnit, { color: colors.textSecondary }]}>{copy.credits}</Text>
</TouchableOpacity>
))}
</View>
<TouchableOpacity onPress={onDismiss} style={styles.laterBtn}>
<Text style={[styles.laterText, { color: colors.primary }]}>{copy.later}</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: { flex: 1, backgroundColor: 'rgba(10,17,11,0.45)', justifyContent: 'flex-end' },
backdropTouchable: { flex: 1 },
sheet: { borderTopLeftRadius: 26, borderTopRightRadius: 26, paddingHorizontal: 24, paddingTop: 10, paddingBottom: 34, alignItems: 'center' },
handle: { width: 44, height: 5, borderRadius: 3, marginBottom: 18 },
iconWrap: { marginBottom: 14 },
iconCircle: { width: 76, height: 76, borderRadius: 38, alignItems: 'center', justifyContent: 'center' },
zeroBadge: { position: 'absolute', top: -2, right: -4, width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
zeroBadgeText: { color: '#fff', fontSize: 13, fontWeight: '900' },
title: { fontSize: 24, fontWeight: '900', textAlign: 'center', marginBottom: 8 },
body: { fontSize: 15, lineHeight: 21, textAlign: 'center', marginBottom: 18, maxWidth: 320 },
cta: { alignSelf: 'stretch', height: 56, borderRadius: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, marginBottom: 16 },
ctaText: { fontSize: 17, fontWeight: '800' },
topupsLabel: { fontSize: 11, fontWeight: '800', letterSpacing: 0.8, marginBottom: 10 },
topupRow: { flexDirection: 'row', gap: 10, alignSelf: 'stretch', marginBottom: 14 },
topupChip: { flex: 1, borderWidth: 1.5, borderRadius: 14, paddingVertical: 14, alignItems: 'center', overflow: 'hidden' },
bestBadge: { position: 'absolute', top: 0, left: 0, right: 0, paddingVertical: 3, alignItems: 'center' },
bestBadgeText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.6 },
topupAmount: { fontSize: 22, fontWeight: '900', marginTop: 6 },
topupUnit: { fontSize: 12, fontWeight: '600' },
laterBtn: { paddingVertical: 8 },
laterText: { fontSize: 15, fontWeight: '800' },
});
import React from 'react';
import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Language } from '../types';
import { useColors } from '../constants/Colors';
type ColorsType = ReturnType<typeof useColors>;
const getCopy = (language: Language, isPro: boolean) => {
if (language === 'de') {
return {
title: isPro ? 'Deine Credits sind aufgebraucht' : 'Deine Gratis-Scans sind aufgebraucht',
body: (date: string) => (isPro
? `Deine Credits erneuern sich am ${date}. Kauf Credits nach, um weiterzuscannen.`
: `Deine 3 Gratis-Scans erneuern sich am ${date}. Hol dir Pro für unbegrenztes Scannen.`),
bodyNoDate: isPro
? 'Kauf Credits nach, um weiterzuscannen.'
: 'Hol dir Pro für unbegrenztes Scannen und deinen 7-Tage-Rettungsplan.',
cta: 'Pro-Pläne ansehen',
topupsLabel: 'Oder einzelne Credits kaufen',
later: 'Vielleicht später',
best: 'BESTE WAHL',
credits: 'Credits',
};
}
if (language === 'es') {
return {
title: isPro ? 'Se acabaron tus créditos' : 'Se acabaron tus escaneos gratis',
body: (date: string) => (isPro
? `Tus créditos se renuevan el ${date}. Compra créditos para seguir escaneando.`
: `Tus 3 escaneos gratis se renuevan el ${date}. Pásate a Pro para escanear sin límites.`),
bodyNoDate: isPro
? 'Compra créditos para seguir escaneando.'
: 'Pásate a Pro para escanear sin límites.',
cta: 'Ver planes Pro',
topupsLabel: 'O compra créditos sueltos',
later: 'Quizás más tarde',
best: 'MEJOR OPCIÓN',
credits: 'créditos',
};
}
return {
title: isPro ? "You're out of credits" : "You're out of free scans",
body: (date: string) => (isPro
? `Your credits renew on ${date}. Top up to keep scanning.`
: `Your 3 free scans renew on ${date}. Upgrade to Pro for unlimited scanning.`),
bodyNoDate: isPro
? 'Top up to keep scanning.'
: 'Upgrade to Pro for unlimited scanning.',
cta: 'See Pro Plans',
topupsLabel: 'Or buy single credits',
later: 'Maybe later',
best: 'BEST',
credits: 'credits',
};
};
const formatRenewalDate = (iso: string | null | undefined, language: Language): string | null => {
if (!iso) return null;
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return null;
const locale = language === 'de' ? 'de-DE' : language === 'es' ? 'es-ES' : 'en-US';
return date.toLocaleDateString(locale, { day: 'numeric', month: 'long' });
};
type Props = {
visible: boolean;
language: Language;
colors: ColorsType;
isPro?: boolean;
renewsAtIso?: string | null;
onSeePlans: () => void;
onTopup: (productId: 'topup_small' | 'topup_medium' | 'topup_large') => void;
onDismiss: () => void;
};
const TOPUPS = [
{ id: 'topup_small' as const, amount: 30, best: false },
{ id: 'topup_medium' as const, amount: 100, best: false },
{ id: 'topup_large' as const, amount: 250, best: true },
];
export function OutOfCreditsSheet({ visible, language, colors, isPro = false, renewsAtIso, onSeePlans, onTopup, onDismiss }: Props) {
const copy = getCopy(language, isPro);
const renewalDate = formatRenewalDate(renewsAtIso, language);
return (
<Modal visible={visible} transparent animationType="slide" onRequestClose={onDismiss}>
<View style={styles.backdrop}>
<TouchableOpacity
style={styles.backdropTouchable}
activeOpacity={1}
onPress={onDismiss}
accessibilityRole="button"
accessibilityLabel={copy.later}
/>
<View style={[styles.sheet, { backgroundColor: colors.surface }]} accessibilityViewIsModal>
<View style={[styles.handle, { backgroundColor: colors.border }]} />
<View style={styles.iconWrap}>
<View style={[styles.iconCircle, { backgroundColor: colors.primarySoft }]}>
<Ionicons name="leaf-outline" size={34} color={colors.primary} />
</View>
<View style={[styles.zeroBadge, { backgroundColor: colors.danger }]}>
<Text style={styles.zeroBadgeText}>0</Text>
</View>
</View>
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
<Text style={[styles.body, { color: colors.textSecondary }]}>
{renewalDate ? copy.body(renewalDate) : copy.bodyNoDate}
</Text>
{!isPro && (
<TouchableOpacity style={[styles.cta, { backgroundColor: colors.primary }]} onPress={onSeePlans} activeOpacity={0.86}>
<Ionicons name="ribbon-outline" size={19} color={colors.onPrimary} />
<Text style={[styles.ctaText, { color: colors.onPrimary }]}>{copy.cta}</Text>
</TouchableOpacity>
)}
<Text style={[styles.topupsLabel, { color: colors.textMuted }]}>{copy.topupsLabel.toUpperCase()}</Text>
<View style={styles.topupRow}>
{TOPUPS.map((topup) => (
<TouchableOpacity
key={topup.id}
style={[styles.topupChip, { borderColor: topup.best ? colors.primary : colors.border, backgroundColor: colors.surfaceMuted }]}
onPress={() => onTopup(topup.id)}
activeOpacity={0.85}
>
{topup.best && (
<View style={[styles.bestBadge, { backgroundColor: colors.primary }]}>
<Text style={[styles.bestBadgeText, { color: colors.onPrimary }]}>{copy.best}</Text>
</View>
)}
<Text style={[styles.topupAmount, { color: colors.primary }]}>+{topup.amount}</Text>
<Text style={[styles.topupUnit, { color: colors.textSecondary }]}>{copy.credits}</Text>
</TouchableOpacity>
))}
</View>
<TouchableOpacity onPress={onDismiss} style={styles.laterBtn}>
<Text style={[styles.laterText, { color: colors.primary }]}>{copy.later}</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: { flex: 1, backgroundColor: 'rgba(10,17,11,0.45)', justifyContent: 'flex-end' },
backdropTouchable: { flex: 1 },
sheet: { borderTopLeftRadius: 26, borderTopRightRadius: 26, paddingHorizontal: 24, paddingTop: 10, paddingBottom: 34, alignItems: 'center' },
handle: { width: 44, height: 5, borderRadius: 3, marginBottom: 18 },
iconWrap: { marginBottom: 14 },
iconCircle: { width: 76, height: 76, borderRadius: 38, alignItems: 'center', justifyContent: 'center' },
zeroBadge: { position: 'absolute', top: -2, right: -4, width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
zeroBadgeText: { color: '#fff', fontSize: 13, fontWeight: '900' },
title: { fontSize: 24, fontWeight: '900', textAlign: 'center', marginBottom: 8 },
body: { fontSize: 15, lineHeight: 21, textAlign: 'center', marginBottom: 18, maxWidth: 320 },
cta: { alignSelf: 'stretch', height: 56, borderRadius: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, marginBottom: 16 },
ctaText: { fontSize: 17, fontWeight: '800' },
topupsLabel: { fontSize: 11, fontWeight: '800', letterSpacing: 0.8, marginBottom: 10 },
topupRow: { flexDirection: 'row', gap: 10, alignSelf: 'stretch', marginBottom: 14 },
topupChip: { flex: 1, borderWidth: 1.5, borderRadius: 14, paddingVertical: 14, alignItems: 'center', overflow: 'hidden' },
bestBadge: { position: 'absolute', top: 0, left: 0, right: 0, paddingVertical: 3, alignItems: 'center' },
bestBadgeText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.6 },
topupAmount: { fontSize: 22, fontWeight: '900', marginTop: 6 },
topupUnit: { fontSize: 12, fontWeight: '600' },
laterBtn: { paddingVertical: 8 },
laterText: { fontSize: 15, fontWeight: '800' },
});