Sell PRO as an in-app unlock, take shared photos, and let FX run negative

- One free Play app (com.locphamtran.recipescamera) with an expo-iap PRO
  unlock, replacing the paid apk for the store channel
- Android SEND/SEND_MULTIPLE share target feeds photos into the editor,
  on cold start and on resume
- BACK button in the TopBar and press-and-hold PEEK replace the chip row
- NOISE REDUCTION and SHARPENING accept -10: a negative SHARPENING softens
  the frame before the grain pass, a negative NOISE REDUCTION re-grains it,
  in the Skia engine, the live preview and the Kotlin export path alike
This commit is contained in:
2026-09-16 06:48:22 +07:00
parent 27a9dfb65f
commit a85ebf0a81
21 changed files with 860 additions and 183 deletions
+115
View File
@@ -0,0 +1,115 @@
import React from 'react';
import { View, Text, Modal, ScrollView, TouchableOpacity } from 'react-native';
import { X, Zap, RefreshCw } from 'lucide-react-native';
import { buyPro, restorePro, useProUnlock } from '../utils/proUnlock';
import { PRO_FRAMES } from '../utils/entitlement';
// The paywall. LITE keeps every look previewable and editable; this sheet is
// the only place the tier is sold, and Google Play does the selling — the price
// shown is the one Play returned for the product.
//
// Play requires a way back to an existing purchase on any screen that sells
// one, so RESTORE is a peer of BUY, not a footnote.
const PERKS = [
'No RECIPESCAM mark on your exports',
'HDF effect',
`${PRO_FRAMES.length} printed frames`,
'GPS location watermark',
'Every platform font for your watermark',
'Unlimited recipes and favorites',
];
export default function ProModal({ visible, onClose }: { visible: boolean; onClose: () => void }) {
const { owned, price, busy, error } = useProUnlock();
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<View className="flex-1 bg-black/70 items-center justify-center px-4">
<TouchableOpacity className="absolute inset-0" activeOpacity={1} onPress={onClose} />
<View
className="w-full max-w-md rounded-2xl border border-zinc-800/70 p-5"
style={{ backgroundColor: 'rgba(8,8,10,0.97)', maxHeight: '85%' }}
>
<View className="flex-row items-center justify-between mb-3">
<View className="flex-row items-center">
<Zap size={13} color="#f59e0b" strokeWidth={1.75} />
<Text className="text-zinc-300 font-mono text-xs font-bold tracking-widest ml-2">
RECIPESCAM PRO
</Text>
</View>
<TouchableOpacity onPress={onClose} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<X size={15} color="#71717a" />
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
<Text className="text-zinc-500 font-mono text-[10px] leading-4">
One payment, kept on your Google account. Nothing to type in, and it comes back on
every phone that has the app.
</Text>
<View className="mt-3 space-y-1.5">
{PERKS.map((p) => (
<View key={p} className="flex-row items-start">
<Text className="text-amber-500 font-mono text-[11px] mr-2">+</Text>
<Text className="text-zinc-300 font-mono text-[11px] flex-1">{p}</Text>
</View>
))}
</View>
{owned ? (
<View className="mt-4 rounded-xl border border-amber-500/40 bg-amber-500/10 px-3 py-2.5">
<Text className="text-amber-500 font-mono text-[11px] font-bold tracking-widest">
PRO ACTIVE
</Text>
<Text className="text-zinc-500 font-mono text-[10px] mt-1">
Every look exports clean: no mark, no limits.
</Text>
</View>
) : (
<TouchableOpacity
onPress={buyPro}
disabled={busy}
activeOpacity={0.7}
className={`mt-4 rounded-xl border px-3 py-3 items-center ${
busy ? 'border-zinc-800/70 bg-black/30' : 'border-amber-500/70 bg-amber-500/15'
}`}
>
<Text
className={`font-mono text-[11px] font-bold tracking-widest ${
busy ? 'text-zinc-500' : 'text-amber-500'
}`}
>
{busy ? 'CONTACTING GOOGLE PLAY…' : price ? `UNLOCK PRO — ${price}` : 'UNLOCK PRO'}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={restorePro}
disabled={busy}
activeOpacity={0.7}
className="mt-2 flex-row items-center justify-center rounded-xl border border-zinc-800/70 bg-black/30 px-3 py-2.5"
>
<RefreshCw size={11} color="#a1a1aa" strokeWidth={1.75} />
<Text className="text-zinc-400 font-mono text-[10px] font-bold tracking-widest ml-2">
RESTORE PURCHASES
</Text>
</TouchableOpacity>
{error ? (
<Text className="text-zinc-500 font-mono text-[10px] mt-2.5 leading-4">{error}</Text>
) : null}
<Text className="text-zinc-600 font-mono text-[10px] mt-3 leading-4">
Bought already but the app still says LITE? Restore, or check that you are signed in
to the Google account that paid.
</Text>
</ScrollView>
</View>
</View>
</Modal>
);
}