feat(mobile): integrate react-native-purchases (RevenueCat)
- 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.
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
"expo-status-bar": "~2.0.0",
|
"expo-status-bar": "~2.0.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
|
"react-native-purchases": "^8.2.0",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-url-polyfill": "^2.0.0"
|
"react-native-url-polyfill": "^2.0.0"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ActivityIndicator, Pressable, ScrollView, Text, TextInput, View } from
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { setActiveRestaurant } from "@/lib/active-restaurant";
|
import { setActiveRestaurant } from "@/lib/active-restaurant";
|
||||||
|
import { configurePurchases } from "@/lib/purchases";
|
||||||
|
|
||||||
interface CreateRestaurantResponse {
|
interface CreateRestaurantResponse {
|
||||||
restaurant: { id: string; slug: string };
|
restaurant: { id: string; slug: string };
|
||||||
@@ -34,6 +35,7 @@ export default function OnboardingRestaurantStep() {
|
|||||||
menuId: menu.id,
|
menuId: menu.id,
|
||||||
slug: restaurant.slug,
|
slug: restaurant.slug,
|
||||||
});
|
});
|
||||||
|
configurePurchases(restaurant.id);
|
||||||
|
|
||||||
router.replace("/(onboarding)/scan");
|
router.replace("/(onboarding)/scan");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -434,6 +434,25 @@ export default function AccountScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Subscription */}
|
||||||
|
<Pressable
|
||||||
|
onPress={() => router.push("/account/subscription")}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: 20,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#E7E5E4",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="star-outline" size={20} color="#C8A96B" />
|
||||||
|
<Text style={{ flex: 1, fontSize: 16, fontWeight: "800", color: "#1C1917" }}>Abonelik</Text>
|
||||||
|
<Ionicons name="chevron-forward" size={20} color="#C8A96B" />
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
{/* Live Menu URL & Quick Actions */}
|
{/* Live Menu URL & Quick Actions */}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { router } from "expo-router";
|
|||||||
import { ActivityIndicator, View } from "react-native";
|
import { ActivityIndicator, View } from "react-native";
|
||||||
import { api, ApiError } from "@/lib/api";
|
import { api, ApiError } from "@/lib/api";
|
||||||
import { clearActiveRestaurant, getActiveRestaurant, setActiveRestaurant } from "@/lib/active-restaurant";
|
import { clearActiveRestaurant, getActiveRestaurant, setActiveRestaurant } from "@/lib/active-restaurant";
|
||||||
|
import { configurePurchases } from "@/lib/purchases";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
|
|
||||||
interface MeRestaurantResponse {
|
interface MeRestaurantResponse {
|
||||||
@@ -31,6 +32,7 @@ export default function Index() {
|
|||||||
|
|
||||||
const cached = await getActiveRestaurant();
|
const cached = await getActiveRestaurant();
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
configurePurchases(cached.restaurantId);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
router.replace("/menu");
|
router.replace("/menu");
|
||||||
}
|
}
|
||||||
@@ -48,6 +50,7 @@ export default function Index() {
|
|||||||
menuId: menu.id,
|
menuId: menu.id,
|
||||||
slug: restaurant.slug,
|
slug: restaurant.slug,
|
||||||
});
|
});
|
||||||
|
configurePurchases(restaurant.id);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
router.replace("/menu");
|
router.replace("/menu");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Platform } from "react-native";
|
||||||
|
import Purchases, { type CustomerInfo, type PurchasesOffering } from "react-native-purchases";
|
||||||
|
|
||||||
|
const API_KEY = process.env.EXPO_PUBLIC_REVENUECAT_API_KEY;
|
||||||
|
export const PRO_ENTITLEMENT_ID = "Menul.io Pro";
|
||||||
|
|
||||||
|
let configuredForRestaurantId: string | null = null;
|
||||||
|
|
||||||
|
// appUserID is the restaurant id — the backend webhook (POST
|
||||||
|
// /subscription/webhook) maps RevenueCat's app_user_id straight to
|
||||||
|
// subscriptions.restaurant_id, so these must always match.
|
||||||
|
export function configurePurchases(restaurantId: string) {
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.warn("EXPO_PUBLIC_REVENUECAT_API_KEY missing — purchases disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (configuredForRestaurantId === restaurantId) return;
|
||||||
|
|
||||||
|
Purchases.configure({ apiKey: API_KEY, appUserID: restaurantId });
|
||||||
|
if (Platform.OS !== "web") {
|
||||||
|
Purchases.setLogLevel(__DEV__ ? Purchases.LOG_LEVEL.DEBUG : Purchases.LOG_LEVEL.WARN);
|
||||||
|
}
|
||||||
|
configuredForRestaurantId = restaurantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentOffering(): Promise<PurchasesOffering | null> {
|
||||||
|
const offerings = await Purchases.getOfferings();
|
||||||
|
return offerings.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCustomerInfo(): Promise<CustomerInfo> {
|
||||||
|
return Purchases.getCustomerInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProActive(info: CustomerInfo): boolean {
|
||||||
|
return info.entitlements.active[PRO_ENTITLEMENT_ID] !== undefined;
|
||||||
|
}
|
||||||
Generated
+20
@@ -104,6 +104,9 @@ importers:
|
|||||||
react-native:
|
react-native:
|
||||||
specifier: 0.76.9
|
specifier: 0.76.9
|
||||||
version: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
version: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
||||||
|
react-native-purchases:
|
||||||
|
specifier: ^8.2.0
|
||||||
|
version: 8.12.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
|
||||||
react-native-safe-area-context:
|
react-native-safe-area-context:
|
||||||
specifier: 4.12.0
|
specifier: 4.12.0
|
||||||
version: 4.12.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
|
version: 4.12.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
|
||||||
@@ -1768,6 +1771,9 @@ packages:
|
|||||||
'@react-navigation/routers@7.6.4':
|
'@react-navigation/routers@7.6.4':
|
||||||
resolution: {integrity: sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w==}
|
resolution: {integrity: sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w==}
|
||||||
|
|
||||||
|
'@revenuecat/purchases-typescript-internal@14.3.0':
|
||||||
|
resolution: {integrity: sha512-P3IhlWvH4wJAM9ypv8HamdIBMQfnLdU9PbjURw+s7NxHOL8LmPGhKuyv+gBINpka27mt1CAsDoOhNHjkexJhkg==}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.62.4':
|
'@rollup/rollup-android-arm-eabi@4.62.4':
|
||||||
resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==}
|
resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
@@ -4123,6 +4129,12 @@ packages:
|
|||||||
react: '*'
|
react: '*'
|
||||||
react-native: '*'
|
react-native: '*'
|
||||||
|
|
||||||
|
react-native-purchases@8.12.0:
|
||||||
|
resolution: {integrity: sha512-0T6WtSDN96swsS6iLeTh7GEGLSedBr4FWP5JsGLnP6Ri7Rp754pf3UJ+S0+ZnT1cqUZ4W2z2oB6zTLyyfNfuKw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 16.6.3'
|
||||||
|
react-native: '*'
|
||||||
|
|
||||||
react-native-safe-area-context@4.12.0:
|
react-native-safe-area-context@4.12.0:
|
||||||
resolution: {integrity: sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==}
|
resolution: {integrity: sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6817,6 +6829,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.18
|
nanoid: 3.3.18
|
||||||
|
|
||||||
|
'@revenuecat/purchases-typescript-internal@14.3.0': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.62.4':
|
'@rollup/rollup-android-arm-eabi@4.62.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -9316,6 +9330,12 @@ snapshots:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-native: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
react-native: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
||||||
|
|
||||||
|
react-native-purchases@8.12.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
'@revenuecat/purchases-typescript-internal': 14.3.0
|
||||||
|
react: 18.3.1
|
||||||
|
react-native: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
||||||
|
|
||||||
react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
|
react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|||||||
Reference in New Issue
Block a user