76cf4c6d7e
Bound the geotag fix with a 2 s deadline and prewarm it at launch, queue a shot pressed before the camera session binds and replay it, pass the options argument capturePhotoToFile requires, save through the native path so the media permission is not needed, and release the Skia surface and bitmap after every export. Store the JS bundle uncompressed and load the export pipeline on demand. Note in RELEASE_NOTES.md that none of this moved the tap threshold.
118 lines
4.9 KiB
TypeScript
118 lines
4.9 KiB
TypeScript
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();
|
|
|
|
if (!visible) return null; // see PhotoViewerModal
|
|
|
|
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>
|
|
);
|
|
}
|