From 396477660be98a08e2aea1b4a9d6fec0f5514584 Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Mon, 6 Jul 2026 13:06:40 +0200 Subject: [PATCH] feat(app): out-of-credits bottom sheet (Stitch design) Co-Authored-By: Claude Fable 5 --- components/OutOfCreditsSheet.tsx | 146 +++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 components/OutOfCreditsSheet.tsx diff --git a/components/OutOfCreditsSheet.tsx b/components/OutOfCreditsSheet.tsx new file mode 100644 index 0000000..55eca45 --- /dev/null +++ b/components/OutOfCreditsSheet.tsx @@ -0,0 +1,146 @@ +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; + +const getCopy = (language: Language) => { + 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.', + cta: 'Pro-Pläne ansehen', + topupsLabel: 'Oder einzelne Credits kaufen', + later: 'Vielleicht später', + best: 'BESTE WAHL', + credits: 'Credits', + }; + } + 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.', + 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: "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.', + 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; + 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, renewsAtIso, onSeePlans, onTopup, onDismiss }: Props) { + const copy = getCopy(language); + const renewalDate = formatRenewalDate(renewsAtIso, language); + + return ( + + + + + + + + + + + 0 + + + {copy.title} + + {renewalDate ? copy.body(renewalDate) : copy.bodyNoDate} + + + + {copy.cta} + + {copy.topupsLabel.toUpperCase()} + + {TOPUPS.map((topup) => ( + onTopup(topup.id)} + activeOpacity={0.85} + > + {topup.best && ( + + {copy.best} + + )} + +{topup.amount} + {copy.credits} + + ))} + + + {copy.later} + + + + + ); +} + +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, backgroundColor: '#C62828', 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' }, +});