TTikotok
This commit is contained in:
2
app.json
2
app.json
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "GreenLens",
|
"name": "GreenLens",
|
||||||
"slug": "greenlens",
|
"slug": "greenlens",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ const getProfileCopy = (language: Language) => {
|
|||||||
logoutConfirmTitle: 'Abmelden?',
|
logoutConfirmTitle: 'Abmelden?',
|
||||||
logoutConfirmMessage: 'Möchtest du dich wirklich abmelden?',
|
logoutConfirmMessage: 'Möchtest du dich wirklich abmelden?',
|
||||||
logoutConfirmBtn: 'Abmelden',
|
logoutConfirmBtn: 'Abmelden',
|
||||||
|
guestTitle: 'Du bist noch nicht angemeldet',
|
||||||
|
guestBody: 'Melde dich an, um Pflanzen zu scannen und deine Sammlung zu sichern.',
|
||||||
|
guestSignIn: 'Anmelden',
|
||||||
|
guestSignUp: 'Kostenlos registrieren',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (language === 'es') {
|
if (language === 'es') {
|
||||||
@@ -79,6 +83,10 @@ const getProfileCopy = (language: Language) => {
|
|||||||
logoutConfirmTitle: '¿Cerrar sesión?',
|
logoutConfirmTitle: '¿Cerrar sesión?',
|
||||||
logoutConfirmMessage: '¿Realmente quieres cerrar sesión?',
|
logoutConfirmMessage: '¿Realmente quieres cerrar sesión?',
|
||||||
logoutConfirmBtn: 'Cerrar sesión',
|
logoutConfirmBtn: 'Cerrar sesión',
|
||||||
|
guestTitle: 'Aún no has iniciado sesión',
|
||||||
|
guestBody: 'Inicia sesión para escanear plantas y guardar tu colección.',
|
||||||
|
guestSignIn: 'Iniciar sesión',
|
||||||
|
guestSignUp: 'Registrarse gratis',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -103,6 +111,10 @@ const getProfileCopy = (language: Language) => {
|
|||||||
logoutConfirmTitle: 'Sign out?',
|
logoutConfirmTitle: 'Sign out?',
|
||||||
logoutConfirmMessage: 'Do you really want to sign out?',
|
logoutConfirmMessage: 'Do you really want to sign out?',
|
||||||
logoutConfirmBtn: 'Sign Out',
|
logoutConfirmBtn: 'Sign Out',
|
||||||
|
guestTitle: "You're not signed in yet",
|
||||||
|
guestBody: 'Sign in to scan plants and keep your collection safe.',
|
||||||
|
guestSignIn: 'Log in',
|
||||||
|
guestSignUp: 'Sign up for free',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,6 +130,7 @@ export default function ProfileScreen() {
|
|||||||
profileName,
|
profileName,
|
||||||
setProfileName,
|
setProfileName,
|
||||||
signOut,
|
signOut,
|
||||||
|
session,
|
||||||
} = useApp();
|
} = useApp();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -199,7 +212,7 @@ export default function ProfileScreen() {
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ label: copy.menuSettings, icon: 'settings-outline', route: '/profile/preferences' as any },
|
{ label: copy.menuSettings, icon: 'settings-outline', route: '/profile/preferences' as any },
|
||||||
{ label: copy.menuBilling, icon: 'card-outline', route: '/profile/billing' as any },
|
{ label: copy.menuBilling, icon: 'card-outline', route: '/profile/billing' as any },
|
||||||
{ label: copy.menuData, icon: 'shield-checkmark-outline', route: '/profile/data' as any },
|
...(session ? [{ label: copy.menuData, icon: 'shield-checkmark-outline', route: '/profile/data' as any }] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -228,6 +241,26 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{!session ? (
|
||||||
|
<View style={[styles.card, styles.accountCard, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder }]}>
|
||||||
|
<Text style={[styles.cardTitle, { color: colors.text }]}>{copy.guestTitle}</Text>
|
||||||
|
<Text style={[styles.guestBody, { color: colors.textSecondary }]}>{copy.guestBody}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.guestPrimaryBtn, { backgroundColor: colors.primary }]}
|
||||||
|
onPress={() => router.push('/auth/signup')}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={[styles.guestPrimaryText, { color: colors.onPrimary }]}>{copy.guestSignUp}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.guestSecondaryBtn}
|
||||||
|
onPress={() => router.push('/auth/login')}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={[styles.guestSecondaryText, { color: colors.primary }]}>{copy.guestSignIn}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<View style={[styles.card, styles.accountCard, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder }]}>
|
<View style={[styles.card, styles.accountCard, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text }]}>{copy.account}</Text>
|
<Text style={[styles.cardTitle, { color: colors.text }]}>{copy.account}</Text>
|
||||||
|
|
||||||
@@ -315,6 +348,7 @@ export default function ProfileScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder, padding: 0, overflow: 'hidden' }]}>
|
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder, padding: 0, overflow: 'hidden' }]}>
|
||||||
{menuItems.map((item, idx) => (
|
{menuItems.map((item, idx) => (
|
||||||
@@ -336,6 +370,7 @@ export default function ProfileScreen() {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
{/* Logout */}
|
{/* Logout */}
|
||||||
|
{session && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.logoutBtn, { borderColor: colors.dangerSoft, backgroundColor: colors.dangerSoft }]}
|
style={[styles.logoutBtn, { borderColor: colors.dangerSoft, backgroundColor: colors.dangerSoft }]}
|
||||||
activeOpacity={0.78}
|
activeOpacity={0.78}
|
||||||
@@ -356,6 +391,7 @@ export default function ProfileScreen() {
|
|||||||
<Ionicons name="log-out-outline" size={18} color={colors.danger} />
|
<Ionicons name="log-out-outline" size={18} color={colors.danger} />
|
||||||
<Text style={[styles.logoutText, { color: colors.danger }]}>{copy.logout}</Text>
|
<Text style={[styles.logoutText, { color: colors.danger }]}>{copy.logout}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={{ height: 40 }} />
|
<View style={{ height: 40 }} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -387,6 +423,30 @@ const styles = StyleSheet.create({
|
|||||||
accountCard: {
|
accountCard: {
|
||||||
marginBottom: 14,
|
marginBottom: 14,
|
||||||
},
|
},
|
||||||
|
guestBody: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 20,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
guestPrimaryBtn: {
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 14,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
guestPrimaryText: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '800',
|
||||||
|
},
|
||||||
|
guestSecondaryBtn: {
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
guestSecondaryText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
logoutBtn: {
|
logoutBtn: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ function RootLayoutInner() {
|
|||||||
colorPalette,
|
colorPalette,
|
||||||
signOut,
|
signOut,
|
||||||
session,
|
session,
|
||||||
|
hasCompletedOnboarding,
|
||||||
isInitializing,
|
isInitializing,
|
||||||
isLoadingPlants,
|
isLoadingPlants,
|
||||||
} = useApp();
|
} = useApp();
|
||||||
@@ -124,7 +125,9 @@ function RootLayoutInner() {
|
|||||||
let content = null;
|
let content = null;
|
||||||
|
|
||||||
if (isAppReady) {
|
if (isAppReady) {
|
||||||
if (!session) {
|
// Guests who finished onboarding (e.g. dismissed the soft paywall) browse the
|
||||||
|
// full app; paid/credit actions are gated at the point of use instead.
|
||||||
|
if (!session && !hasCompletedOnboarding) {
|
||||||
// Only redirect if we are not already on an auth-related page or the scanner
|
// Only redirect if we are not already on an auth-related page or the scanner
|
||||||
if (!isAllowedWithoutSession) {
|
if (!isAllowedWithoutSession) {
|
||||||
content = <Redirect href="/onboarding" />;
|
content = <Redirect href="/onboarding" />;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type SubscriptionProductId = 'monthly_pro' | 'yearly_pro';
|
|||||||
type TopupProductId = Extract<PurchaseProductId, 'topup_small' | 'topup_medium' | 'topup_large'>;
|
type TopupProductId = Extract<PurchaseProductId, 'topup_small' | 'topup_medium' | 'topup_large'>;
|
||||||
type SubscriptionPackages = Partial<Record<SubscriptionProductId, PurchasesPackage>>;
|
type SubscriptionPackages = Partial<Record<SubscriptionProductId, PurchasesPackage>>;
|
||||||
type TopupProducts = Partial<Record<TopupProductId, PurchasesStoreProduct>>;
|
type TopupProducts = Partial<Record<TopupProductId, PurchasesStoreProduct>>;
|
||||||
type PaywallPlanId = 'weekly' | 'yearly';
|
type PaywallPlanId = 'monthly' | 'yearly';
|
||||||
|
|
||||||
const PAYWALL_BACKGROUND = require('../../assets/paywall_scan_background.png');
|
const PAYWALL_BACKGROUND = require('../../assets/paywall_scan_background.png');
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ const getBillingCopy = (language: Language) => {
|
|||||||
planCardBody: 'Unbegrenzte KI-Scans, Gesundheitsdiagnose, 7-Tage-Rettungspläne, 100 Credits/Monat',
|
planCardBody: 'Unbegrenzte KI-Scans, Gesundheitsdiagnose, 7-Tage-Rettungspläne, 100 Credits/Monat',
|
||||||
planCardPriceTrial: (price: string) => `7 Tage gratis, dann ${price}/Jahr`,
|
planCardPriceTrial: (price: string) => `7 Tage gratis, dann ${price}/Jahr`,
|
||||||
planCardPriceMonthly: (price: string) => `${price}/Monat`,
|
planCardPriceMonthly: (price: string) => `${price}/Monat`,
|
||||||
trialToggleLabel: 'Gratis-Test aktiviert',
|
trialToggleLabel: '7 Tage gratis testen',
|
||||||
dueTodayTrial: 'Fällig heute — 7 Tage gratis',
|
dueTodayTrial: 'Fällig heute — 7 Tage gratis',
|
||||||
dueTodayAmount: '0,00 €',
|
dueTodayAmount: '0,00 €',
|
||||||
dueLater: (date: string) => `Fällig am ${date}`,
|
dueLater: (date: string) => `Fällig am ${date}`,
|
||||||
@@ -224,7 +224,7 @@ const getBillingCopy = (language: Language) => {
|
|||||||
planCardBody: 'Escaneos IA ilimitados, diagnóstico de salud, planes de rescate de 7 días, 100 créditos/mes',
|
planCardBody: 'Escaneos IA ilimitados, diagnóstico de salud, planes de rescate de 7 días, 100 créditos/mes',
|
||||||
planCardPriceTrial: (price: string) => `7 días gratis, luego ${price}/año`,
|
planCardPriceTrial: (price: string) => `7 días gratis, luego ${price}/año`,
|
||||||
planCardPriceMonthly: (price: string) => `${price}/mes`,
|
planCardPriceMonthly: (price: string) => `${price}/mes`,
|
||||||
trialToggleLabel: 'Prueba gratis activada',
|
trialToggleLabel: 'Probar 7 días gratis',
|
||||||
dueTodayTrial: 'Hoy — 7 días gratis',
|
dueTodayTrial: 'Hoy — 7 días gratis',
|
||||||
dueTodayAmount: '0,00 €',
|
dueTodayAmount: '0,00 €',
|
||||||
dueLater: (date: string) => `El ${date}`,
|
dueLater: (date: string) => `El ${date}`,
|
||||||
@@ -289,7 +289,7 @@ const getBillingCopy = (language: Language) => {
|
|||||||
planCardBody: 'Unlimited AI scans, health diagnosis, 7-day rescue plans, 100 credits/month',
|
planCardBody: 'Unlimited AI scans, health diagnosis, 7-day rescue plans, 100 credits/month',
|
||||||
planCardPriceTrial: (price: string) => `Free for 7 days, then ${price}/year`,
|
planCardPriceTrial: (price: string) => `Free for 7 days, then ${price}/year`,
|
||||||
planCardPriceMonthly: (price: string) => `${price}/month`,
|
planCardPriceMonthly: (price: string) => `${price}/month`,
|
||||||
trialToggleLabel: 'Free Trial Enabled',
|
trialToggleLabel: 'Try 7 days free',
|
||||||
dueTodayTrial: 'Due today — 7 days free',
|
dueTodayTrial: 'Due today — 7 days free',
|
||||||
dueTodayAmount: '€0.00',
|
dueTodayAmount: '€0.00',
|
||||||
dueLater: (date: string) => `Due ${date}`,
|
dueLater: (date: string) => `Due ${date}`,
|
||||||
@@ -308,7 +308,7 @@ export default function BillingScreen() {
|
|||||||
const params = useLocalSearchParams<{ view?: string; context?: string }>();
|
const params = useLocalSearchParams<{ view?: string; context?: string }>();
|
||||||
const paywallRequested = params.view === 'paywall';
|
const paywallRequested = params.view === 'paywall';
|
||||||
const onboardingContext = params.context === 'onboarding';
|
const onboardingContext = params.context === 'onboarding';
|
||||||
const { isDarkMode, language, billingSummary, isLoadingBilling, simulatePurchase, simulateWebhookEvent, syncRevenueCatState, colorPalette, session } = useApp();
|
const { isDarkMode, language, billingSummary, isLoadingBilling, simulatePurchase, simulateWebhookEvent, syncRevenueCatState, colorPalette, session, hasCompletedOnboarding, markOnboardingCompleted } = useApp();
|
||||||
const colors = useColors(isDarkMode, colorPalette);
|
const colors = useColors(isDarkMode, colorPalette);
|
||||||
const posthog = useSafeAnalytics();
|
const posthog = useSafeAnalytics();
|
||||||
const copy = getBillingCopy(language);
|
const copy = getBillingCopy(language);
|
||||||
@@ -320,7 +320,8 @@ export default function BillingScreen() {
|
|||||||
const [storeError, setStoreError] = useState<string | null>(null);
|
const [storeError, setStoreError] = useState<string | null>(null);
|
||||||
const [subscriptionPackages, setSubscriptionPackages] = useState<SubscriptionPackages>({});
|
const [subscriptionPackages, setSubscriptionPackages] = useState<SubscriptionPackages>({});
|
||||||
const [topupProducts, setTopupProducts] = useState<TopupProducts>({});
|
const [topupProducts, setTopupProducts] = useState<TopupProducts>({});
|
||||||
const [selectedPaywallPlan, setSelectedPaywallPlan] = useState<PaywallPlanId>('yearly');
|
// Monthly is the default; the 7-day trial (yearly plan) must be opted into via the toggle.
|
||||||
|
const [selectedPaywallPlan, setSelectedPaywallPlan] = useState<PaywallPlanId>('monthly');
|
||||||
|
|
||||||
// Cancel Flow State
|
// Cancel Flow State
|
||||||
const [cancelStep, setCancelStep] = useState<'none' | 'survey' | 'offer'>('none');
|
const [cancelStep, setCancelStep] = useState<'none' | 'survey' | 'offer'>('none');
|
||||||
@@ -426,7 +427,9 @@ export default function BillingScreen() {
|
|||||||
if (showPaywallPlans) {
|
if (showPaywallPlans) {
|
||||||
posthog.capture('paywall_dismissed', { context: onboardingContext ? 'onboarding' : 'in_app' });
|
posthog.capture('paywall_dismissed', { context: onboardingContext ? 'onboarding' : 'in_app' });
|
||||||
if (onboardingContext) {
|
if (onboardingContext) {
|
||||||
router.replace('/auth/signup');
|
// Guests continue into the app; paid actions are gated at the point of use.
|
||||||
|
markOnboardingCompleted();
|
||||||
|
router.replace('/(tabs)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (session) {
|
if (session) {
|
||||||
@@ -434,7 +437,7 @@ export default function BillingScreen() {
|
|||||||
else router.replace('/(tabs)');
|
else router.replace('/(tabs)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.replace('/onboarding');
|
router.replace(hasCompletedOnboarding ? '/(tabs)' : '/onboarding');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (router.canGoBack()) {
|
if (router.canGoBack()) {
|
||||||
@@ -442,7 +445,7 @@ export default function BillingScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.replace('/(tabs)');
|
router.replace('/(tabs)');
|
||||||
}, [router, showPaywallPlans, onboardingContext, session, posthog]);
|
}, [router, showPaywallPlans, onboardingContext, session, posthog, hasCompletedOnboarding, markOnboardingCompleted]);
|
||||||
|
|
||||||
const postPurchaseRoute = onboardingContext ? '/auth/signup' : '/(tabs)';
|
const postPurchaseRoute = onboardingContext ? '/auth/signup' : '/(tabs)';
|
||||||
|
|
||||||
@@ -653,7 +656,7 @@ export default function BillingScreen() {
|
|||||||
<Text style={[styles.trialToggleLabel, { color: colors.text }]}>{copy.trialToggleLabel}</Text>
|
<Text style={[styles.trialToggleLabel, { color: colors.text }]}>{copy.trialToggleLabel}</Text>
|
||||||
<Switch
|
<Switch
|
||||||
value={trialEnabled}
|
value={trialEnabled}
|
||||||
onValueChange={(next) => setSelectedPaywallPlan(next ? 'yearly' : 'weekly')}
|
onValueChange={(next) => setSelectedPaywallPlan(next ? 'yearly' : 'monthly')}
|
||||||
trackColor={{ true: colors.primary, false: colors.border }}
|
trackColor={{ true: colors.primary, false: colors.border }}
|
||||||
thumbColor="#FFFFFF"
|
thumbColor="#FFFFFF"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import { PlantRecognitionService } from '../services/plantRecognitionService';
|
|||||||
import { IdentificationResult } from '../types';
|
import { IdentificationResult } from '../types';
|
||||||
import { ResultCard } from '../components/ResultCard';
|
import { ResultCard } from '../components/ResultCard';
|
||||||
import { backendApiClient, isInsufficientCreditsError, isNetworkError, isTimeoutError } from '../services/backend/backendApiClient';
|
import { backendApiClient, isInsufficientCreditsError, isNetworkError, isTimeoutError } from '../services/backend/backendApiClient';
|
||||||
import { isBackendApiError } from '../services/backend/contracts';
|
import { isBackendApiError } from '../services/backend/contracts';
|
||||||
import { createIdempotencyKey } from '../utils/idempotency';
|
import { createIdempotencyKey } from '../utils/idempotency';
|
||||||
import { AuthService } from '../services/authService';
|
import { AuthService } from '../services/authService';
|
||||||
import { consumeSharedImageUri, SHARE_INTENT_KEY } from '../utils/shareHandoff';
|
import { consumeSharedImageUri, SHARE_INTENT_KEY } from '../utils/shareHandoff';
|
||||||
import { OutOfCreditsSheet } from '../components/OutOfCreditsSheet';
|
import { OutOfCreditsSheet } from '../components/OutOfCreditsSheet';
|
||||||
|
|
||||||
const DEMO_SCAN_LIMIT = 3;
|
const DEMO_SCAN_LIMIT = 1;
|
||||||
|
|
||||||
const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||||
if (language === 'de') {
|
if (language === 'de') {
|
||||||
@@ -51,8 +51,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
|||||||
demoTitle: 'Rettungsplan bereit',
|
demoTitle: 'Rettungsplan bereit',
|
||||||
demoMessage: 'Wir haben mögliche Ursachen erkannt. Schalte die vollständige KI-Diagnose und deinen 7-Tage-Rettungsplan frei.',
|
demoMessage: 'Wir haben mögliche Ursachen erkannt. Schalte die vollständige KI-Diagnose und deinen 7-Tage-Rettungsplan frei.',
|
||||||
demoNoCreditsTitle: 'Demo-Scans aufgebraucht',
|
demoNoCreditsTitle: 'Demo-Scans aufgebraucht',
|
||||||
demoNoCreditsMessage: 'Du hast deine 3 kostenlosen Demo-Scans auf diesem Gerät genutzt. Starte Pro, um weiter Pflanzen zu scannen.',
|
demoNoCreditsMessage: 'Du hast deinen kostenlosen Demo-Scan auf diesem Gerät genutzt. Starte Pro, um weiter Pflanzen zu scannen.',
|
||||||
demoCreditsRemaining: (count: number) => `${count} Demo-Scans übrig`,
|
demoCreditsRemaining: (count: number) => (count === 1 ? '1 Demo-Scan übrig' : `${count} Demo-Scans übrig`),
|
||||||
creditsRemaining: (count: number) => `${count} Scans übrig`,
|
creditsRemaining: (count: number) => `${count} Scans übrig`,
|
||||||
appleCta: 'Mit Apple fortfahren',
|
appleCta: 'Mit Apple fortfahren',
|
||||||
emailCta: 'Mit E-Mail fortfahren',
|
emailCta: 'Mit E-Mail fortfahren',
|
||||||
@@ -83,8 +83,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
|||||||
demoTitle: 'Plan de rescate listo',
|
demoTitle: 'Plan de rescate listo',
|
||||||
demoMessage: 'Detectamos posibles causas. Desbloquea el diagnóstico completo con IA y tu plan de rescate de 7 días.',
|
demoMessage: 'Detectamos posibles causas. Desbloquea el diagnóstico completo con IA y tu plan de rescate de 7 días.',
|
||||||
demoNoCreditsTitle: 'Escaneos demo agotados',
|
demoNoCreditsTitle: 'Escaneos demo agotados',
|
||||||
demoNoCreditsMessage: 'Ya usaste tus 3 escaneos demo gratuitos en este dispositivo. Inicia Pro para seguir escaneando plantas.',
|
demoNoCreditsMessage: 'Ya usaste tu escaneo demo gratuito en este dispositivo. Inicia Pro para seguir escaneando plantas.',
|
||||||
demoCreditsRemaining: (count: number) => `${count} escaneos demo restantes`,
|
demoCreditsRemaining: (count: number) => (count === 1 ? '1 escaneo demo restante' : `${count} escaneos demo restantes`),
|
||||||
creditsRemaining: (count: number) => `${count} escaneos restantes`,
|
creditsRemaining: (count: number) => `${count} escaneos restantes`,
|
||||||
appleCta: 'Continuar con Apple',
|
appleCta: 'Continuar con Apple',
|
||||||
emailCta: 'Continuar con email',
|
emailCta: 'Continuar con email',
|
||||||
@@ -114,8 +114,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
|||||||
demoTitle: 'Rescue plan ready',
|
demoTitle: 'Rescue plan ready',
|
||||||
demoMessage: 'We found possible causes. Unlock the full AI diagnosis and your 7-day rescue plan.',
|
demoMessage: 'We found possible causes. Unlock the full AI diagnosis and your 7-day rescue plan.',
|
||||||
demoNoCreditsTitle: 'Demo scans used',
|
demoNoCreditsTitle: 'Demo scans used',
|
||||||
demoNoCreditsMessage: 'You used your 3 free demo scans on this device. Start Pro to keep scanning plants.',
|
demoNoCreditsMessage: 'You used your free demo scan on this device. Start Pro to keep scanning plants.',
|
||||||
demoCreditsRemaining: (count: number) => `${count} demo scans left`,
|
demoCreditsRemaining: (count: number) => (count === 1 ? '1 demo scan left' : `${count} demo scans left`),
|
||||||
creditsRemaining: (count: number) => `${count} scans left`,
|
creditsRemaining: (count: number) => `${count} scans left`,
|
||||||
appleCta: 'Continue with Apple',
|
appleCta: 'Continue with Apple',
|
||||||
emailCta: 'Continue with email',
|
emailCta: 'Continue with email',
|
||||||
@@ -142,6 +142,7 @@ export default function ScannerScreen() {
|
|||||||
setPendingPlant,
|
setPendingPlant,
|
||||||
guestScanCount,
|
guestScanCount,
|
||||||
incrementGuestScanCount,
|
incrementGuestScanCount,
|
||||||
|
hasCompletedOnboarding,
|
||||||
} = useApp();
|
} = useApp();
|
||||||
const colors = useColors(isDarkMode, colorPalette);
|
const colors = useColors(isDarkMode, colorPalette);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -160,7 +161,7 @@ export default function ScannerScreen() {
|
|||||||
: params.sharedImageKey;
|
: params.sharedImageKey;
|
||||||
const hasActiveEntitlement = billingSummary?.entitlement?.plan === 'pro'
|
const hasActiveEntitlement = billingSummary?.entitlement?.plan === 'pro'
|
||||||
&& billingSummary?.entitlement?.status === 'active';
|
&& billingSummary?.entitlement?.status === 'active';
|
||||||
const isDemoMode = !session; // guests get limited AI demo scans; signed-in users burn real credits
|
const isDemoMode = !session; // guests get limited AI demo scans; signed-in users burn real credits
|
||||||
const availableCredits = billingSummary?.credits.available ?? 0;
|
const availableCredits = billingSummary?.credits.available ?? 0;
|
||||||
const demoScansRemaining = Math.max(0, DEMO_SCAN_LIMIT - guestScanCount);
|
const demoScansRemaining = Math.max(0, DEMO_SCAN_LIMIT - guestScanCount);
|
||||||
|
|
||||||
@@ -293,22 +294,22 @@ export default function ScannerScreen() {
|
|||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isDemoMode) {
|
if (isDemoMode) {
|
||||||
posthog.capture('demo_scan_started', {
|
posthog.capture('demo_scan_started', {
|
||||||
authenticated: Boolean(session),
|
authenticated: Boolean(session),
|
||||||
scan_type: isHealthMode ? 'health_check' : 'identification',
|
scan_type: isHealthMode ? 'health_check' : 'identification',
|
||||||
demo_scans_used: guestScanCount,
|
demo_scans_used: guestScanCount,
|
||||||
demo_scans_remaining: demoScansRemaining,
|
demo_scans_remaining: demoScansRemaining,
|
||||||
});
|
});
|
||||||
const demoResult = await PlantRecognitionService.identify(imageUri, language, {
|
const demoResult = await PlantRecognitionService.identify(imageUri, language, {
|
||||||
idempotencyKey: createIdempotencyKey('demo-scan-plant'),
|
idempotencyKey: createIdempotencyKey('demo-scan-plant'),
|
||||||
});
|
});
|
||||||
setAnalysisProgress(100);
|
setAnalysisProgress(100);
|
||||||
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
await new Promise(resolve => setTimeout(resolve, 350));
|
await new Promise(resolve => setTimeout(resolve, 350));
|
||||||
incrementGuestScanCount();
|
incrementGuestScanCount();
|
||||||
setAnalysisResult(demoResult);
|
setAnalysisResult(demoResult);
|
||||||
posthog.capture('demo_scan_completed', {
|
posthog.capture('demo_scan_completed', {
|
||||||
authenticated: Boolean(session),
|
authenticated: Boolean(session),
|
||||||
latency_ms: Date.now() - startTime,
|
latency_ms: Date.now() - startTime,
|
||||||
demo_scans_used_after: guestScanCount + 1,
|
demo_scans_used_after: guestScanCount + 1,
|
||||||
@@ -600,7 +601,7 @@ export default function ScannerScreen() {
|
|||||||
router.back();
|
router.back();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.replace('/onboarding');
|
router.replace(hasCompletedOnboarding ? '/(tabs)' : '/onboarding');
|
||||||
};
|
};
|
||||||
|
|
||||||
const controlsPaddingBottom = Math.max(20, insets.bottom + 10);
|
const controlsPaddingBottom = Math.max(20, insets.bottom + 10);
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ interface AppState {
|
|||||||
getPendingPlant: () => { result: IdentificationResult; imageUri: string } | null;
|
getPendingPlant: () => { result: IdentificationResult; imageUri: string } | null;
|
||||||
guestScanCount: number;
|
guestScanCount: number;
|
||||||
incrementGuestScanCount: () => void;
|
incrementGuestScanCount: () => void;
|
||||||
|
hasCompletedOnboarding: boolean;
|
||||||
|
markOnboardingCompleted: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppContext = createContext<AppState | null>(null);
|
const AppContext = createContext<AppState | null>(null);
|
||||||
@@ -153,6 +155,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const [profileImageUri, setProfileImageUri] = useState<string | null>(null);
|
const [profileImageUri, setProfileImageUri] = useState<string | null>(null);
|
||||||
const [pendingPlant, setPendingPlantState] = useState<{ result: IdentificationResult; imageUri: string } | null>(null);
|
const [pendingPlant, setPendingPlantState] = useState<{ result: IdentificationResult; imageUri: string } | null>(null);
|
||||||
const [guestScanCount, setGuestScanCount] = useState(0);
|
const [guestScanCount, setGuestScanCount] = useState(0);
|
||||||
|
const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState(false);
|
||||||
const [isInitializing, setIsInitializing] = useState(true);
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
const [isLoadingPlants, setIsLoadingPlants] = useState(true);
|
const [isLoadingPlants, setIsLoadingPlants] = useState(true);
|
||||||
const [billingSummary, setBillingSummary] = useState<BillingSummary | null>(null);
|
const [billingSummary, setBillingSummary] = useState<BillingSummary | null>(null);
|
||||||
@@ -372,6 +375,10 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
setGuestScanCount(parseInt(savedCount, 10) || 0);
|
setGuestScanCount(parseInt(savedCount, 10) || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (AppMetaDb.get('onboarding_completed') === '1') {
|
||||||
|
setHasCompletedOnboarding(true);
|
||||||
|
}
|
||||||
|
|
||||||
const s = await AuthService.getSession();
|
const s = await AuthService.getSession();
|
||||||
if (!s) {
|
if (!s) {
|
||||||
resetStateForSignedOutUser();
|
resetStateForSignedOutUser();
|
||||||
@@ -574,6 +581,11 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const markOnboardingCompleted = useCallback(() => {
|
||||||
|
AppMetaDb.set('onboarding_completed', '1');
|
||||||
|
setHasCompletedOnboarding(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{
|
<AppContext.Provider value={{
|
||||||
session,
|
session,
|
||||||
@@ -613,6 +625,8 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
getPendingPlant,
|
getPendingPlant,
|
||||||
guestScanCount,
|
guestScanCount,
|
||||||
incrementGuestScanCount,
|
incrementGuestScanCount,
|
||||||
|
hasCompletedOnboarding,
|
||||||
|
markOnboardingCompleted,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "greenlens",
|
"name": "greenlens",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "greenlens",
|
"name": "greenlens",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "greenlens",
|
"name": "greenlens",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -204,11 +204,11 @@ const resolveIdempotencyKey = (request) => {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureNotGuest = (userId, requiredCredits) => {
|
const ensureNotGuest = (userId, requiredCredits) => {
|
||||||
// Guests may use the limited pre-auth demo identification scan. Other
|
// Guests may use the limited pre-auth demo identification scan. Other
|
||||||
// endpoints must not consume credits from the shared 'guest' account.
|
// endpoints must not consume credits from the shared 'guest' account.
|
||||||
if (isGuest(userId)) {
|
if (isGuest(userId)) {
|
||||||
const error = new Error('Sign in to use scan credits.');
|
const error = new Error('Sign in to use scan credits.');
|
||||||
error.code = 'INSUFFICIENT_CREDITS';
|
error.code = 'INSUFFICIENT_CREDITS';
|
||||||
error.status = 402;
|
error.status = 402;
|
||||||
error.metadata = { required: requiredCredits, available: 0 };
|
error.metadata = { required: requiredCredits, available: 0 };
|
||||||
@@ -726,20 +726,20 @@ app.post('/v1/scan', async (request, response) => {
|
|||||||
let modelUsed = null;
|
let modelUsed = null;
|
||||||
let modelFallbackCount = 0;
|
let modelFallbackCount = 0;
|
||||||
|
|
||||||
const [accountSnapshot, catalogEntries] = await Promise.all([
|
const [accountSnapshot, catalogEntries] = await Promise.all([
|
||||||
getAccountSnapshot(db, userId),
|
getAccountSnapshot(db, userId),
|
||||||
getCachedCatalogEntries(db),
|
getCachedCatalogEntries(db),
|
||||||
]);
|
]);
|
||||||
if (isGuest(userId)) {
|
if (isGuest(userId)) {
|
||||||
modelPath.push('guest-demo-no-credit');
|
modelPath.push('guest-demo-no-credit');
|
||||||
} else {
|
} else {
|
||||||
creditsCharged += await consumeCreditsWithIdempotency(
|
creditsCharged += await consumeCreditsWithIdempotency(
|
||||||
db,
|
db,
|
||||||
userId,
|
userId,
|
||||||
chargeKey('scan-primary', userId, idempotencyKey),
|
chargeKey('scan-primary', userId, idempotencyKey),
|
||||||
SCAN_PRIMARY_COST,
|
SCAN_PRIMARY_COST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free tier gets the same model quality; quantity (3 credits/month) is the differentiator.
|
// Free tier gets the same model quality; quantity (3 credits/month) is the differentiator.
|
||||||
const scanPlan = 'pro';
|
const scanPlan = 'pro';
|
||||||
@@ -767,14 +767,14 @@ app.post('/v1/scan', async (request, response) => {
|
|||||||
usedOpenAi = true;
|
usedOpenAi = true;
|
||||||
modelUsed = openAiPrimary.modelUsed || modelUsed;
|
modelUsed = openAiPrimary.modelUsed || modelUsed;
|
||||||
modelPath.push('openai-primary');
|
modelPath.push('openai-primary');
|
||||||
if (grounded.grounded) modelPath.push('catalog-grounded-primary');
|
if (grounded.grounded) modelPath.push('catalog-grounded-primary');
|
||||||
} else {
|
} else {
|
||||||
if (isGuest(userId)) {
|
if (isGuest(userId)) {
|
||||||
const error = new Error('AI demo scan failed. Please try again with a clearer plant photo.');
|
const error = new Error('AI demo scan failed. Please try again with a clearer plant photo.');
|
||||||
error.code = 'PROVIDER_ERROR';
|
error.code = 'PROVIDER_ERROR';
|
||||||
error.status = 502;
|
error.status = 502;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
console.warn(`OpenAI primary identification returned null for user ${userId} — using catalog fallback.`, {
|
console.warn(`OpenAI primary identification returned null for user ${userId} — using catalog fallback.`, {
|
||||||
attemptedModels: openAiPrimary?.attemptedModels,
|
attemptedModels: openAiPrimary?.attemptedModels,
|
||||||
plant: result?.name,
|
plant: result?.name,
|
||||||
@@ -782,14 +782,14 @@ app.post('/v1/scan', async (request, response) => {
|
|||||||
modelPath.push('openai-primary-failed');
|
modelPath.push('openai-primary-failed');
|
||||||
modelPath.push('catalog-primary-fallback');
|
modelPath.push('catalog-primary-fallback');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isGuest(userId)) {
|
if (isGuest(userId)) {
|
||||||
const error = new Error('AI demo scan is unavailable. Please configure OpenAI before enabling guest demo scans.');
|
const error = new Error('AI demo scan is unavailable. Please configure OpenAI before enabling guest demo scans.');
|
||||||
error.code = 'PROVIDER_ERROR';
|
error.code = 'PROVIDER_ERROR';
|
||||||
error.status = 502;
|
error.status = 502;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
console.log(`OpenAI not configured, using catalog fallback for user ${userId}`);
|
console.log(`OpenAI not configured, using catalog fallback for user ${userId}`);
|
||||||
modelPath.push('openai-not-configured');
|
modelPath.push('openai-not-configured');
|
||||||
modelPath.push('catalog-primary-fallback');
|
modelPath.push('catalog-primary-fallback');
|
||||||
}
|
}
|
||||||
@@ -1219,7 +1219,7 @@ app.get('/api/tiktok/connect', (request, response) => {
|
|||||||
|
|
||||||
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
|
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
|
||||||
authUrl.searchParams.set('client_key', clientKey);
|
authUrl.searchParams.set('client_key', clientKey);
|
||||||
authUrl.searchParams.set('scope', 'user.info.basic,video.publish');
|
authUrl.searchParams.set('scope', 'user.info.basic,video.upload,video.publish');
|
||||||
authUrl.searchParams.set('response_type', 'code');
|
authUrl.searchParams.set('response_type', 'code');
|
||||||
authUrl.searchParams.set('redirect_uri', redirectUri);
|
authUrl.searchParams.set('redirect_uri', redirectUri);
|
||||||
authUrl.searchParams.set('state', oauthState);
|
authUrl.searchParams.set('state', oauthState);
|
||||||
|
|||||||
@@ -29,47 +29,47 @@ export const translations = {
|
|||||||
|
|
||||||
// Info
|
// Info
|
||||||
noPlants: 'Noch keine Pflanzen.',
|
noPlants: 'Noch keine Pflanzen.',
|
||||||
nextStepsTitle: 'Deine nächsten Schritte',
|
nextStepsTitle: 'Deine nächsten Schritte',
|
||||||
stepScan: 'Erste Pflanze scannen',
|
stepScan: 'Erste Pflanze scannen',
|
||||||
stepReminder: 'Smart Reminders aktivieren',
|
stepReminder: 'Smart Reminders aktivieren',
|
||||||
stepLexicon: 'Pflanzenlexikon erkunden',
|
stepLexicon: 'Pflanzenlexikon erkunden',
|
||||||
stepTheme: 'App anpassen',
|
stepTheme: 'App anpassen',
|
||||||
stepCollection: '3 Pflanzen speichern',
|
stepCollection: '3 Pflanzen speichern',
|
||||||
onboardingChecklistTitle: 'Dein Start in GreenLens',
|
onboardingChecklistTitle: 'Dein Start in GreenLens',
|
||||||
onboardingChecklistIntro: 'Onboarding',
|
onboardingChecklistIntro: 'Onboarding',
|
||||||
onboardingChecklistProgress: '{0} von {1} erledigt',
|
onboardingChecklistProgress: '{0} von {1} erledigt',
|
||||||
onboardingChecklistNextLabel: 'Als nächstes: {0}',
|
onboardingChecklistNextLabel: 'Als nächstes: {0}',
|
||||||
onboardingChecklistDone: 'Du hast die wichtigsten Startschritte abgeschlossen.',
|
onboardingChecklistDone: 'Du hast die wichtigsten Startschritte abgeschlossen.',
|
||||||
customizeOnboardingTitle: 'Mach GreenLens zu deiner App',
|
customizeOnboardingTitle: 'Mach GreenLens zu deiner App',
|
||||||
customizeOnboardingSubtitle: 'Wähle Look, Farben und Sprache jetzt kurz aus. Das kostet kaum Zeit, erhöht aber die Bindung.',
|
customizeOnboardingSubtitle: 'Wähle Look, Farben und Sprache jetzt kurz aus. Das kostet kaum Zeit, erhöht aber die Bindung.',
|
||||||
customizeOnboardingPreview: 'Live-Vorschau',
|
customizeOnboardingPreview: 'Live-Vorschau',
|
||||||
customizeOnboardingContinue: 'Fertig und weiter',
|
customizeOnboardingContinue: 'Fertig und weiter',
|
||||||
customizeOnboardingSkip: 'Später',
|
customizeOnboardingSkip: 'Später',
|
||||||
sourceOnboardingTitle: 'Wie hast du GreenLens gefunden?',
|
sourceOnboardingTitle: 'Wie hast du GreenLens gefunden?',
|
||||||
sourceOnboardingSubtitle: 'Eine kurze Antwort hilft uns zu verstehen, welche Kanäle wirklich funktionieren.',
|
sourceOnboardingSubtitle: 'Eine kurze Antwort hilft uns zu verstehen, welche Kanäle wirklich funktionieren.',
|
||||||
sourceOnboardingContinue: 'Weiter',
|
sourceOnboardingContinue: 'Weiter',
|
||||||
sourceOnboardingSkip: 'Überspringen',
|
sourceOnboardingSkip: 'Überspringen',
|
||||||
sourceOptionAppStore: 'App Store / Play Store',
|
sourceOptionAppStore: 'App Store / Play Store',
|
||||||
sourceOptionInstagram: 'Instagram',
|
sourceOptionInstagram: 'Instagram',
|
||||||
sourceOptionTikTok: 'TikTok',
|
sourceOptionTikTok: 'TikTok',
|
||||||
sourceOptionFriend: 'Freunde oder Familie',
|
sourceOptionFriend: 'Freunde oder Familie',
|
||||||
sourceOptionSearch: 'Google oder Suche',
|
sourceOptionSearch: 'Google oder Suche',
|
||||||
sourceOptionOther: 'Etwas anderes',
|
sourceOptionOther: 'Etwas anderes',
|
||||||
goalOnboardingTitle: 'Was willst du zuerst erreichen?',
|
goalOnboardingTitle: 'Was willst du zuerst erreichen?',
|
||||||
goalOnboardingSubtitle: 'So können wir deinen Einstieg besser ausrichten und die richtigen nächsten Schritte zeigen.',
|
goalOnboardingSubtitle: 'So können wir deinen Einstieg besser ausrichten und die richtigen nächsten Schritte zeigen.',
|
||||||
goalOnboardingContinue: 'Weiter',
|
goalOnboardingContinue: 'Weiter',
|
||||||
goalOnboardingSkip: 'Überspringen',
|
goalOnboardingSkip: 'Überspringen',
|
||||||
goalOptionIdentify: 'Pflanzen schnell erkennen',
|
goalOptionIdentify: 'Pflanzen schnell erkennen',
|
||||||
goalOptionCare: 'Pflanzen besser pflegen',
|
goalOptionCare: 'Pflanzen besser pflegen',
|
||||||
goalOptionCollection: 'Meine Sammlung aufbauen',
|
goalOptionCollection: 'Meine Sammlung aufbauen',
|
||||||
goalOptionLearn: 'Mehr über Pflanzen lernen',
|
goalOptionLearn: 'Mehr über Pflanzen lernen',
|
||||||
experienceOnboardingTitle: 'Wie fit bist du bei Pflanzen?',
|
experienceOnboardingTitle: 'Wie fit bist du bei Pflanzen?',
|
||||||
experienceOnboardingSubtitle: 'Damit Tipps und Sprache besser zu deinem Level passen.',
|
experienceOnboardingSubtitle: 'Damit Tipps und Sprache besser zu deinem Level passen.',
|
||||||
experienceOnboardingContinue: 'Fertig',
|
experienceOnboardingContinue: 'Fertig',
|
||||||
experienceOnboardingSkip: 'Überspringen',
|
experienceOnboardingSkip: 'Überspringen',
|
||||||
experienceOptionBeginner: 'Anfänger:in',
|
experienceOptionBeginner: 'Anfänger:in',
|
||||||
experienceOptionIntermediate: 'Schon etwas Erfahrung',
|
experienceOptionIntermediate: 'Schon etwas Erfahrung',
|
||||||
experienceOptionAdvanced: 'Sehr erfahren',
|
experienceOptionAdvanced: 'Sehr erfahren',
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
allGood: 'Alles gut',
|
allGood: 'Alles gut',
|
||||||
@@ -226,10 +226,10 @@ export const translations = {
|
|||||||
tourFabDesc: "Tippe hier um eine Pflanze zu fotografieren — die KI erkennt sie sofort.",
|
tourFabDesc: "Tippe hier um eine Pflanze zu fotografieren — die KI erkennt sie sofort.",
|
||||||
tourSearchTitle: "🔍 Pflanzenlexikon",
|
tourSearchTitle: "🔍 Pflanzenlexikon",
|
||||||
tourSearchDesc: "Durchsuche tausende Pflanzen oder lass die KI nach der perfekten suchen.",
|
tourSearchDesc: "Durchsuche tausende Pflanzen oder lass die KI nach der perfekten suchen.",
|
||||||
tourProfileTitle: "👤 Dein Profil",
|
tourProfileTitle: "👤 Dein Profil",
|
||||||
tourProfileDesc: "Passe Design, Sprache und Benachrichtigungen ganz nach deinem Geschmack an.",
|
tourProfileDesc: "Passe Design, Sprache und Benachrichtigungen ganz nach deinem Geschmack an.",
|
||||||
tourChecklistTitle: "✅ Dein Onboarding",
|
tourChecklistTitle: "✅ Dein Onboarding",
|
||||||
tourChecklistDesc: "Hier siehst du deine nächsten Schritte und kommst schneller zu echtem Nutzen in der App.",
|
tourChecklistDesc: "Hier siehst du deine nächsten Schritte und kommst schneller zu echtem Nutzen in der App.",
|
||||||
coachSkip: "Ueberspringen",
|
coachSkip: "Ueberspringen",
|
||||||
coachNext: "Weiter",
|
coachNext: "Weiter",
|
||||||
coachDone: "Fertig",
|
coachDone: "Fertig",
|
||||||
@@ -247,23 +247,23 @@ export const translations = {
|
|||||||
onboardingFeatureScan: "Pflanzen scannen & erkennen",
|
onboardingFeatureScan: "Pflanzen scannen & erkennen",
|
||||||
onboardingFeatureReminder: "Gießerinnerungen & Pflege",
|
onboardingFeatureReminder: "Gießerinnerungen & Pflege",
|
||||||
onboardingFeatureLexicon: "Digitales Pflanzen-Lexikon",
|
onboardingFeatureLexicon: "Digitales Pflanzen-Lexikon",
|
||||||
onboardingScanBtn: "Pflanze scannen",
|
onboardingScanBtn: "Pflanze scannen",
|
||||||
onboardingRegister: "Registrieren",
|
onboardingRegister: "Registrieren",
|
||||||
onboardingLogin: "Anmelden",
|
onboardingLogin: "Anmelden",
|
||||||
onboardingDisclaimer: "Deine Daten bleiben privat und lokal auf deinem Gerät.",
|
onboardingDisclaimer: "Deine Daten bleiben privat und lokal auf deinem Gerät.",
|
||||||
welcomeHeadline: "Pflanzenpflege\nbeginnt hier",
|
welcomeHeadline: "Pflanzenpflege\nbeginnt hier",
|
||||||
welcomeSubheadline: "Scanne ein Blatt, erkenne die Pflanze und halte sie gesund.",
|
welcomeSubheadline: "Scanne ein Blatt, erkenne die Pflanze und halte sie gesund.",
|
||||||
welcomeFeatureIdentifyTitle: "KI-Pflanzenerkennung",
|
welcomeFeatureIdentifyTitle: "KI-Pflanzenerkennung",
|
||||||
welcomeFeatureIdentifyDesc: "Teste bis zu 5 Demo-Scans direkt auf diesem Gerät.",
|
welcomeFeatureIdentifyDesc: "Teste einen kostenlosen Demo-Scan direkt auf diesem Gerät.",
|
||||||
welcomeFeatureReminderTitle: "Pflegeplan & Erinnerungen",
|
welcomeFeatureReminderTitle: "Pflegeplan & Erinnerungen",
|
||||||
welcomeFeatureReminderDesc: "Erhalte klare Tipps für Gießen, Licht und Standort.",
|
welcomeFeatureReminderDesc: "Erhalte klare Tipps für Gießen, Licht und Standort.",
|
||||||
welcomeFeatureLibraryTitle: "Pflanzen speichern",
|
welcomeFeatureLibraryTitle: "Pflanzen speichern",
|
||||||
welcomeFeatureLibraryDesc: "Registriere dich, um deine gescannten Pflanzen zu sichern.",
|
welcomeFeatureLibraryDesc: "Registriere dich, um deine gescannten Pflanzen zu sichern.",
|
||||||
welcomeDemoScan: "Demo-Scan testen",
|
welcomeDemoScan: "Demo-Scan testen",
|
||||||
welcomeSubscriptionPlans: "Abo-Pläne & Preise ansehen",
|
welcomeSubscriptionPlans: "Abo-Pläne & Preise ansehen",
|
||||||
welcomeLegal: "Abo-Details, Wiederherstellen, Nutzungsbedingungen und Datenschutzrichtlinie werden vor dem Kauf angezeigt.",
|
welcomeLegal: "Abo-Details, Wiederherstellen, Nutzungsbedingungen und Datenschutzrichtlinie werden vor dem Kauf angezeigt.",
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
createAccount: "Konto erstellen",
|
createAccount: "Konto erstellen",
|
||||||
welcomeBack: "Willkommen zurück",
|
welcomeBack: "Willkommen zurück",
|
||||||
namePlaceholder: "Dein Name",
|
namePlaceholder: "Dein Name",
|
||||||
@@ -313,47 +313,47 @@ export const translations = {
|
|||||||
paletteSunset: 'Sunset',
|
paletteSunset: 'Sunset',
|
||||||
paletteMono: 'Mono',
|
paletteMono: 'Mono',
|
||||||
noPlants: 'No plants yet.',
|
noPlants: 'No plants yet.',
|
||||||
nextStepsTitle: 'Your next steps',
|
nextStepsTitle: 'Your next steps',
|
||||||
stepScan: 'Scan first plant',
|
stepScan: 'Scan first plant',
|
||||||
stepReminder: 'Enable smart reminders',
|
stepReminder: 'Enable smart reminders',
|
||||||
stepLexicon: 'Explore plant lexicon',
|
stepLexicon: 'Explore plant lexicon',
|
||||||
stepTheme: 'Customize app',
|
stepTheme: 'Customize app',
|
||||||
stepCollection: 'Save 3 plants',
|
stepCollection: 'Save 3 plants',
|
||||||
onboardingChecklistTitle: 'Your GreenLens kickoff',
|
onboardingChecklistTitle: 'Your GreenLens kickoff',
|
||||||
onboardingChecklistIntro: 'Onboarding',
|
onboardingChecklistIntro: 'Onboarding',
|
||||||
onboardingChecklistProgress: '{0} of {1} done',
|
onboardingChecklistProgress: '{0} of {1} done',
|
||||||
onboardingChecklistNextLabel: 'Next up: {0}',
|
onboardingChecklistNextLabel: 'Next up: {0}',
|
||||||
onboardingChecklistDone: 'You completed the core getting-started steps.',
|
onboardingChecklistDone: 'You completed the core getting-started steps.',
|
||||||
customizeOnboardingTitle: 'Make GreenLens feel like yours',
|
customizeOnboardingTitle: 'Make GreenLens feel like yours',
|
||||||
customizeOnboardingSubtitle: 'Set your look, colors, and language now. It is quick and makes the app feel more personal.',
|
customizeOnboardingSubtitle: 'Set your look, colors, and language now. It is quick and makes the app feel more personal.',
|
||||||
customizeOnboardingPreview: 'Live preview',
|
customizeOnboardingPreview: 'Live preview',
|
||||||
customizeOnboardingContinue: 'Continue',
|
customizeOnboardingContinue: 'Continue',
|
||||||
customizeOnboardingSkip: 'Maybe later',
|
customizeOnboardingSkip: 'Maybe later',
|
||||||
sourceOnboardingTitle: 'How did you find GreenLens?',
|
sourceOnboardingTitle: 'How did you find GreenLens?',
|
||||||
sourceOnboardingSubtitle: 'One quick answer helps us understand which channels are actually working.',
|
sourceOnboardingSubtitle: 'One quick answer helps us understand which channels are actually working.',
|
||||||
sourceOnboardingContinue: 'Continue',
|
sourceOnboardingContinue: 'Continue',
|
||||||
sourceOnboardingSkip: 'Skip',
|
sourceOnboardingSkip: 'Skip',
|
||||||
sourceOptionAppStore: 'App Store / Play Store',
|
sourceOptionAppStore: 'App Store / Play Store',
|
||||||
sourceOptionInstagram: 'Instagram',
|
sourceOptionInstagram: 'Instagram',
|
||||||
sourceOptionTikTok: 'TikTok',
|
sourceOptionTikTok: 'TikTok',
|
||||||
sourceOptionFriend: 'Friends or family',
|
sourceOptionFriend: 'Friends or family',
|
||||||
sourceOptionSearch: 'Google or search',
|
sourceOptionSearch: 'Google or search',
|
||||||
sourceOptionOther: 'Something else',
|
sourceOptionOther: 'Something else',
|
||||||
goalOnboardingTitle: 'What do you want to achieve first?',
|
goalOnboardingTitle: 'What do you want to achieve first?',
|
||||||
goalOnboardingSubtitle: 'This helps us tailor your first steps and show the most relevant actions.',
|
goalOnboardingSubtitle: 'This helps us tailor your first steps and show the most relevant actions.',
|
||||||
goalOnboardingContinue: 'Continue',
|
goalOnboardingContinue: 'Continue',
|
||||||
goalOnboardingSkip: 'Skip',
|
goalOnboardingSkip: 'Skip',
|
||||||
goalOptionIdentify: 'Identify plants quickly',
|
goalOptionIdentify: 'Identify plants quickly',
|
||||||
goalOptionCare: 'Take better care of plants',
|
goalOptionCare: 'Take better care of plants',
|
||||||
goalOptionCollection: 'Build my collection',
|
goalOptionCollection: 'Build my collection',
|
||||||
goalOptionLearn: 'Learn more about plants',
|
goalOptionLearn: 'Learn more about plants',
|
||||||
experienceOnboardingTitle: 'How experienced are you with plants?',
|
experienceOnboardingTitle: 'How experienced are you with plants?',
|
||||||
experienceOnboardingSubtitle: 'So tips and wording can better match your level.',
|
experienceOnboardingSubtitle: 'So tips and wording can better match your level.',
|
||||||
experienceOnboardingContinue: 'Finish',
|
experienceOnboardingContinue: 'Finish',
|
||||||
experienceOnboardingSkip: 'Skip',
|
experienceOnboardingSkip: 'Skip',
|
||||||
experienceOptionBeginner: 'Beginner',
|
experienceOptionBeginner: 'Beginner',
|
||||||
experienceOptionIntermediate: 'Some experience',
|
experienceOptionIntermediate: 'Some experience',
|
||||||
experienceOptionAdvanced: 'Very experienced',
|
experienceOptionAdvanced: 'Very experienced',
|
||||||
allGood: 'All good',
|
allGood: 'All good',
|
||||||
toWater: 'To water',
|
toWater: 'To water',
|
||||||
searchPlaceholder: 'Search plants...',
|
searchPlaceholder: 'Search plants...',
|
||||||
@@ -498,10 +498,10 @@ registerToSave: "Sign up to save",
|
|||||||
tourFabDesc: "Tap here to photograph a plant — the AI recognizes it instantly.",
|
tourFabDesc: "Tap here to photograph a plant — the AI recognizes it instantly.",
|
||||||
tourSearchTitle: "🔍 Plant Encyclopedia",
|
tourSearchTitle: "🔍 Plant Encyclopedia",
|
||||||
tourSearchDesc: "Search thousands of plants or let the AI find the perfect one.",
|
tourSearchDesc: "Search thousands of plants or let the AI find the perfect one.",
|
||||||
tourProfileTitle: "👤 Your Profile",
|
tourProfileTitle: "👤 Your Profile",
|
||||||
tourProfileDesc: "Customize design, language, and notifications to your liking.",
|
tourProfileDesc: "Customize design, language, and notifications to your liking.",
|
||||||
tourChecklistTitle: "✅ Your onboarding",
|
tourChecklistTitle: "✅ Your onboarding",
|
||||||
tourChecklistDesc: "This keeps your next actions visible so you reach real value faster.",
|
tourChecklistDesc: "This keeps your next actions visible so you reach real value faster.",
|
||||||
coachSkip: "Skip",
|
coachSkip: "Skip",
|
||||||
coachNext: "Next",
|
coachNext: "Next",
|
||||||
coachDone: "Done",
|
coachDone: "Done",
|
||||||
@@ -519,23 +519,23 @@ registerToSave: "Sign up to save",
|
|||||||
onboardingFeatureScan: "Scan & identify plants",
|
onboardingFeatureScan: "Scan & identify plants",
|
||||||
onboardingFeatureReminder: "Watering reminders & care",
|
onboardingFeatureReminder: "Watering reminders & care",
|
||||||
onboardingFeatureLexicon: "Digital plant encyclopedia",
|
onboardingFeatureLexicon: "Digital plant encyclopedia",
|
||||||
onboardingScanBtn: "Scan Plant",
|
onboardingScanBtn: "Scan Plant",
|
||||||
onboardingRegister: "Sign Up",
|
onboardingRegister: "Sign Up",
|
||||||
onboardingLogin: "Log In",
|
onboardingLogin: "Log In",
|
||||||
onboardingDisclaimer: "Your data stays private and local on your device.",
|
onboardingDisclaimer: "Your data stays private and local on your device.",
|
||||||
welcomeHeadline: "Plant care\nstarts here",
|
welcomeHeadline: "Plant care\nstarts here",
|
||||||
welcomeSubheadline: "Scan a leaf, learn the plant, keep it healthy.",
|
welcomeSubheadline: "Scan a leaf, learn the plant, keep it healthy.",
|
||||||
welcomeFeatureIdentifyTitle: "AI plant identification",
|
welcomeFeatureIdentifyTitle: "AI plant identification",
|
||||||
welcomeFeatureIdentifyDesc: "Try up to 3 demo scans on this device.",
|
welcomeFeatureIdentifyDesc: "Try one free demo scan on this device.",
|
||||||
welcomeFeatureReminderTitle: "Care plan & reminders",
|
welcomeFeatureReminderTitle: "Care plan & reminders",
|
||||||
welcomeFeatureReminderDesc: "Get clear guidance for water, light, and placement.",
|
welcomeFeatureReminderDesc: "Get clear guidance for water, light, and placement.",
|
||||||
welcomeFeatureLibraryTitle: "Save your plants",
|
welcomeFeatureLibraryTitle: "Save your plants",
|
||||||
welcomeFeatureLibraryDesc: "Sign up to keep scanned plants in your collection.",
|
welcomeFeatureLibraryDesc: "Sign up to keep scanned plants in your collection.",
|
||||||
welcomeDemoScan: "Try Demo Scan",
|
welcomeDemoScan: "Try Demo Scan",
|
||||||
welcomeSubscriptionPlans: "View Subscription Plans & Pricing",
|
welcomeSubscriptionPlans: "View Subscription Plans & Pricing",
|
||||||
welcomeLegal: "Subscription details, Restore, Terms of Use, and Privacy Policy are shown before purchase.",
|
welcomeLegal: "Subscription details, Restore, Terms of Use, and Privacy Policy are shown before purchase.",
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
createAccount: "Create Account",
|
createAccount: "Create Account",
|
||||||
welcomeBack: "Welcome back",
|
welcomeBack: "Welcome back",
|
||||||
namePlaceholder: "Your name",
|
namePlaceholder: "Your name",
|
||||||
@@ -585,47 +585,47 @@ registerToSave: "Sign up to save",
|
|||||||
paletteSunset: 'Sunset',
|
paletteSunset: 'Sunset',
|
||||||
paletteMono: 'Mono',
|
paletteMono: 'Mono',
|
||||||
noPlants: 'Aún no hay plantas.',
|
noPlants: 'Aún no hay plantas.',
|
||||||
nextStepsTitle: 'Tus próximos pasos',
|
nextStepsTitle: 'Tus próximos pasos',
|
||||||
stepScan: 'Escanear primera planta',
|
stepScan: 'Escanear primera planta',
|
||||||
stepReminder: 'Activar recordatorios inteligentes',
|
stepReminder: 'Activar recordatorios inteligentes',
|
||||||
stepLexicon: 'Explorar enciclopedia',
|
stepLexicon: 'Explorar enciclopedia',
|
||||||
stepTheme: 'Personalizar app',
|
stepTheme: 'Personalizar app',
|
||||||
stepCollection: 'Guardar 3 plantas',
|
stepCollection: 'Guardar 3 plantas',
|
||||||
onboardingChecklistTitle: 'Tu inicio en GreenLens',
|
onboardingChecklistTitle: 'Tu inicio en GreenLens',
|
||||||
onboardingChecklistIntro: 'Onboarding',
|
onboardingChecklistIntro: 'Onboarding',
|
||||||
onboardingChecklistProgress: '{0} de {1} completados',
|
onboardingChecklistProgress: '{0} de {1} completados',
|
||||||
onboardingChecklistNextLabel: 'Siguiente paso: {0}',
|
onboardingChecklistNextLabel: 'Siguiente paso: {0}',
|
||||||
onboardingChecklistDone: 'Ya completaste los pasos iniciales clave.',
|
onboardingChecklistDone: 'Ya completaste los pasos iniciales clave.',
|
||||||
customizeOnboardingTitle: 'Haz que GreenLens se sienta tuya',
|
customizeOnboardingTitle: 'Haz que GreenLens se sienta tuya',
|
||||||
customizeOnboardingSubtitle: 'Elige apariencia, colores e idioma ahora. Es rápido y hace la app más personal.',
|
customizeOnboardingSubtitle: 'Elige apariencia, colores e idioma ahora. Es rápido y hace la app más personal.',
|
||||||
customizeOnboardingPreview: 'Vista previa',
|
customizeOnboardingPreview: 'Vista previa',
|
||||||
customizeOnboardingContinue: 'Continuar',
|
customizeOnboardingContinue: 'Continuar',
|
||||||
customizeOnboardingSkip: 'Más tarde',
|
customizeOnboardingSkip: 'Más tarde',
|
||||||
sourceOnboardingTitle: '¿Cómo encontraste GreenLens?',
|
sourceOnboardingTitle: '¿Cómo encontraste GreenLens?',
|
||||||
sourceOnboardingSubtitle: 'Una respuesta rápida nos ayuda a entender qué canales realmente funcionan.',
|
sourceOnboardingSubtitle: 'Una respuesta rápida nos ayuda a entender qué canales realmente funcionan.',
|
||||||
sourceOnboardingContinue: 'Continuar',
|
sourceOnboardingContinue: 'Continuar',
|
||||||
sourceOnboardingSkip: 'Omitir',
|
sourceOnboardingSkip: 'Omitir',
|
||||||
sourceOptionAppStore: 'App Store / Play Store',
|
sourceOptionAppStore: 'App Store / Play Store',
|
||||||
sourceOptionInstagram: 'Instagram',
|
sourceOptionInstagram: 'Instagram',
|
||||||
sourceOptionTikTok: 'TikTok',
|
sourceOptionTikTok: 'TikTok',
|
||||||
sourceOptionFriend: 'Amigos o familia',
|
sourceOptionFriend: 'Amigos o familia',
|
||||||
sourceOptionSearch: 'Google o búsqueda',
|
sourceOptionSearch: 'Google o búsqueda',
|
||||||
sourceOptionOther: 'Otra cosa',
|
sourceOptionOther: 'Otra cosa',
|
||||||
goalOnboardingTitle: '¿Qué quieres lograr primero?',
|
goalOnboardingTitle: '¿Qué quieres lograr primero?',
|
||||||
goalOnboardingSubtitle: 'Así podemos ajustar mejor tus primeros pasos y mostrarte las acciones correctas.',
|
goalOnboardingSubtitle: 'Así podemos ajustar mejor tus primeros pasos y mostrarte las acciones correctas.',
|
||||||
goalOnboardingContinue: 'Continuar',
|
goalOnboardingContinue: 'Continuar',
|
||||||
goalOnboardingSkip: 'Omitir',
|
goalOnboardingSkip: 'Omitir',
|
||||||
goalOptionIdentify: 'Identificar plantas rápido',
|
goalOptionIdentify: 'Identificar plantas rápido',
|
||||||
goalOptionCare: 'Cuidar mejor mis plantas',
|
goalOptionCare: 'Cuidar mejor mis plantas',
|
||||||
goalOptionCollection: 'Construir mi colección',
|
goalOptionCollection: 'Construir mi colección',
|
||||||
goalOptionLearn: 'Aprender más sobre plantas',
|
goalOptionLearn: 'Aprender más sobre plantas',
|
||||||
experienceOnboardingTitle: '¿Qué nivel tienes con plantas?',
|
experienceOnboardingTitle: '¿Qué nivel tienes con plantas?',
|
||||||
experienceOnboardingSubtitle: 'Para que los consejos y el lenguaje encajen mejor con tu nivel.',
|
experienceOnboardingSubtitle: 'Para que los consejos y el lenguaje encajen mejor con tu nivel.',
|
||||||
experienceOnboardingContinue: 'Finalizar',
|
experienceOnboardingContinue: 'Finalizar',
|
||||||
experienceOnboardingSkip: 'Omitir',
|
experienceOnboardingSkip: 'Omitir',
|
||||||
experienceOptionBeginner: 'Principiante',
|
experienceOptionBeginner: 'Principiante',
|
||||||
experienceOptionIntermediate: 'Algo de experiencia',
|
experienceOptionIntermediate: 'Algo de experiencia',
|
||||||
experienceOptionAdvanced: 'Muy avanzado',
|
experienceOptionAdvanced: 'Muy avanzado',
|
||||||
allGood: 'Todo bien',
|
allGood: 'Todo bien',
|
||||||
toWater: 'Regar',
|
toWater: 'Regar',
|
||||||
searchPlaceholder: 'Buscar plantas...',
|
searchPlaceholder: 'Buscar plantas...',
|
||||||
@@ -770,10 +770,10 @@ registerToSave: "Regístrate para guardar",
|
|||||||
tourFabDesc: "Toca aquí para fotografiar una planta — la IA la reconoce al instante.",
|
tourFabDesc: "Toca aquí para fotografiar una planta — la IA la reconoce al instante.",
|
||||||
tourSearchTitle: "🔍 Enciclopedia",
|
tourSearchTitle: "🔍 Enciclopedia",
|
||||||
tourSearchDesc: "Busca en miles de plantas o deja que la IA encuentre la perfecta.",
|
tourSearchDesc: "Busca en miles de plantas o deja que la IA encuentre la perfecta.",
|
||||||
tourProfileTitle: "👤 Tu Perfil",
|
tourProfileTitle: "👤 Tu Perfil",
|
||||||
tourProfileDesc: "Personaliza diseño, idioma y notificaciones a tu gusto.",
|
tourProfileDesc: "Personaliza diseño, idioma y notificaciones a tu gusto.",
|
||||||
tourChecklistTitle: "✅ Tu onboarding",
|
tourChecklistTitle: "✅ Tu onboarding",
|
||||||
tourChecklistDesc: "Aquí ves tus siguientes pasos para llegar antes al valor real de la app.",
|
tourChecklistDesc: "Aquí ves tus siguientes pasos para llegar antes al valor real de la app.",
|
||||||
coachSkip: "Saltar",
|
coachSkip: "Saltar",
|
||||||
coachNext: "Siguiente",
|
coachNext: "Siguiente",
|
||||||
coachDone: "Listo",
|
coachDone: "Listo",
|
||||||
@@ -791,23 +791,23 @@ registerToSave: "Regístrate para guardar",
|
|||||||
onboardingFeatureScan: "Escanea e identifica plantas",
|
onboardingFeatureScan: "Escanea e identifica plantas",
|
||||||
onboardingFeatureReminder: "Recordatorios de riego y cuidado",
|
onboardingFeatureReminder: "Recordatorios de riego y cuidado",
|
||||||
onboardingFeatureLexicon: "Enciclopedia digital de plantas",
|
onboardingFeatureLexicon: "Enciclopedia digital de plantas",
|
||||||
onboardingScanBtn: "Escanear Planta",
|
onboardingScanBtn: "Escanear Planta",
|
||||||
onboardingRegister: "Registrarse",
|
onboardingRegister: "Registrarse",
|
||||||
onboardingLogin: "Iniciar sesión",
|
onboardingLogin: "Iniciar sesión",
|
||||||
onboardingDisclaimer: "Tus datos permanecen privados y locales en tu dispositivo.",
|
onboardingDisclaimer: "Tus datos permanecen privados y locales en tu dispositivo.",
|
||||||
welcomeHeadline: "El cuidado\nempieza aquí",
|
welcomeHeadline: "El cuidado\nempieza aquí",
|
||||||
welcomeSubheadline: "Escanea una hoja, conoce la planta y mantenla sana.",
|
welcomeSubheadline: "Escanea una hoja, conoce la planta y mantenla sana.",
|
||||||
welcomeFeatureIdentifyTitle: "Identificación con IA",
|
welcomeFeatureIdentifyTitle: "Identificación con IA",
|
||||||
welcomeFeatureIdentifyDesc: "Prueba hasta 3 escaneos demo en este dispositivo.",
|
welcomeFeatureIdentifyDesc: "Prueba un escaneo demo gratuito en este dispositivo.",
|
||||||
welcomeFeatureReminderTitle: "Plan de cuidado y recordatorios",
|
welcomeFeatureReminderTitle: "Plan de cuidado y recordatorios",
|
||||||
welcomeFeatureReminderDesc: "Recibe consejos claros sobre riego, luz y ubicación.",
|
welcomeFeatureReminderDesc: "Recibe consejos claros sobre riego, luz y ubicación.",
|
||||||
welcomeFeatureLibraryTitle: "Guardar tus plantas",
|
welcomeFeatureLibraryTitle: "Guardar tus plantas",
|
||||||
welcomeFeatureLibraryDesc: "Regístrate para conservar las plantas escaneadas.",
|
welcomeFeatureLibraryDesc: "Regístrate para conservar las plantas escaneadas.",
|
||||||
welcomeDemoScan: "Probar escaneo demo",
|
welcomeDemoScan: "Probar escaneo demo",
|
||||||
welcomeSubscriptionPlans: "Ver planes y precios",
|
welcomeSubscriptionPlans: "Ver planes y precios",
|
||||||
welcomeLegal: "Los detalles de suscripción, Restaurar, Términos de uso y Política de privacidad se muestran antes de comprar.",
|
welcomeLegal: "Los detalles de suscripción, Restaurar, Términos de uso y Política de privacidad se muestran antes de comprar.",
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
createAccount: "Crear cuenta",
|
createAccount: "Crear cuenta",
|
||||||
welcomeBack: "Bienvenido de vuelta",
|
welcomeBack: "Bienvenido de vuelta",
|
||||||
namePlaceholder: "Tu nombre",
|
namePlaceholder: "Tu nombre",
|
||||||
|
|||||||
Reference in New Issue
Block a user