Keep LITE exports clean of PRO looks, and gate CREATE, FAVORITED and + SAVE behind PRO
LITE previews every look and feature but export/render is blocked while a PRO look is in use, and every LITE export carries the RECIPESCAM mark. Keys are generated offline (FNV-1a checksum over a fixed secret, 16 chars from a no-ambiguity alphabet) and activate one machine or a thousand: nothing is bound to a device, so revoking a leaked key needs Play Billing plus server validation. CREATE, FAVORITED, the star chip and the TopBar + SAVE now answer with a PRO prompt instead of a silent no-op; CREATE still drafts and applies the look so LITE can try it, it just cannot persist. The SETTINGS card also lifts above the keyboard now: RN Modal is its own dialog window, so the IME never resizes or pans it and the UNLOCK row was sitting under the keys.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, Modal, TouchableOpacity } from 'react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, Keyboard, Modal, ScrollView, TouchableOpacity, TextInput } from 'react-native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { X, Volume2, Info, ChevronRight, MapPin, Settings as SettingsIcon } from 'lucide-react-native';
|
||||
import { AspectRatio, MeterMode, ShutterSound, StartupMode } from '../types';
|
||||
@@ -35,6 +35,11 @@ interface SettingsModalProps {
|
||||
onGpsEnabled: (enabled: boolean) => void;
|
||||
startupMode: StartupMode;
|
||||
onStartupMode: (mode: StartupMode) => void;
|
||||
// Lite vs PRO. The unlock key is verified in App (the entitlement module owns
|
||||
// the maths) and comes back false for a bad key, which is all this sheet needs
|
||||
// to know — let alone store.
|
||||
pro: boolean;
|
||||
onUnlock: (key: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
function ChipRow({ chips }: { chips: Chip[] }) {
|
||||
@@ -94,20 +99,40 @@ export default function SettingsModal({
|
||||
onGpsEnabled,
|
||||
startupMode,
|
||||
onStartupMode,
|
||||
pro,
|
||||
onUnlock,
|
||||
}: SettingsModalProps) {
|
||||
const [showCredits, setShowCredits] = useState(false);
|
||||
const [keyDraft, setKeyDraft] = useState('');
|
||||
const [keyError, setKeyError] = useState(false);
|
||||
// The RN Modal is its own dialog window, so the IME does not resize it and
|
||||
// the card ends up under the keyboard. Track the keyboard height and lift the
|
||||
// card by that much instead.
|
||||
const [kb, setKb] = useState(0);
|
||||
useEffect(() => {
|
||||
const show = Keyboard.addListener('keyboardDidShow', (e) => setKb(e.endCoordinates.height));
|
||||
const hide = Keyboard.addListener('keyboardDidHide', () => setKb(0));
|
||||
return () => {
|
||||
show.remove();
|
||||
hide.remove();
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
||||
<View className="flex-1 bg-black/70 items-center justify-center px-6">
|
||||
<View className="flex-1 bg-black/70 items-center justify-center px-6" style={{ paddingBottom: kb }}>
|
||||
<TouchableOpacity
|
||||
className="absolute inset-0"
|
||||
activeOpacity={1}
|
||||
onPress={onClose}
|
||||
accessibilityLabel="Close settings"
|
||||
/>
|
||||
<View
|
||||
className="w-full max-w-md rounded-2xl border border-zinc-800/70 p-5 pb-6 space-y-5"
|
||||
style={{ backgroundColor: 'rgba(8,8,10,0.97)' }}
|
||||
<ScrollView
|
||||
className="w-full max-w-md rounded-2xl border border-zinc-800/70"
|
||||
style={{ maxHeight: '100%', flexGrow: 0 }}
|
||||
contentContainerClassName="p-5 pb-6 space-y-5"
|
||||
contentContainerStyle={{ backgroundColor: 'rgba(8,8,10,0.97)' }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="flex-row items-center justify-between px-1">
|
||||
<View className="flex-row items-center">
|
||||
@@ -213,6 +238,54 @@ export default function SettingsModal({
|
||||
<CameraOnlyNote cameraMode={cameraMode} />
|
||||
</View>
|
||||
|
||||
{/* Lite vs PRO. Every look is free to preview and edit; the key buys
|
||||
the export: no mark, and PRO presets/frames/watermarks allowed. */}
|
||||
<View>
|
||||
<SectionLabel>PRO</SectionLabel>
|
||||
<ChipRow
|
||||
chips={[
|
||||
{ key: 'plan', label: pro ? 'PRO ACTIVE' : 'LITE', active: pro, disabled: true, onPress: () => {} },
|
||||
]}
|
||||
/>
|
||||
{!pro && (
|
||||
<View className="flex-row items-center mt-2">
|
||||
<TextInput
|
||||
value={keyDraft}
|
||||
onChangeText={(t) => {
|
||||
setKeyDraft(t);
|
||||
setKeyError(false);
|
||||
}}
|
||||
placeholder="XXXX-XXXX-XXXX-XXXX"
|
||||
placeholderTextColor="#52525b"
|
||||
autoCapitalize="characters"
|
||||
autoCorrect={false}
|
||||
className={`flex-1 rounded-md border bg-black/40 px-3 py-2 font-mono text-xs font-bold ${
|
||||
keyError ? 'border-red-500/70 text-red-400' : 'border-zinc-700 text-zinc-200'
|
||||
}`}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={async () => {
|
||||
haptic();
|
||||
const ok = await onUnlock(keyDraft);
|
||||
setKeyError(!ok);
|
||||
if (ok) setKeyDraft('');
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
className="ml-2 rounded-md border border-amber-500/70 bg-amber-500/15 px-3 py-2"
|
||||
>
|
||||
<Text className="font-mono text-[11px] font-bold text-amber-500">UNLOCK</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
<Text className="text-zinc-600 font-mono text-[10px] mt-1.5 ml-1">
|
||||
{keyError
|
||||
? 'That key is not valid. Check all 16 characters.'
|
||||
: pro
|
||||
? 'Every look exports clean: no mark, no limits. Thanks!'
|
||||
: 'LITE is free forever. Any preset, frame and watermark can be previewed and edited, but a PRO look cannot be exported and every export carries the RECIPESCAM mark. Enter your key to lift both.'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* About & open-source credits */}
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
@@ -235,7 +308,7 @@ export default function SettingsModal({
|
||||
App info + third-party open-source licenses.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<CreditsModal visible={showCredits} onClose={() => setShowCredits(false)} />
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user