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