- Purchases.configure() runs with appUserID = restaurantId at every point the id becomes known (login resolve, cold-start rehydration, onboarding restaurant creation) — this must match what the backend webhook expects (see apps/api/src/routes/subscription.ts). - New account/subscription.tsx paywall: current entitlement status, offering packages with real store prices, purchase, restore. - Linked from the account screen. Native module — requires an EAS/dev-client build, not plain Expo Go.
168 lines
6.3 KiB
TypeScript
168 lines
6.3 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
||
import { router } from "expo-router";
|
||
import { ActivityIndicator, Alert, Pressable, ScrollView, Text, View } from "react-native";
|
||
import { Ionicons } from "@expo/vector-icons";
|
||
import type { CustomerInfo, PurchasesOffering, PurchasesPackage } from "react-native-purchases";
|
||
import Purchases from "react-native-purchases";
|
||
import { getActiveRestaurant } from "@/lib/active-restaurant";
|
||
import { configurePurchases, getCurrentOffering, getCustomerInfo, isProActive, PRO_ENTITLEMENT_ID } from "@/lib/purchases";
|
||
|
||
export default function SubscriptionScreen() {
|
||
const [loading, setLoading] = useState(true);
|
||
const [offering, setOffering] = useState<PurchasesOffering | null>(null);
|
||
const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
|
||
const [purchasingPackageId, setPurchasingPackageId] = useState<string | null>(null);
|
||
const [restoring, setRestoring] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
const active = await getActiveRestaurant();
|
||
if (!active) {
|
||
router.back();
|
||
return;
|
||
}
|
||
configurePurchases(active.restaurantId);
|
||
|
||
try {
|
||
const [offer, info] = await Promise.all([getCurrentOffering(), getCustomerInfo()]);
|
||
setOffering(offer);
|
||
setCustomerInfo(info);
|
||
} catch (err) {
|
||
Alert.alert("Hata", err instanceof Error ? err.message : "Abonelik bilgisi yüklenemedi.");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
load();
|
||
}, [load]);
|
||
|
||
async function onPurchase(pkg: PurchasesPackage) {
|
||
setPurchasingPackageId(pkg.identifier);
|
||
try {
|
||
const { customerInfo: updated } = await Purchases.purchasePackage(pkg);
|
||
setCustomerInfo(updated);
|
||
if (isProActive(updated)) {
|
||
Alert.alert("Teşekkürler!", "Menulio Pro aktif edildi.");
|
||
}
|
||
} catch (err) {
|
||
const rcError = err as { userCancelled?: boolean; message?: string };
|
||
if (!rcError.userCancelled) {
|
||
Alert.alert("Satın alma başarısız", rcError.message ?? "Bir hata oluştu.");
|
||
}
|
||
} finally {
|
||
setPurchasingPackageId(null);
|
||
}
|
||
}
|
||
|
||
async function onRestore() {
|
||
setRestoring(true);
|
||
try {
|
||
const info = await Purchases.restorePurchases();
|
||
setCustomerInfo(info);
|
||
Alert.alert(
|
||
isProActive(info) ? "Abonelik bulundu" : "Aktif abonelik yok",
|
||
isProActive(info) ? "Menulio Pro aboneliğiniz geri yüklendi." : "Bu hesaba bağlı aktif bir abonelik bulunamadı.",
|
||
);
|
||
} catch (err) {
|
||
Alert.alert("Hata", err instanceof Error ? err.message : "Abonelik geri yüklenemedi.");
|
||
} finally {
|
||
setRestoring(false);
|
||
}
|
||
}
|
||
|
||
const isPro = customerInfo ? isProActive(customerInfo) : false;
|
||
const expiresAt = customerInfo?.entitlements.active[PRO_ENTITLEMENT_ID]?.expirationDate;
|
||
|
||
if (loading) {
|
||
return (
|
||
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: "#FAF8F5" }}>
|
||
<ActivityIndicator size="large" color="#C8A96B" />
|
||
</View>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<ScrollView style={{ flex: 1, backgroundColor: "#FAF8F5" }} contentContainerStyle={{ padding: 20, gap: 16 }}>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||
<Pressable onPress={() => router.back()} style={{ padding: 4 }}>
|
||
<Ionicons name="arrow-back" size={22} color="#1C1917" />
|
||
</Pressable>
|
||
<Text style={{ fontSize: 22, fontWeight: "800", color: "#1C1917" }}>Abonelik</Text>
|
||
</View>
|
||
|
||
<View
|
||
style={{
|
||
backgroundColor: isPro ? "#F0FDF4" : "#FFFFFF",
|
||
borderRadius: 16,
|
||
padding: 16,
|
||
borderWidth: 1,
|
||
borderColor: isPro ? "#BBF7D0" : "#E7E5E4",
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<Ionicons name={isPro ? "checkmark-circle" : "information-circle-outline"} size={22} color={isPro ? "#059669" : "#78716C"} />
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={{ fontWeight: "700", color: "#1C1917" }}>{isPro ? "Menulio Pro aktif" : "Ücretsiz plan"}</Text>
|
||
{isPro && expiresAt ? (
|
||
<Text style={{ fontSize: 12, color: "#78716C", marginTop: 2 }}>
|
||
Yenileme: {new Date(expiresAt).toLocaleDateString("tr-TR")}
|
||
</Text>
|
||
) : null}
|
||
</View>
|
||
</View>
|
||
|
||
{!offering || offering.availablePackages.length === 0 ? (
|
||
<Text style={{ color: "#78716C", textAlign: "center", marginTop: 24 }}>
|
||
Şu anda satın alınabilir bir paket bulunamadı.
|
||
</Text>
|
||
) : (
|
||
<View style={{ gap: 12 }}>
|
||
{offering.availablePackages.map((pkg) => (
|
||
<Pressable
|
||
key={pkg.identifier}
|
||
onPress={() => onPurchase(pkg)}
|
||
disabled={purchasingPackageId !== null}
|
||
style={{
|
||
backgroundColor: "#FFFFFF",
|
||
borderRadius: 16,
|
||
padding: 16,
|
||
borderWidth: 1.5,
|
||
borderColor: "#C8A96B",
|
||
flexDirection: "row",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
opacity: purchasingPackageId && purchasingPackageId !== pkg.identifier ? 0.5 : 1,
|
||
}}
|
||
>
|
||
<View>
|
||
<Text style={{ fontSize: 16, fontWeight: "800", color: "#1C1917" }}>
|
||
{pkg.packageType === "ANNUAL" ? "Yıllık" : "Aylık"}
|
||
</Text>
|
||
<Text style={{ fontSize: 13, color: "#78716C", marginTop: 2 }}>
|
||
{pkg.product.priceString} / {pkg.packageType === "ANNUAL" ? "yıl" : "ay"}
|
||
</Text>
|
||
</View>
|
||
{purchasingPackageId === pkg.identifier ? (
|
||
<ActivityIndicator color="#C8A96B" />
|
||
) : (
|
||
<Ionicons name="chevron-forward" size={20} color="#C8A96B" />
|
||
)}
|
||
</Pressable>
|
||
))}
|
||
</View>
|
||
)}
|
||
|
||
<Pressable onPress={onRestore} disabled={restoring} style={{ padding: 12, alignItems: "center" }}>
|
||
{restoring ? (
|
||
<ActivityIndicator color="#78716C" />
|
||
) : (
|
||
<Text style={{ color: "#78716C", fontSize: 14, fontWeight: "600" }}>Satın Alımları Geri Yükle</Text>
|
||
)}
|
||
</Pressable>
|
||
</ScrollView>
|
||
);
|
||
}
|