fix(app): review fixes — billing-unknown pre-check, free-user top-ups, pro out-of-credits copy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Timo Knuth
2026-07-06 13:19:15 +02:00
parent 4ddf16ed20
commit 88b12aeb00
3 changed files with 49 additions and 41 deletions

View File

@@ -6,12 +6,16 @@ import { useColors } from '../constants/Colors';
type ColorsType = ReturnType<typeof useColors>;
const getCopy = (language: Language) => {
const getCopy = (language: Language, isPro: boolean) => {
if (language === 'de') {
return {
title: 'Deine Gratis-Scans sind aufgebraucht',
body: (date: string) => `Deine 3 Gratis-Scans erneuern sich am ${date}. Hol dir Pro für unbegrenztes Scannen.`,
bodyNoDate: 'Hol dir Pro für unbegrenztes Scannen und deinen 7-Tage-Rettungsplan.',
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',
@@ -21,9 +25,13 @@ const getCopy = (language: Language) => {
}
if (language === 'es') {
return {
title: 'Se acabaron tus escaneos gratis',
body: (date: string) => `Tus 3 escaneos gratis se renuevan el ${date}. Pásate a Pro para escanear sin límites.`,
bodyNoDate: 'Pásate a Pro para escanear sin límites.',
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',
@@ -32,9 +40,13 @@ const getCopy = (language: Language) => {
};
}
return {
title: "You're out of free scans",
body: (date: string) => `Your 3 free scans renew on ${date}. Upgrade to Pro for unlimited scanning.`,
bodyNoDate: 'Upgrade to Pro for unlimited scanning.',
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',
@@ -55,6 +67,7 @@ type Props = {
visible: boolean;
language: Language;
colors: ColorsType;
isPro?: boolean;
renewsAtIso?: string | null;
onSeePlans: () => void;
onTopup: (productId: 'topup_small' | 'topup_medium' | 'topup_large') => void;
@@ -67,21 +80,27 @@ const TOPUPS = [
{ id: 'topup_large' as const, amount: 250, best: true },
];
export function OutOfCreditsSheet({ visible, language, colors, renewsAtIso, onSeePlans, onTopup, onDismiss }: Props) {
const copy = getCopy(language);
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} />
<View style={[styles.sheet, { backgroundColor: colors.surface }]}>
<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}>
<View style={[styles.zeroBadge, { backgroundColor: colors.danger }]}>
<Text style={styles.zeroBadgeText}>0</Text>
</View>
</View>
@@ -89,10 +108,12 @@ export function OutOfCreditsSheet({ visible, language, colors, renewsAtIso, onSe
<Text style={[styles.body, { color: colors.textSecondary }]}>
{renewalDate ? copy.body(renewalDate) : copy.bodyNoDate}
</Text>
<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>
{!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) => (
@@ -128,7 +149,7 @@ const styles = StyleSheet.create({
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, backgroundColor: '#C62828', width: 26, height: 26, borderRadius: 13, 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 },