Give LITE the bundled recipes, three of its own, and its own mark

LITE used to hand back the RECIPESCAM mark instead of the user's own
watermark and treated every saved recipe as PRO. That is the wrong half of
the product: the looks the user dials in are the reason to shoot, the
printing and the paid extras are the reason to pay.

What LITE now owns outright: the eight film sims, the bundled recipes, up
to three recipes of its own, its own watermark text (in one basic face,
the platform families stay PRO), the plain frames, and FAVORITED. What
needs PRO: the three printed frames, the GPS stamp, the other fonts, and
the next recipe past the limit.

- entitlement: `Look` is {frame, gpsWm}; `isFreeRecipe` is gone, the
  recipe gate is a count now, decided in App (`liteSaveBlocked`).
- export: a LITE export draws the custom mark AND the RECIPESCAM mark, so
  `liteMark` joins `watermark` in the options and the stamp block loops
  over both with one set of bounds.
- panel: past the limit the strip's trailing chip is a `+` carrying the
  PRO tag, which names the build instead of opening the form.
This commit is contained in:
2026-09-14 21:01:13 +07:00
parent 499e61644d
commit 06d7e8f384
4 changed files with 91 additions and 61 deletions
+21 -4
View File
@@ -6,7 +6,7 @@ import { Star, Trash2 } from 'lucide-react-native';
import { Skia } from '@shopify/react-native-skia';
import { Recipe, ColorAdjustments, CustomWatermark, GpsWatermark, FrameId, BaseFilter, CropRatio } from '../types';
import { FRAMES } from '../utils/frameUtils';
import { IS_PRO, PRO_FRAMES, isFreeRecipe } from '../utils/entitlement';
import { IS_PRO, PRO_FRAMES } from '../utils/entitlement';
import { PARAM_DEFS, ContinuousParamDef } from '../utils/paramDefs';
import { FILM_SIMS, filmSimToRecipe } from '../utils/defaultRecipes';
import RecipeCreateModal from './RecipeCreateModal';
@@ -112,6 +112,10 @@ interface AdjustmentPanelProps {
// IMPORT: pick a recipe file shared from another phone and store it as a
// normal custom recipe.
onImportRecipe: () => void;
// LITE: this many of its own recipes exist, so the next one is the paid
// step. The strip's trailing + chip reads it, the save paths gate on it.
recipeLimitReached: boolean;
onRecipeLimit: () => void;
onDeleteRecipe: (id: string) => void;
// WATERMARK tab state — lifted to App so the live Viewfinder overlay, this
// input and the export options all read the same object.
@@ -245,6 +249,8 @@ export default function AdjustmentPanel({
onToggleWallLandscape,
onToggleGeotag,
onImportRecipe,
recipeLimitReached,
onRecipeLimit,
onDeleteRecipe,
customWm,
onUpdateCustomWm,
@@ -546,6 +552,9 @@ export default function AdjustmentPanel({
// manager exposes (the device's own system fonts); DEFAULT keeps the face
// the mark shipped with.
const wmFontOptions = useMemo(() => {
// LITE ships a single basic face: the platform's own families (what
// DEFAULT is not) are the paid half of the FONT strip.
if (!IS_PRO) return [{ v: 'DEFAULT', d: 'DEFAULT' }];
const mgr = Skia.FontMgr.System();
const names = new Set<string>();
for (let i = 0; i < mgr.countFamilies() && names.size < 120; i++) {
@@ -723,6 +732,7 @@ export default function AdjustmentPanel({
key: 'gps',
label: useGeotag ? 'GPS WATERMARK ON' : 'GPS WATERMARK OFF',
active: useGeotag,
pro: true,
onPress: () => onToggleGeotag(!useGeotag),
},
...(useGeotag ? [groupChip('gpsColor'), groupChip('gpsName'), groupChip('gpsTime')] : []),
@@ -730,7 +740,6 @@ export default function AdjustmentPanel({
key: 'custom-wm',
label: customWm.enabled ? 'CUSTOM WATERMARK ON' : 'CUSTOM WATERMARK OFF',
active: customWm.enabled,
pro: true,
onPress: () => onUpdateCustomWm({ ...customWm, enabled: !customWm.enabled }),
},
...(wmOn
@@ -841,7 +850,6 @@ export default function AdjustmentPanel({
key: r.id,
label: r.name,
active: r.id === currentRecipeId,
pro: !isFreeRecipe(r.id),
star: true,
starred: true,
onPress: () => onSelectRecipe(r),
@@ -1024,7 +1032,6 @@ export default function AdjustmentPanel({
key: r.id,
label: r.name,
active: r.id === currentRecipeId,
pro: !isFreeRecipe(r.id),
star: true,
starred: favoriteIds.includes(r.id),
trash: !!r.isCustom,
@@ -1033,6 +1040,16 @@ export default function AdjustmentPanel({
onPressDelete: () => onDeleteRecipe(r.id),
})
)}
{/* The next recipe, right after the last one. Past the LITE limit
this is the only chip that cannot be pressed: it carries the
PRO tag and names the build instead of opening the form. */}
{renderChip({
key: 'add-recipe',
label: '+',
active: false,
pro: recipeLimitReached,
onPress: () => (recipeLimitReached ? onRecipeLimit() : setCreateVisible(true)),
})}
</ScrollView>
</View>
)}