Initial commit for Greenlens
This commit is contained in:
657
app/profile/billing.tsx
Normal file
657
app/profile/billing.tsx
Normal file
@@ -0,0 +1,657 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Platform } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import RevenueCatUI, { PAYWALL_RESULT } from "react-native-purchases-ui";
|
||||
import { usePostHog } from 'posthog-react-native';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { Language } from '../../types';
|
||||
import { PurchaseProductId } from '../../services/backend/contracts';
|
||||
|
||||
const getBillingCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
title: 'Abo und Credits',
|
||||
planLabel: 'Aktueller Plan',
|
||||
planFree: 'Free',
|
||||
planPro: 'Pro',
|
||||
creditsAvailableLabel: 'Verfügbare Credits',
|
||||
manageSubscription: 'Abo verwalten',
|
||||
subscriptionTitle: 'Abos',
|
||||
subscriptionHint: 'Wähle ein Abo und schalte stärkere KI-Scans sowie mehr Credits frei.',
|
||||
freePlanName: 'Free',
|
||||
freePlanPrice: '0 EUR / Monat',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4.99 EUR / Monat',
|
||||
proBadgeText: 'EMPFOHLEN',
|
||||
proYearlyPlanName: 'Pro',
|
||||
proYearlyPlanPrice: '39.99 EUR / Jahr',
|
||||
proYearlyBadgeText: 'SPAREN',
|
||||
proBenefits: [
|
||||
'250 Credits jeden Monat',
|
||||
'Pro-Scans mit GPT-5.4',
|
||||
'Unbegrenzte Historie & Galerie',
|
||||
'KI-Pflanzendoktor inklusive',
|
||||
'Priorisierter Support'
|
||||
],
|
||||
topupTitle: 'Credits Aufladen',
|
||||
topupSmall: '25 Credits – 1,99 €',
|
||||
topupMedium: '120 Credits – 6,99 €',
|
||||
topupLarge: '300 Credits – 12,99 €',
|
||||
topupBestValue: 'BESTES ANGEBOT',
|
||||
cancelTitle: 'Schade, dass du gehst',
|
||||
cancelQuestion: 'Dürfen wir fragen, warum du kündigst?',
|
||||
reasonTooExpensive: 'Es ist mir zu teuer',
|
||||
reasonNotUsing: 'Ich nutze die App zu selten',
|
||||
reasonOther: 'Ein anderer Grund',
|
||||
offerTitle: 'Ein Geschenk für dich!',
|
||||
offerText: 'Bleib dabei und erhalte den nächsten Monat für nur 2,49 € (50% Rabatt).',
|
||||
offerAccept: 'Rabatt sichern',
|
||||
offerDecline: 'Nein, Kündigung fortsetzen',
|
||||
confirmCancelBtn: 'Jetzt kündigen',
|
||||
};
|
||||
} else if (language === 'es') {
|
||||
return {
|
||||
title: 'Suscripción y Créditos',
|
||||
planLabel: 'Plan Actual',
|
||||
planFree: 'Gratis',
|
||||
planPro: 'Pro',
|
||||
creditsAvailableLabel: 'Créditos Disponibles',
|
||||
manageSubscription: 'Administrar Suscripción',
|
||||
subscriptionTitle: 'Suscripciones',
|
||||
subscriptionHint: 'Elige un plan y desbloquea escaneos con IA más potentes y más créditos.',
|
||||
freePlanName: 'Gratis',
|
||||
freePlanPrice: '0 EUR / Mes',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4.99 EUR / Mes',
|
||||
proBadgeText: 'RECOMENDADO',
|
||||
proYearlyPlanName: 'Pro',
|
||||
proYearlyPlanPrice: '39.99 EUR / Año',
|
||||
proYearlyBadgeText: 'AHORRAR',
|
||||
proBenefits: [
|
||||
'250 créditos cada mes',
|
||||
'Escaneos Pro con GPT-5.4',
|
||||
'Historial y galería ilimitados',
|
||||
'Doctor de plantas de IA incluido',
|
||||
'Soporte prioritario'
|
||||
],
|
||||
topupTitle: 'Recargar Créditos',
|
||||
topupSmall: '25 Créditos – 1,99 €',
|
||||
topupMedium: '120 Créditos – 6,99 €',
|
||||
topupLarge: '300 Créditos – 12,99 €',
|
||||
topupBestValue: 'MEJOR OFERTA',
|
||||
cancelTitle: 'Lamentamos verte ir',
|
||||
cancelQuestion: '¿Podemos saber por qué cancelas?',
|
||||
reasonTooExpensive: 'Es muy caro',
|
||||
reasonNotUsing: 'No lo uso suficiente',
|
||||
reasonOther: 'Otra razón',
|
||||
offerTitle: '¡Un regalo para ti!',
|
||||
offerText: 'Quédate y obtén el próximo mes por solo 2,49 € (50% de descuento).',
|
||||
offerAccept: 'Aceptar descuento',
|
||||
offerDecline: 'No, continuar cancelando',
|
||||
confirmCancelBtn: 'Cancelar ahora',
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: 'Billing & Credits',
|
||||
planLabel: 'Current Plan',
|
||||
planFree: 'Free',
|
||||
planPro: 'Pro',
|
||||
creditsAvailableLabel: 'Available Credits',
|
||||
manageSubscription: 'Manage Subscription',
|
||||
subscriptionTitle: 'Subscriptions',
|
||||
subscriptionHint: 'Choose a plan to unlock stronger AI scans and more credits.',
|
||||
freePlanName: 'Free',
|
||||
freePlanPrice: '0 EUR / Month',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4.99 EUR / Month',
|
||||
proBadgeText: 'RECOMMENDED',
|
||||
proYearlyPlanName: 'Pro',
|
||||
proYearlyPlanPrice: '39.99 EUR / Year',
|
||||
proYearlyBadgeText: 'SAVE',
|
||||
proBenefits: [
|
||||
'250 credits every month',
|
||||
'Pro scans with GPT-5.4',
|
||||
'Unlimited history & gallery',
|
||||
'AI Plant Doctor included',
|
||||
'Priority support'
|
||||
],
|
||||
topupTitle: 'Topup Credits',
|
||||
topupSmall: '25 Credits – €1.99',
|
||||
topupMedium: '120 Credits – €6.99',
|
||||
topupLarge: '300 Credits – €12.99',
|
||||
topupBestValue: 'BEST VALUE',
|
||||
cancelTitle: 'Sorry to see you go',
|
||||
cancelQuestion: 'May we ask why you are cancelling?',
|
||||
reasonTooExpensive: 'It is too expensive',
|
||||
reasonNotUsing: 'I don\'t use it enough',
|
||||
reasonOther: 'Other reason',
|
||||
offerTitle: 'A gift for you!',
|
||||
offerText: 'Stay with us and get your next month for just €2.49 (50% off).',
|
||||
offerAccept: 'Claim discount',
|
||||
offerDecline: 'No, continue cancelling',
|
||||
confirmCancelBtn: 'Cancel now',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default function BillingScreen() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode, language, billingSummary, isLoadingBilling, simulatePurchase, simulateWebhookEvent, appearanceMode, colorPalette, session } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const copy = getBillingCopy(language);
|
||||
const posthog = usePostHog();
|
||||
|
||||
const [subModalVisible, setSubModalVisible] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
// Cancel Flow State
|
||||
const [cancelStep, setCancelStep] = useState<'none' | 'survey' | 'offer'>('none');
|
||||
const [cancelReason, setCancelReason] = useState<string | null>(null);
|
||||
|
||||
const planId = billingSummary?.entitlement?.plan || 'free';
|
||||
const credits = isLoadingBilling && !billingSummary ? '...' : (billingSummary?.credits?.available ?? '--');
|
||||
|
||||
const handlePurchase = async (productId: PurchaseProductId) => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const paywallResult: PAYWALL_RESULT = await RevenueCatUI.presentPaywall();
|
||||
switch (paywallResult) {
|
||||
case PAYWALL_RESULT.NOT_PRESENTED:
|
||||
case PAYWALL_RESULT.ERROR:
|
||||
case PAYWALL_RESULT.CANCELLED:
|
||||
break;
|
||||
case PAYWALL_RESULT.PURCHASED:
|
||||
case PAYWALL_RESULT.RESTORED:
|
||||
await simulatePurchase(productId);
|
||||
setSubModalVisible(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error('Payment failed', e);
|
||||
if (__DEV__ && (msg.toLowerCase().includes('native') || msg.toLowerCase().includes('not found') || msg.toLowerCase().includes('undefined'))) {
|
||||
// Fallback for Expo Go since RevenueCat native module is not available
|
||||
console.log('Falling back to simulated purchase in Expo Go');
|
||||
await simulatePurchase(productId);
|
||||
setSubModalVisible(false);
|
||||
} else {
|
||||
Alert.alert('Unerwarteter Fehler', msg);
|
||||
}
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSimulatePurchase = async (productId: PurchaseProductId) => {
|
||||
// Fallback for free option
|
||||
setIsUpdating(true);
|
||||
await simulatePurchase(productId);
|
||||
setIsUpdating(false);
|
||||
setSubModalVisible(false);
|
||||
};
|
||||
|
||||
const handleDowngrade = async () => {
|
||||
if (planId === 'free') return; // already on free plan
|
||||
setCancelStep('survey');
|
||||
};
|
||||
|
||||
const finalizeCancel = async () => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await simulateWebhookEvent('entitlement_revoked');
|
||||
setCancelStep('none');
|
||||
setSubModalVisible(false);
|
||||
} catch (e) {
|
||||
console.error('Downgrade failed', e);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<ThemeBackdrop colors={colors} />
|
||||
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
{isLoadingBilling ? (
|
||||
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 40 }} />
|
||||
) : (
|
||||
<>
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.planLabel}</Text>
|
||||
<View style={[styles.row, { marginBottom: 16 }]}>
|
||||
<Text style={[styles.value, { color: colors.text }]}>
|
||||
{planId === 'pro' ? copy.planPro : copy.planFree}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.manageBtn, { backgroundColor: colors.primary }]}
|
||||
onPress={() => setSubModalVisible(true)}
|
||||
>
|
||||
<Text style={styles.manageBtnText}>{copy.manageSubscription}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.creditsAvailableLabel}</Text>
|
||||
<Text style={[styles.creditsValue, { color: colors.text }]}>{credits}</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.topupTitle}</Text>
|
||||
<View style={{ gap: 10, marginTop: 8 }}>
|
||||
{([
|
||||
{ id: 'topup_small' as PurchaseProductId, label: copy.topupSmall },
|
||||
{ id: 'topup_medium' as PurchaseProductId, label: copy.topupMedium, badge: copy.topupBestValue },
|
||||
{ id: 'topup_large' as PurchaseProductId, label: copy.topupLarge },
|
||||
] as { id: PurchaseProductId; label: string; badge?: string }[]).map((pack) => (
|
||||
<TouchableOpacity
|
||||
key={pack.id}
|
||||
style={[
|
||||
styles.topupBtn,
|
||||
{
|
||||
borderColor: pack.badge ? colors.primary : colors.border,
|
||||
backgroundColor: pack.badge ? colors.primary + '15' : 'transparent',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
}
|
||||
]}
|
||||
onPress={() => handlePurchase(pack.id)}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Ionicons name="flash" size={18} color={colors.primary} />
|
||||
<Text style={[styles.topupText, { color: colors.text }]}>
|
||||
{isUpdating ? '...' : pack.label}
|
||||
</Text>
|
||||
</View>
|
||||
{pack.badge && (
|
||||
<View style={{ backgroundColor: colors.primary, borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2 }}>
|
||||
<Text style={{ color: '#fff', fontSize: 10, fontWeight: '700' }}>{pack.badge}</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
|
||||
<Modal visible={subModalVisible} transparent animationType="slide" onRequestClose={() => setSubModalVisible(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalContent, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={[styles.modalTitle, { color: colors.text }]}>
|
||||
{cancelStep === 'survey' ? copy.cancelTitle : cancelStep === 'offer' ? copy.offerTitle : copy.subscriptionTitle}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setSubModalVisible(false);
|
||||
setCancelStep('none');
|
||||
}}>
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{cancelStep === 'none' ? (
|
||||
<>
|
||||
<Text style={[styles.modalHint, { color: colors.text + '80' }]}>{copy.subscriptionHint}</Text>
|
||||
<View style={styles.plansContainer}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.planOption,
|
||||
{ borderColor: colors.border },
|
||||
planId === 'free' && { borderColor: colors.primary, backgroundColor: colors.primary + '10' }
|
||||
]}
|
||||
onPress={handleDowngrade}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<View>
|
||||
<Text style={[styles.planName, { color: colors.text }]}>{copy.freePlanName}</Text>
|
||||
<Text style={[styles.planPrice, { color: colors.text + '80' }]}>{copy.freePlanPrice}</Text>
|
||||
</View>
|
||||
{planId === 'free' && <Ionicons name="checkmark-circle" size={24} color={colors.primary} />}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.planOption,
|
||||
{ borderColor: colors.border },
|
||||
planId === 'pro' && { borderColor: colors.primary, backgroundColor: colors.primary + '10' }
|
||||
]}
|
||||
onPress={() => handlePurchase('monthly_pro')}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.planHeaderRow}>
|
||||
<Text style={[styles.planName, { color: colors.text }]}>{copy.proPlanName}</Text>
|
||||
<View style={[styles.proBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.proBadgeText}>{copy.proBadgeText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[styles.planPrice, { color: colors.text + '80' }]}>{copy.proPlanPrice}</Text>
|
||||
|
||||
<View style={styles.proBenefits}>
|
||||
{copy.proBenefits.map((b, i) => (
|
||||
<View key={i} style={styles.benefitRow}>
|
||||
<Ionicons name="checkmark" size={14} color={colors.primary} />
|
||||
<Text style={[styles.benefitText, { color: colors.textSecondary }]}>{b}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
{planId === 'pro' && <Ionicons name="checkmark-circle" size={24} color={colors.primary} />}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.planOption,
|
||||
{ borderColor: colors.border },
|
||||
planId === 'pro' && { borderColor: colors.primary, backgroundColor: colors.primary + '10' }
|
||||
]}
|
||||
onPress={() => handlePurchase('yearly_pro')}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.planHeaderRow}>
|
||||
<Text style={[styles.planName, { color: colors.text }]}>{copy.proYearlyPlanName}</Text>
|
||||
<View style={[styles.proBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.proBadgeText}>{copy.proYearlyBadgeText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[styles.planPrice, { color: colors.text + '80' }]}>{copy.proYearlyPlanPrice}</Text>
|
||||
|
||||
<View style={styles.proBenefits}>
|
||||
{copy.proBenefits.map((b, i) => (
|
||||
<View key={i} style={styles.benefitRow}>
|
||||
<Ionicons name="checkmark" size={14} color={colors.primary} />
|
||||
<Text style={[styles.benefitText, { color: colors.textSecondary }]}>{b}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
{planId === 'pro' && <Ionicons name="checkmark-circle" size={24} color={colors.primary} />}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
) : cancelStep === 'survey' ? (
|
||||
<View style={styles.cancelFlowContainer}>
|
||||
<Text style={[styles.cancelHint, { color: colors.textSecondary }]}>{copy.cancelQuestion}</Text>
|
||||
<View style={styles.reasonList}>
|
||||
{[
|
||||
{ id: 'expensive', label: copy.reasonTooExpensive, icon: 'cash-outline' },
|
||||
{ id: 'not_using', label: copy.reasonNotUsing, icon: 'calendar-outline' },
|
||||
{ id: 'other', label: copy.reasonOther, icon: 'ellipsis-horizontal-outline' },
|
||||
].map((reason) => (
|
||||
<TouchableOpacity
|
||||
key={reason.id}
|
||||
style={[styles.reasonOption, { borderColor: colors.border }]}
|
||||
onPress={() => {
|
||||
setCancelReason(reason.id);
|
||||
setCancelStep('offer');
|
||||
}}
|
||||
>
|
||||
<View style={[styles.reasonIcon, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Ionicons name={reason.icon as any} size={20} color={colors.textSecondary} />
|
||||
</View>
|
||||
<Text style={[styles.reasonText, { color: colors.text }]}>{reason.label}</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.borderStrong} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.cancelFlowContainer}>
|
||||
<View style={[styles.offerCard, { backgroundColor: colors.primarySoft }]}>
|
||||
<View style={[styles.offerIconWrap, { backgroundColor: colors.primary }]}>
|
||||
<Ionicons name="gift" size={28} color="#fff" />
|
||||
</View>
|
||||
<Text style={[styles.offerText, { color: colors.text }]}>{copy.offerText}</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.offerAcceptBtn, { backgroundColor: colors.primary }]}
|
||||
onPress={() => {
|
||||
// Handle applying discount here (future implementation)
|
||||
Alert.alert('Erfolg', 'Rabatt angewendet! (Mock)');
|
||||
setCancelStep('none');
|
||||
setSubModalVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.offerAcceptBtnText}>{copy.offerAccept}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.offerDeclineBtn}
|
||||
onPress={finalizeCancel}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<Text style={[styles.offerDeclineBtnText, { color: colors.textMuted }]}>{copy.offerDecline}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
{isUpdating && cancelStep === 'none' && <ActivityIndicator color={colors.primary} style={{ marginTop: 16 }} />}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', padding: 16 },
|
||||
backButton: { width: 40, height: 40, justifyContent: 'center' },
|
||||
title: { flex: 1, fontSize: 20, fontWeight: '700', textAlign: 'center' },
|
||||
scrollContent: { padding: 16, gap: 16 },
|
||||
card: {
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 8,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
value: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
manageBtn: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
},
|
||||
manageBtnText: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
creditsValue: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
},
|
||||
topupBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
gap: 8,
|
||||
},
|
||||
topupText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: '#00000080',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
padding: 24,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
},
|
||||
modalHint: {
|
||||
fontSize: 14,
|
||||
marginBottom: 24,
|
||||
},
|
||||
plansContainer: {
|
||||
gap: 12,
|
||||
},
|
||||
planOption: {
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
planName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
planPrice: {
|
||||
fontSize: 14,
|
||||
},
|
||||
planHeaderRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 2,
|
||||
},
|
||||
proBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 6,
|
||||
},
|
||||
proBadgeText: {
|
||||
color: '#fff',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
},
|
||||
proBenefits: {
|
||||
marginTop: 12,
|
||||
gap: 6,
|
||||
},
|
||||
benefitRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
benefitText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
cancelFlowContainer: {
|
||||
marginTop: 8,
|
||||
},
|
||||
cancelHint: {
|
||||
fontSize: 15,
|
||||
marginBottom: 16,
|
||||
},
|
||||
reasonList: {
|
||||
gap: 12,
|
||||
},
|
||||
reasonOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
},
|
||||
reasonIcon: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
reasonText: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
offerCard: {
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
offerIconWrap: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
offerText: {
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
marginBottom: 24,
|
||||
fontWeight: '500',
|
||||
},
|
||||
offerAcceptBtn: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 24,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
offerAcceptBtnText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
offerDeclineBtn: {
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
offerDeclineBtnText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
226
app/profile/data.tsx
Normal file
226
app/profile/data.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Share, Alert } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { Language } from '../../types';
|
||||
|
||||
const getDataCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
title: 'Daten & Datenschutz',
|
||||
exportData: 'Daten exportieren',
|
||||
exportHint: 'Teilt deine Sammlung als JSON.',
|
||||
clearHistory: 'Lexikon-Verlauf loeschen',
|
||||
clearHistoryHint: 'Entfernt alle letzten Suchbegriffe.',
|
||||
clearHistoryDoneTitle: 'Verlauf geloescht',
|
||||
clearHistoryDoneMessage: 'Der Suchverlauf wurde entfernt.',
|
||||
logout: 'Abmelden',
|
||||
logoutHint: 'Zurueck zum Onboarding und Profil zuruecksetzen.',
|
||||
logoutConfirmTitle: 'Abmelden?',
|
||||
logoutConfirmMessage: 'Du wirst auf den Startbildschirm zurueckgesetzt.',
|
||||
deleteAccount: 'Konto unwiderruflich löschen',
|
||||
deleteAccountHint: 'Löscht alle deine Daten, Pflanzen und Abos permanent.',
|
||||
deleteConfirmTitle: 'Konto wirklich löschen?',
|
||||
deleteConfirmMessage: 'Achtung: Dieser Schritt kann nicht rückgängig gemacht werden. Alle deine Pflanzen, Scans und Credits gehen sofort verloren.',
|
||||
deleteActionBtn: 'Ja, dauerhaft löschen',
|
||||
genericErrorTitle: 'Fehler',
|
||||
genericErrorMessage: 'Aktion konnte nicht abgeschlossen werden.',
|
||||
};
|
||||
}
|
||||
|
||||
if (language === 'es') {
|
||||
return {
|
||||
title: 'Datos y Privacidad',
|
||||
exportData: 'Exportar Datos',
|
||||
exportHint: 'Comparte tu coleccion como JSON.',
|
||||
clearHistory: 'Borrar historial',
|
||||
clearHistoryHint: 'Elimina las busquedas recientes.',
|
||||
clearHistoryDoneTitle: 'Historial borrado',
|
||||
clearHistoryDoneMessage: 'El historial de busqueda ha sido eliminado.',
|
||||
logout: 'Cerrar sesion',
|
||||
logoutHint: 'Volver a la pantalla de inicio y reiniciar perfil.',
|
||||
logoutConfirmTitle: 'Cerrar sesion?',
|
||||
logoutConfirmMessage: 'Seras enviado a la pantalla de inicio.',
|
||||
deleteAccount: 'Eliminar cuenta permanentemente',
|
||||
deleteAccountHint: 'Elimina todos tus datos, plantas y suscripciones.',
|
||||
deleteConfirmTitle: '¿Seguro que quieres eliminar tu cuenta?',
|
||||
deleteConfirmMessage: 'Atención: Este paso no se puede deshacer. Todas tus plantas, escaneos y créditos se perderán inmediatamente.',
|
||||
deleteActionBtn: 'Sí, eliminar permanentemente',
|
||||
genericErrorTitle: 'Error',
|
||||
genericErrorMessage: 'La accion no pudo ser completada.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Data & Privacy',
|
||||
exportData: 'Export Data',
|
||||
exportHint: 'Share your collection as JSON.',
|
||||
clearHistory: 'Clear Search History',
|
||||
clearHistoryHint: 'Removes recent search queries.',
|
||||
clearHistoryDoneTitle: 'History Cleared',
|
||||
clearHistoryDoneMessage: 'Search history has been removed.',
|
||||
logout: 'Log Out',
|
||||
logoutHint: 'Return to onboarding and reset profile.',
|
||||
logoutConfirmTitle: 'Log Out?',
|
||||
logoutConfirmMessage: 'You will be returned to the start screen.',
|
||||
deleteAccount: 'Delete Account Permanently',
|
||||
deleteAccountHint: 'Permanently deletes all your data, plants, and subscriptions.',
|
||||
deleteConfirmTitle: 'Are you sure?',
|
||||
deleteConfirmMessage: 'Warning: This cannot be undone. All your plants, scans, and credits will be lost immediately.',
|
||||
deleteActionBtn: 'Yes, delete permanently',
|
||||
genericErrorTitle: 'Error',
|
||||
genericErrorMessage: 'Action could not be completed.',
|
||||
};
|
||||
};
|
||||
|
||||
export default function DataScreen() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode, language, plants, appearanceMode, colorPalette, clearLexiconSearchHistory, signOut } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const copy = getDataCopy(language);
|
||||
|
||||
const handleExportData = async () => {
|
||||
try {
|
||||
const dataStr = JSON.stringify(plants, null, 2);
|
||||
await Share.share({
|
||||
message: dataStr,
|
||||
title: 'GreenLens_Export.json',
|
||||
});
|
||||
} catch {
|
||||
Alert.alert(copy.genericErrorTitle, copy.genericErrorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearHistory = () => {
|
||||
clearLexiconSearchHistory();
|
||||
Alert.alert(copy.clearHistoryDoneTitle, copy.clearHistoryDoneMessage);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
Alert.alert(copy.logoutConfirmTitle, copy.logoutConfirmMessage, [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: copy.logout,
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await signOut();
|
||||
router.replace('/auth/login');
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleDeleteAccount = () => {
|
||||
Alert.alert(copy.deleteConfirmTitle, copy.deleteConfirmMessage, [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: copy.deleteActionBtn,
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
// Future implementation: call backend to wipe user data, cancel active Stripe subscriptions
|
||||
await signOut();
|
||||
router.replace('/onboarding');
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<ThemeBackdrop colors={colors} />
|
||||
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<TouchableOpacity style={[styles.actionRow, { backgroundColor: colors.cardBg, borderColor: colors.border }]} onPress={handleExportData}>
|
||||
<View style={styles.actionIcon}>
|
||||
<Ionicons name="download-outline" size={24} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.actionTextContainer}>
|
||||
<Text style={[styles.actionTitle, { color: colors.text }]}>{copy.exportData}</Text>
|
||||
<Text style={[styles.actionHint, { color: `${colors.text}80` }]}>{copy.exportHint}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.actionRow, { backgroundColor: colors.cardBg, borderColor: colors.border }]} onPress={handleClearHistory}>
|
||||
<View style={styles.actionIcon}>
|
||||
<Ionicons name="time-outline" size={24} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.actionTextContainer}>
|
||||
<Text style={[styles.actionTitle, { color: colors.text }]}>{copy.clearHistory}</Text>
|
||||
<Text style={[styles.actionHint, { color: `${colors.text}80` }]}>{copy.clearHistoryHint}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.actionRow, { backgroundColor: colors.cardBg, borderColor: colors.border }]} onPress={handleLogout}>
|
||||
<View style={styles.actionIcon}>
|
||||
<Ionicons name="log-out-outline" size={24} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.actionTextContainer}>
|
||||
<Text style={[styles.actionTitle, { color: colors.text }]}>{copy.logout}</Text>
|
||||
<Text style={[styles.actionHint, { color: `${colors.text}80` }]}>{copy.logoutHint}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={{ marginTop: 24 }}>
|
||||
<TouchableOpacity style={[styles.actionRow, { backgroundColor: '#FF3B3015', borderColor: '#FF3B3050', marginBottom: 0 }]} onPress={handleDeleteAccount}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: '#FF3B3020' }]}>
|
||||
<Ionicons name="trash-bin-outline" size={24} color="#FF3B30" />
|
||||
</View>
|
||||
<View style={styles.actionTextContainer}>
|
||||
<Text style={[styles.actionTitle, { color: '#FF3B30' }]}>{copy.deleteAccount}</Text>
|
||||
<Text style={[styles.actionHint, { color: '#FF3B3080' }]}>{copy.deleteAccountHint}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', padding: 16 },
|
||||
backButton: { width: 40, height: 40, justifyContent: 'center' },
|
||||
title: { flex: 1, fontSize: 20, fontWeight: '700', textAlign: 'center' },
|
||||
scrollContent: { padding: 16, gap: 16 },
|
||||
actionRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
marginBottom: 16,
|
||||
},
|
||||
actionIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#00000010',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
actionTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
actionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
actionHint: {
|
||||
fontSize: 13,
|
||||
},
|
||||
});
|
||||
229
app/profile/preferences.tsx
Normal file
229
app/profile/preferences.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { AppearanceMode, ColorPalette, Language } from '../../types';
|
||||
|
||||
const PALETTE_SWATCHES: Record<ColorPalette, string[]> = {
|
||||
forest: ['#5fa779', '#3d7f57'],
|
||||
ocean: ['#5a90be', '#3d6f99'],
|
||||
sunset: ['#c98965', '#a36442'],
|
||||
mono: ['#7b8796', '#5b6574'],
|
||||
};
|
||||
|
||||
const getPreferencesCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
title: 'Einstellungen',
|
||||
appearanceMode: 'Modell',
|
||||
colorPalette: 'Farbpalette',
|
||||
languageLabel: 'Sprache',
|
||||
themeSystem: 'System',
|
||||
themeLight: 'Hell',
|
||||
themeDark: 'Dunkel',
|
||||
paletteForest: 'Forest',
|
||||
paletteOcean: 'Ocean',
|
||||
paletteSunset: 'Sunset',
|
||||
paletteMono: 'Mono',
|
||||
};
|
||||
} else if (language === 'es') {
|
||||
return {
|
||||
title: 'Ajustes',
|
||||
appearanceMode: 'Modo',
|
||||
colorPalette: 'Paleta',
|
||||
languageLabel: 'Idioma',
|
||||
themeSystem: 'Sistema',
|
||||
themeLight: 'Claro',
|
||||
themeDark: 'Oscuro',
|
||||
paletteForest: 'Forest',
|
||||
paletteOcean: 'Ocean',
|
||||
paletteSunset: 'Sunset',
|
||||
paletteMono: 'Mono',
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: 'Preferences',
|
||||
appearanceMode: 'Appearance Mode',
|
||||
colorPalette: 'Color Palette',
|
||||
languageLabel: 'Language',
|
||||
themeSystem: 'System',
|
||||
themeLight: 'Light',
|
||||
themeDark: 'Dark',
|
||||
paletteForest: 'Forest',
|
||||
paletteOcean: 'Ocean',
|
||||
paletteSunset: 'Sunset',
|
||||
paletteMono: 'Mono',
|
||||
};
|
||||
};
|
||||
|
||||
export default function PreferencesScreen() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode, appearanceMode, colorPalette, language, setAppearanceMode, setColorPalette, changeLanguage } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const copy = getPreferencesCopy(language);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<ThemeBackdrop colors={colors} />
|
||||
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.appearanceMode}</Text>
|
||||
<View style={styles.segmentedControl}>
|
||||
{(['system', 'light', 'dark'] as AppearanceMode[]).map((mode) => {
|
||||
const isActive = appearanceMode === mode;
|
||||
const label = mode === 'system' ? copy.themeSystem : mode === 'light' ? copy.themeLight : copy.themeDark;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={mode}
|
||||
style={[
|
||||
styles.segmentBtn,
|
||||
isActive && { backgroundColor: colors.primary },
|
||||
]}
|
||||
onPress={() => setAppearanceMode(mode)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.segmentText,
|
||||
{ color: isActive ? '#fff' : colors.text }
|
||||
]}>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.colorPalette}</Text>
|
||||
<View style={styles.swatchContainer}>
|
||||
{(['forest', 'ocean', 'sunset', 'mono'] as ColorPalette[]).map((p) => {
|
||||
const isActive = colorPalette === p;
|
||||
const swatch = PALETTE_SWATCHES[p] || ['#ccc', '#999'];
|
||||
const label = p === 'forest' ? copy.paletteForest : p === 'ocean' ? copy.paletteOcean : p === 'sunset' ? copy.paletteSunset : copy.paletteMono;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={p}
|
||||
style={[
|
||||
styles.swatchWrap,
|
||||
isActive && { borderColor: colors.primary, borderWidth: 2 }
|
||||
]}
|
||||
onPress={() => setColorPalette(p)}
|
||||
>
|
||||
<View style={[styles.swatch, { backgroundColor: swatch[0] }]} />
|
||||
<Text style={[styles.swatchLabel, { color: colors.text }]}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.languageLabel}</Text>
|
||||
<View style={styles.langRow}>
|
||||
{(['en', 'de', 'es'] as Language[]).map(lang => {
|
||||
const isActive = language === lang;
|
||||
const label = lang === 'en' ? 'English' : lang === 'de' ? 'Deutsch' : 'Español';
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={lang}
|
||||
style={[
|
||||
styles.langBtn,
|
||||
isActive && { backgroundColor: colors.primary }
|
||||
]}
|
||||
onPress={() => changeLanguage(lang)}
|
||||
>
|
||||
<Text style={isActive ? { color: '#fff', fontWeight: '600' } : { color: colors.text }}>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', padding: 16 },
|
||||
backButton: { width: 40, height: 40, justifyContent: 'center' },
|
||||
title: { flex: 1, fontSize: 20, fontWeight: '700', textAlign: 'center' },
|
||||
scrollContent: { padding: 16, gap: 16 },
|
||||
card: {
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 16,
|
||||
},
|
||||
segmentedControl: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#00000010',
|
||||
borderRadius: 12,
|
||||
padding: 4,
|
||||
},
|
||||
segmentBtn: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
},
|
||||
segmentText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
swatchContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
swatchWrap: {
|
||||
alignItems: 'center',
|
||||
padding: 4,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
gap: 6,
|
||||
},
|
||||
swatch: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
},
|
||||
swatchLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
langRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
},
|
||||
langBtn: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#00000010',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user