generated from knso/webklar-preview-template
Landingpage mit Warteliste + Aktions-Code (5 statt 3 Gratis-Analysen)
- Webseite: neue Landingpage (Hero, Features, Warteliste-Anmeldung per Mail-Interim, FAQ); Datenschutz um Warteliste-Abschnitt ergänzt - App: Aktions-Code-Einlösung in den Einstellungen (FRUEHSTART -> 5 Gratis-Analysen), ScanScreen nutzt dynamisches Kontingent - Newsletter-Dienst (Brevo/MailerLite) folgt: Formular-Embed ersetzt dann den Mail-Button, Datenschutz-Platzhalter ausfuellen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,14 @@ import { Alert, ScrollView, StyleSheet, Text, TextInput, View } from 'react-nati
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../types';
|
||||
import { holeApiKey, speichereApiKey, loescheAlleDaten } from '../services/storage';
|
||||
import {
|
||||
BONUS_ANALYSEN,
|
||||
holeApiKey,
|
||||
holeBonusAktiv,
|
||||
loeseBonusCodeEin,
|
||||
loescheAlleDaten,
|
||||
speichereApiKey,
|
||||
} from '../services/storage';
|
||||
import { NUTZT_PROXY } from '../services/claudeClient';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { GrossButton } from '../components/GrossButton';
|
||||
@@ -21,11 +28,31 @@ export function EinstellungenScreen({ navigation }: Props) {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [keyVorhanden, setKeyVorhanden] = useState(false);
|
||||
const [gespeichert, setGespeichert] = useState(false);
|
||||
const [bonusCode, setBonusCode] = useState('');
|
||||
const [bonusAktiv, setBonusAktiv] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
holeApiKey().then((k) => setKeyVorhanden(!!k));
|
||||
holeBonusAktiv().then(setBonusAktiv);
|
||||
}, []);
|
||||
|
||||
const codeEinloesen = async () => {
|
||||
const erfolg = await loeseBonusCodeEin(bonusCode);
|
||||
if (erfolg) {
|
||||
setBonusAktiv(true);
|
||||
setBonusCode('');
|
||||
Alert.alert(
|
||||
'Code eingelöst! 🎉',
|
||||
`Sie haben jetzt ${BONUS_ANALYSEN} kostenlose Brief-Analysen.`
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Code ungültig',
|
||||
'Bitte prüfen Sie die Schreibweise. Den Code finden Sie in Ihrer Anmelde-E-Mail.'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const speichern = async () => {
|
||||
await speichereApiKey(apiKey);
|
||||
setKeyVorhanden(!!apiKey.trim());
|
||||
@@ -94,6 +121,40 @@ export function EinstellungenScreen({ navigation }: Props) {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text style={styles.abschnittTitel}>Aktions-Code</Text>
|
||||
{bonusAktiv ? (
|
||||
<Text style={styles.text}>
|
||||
<Text style={{ fontWeight: '700', color: farben.ampelGruen }}>
|
||||
Code eingelöst ✓
|
||||
</Text>{' '}
|
||||
— Sie haben {BONUS_ANALYSEN} kostenlose Analysen.
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.hinweis}>
|
||||
Sie haben sich auf behoerdenklar.pages.dev angemeldet? Lösen Sie
|
||||
hier den Code aus Ihrer E-Mail ein.
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.eingabe}
|
||||
placeholder="Code eingeben"
|
||||
placeholderTextColor={farben.textSekundaer}
|
||||
value={bonusCode}
|
||||
onChangeText={setBonusCode}
|
||||
autoCapitalize="characters"
|
||||
autoCorrect={false}
|
||||
accessibilityLabel="Aktions-Code eingeben"
|
||||
/>
|
||||
<GrossButton
|
||||
titel="Code einlösen"
|
||||
onPress={codeEinloesen}
|
||||
deaktiviert={!bonusCode.trim()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={styles.trenner} />
|
||||
|
||||
<Text style={styles.abschnittTitel}>Datenschutz</Text>
|
||||
<View style={styles.karte}>
|
||||
<Text style={styles.text}>
|
||||
|
||||
@@ -13,7 +13,12 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList, BriefEintrag } from '../types';
|
||||
import { analysiereBrief } from '../services/analyse';
|
||||
import { ClaudeFehler, NUTZT_PROXY } from '../services/claudeClient';
|
||||
import { GRATIS_ANALYSEN, holeAnzahlAnalysen, zaehleAnalyse } from '../services/storage';
|
||||
import {
|
||||
GRATIS_ANALYSEN,
|
||||
holeAnzahlAnalysen,
|
||||
holeGratisKontingent,
|
||||
zaehleAnalyse,
|
||||
} from '../services/storage';
|
||||
import { planeErinnerungen } from '../services/erinnerungen';
|
||||
import { useAppStore, neueId } from '../store/useAppStore';
|
||||
import { GrossButton } from '../components/GrossButton';
|
||||
@@ -54,17 +59,21 @@ export function ScanScreen({ navigation }: Props) {
|
||||
const addBrief = useAppStore((s) => s.addBrief);
|
||||
const [laedt, setLaedt] = useState(false);
|
||||
const [verbraucht, setVerbraucht] = useState(0);
|
||||
const [kontingent, setKontingent] = useState(GRATIS_ANALYSEN);
|
||||
|
||||
// Zähler bei jedem Screen-Besuch frisch laden
|
||||
// Zähler + Kontingent bei jedem Screen-Besuch frisch laden
|
||||
// (Kontingent kann sich ändern, wenn in den Einstellungen ein
|
||||
// Aktions-Code eingelöst wurde)
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
holeAnzahlAnalysen().then(setVerbraucht);
|
||||
holeGratisKontingent().then(setKontingent);
|
||||
}, [])
|
||||
);
|
||||
|
||||
// Freemium gilt nur im Produktionsmodus (Proxy). Im Dev-Modus mit
|
||||
// eigenem API-Key zahlt der Entwickler selbst — kein Limit nötig.
|
||||
const uebrig = GRATIS_ANALYSEN - verbraucht;
|
||||
const uebrig = kontingent - verbraucht;
|
||||
const kontingentLeer = NUTZT_PROXY && uebrig <= 0;
|
||||
|
||||
/** Prüft das Gratis-Kontingent; false = Analyse nicht starten. */
|
||||
@@ -72,7 +81,7 @@ export function ScanScreen({ navigation }: Props) {
|
||||
if (!kontingentLeer) return true;
|
||||
Alert.alert(
|
||||
'Gratis-Analysen aufgebraucht',
|
||||
`Sie haben Ihre ${GRATIS_ANALYSEN} kostenlosen Brief-Analysen genutzt. Bald können Sie hier ein Abo abschließen, um weiter Briefe zu analysieren.`
|
||||
`Sie haben Ihre ${kontingent} kostenlosen Brief-Analysen genutzt. Bald können Sie hier ein Abo abschließen, um weiter Briefe zu analysieren.`
|
||||
);
|
||||
return false;
|
||||
};
|
||||
@@ -180,7 +189,7 @@ export function ScanScreen({ navigation }: Props) {
|
||||
<Text style={styles.kontingent}>
|
||||
{kontingentLeer
|
||||
? 'Gratis-Analysen aufgebraucht — Abo folgt in Kürze'
|
||||
: `Noch ${uebrig} von ${GRATIS_ANALYSEN} kostenlosen Analysen`}
|
||||
: `Noch ${uebrig} von ${kontingent} kostenlosen Analysen`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -25,6 +25,33 @@ const KEY_SCAN_ANZAHL = 'behoerdenklar_scan_anzahl';
|
||||
*/
|
||||
export const GRATIS_ANALYSEN = 3;
|
||||
|
||||
/** Erhöhtes Kontingent für Warteliste-Anmelder (Code aus der Willkommens-Mail). */
|
||||
export const BONUS_ANALYSEN = 5;
|
||||
const KEY_BONUS = 'behoerdenklar_bonus';
|
||||
|
||||
/**
|
||||
* Aktions-Codes der Landingpage-Warteliste. Bewusst ein geteilter Code
|
||||
* (kein Einmal-Code): Der "Schaden" bei Weitergabe sind 2 Extra-Analysen
|
||||
* (~5 Cent) — dafür brauchen wir keinerlei Server-Logik.
|
||||
*/
|
||||
const GUELTIGE_CODES = ['FRUEHSTART'];
|
||||
|
||||
export async function holeBonusAktiv(): Promise<boolean> {
|
||||
return (await AsyncStorage.getItem(KEY_BONUS)) === 'ja';
|
||||
}
|
||||
|
||||
/** true = Code war gültig und wurde eingelöst. */
|
||||
export async function loeseBonusCodeEin(code: string): Promise<boolean> {
|
||||
if (!GUELTIGE_CODES.includes(code.trim().toUpperCase())) return false;
|
||||
await AsyncStorage.setItem(KEY_BONUS, 'ja');
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Aktuelles Gratis-Kontingent dieses Geräts (3, mit Aktions-Code 5). */
|
||||
export async function holeGratisKontingent(): Promise<number> {
|
||||
return (await holeBonusAktiv()) ? BONUS_ANALYSEN : GRATIS_ANALYSEN;
|
||||
}
|
||||
|
||||
// ---- API-Key (sensibel -> SecureStore) ----
|
||||
|
||||
export async function holeApiKey(): Promise<string | null> {
|
||||
@@ -113,6 +140,7 @@ export async function loescheAlleDaten(): Promise<void> {
|
||||
KEY_CONSENT,
|
||||
KEY_GERAETE_ID,
|
||||
KEY_SCAN_ANZAHL,
|
||||
KEY_BONUS,
|
||||
]);
|
||||
await SecureStore.deleteItemAsync(KEY_API);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user