import React, { useEffect, useState } from "react"; import { View, Text, StyleSheet, Modal, TouchableOpacity, ActivityIndicator, ScrollView, Platform, Pressable, Linking, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { LinearGradient } from "expo-linear-gradient"; import { useSubscriptionStore } from "../../store"; import { colors, gradients, spacing, radius, typography } from "../../theme"; import type { PurchasesPackage } from "../../lib/revenuecat"; import { Events } from "../../lib/analytics"; interface PaywallProps { visible: boolean; onClose: () => void; onSuccess?: () => void; } const FEATURES = [ { icon: "diamond" as const, title: "Unlock All Packs", description: "Access every premium collection", }, { icon: "sparkles" as const, title: "Exclusive Content", description: "New packs added regularly", }, { icon: "heart" as const, title: "Share with Partner", description: "Both of you get Pro access", }, ]; export function Paywall({ visible, onClose, onSuccess }: PaywallProps) { const handleClose = (reason: "dismiss" | "success" | "restore" | "system") => { Events.paywallClosed("paywall", reason); onClose(); }; const { offerings, isLoadingOfferings, isPurchasing, error, fetchOfferings, purchasePackage, restorePurchases, } = useSubscriptionStore(); const [selectedPackage, setSelectedPackage] = useState( null ); useEffect(() => { if (visible && !offerings) { fetchOfferings(); } if (visible) { Events.paywallShown("paywall"); } }, [visible, offerings, fetchOfferings]); useEffect(() => { // Auto-select annual package as default (better value) if (offerings?.availablePackages) { const annual = offerings.availablePackages.find( (p) => p.packageType === "ANNUAL" ); setSelectedPackage(annual || offerings.availablePackages[0]); } }, [offerings]); const handlePurchase = async () => { if (!selectedPackage) return; const packageType = selectedPackage.packageType || "unknown"; Events.purchaseInitiated(packageType); const result = await purchasePackage(selectedPackage); if (result.success) { Events.purchaseCompleted(packageType); onSuccess?.(); handleClose("success"); return; } if (result.cancelled) { Events.purchaseCancelled(packageType, result.errorCode); return; } Events.purchaseFailed(packageType, result.errorCode, result.errorMessage); }; const handleRestore = async () => { const restored = await restorePurchases(); if (restored) { Events.purchaseRestored(); onSuccess?.(); handleClose("restore"); } }; const formatPrice = (pkg: PurchasesPackage) => { const price = pkg.product.priceString; if (pkg.packageType === "ANNUAL") { const monthlyPrice = (pkg.product.price / 12).toFixed(2); return `${price}/year (${pkg.product.currencyCode} ${monthlyPrice}/mo)`; } return `${price}/month`; }; const calculateSavings = () => { if (!offerings?.availablePackages) return null; const monthly = offerings.availablePackages.find( (p) => p.packageType === "MONTHLY" ); const annual = offerings.availablePackages.find( (p) => p.packageType === "ANNUAL" ); if (monthly && annual) { const yearlyFromMonthly = monthly.product.price * 12; const savings = Math.round( (1 - annual.product.price / yearlyFromMonthly) * 100 ); return savings; } return null; }; const savings = calculateSavings(); return ( handleClose("system")} > {/* Premium gradient background */} {/* Top silk highlight */} {/* Premium border accent */} {/* Close Button */} handleClose("dismiss")} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > {/* Header */} {/* Premium label */} EXCLUSIVE ACCESS {/* Icon with glow */} Sauci Pro {/* Decorative separator */} Unlock all premium content for you and your partner {/* Features */} {FEATURES.map((feature, index) => ( {feature.title} {feature.description} ))} {/* Packages */} {isLoadingOfferings ? ( Loading options... ) : offerings?.availablePackages ? ( offerings.availablePackages.map((pkg) => { const isSelected = selectedPackage?.identifier === pkg.identifier; const isAnnual = pkg.packageType === "ANNUAL"; return ( setSelectedPackage(pkg)} style={[ styles.packageCard, isSelected && styles.packageCardSelected, isAnnual && styles.packageCardAnnual, ]} > {isAnnual && ( )} {isAnnual && savings && savings > 0 && ( SAVE {savings}% )} {isSelected && } {isAnnual ? "Annual" : "Monthly"} {isAnnual && ( BEST VALUE )} {formatPrice(pkg)} ); }) ) : ( Unable to load subscription options. {error && ( {error} )} Retry )} {/* Error Message */} {error && ( {error} )} {/* Purchase Button */} {isPurchasing ? ( ) : ( Unlock Pro )} Restore Purchases {/* Legal */} Payment will be charged to your Apple ID account at confirmation. Subscription automatically renews unless cancelled at least 24 hours before the end of the current period. Linking.openURL("https://sauci.app/terms")} > Terms of Use • Linking.openURL("https://sauci.app/privacy")} > Privacy Policy ); } const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: "rgba(0, 0, 0, 0.85)", justifyContent: "flex-end", }, content: { backgroundColor: colors.background, borderTopLeftRadius: radius.xxl, borderTopRightRadius: radius.xxl, maxHeight: "90%", borderWidth: 1, borderBottomWidth: 0, borderColor: 'rgba(212, 175, 55, 0.2)', overflow: 'hidden', }, silkHighlight: { position: 'absolute', top: 0, left: 0, right: 0, height: 150, borderTopLeftRadius: radius.xxl, borderTopRightRadius: radius.xxl, }, premiumBorderAccent: { position: 'absolute', top: 0, left: 0, right: 0, height: 2, backgroundColor: colors.premium.gold, opacity: 0.4, }, scrollContent: { padding: spacing.lg, paddingTop: spacing.xl, paddingBottom: Platform.OS === "ios" ? 40 : spacing.lg, }, closeButton: { position: "absolute", top: spacing.md, right: spacing.md, width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.08)', justifyContent: "center", alignItems: "center", zIndex: 10, borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.1)', }, header: { alignItems: "center", marginBottom: spacing.xl, }, label: { ...typography.caption1, fontWeight: '600', letterSpacing: 3, color: colors.premium.gold, textAlign: 'center', marginBottom: spacing.md, }, iconContainer: { width: 72, height: 72, borderRadius: 36, justifyContent: "center", alignItems: "center", overflow: 'hidden', marginBottom: spacing.md, shadowColor: colors.premium.gold, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.6, shadowRadius: 20, elevation: 12, }, title: { ...typography.title1, color: colors.text, marginBottom: spacing.xs, textShadowColor: 'rgba(212, 175, 55, 0.3)', textShadowOffset: { width: 0, height: 0 }, textShadowRadius: 10, }, separator: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginVertical: spacing.md, width: 140, }, separatorLine: { flex: 1, height: 1, backgroundColor: 'rgba(212, 175, 55, 0.3)', }, separatorDiamond: { width: 6, height: 6, backgroundColor: colors.premium.gold, transform: [{ rotate: '45deg' }], marginHorizontal: spacing.md, opacity: 0.8, }, subtitle: { ...typography.body, color: colors.textSecondary, textAlign: "center", lineHeight: 24, }, features: { marginBottom: spacing.xl, backgroundColor: 'rgba(212, 175, 55, 0.05)', borderRadius: radius.lg, padding: spacing.md, borderWidth: 1, borderColor: 'rgba(212, 175, 55, 0.1)', }, featureRow: { flexDirection: "row", alignItems: "center", paddingVertical: spacing.sm, }, featureIcon: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(212, 175, 55, 0.1)', justifyContent: "center", alignItems: "center", marginRight: spacing.md, borderWidth: 1, borderColor: 'rgba(212, 175, 55, 0.2)', }, featureText: { flex: 1, }, featureTitle: { ...typography.headline, color: colors.text, }, featureDescription: { ...typography.subhead, color: colors.textSecondary, }, packages: { marginBottom: spacing.lg, gap: spacing.md, }, loadingContainer: { alignItems: "center", padding: spacing.xl, }, loadingText: { ...typography.subhead, color: colors.textSecondary, marginTop: spacing.md, }, packageCard: { position: "relative", backgroundColor: 'rgba(22, 33, 62, 0.4)', borderRadius: radius.lg, borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.08)', padding: spacing.md, overflow: "hidden", }, packageCardSelected: { borderColor: colors.premium.gold, borderWidth: 2, }, packageCardAnnual: { borderColor: 'rgba(212, 175, 55, 0.3)', }, savingsBadge: { position: "absolute", top: -1, right: spacing.md, flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: spacing.sm, paddingVertical: 4, borderBottomLeftRadius: radius.sm, borderBottomRightRadius: radius.sm, overflow: 'hidden', }, savingsText: { ...typography.caption2, color: colors.background, fontWeight: "800", letterSpacing: 0.5, }, packageContent: { flexDirection: "row", alignItems: "center", }, radioOuter: { width: 24, height: 24, borderRadius: 12, borderWidth: 2, borderColor: 'rgba(255, 255, 255, 0.3)', justifyContent: "center", alignItems: "center", marginRight: spacing.md, }, radioOuterSelected: { borderColor: colors.premium.gold, }, radioInner: { width: 12, height: 12, borderRadius: 6, backgroundColor: colors.premium.gold, }, packageInfo: { flex: 1, }, packageTitleRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, }, packageTitle: { ...typography.headline, color: colors.text, }, packageTitleAnnual: { color: colors.premium.champagne, }, bestValueBadge: { backgroundColor: 'rgba(212, 175, 55, 0.2)', paddingHorizontal: spacing.xs, paddingVertical: 2, borderRadius: radius.xs, }, bestValueText: { ...typography.caption2, color: colors.premium.gold, fontWeight: '700', letterSpacing: 0.5, }, packagePrice: { ...typography.subhead, color: colors.textSecondary, marginTop: 2, }, packagePriceAnnual: { color: colors.premium.champagne, opacity: 0.8, }, errorContainer: { alignItems: "center", padding: spacing.lg, }, errorText: { ...typography.subhead, color: colors.textSecondary, textAlign: "center", marginBottom: spacing.md, }, retryButton: { backgroundColor: 'rgba(212, 175, 55, 0.1)', paddingHorizontal: spacing.lg, paddingVertical: spacing.sm, borderRadius: radius.md, borderWidth: 1, borderColor: 'rgba(212, 175, 55, 0.2)', }, retryButtonText: { ...typography.subhead, color: colors.premium.gold, fontWeight: "600", }, errorBanner: { backgroundColor: colors.errorLight, padding: spacing.md, borderRadius: radius.md, marginBottom: spacing.md, }, errorBannerText: { ...typography.subhead, color: colors.error, textAlign: "center", }, actions: { marginBottom: spacing.lg, }, purchaseButton: { borderRadius: radius.lg, overflow: "hidden", shadowColor: colors.premium.gold, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 8, }, purchaseButtonDisabled: { opacity: 0.5, shadowOpacity: 0, }, purchaseButtonLoading: { paddingVertical: spacing.md, alignItems: "center", backgroundColor: colors.premium.gold, }, purchaseButtonGradient: { flexDirection: 'row', paddingVertical: spacing.md, alignItems: "center", justifyContent: 'center', }, purchaseButtonText: { ...typography.headline, color: colors.background, fontWeight: '700', }, restoreButton: { alignItems: "center", marginTop: spacing.md, padding: spacing.sm, }, restoreText: { ...typography.subhead, color: colors.premium.gold, opacity: 0.8, }, legal: { paddingHorizontal: spacing.md, }, legalText: { ...typography.caption1, color: colors.textTertiary, textAlign: "center", lineHeight: 18, }, legalLinks: { flexDirection: "row", justifyContent: "center", alignItems: "center", marginTop: spacing.sm, gap: spacing.xs, }, legalLinkText: { ...typography.caption1, color: colors.premium.gold, textDecorationLine: "underline", }, legalSeparator: { ...typography.caption1, color: colors.textTertiary, }, }); export default Paywall;