This commit is contained in:
2026-07-08 22:07:59 +02:00
parent 0b2305a3ad
commit 9be9b2987b
10 changed files with 349 additions and 268 deletions

View File

@@ -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",

View File

@@ -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',

View File

@@ -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" />;

View File

@@ -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"
/> />

View File

@@ -25,7 +25,7 @@ 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();
@@ -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);

View File

@@ -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
View File

@@ -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",

View File

@@ -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": {

View File

@@ -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);

View File

@@ -254,7 +254,7 @@ export const translations = {
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",
@@ -526,7 +526,7 @@ registerToSave: "Sign up to save",
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",
@@ -798,7 +798,7 @@ registerToSave: "Regístrate para guardar",
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",