refactor(companion): remove react-native-context-menu-view dependency and enhance profile button with user avatar support (#26357)
- Removed `react-native-context-menu-view` from `package.json` and `bun.lock`. - Updated profile button in `Availability`, `EventTypesIOS`, and `More` components to display user avatar if available, enhancing user experience. - Cleaned up code formatting for better readability.
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert, Platform } from "react-native";
|
||||
import { Alert, Platform, Pressable } from "react-native";
|
||||
import { AvailabilityListScreen } from "@/components/screens/AvailabilityListScreen";
|
||||
import { useCreateSchedule } from "@/hooks";
|
||||
import { useCreateSchedule, useUserProfile } from "@/hooks";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { getAvatarUrl } from "@/utils/getAvatarUrl";
|
||||
import { Image } from "expo-image";
|
||||
|
||||
export default function Availability() {
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const { mutate: createScheduleMutation } = useCreateSchedule();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
|
||||
const handleCreateNew = () => {
|
||||
// Use native iOS Alert.prompt for a native look
|
||||
@@ -68,7 +71,10 @@ export default function Availability() {
|
||||
console.error("Failed to create schedule", message);
|
||||
if (__DEV__) {
|
||||
const stack = error instanceof Error ? error.stack : undefined;
|
||||
console.debug("[Availability] createSchedule failed", { message, stack });
|
||||
console.debug("[Availability] createSchedule failed", {
|
||||
message,
|
||||
stack,
|
||||
});
|
||||
}
|
||||
showErrorAlert("Error", "Failed to create schedule. Please try again.");
|
||||
},
|
||||
@@ -88,8 +94,7 @@ export default function Availability() {
|
||||
<Stack.Header
|
||||
style={{ backgroundColor: "transparent", shadowColor: "transparent" }}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}
|
||||
hidden={Platform.OS === "android"}
|
||||
>
|
||||
hidden={Platform.OS === "android"}>
|
||||
<Stack.Header.Title large>Availability</Stack.Header.Title>
|
||||
<Stack.Header.Right>
|
||||
{/* New Menu */}
|
||||
@@ -102,9 +107,20 @@ export default function Availability() {
|
||||
</Stack.Header.Menu>
|
||||
|
||||
{/* Profile Button */}
|
||||
<Stack.Header.Button onPress={() => router.push("/profile-sheet")}>
|
||||
<Stack.Header.Icon sf="person.circle.fill" />
|
||||
</Stack.Header.Button>
|
||||
{userProfile?.avatarUrl ? (
|
||||
<Stack.Header.View>
|
||||
<Pressable onPress={() => router.push("/profile-sheet")}>
|
||||
<Image
|
||||
source={{ uri: getAvatarUrl(userProfile.avatarUrl) }}
|
||||
style={{ width: 32, height: 32, borderRadius: 16 }}
|
||||
/>
|
||||
</Pressable>
|
||||
</Stack.Header.View>
|
||||
) : (
|
||||
<Stack.Header.Button onPress={() => router.push("/profile-sheet")}>
|
||||
<Stack.Header.Icon sf="person.circle.fill" />
|
||||
</Stack.Header.Button>
|
||||
)}
|
||||
</Stack.Header.Right>
|
||||
<Stack.Header.SearchBar
|
||||
placeholder="Search schedules"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useMemo, useState } from "react";
|
||||
import {
|
||||
ActionSheetIOS,
|
||||
Alert,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Share,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
useDeleteEventType,
|
||||
useDuplicateEventType,
|
||||
useEventTypes,
|
||||
useUserProfile,
|
||||
} from "@/hooks";
|
||||
import { CalComAPIService, type EventType } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
@@ -32,10 +34,13 @@ import { getEventDuration } from "@/utils/getEventDuration";
|
||||
import { offlineAwareRefresh } from "@/utils/network";
|
||||
import { normalizeMarkdown } from "@/utils/normalizeMarkdown";
|
||||
import { slugify } from "@/utils/slugify";
|
||||
import { Image } from "expo-image";
|
||||
import { getAvatarUrl } from "@/utils/getAvatarUrl";
|
||||
|
||||
export default function EventTypesIOS() {
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { data: userProfile } = useUserProfile();
|
||||
|
||||
// No modal state needed for iOS - using native Alert.prompt
|
||||
|
||||
@@ -178,7 +183,10 @@ export default function EventTypesIOS() {
|
||||
console.error("Failed to delete event type", message);
|
||||
if (__DEV__) {
|
||||
const stack = deleteError instanceof Error ? deleteError.stack : undefined;
|
||||
console.debug("[EventTypes] deleteEventType failed", { message, stack });
|
||||
console.debug("[EventTypes] deleteEventType failed", {
|
||||
message,
|
||||
stack,
|
||||
});
|
||||
}
|
||||
showErrorAlert("Error", "Failed to delete event type. Please try again.");
|
||||
},
|
||||
@@ -211,12 +219,14 @@ export default function EventTypesIOS() {
|
||||
});
|
||||
},
|
||||
onError: (duplicateError) => {
|
||||
const message =
|
||||
duplicateError instanceof Error ? duplicateError.message : String(duplicateError);
|
||||
const message = duplicateError instanceof Error ? duplicateError.message : String(duplicateError);
|
||||
console.error("Failed to duplicate event type", message);
|
||||
if (__DEV__) {
|
||||
const stack = duplicateError instanceof Error ? duplicateError.stack : undefined;
|
||||
console.debug("[EventTypes] duplicateEventType failed", { message, stack });
|
||||
console.debug("[EventTypes] duplicateEventType failed", {
|
||||
message,
|
||||
stack,
|
||||
});
|
||||
}
|
||||
showErrorAlert("Error", "Failed to duplicate event type. Please try again.");
|
||||
},
|
||||
@@ -272,22 +282,20 @@ export default function EventTypesIOS() {
|
||||
id: newEventType.id.toString(),
|
||||
title: newEventType.title,
|
||||
description: newEventType.description || "",
|
||||
duration: (
|
||||
newEventType.lengthInMinutes ||
|
||||
newEventType.length ||
|
||||
15
|
||||
).toString(),
|
||||
duration: (newEventType.lengthInMinutes || newEventType.length || 15).toString(),
|
||||
slug: newEventType.slug || "",
|
||||
},
|
||||
});
|
||||
},
|
||||
onError: (createError) => {
|
||||
const message =
|
||||
createError instanceof Error ? createError.message : String(createError);
|
||||
const message = createError instanceof Error ? createError.message : String(createError);
|
||||
console.error("Failed to create event type", message);
|
||||
if (__DEV__) {
|
||||
const stack = createError instanceof Error ? createError.stack : undefined;
|
||||
console.debug("[EventTypes] createEventType failed", { message, stack });
|
||||
console.debug("[EventTypes] createEventType failed", {
|
||||
message,
|
||||
stack,
|
||||
});
|
||||
}
|
||||
showErrorAlert("Error", "Failed to create event type. Please try again.");
|
||||
},
|
||||
@@ -366,8 +374,7 @@ export default function EventTypesIOS() {
|
||||
{/* iOS Native Header with Glass UI */}
|
||||
<Stack.Header
|
||||
style={{ backgroundColor: "transparent", shadowColor: "transparent" }}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}
|
||||
>
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}>
|
||||
<Stack.Header.Title large>Event Types</Stack.Header.Title>
|
||||
|
||||
<Stack.Header.Right>
|
||||
@@ -379,14 +386,12 @@ export default function EventTypesIOS() {
|
||||
<Stack.Header.Menu title="Sort by">
|
||||
<Stack.Header.MenuAction
|
||||
icon="textformat.abc"
|
||||
onPress={() => handleSortByOption("alphabetical")}
|
||||
>
|
||||
onPress={() => handleSortByOption("alphabetical")}>
|
||||
Alphabetical
|
||||
</Stack.Header.MenuAction>
|
||||
<Stack.Header.MenuAction
|
||||
icon="calendar.badge.clock"
|
||||
onPress={() => handleSortByOption("newest")}
|
||||
>
|
||||
onPress={() => handleSortByOption("newest")}>
|
||||
Newest First
|
||||
</Stack.Header.MenuAction>
|
||||
<Stack.Header.MenuAction icon="clock" onPress={() => handleSortByOption("duration")}>
|
||||
@@ -396,28 +401,33 @@ export default function EventTypesIOS() {
|
||||
|
||||
{/* Filter Submenu - opens as separate submenu */}
|
||||
<Stack.Header.Menu title="Filter">
|
||||
<Stack.Header.MenuAction
|
||||
icon="checkmark.circle"
|
||||
onPress={() => handleFilterOption("all")}
|
||||
>
|
||||
<Stack.Header.MenuAction icon="checkmark.circle" onPress={() => handleFilterOption("all")}>
|
||||
All Event Types
|
||||
</Stack.Header.MenuAction>
|
||||
<Stack.Header.MenuAction icon="eye" onPress={() => handleFilterOption("active")}>
|
||||
Active Only
|
||||
</Stack.Header.MenuAction>
|
||||
<Stack.Header.MenuAction
|
||||
icon="dollarsign.circle"
|
||||
onPress={() => handleFilterOption("paid")}
|
||||
>
|
||||
<Stack.Header.MenuAction icon="dollarsign.circle" onPress={() => handleFilterOption("paid")}>
|
||||
Paid Events
|
||||
</Stack.Header.MenuAction>
|
||||
</Stack.Header.Menu>
|
||||
</Stack.Header.Menu>
|
||||
|
||||
{/* Profile Button - Opens bottom sheet */}
|
||||
<Stack.Header.Button onPress={() => router.push("/profile-sheet")}>
|
||||
<Stack.Header.Icon sf="person.circle.fill" />
|
||||
</Stack.Header.Button>
|
||||
{userProfile?.avatarUrl ? (
|
||||
<Stack.Header.View>
|
||||
<Pressable onPress={() => router.push("/profile-sheet")}>
|
||||
<Image
|
||||
source={{ uri: getAvatarUrl(userProfile.avatarUrl) }}
|
||||
style={{ width: 32, height: 32, borderRadius: 16 }}
|
||||
/>
|
||||
</Pressable>
|
||||
</Stack.Header.View>
|
||||
) : (
|
||||
<Stack.Header.Button onPress={() => router.push("/profile-sheet")}>
|
||||
<Stack.Header.Icon sf="person.circle.fill" />
|
||||
</Stack.Header.Button>
|
||||
)}
|
||||
</Stack.Header.Right>
|
||||
|
||||
{/* Search Bar */}
|
||||
@@ -435,8 +445,7 @@ export default function EventTypesIOS() {
|
||||
contentContainerStyle={{ paddingBottom: 120 }}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
>
|
||||
contentInsetAdjustmentBehavior="automatic">
|
||||
{filteredEventTypes.length === 0 && searchQuery.trim() !== "" ? (
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5 pt-20">
|
||||
<EmptyScreen
|
||||
@@ -474,8 +483,7 @@ export default function EventTypesIOS() {
|
||||
<Host matchContents>
|
||||
<ContextMenu
|
||||
modifiers={[buttonStyle(isLiquidGlassAvailable() ? "glass" : "bordered"), padding()]}
|
||||
activationMethod="singlePress"
|
||||
>
|
||||
activationMethod="singlePress">
|
||||
<ContextMenu.Items>
|
||||
<Button systemImage="link" onPress={handleOpenCreateModal} label="New Event Type" />
|
||||
</ContextMenu.Items>
|
||||
@@ -502,8 +510,7 @@ export default function EventTypesIOS() {
|
||||
setShowDeleteModal(false);
|
||||
setEventTypeToDelete(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<View className="flex-1 items-center justify-center bg-black/50 p-4">
|
||||
<View className="w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
||||
{/* Header with icon and title */}
|
||||
@@ -516,14 +523,12 @@ export default function EventTypesIOS() {
|
||||
|
||||
{/* Title and description */}
|
||||
<View className="flex-1">
|
||||
<Text className="mb-2 text-xl font-semibold text-gray-900">
|
||||
Delete Event Type
|
||||
</Text>
|
||||
<Text className="mb-2 text-xl font-semibold text-gray-900">Delete Event Type</Text>
|
||||
<Text className="text-sm leading-5 text-gray-600">
|
||||
{eventTypeToDelete ? (
|
||||
<>
|
||||
This will permanently delete the "{eventTypeToDelete.title}" event type.
|
||||
This action cannot be undone.
|
||||
This will permanently delete the "{eventTypeToDelete.title}" event type. This action
|
||||
cannot be undone.
|
||||
</>
|
||||
) : null}
|
||||
</Text>
|
||||
@@ -536,8 +541,7 @@ export default function EventTypesIOS() {
|
||||
<TouchableOpacity
|
||||
className={`rounded-lg bg-gray-900 px-4 py-2.5 ${isDeleting ? "opacity-50" : ""}`}
|
||||
onPress={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
disabled={isDeleting}>
|
||||
<Text className="text-center text-base font-medium text-white">Delete</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -547,8 +551,7 @@ export default function EventTypesIOS() {
|
||||
setShowDeleteModal(false);
|
||||
setEventTypeToDelete(null);
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
disabled={isDeleting}>
|
||||
<Text className="text-center text-base font-medium text-gray-700">Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -2,11 +2,14 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import { Alert, Pressable, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { getAvatarUrl } from "@/utils/getAvatarUrl";
|
||||
import { Image } from "expo-image";
|
||||
import { useUserProfile } from "@/hooks";
|
||||
|
||||
interface MoreMenuItem {
|
||||
name: string;
|
||||
@@ -20,6 +23,7 @@ export default function More() {
|
||||
const router = useRouter();
|
||||
const { logout } = useAuth();
|
||||
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||
const { data: userProfile } = useUserProfile();
|
||||
|
||||
const performLogout = async () => {
|
||||
try {
|
||||
@@ -84,14 +88,23 @@ export default function More() {
|
||||
{/* iOS Native Header with Glass UI */}
|
||||
<Stack.Header
|
||||
style={{ backgroundColor: "transparent", shadowColor: "transparent" }}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}
|
||||
>
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}>
|
||||
<Stack.Header.Title large>More</Stack.Header.Title>
|
||||
<Stack.Header.Right>
|
||||
{/* Profile Button */}
|
||||
<Stack.Header.Button onPress={() => router.push("/profile-sheet")}>
|
||||
<Stack.Header.Icon sf="person.circle.fill" />
|
||||
</Stack.Header.Button>
|
||||
{userProfile?.avatarUrl ? (
|
||||
<Stack.Header.View>
|
||||
<Pressable onPress={() => router.push("/profile-sheet")}>
|
||||
<Image
|
||||
source={{ uri: getAvatarUrl(userProfile.avatarUrl) }}
|
||||
style={{ width: 32, height: 32, borderRadius: 16 }}
|
||||
/>
|
||||
</Pressable>
|
||||
</Stack.Header.View>
|
||||
) : (
|
||||
<Stack.Header.Button onPress={() => router.push("/profile-sheet")}>
|
||||
<Stack.Header.Icon sf="person.circle.fill" />
|
||||
</Stack.Header.Button>
|
||||
)}
|
||||
</Stack.Header.Right>
|
||||
</Stack.Header>
|
||||
|
||||
@@ -100,8 +113,7 @@ export default function More() {
|
||||
style={{ backgroundColor: "#f8f9fa" }}
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 120 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
>
|
||||
contentInsetAdjustmentBehavior="automatic">
|
||||
<View className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
{menuItems.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
@@ -109,8 +121,7 @@ export default function More() {
|
||||
onPress={item.onPress}
|
||||
className={`flex-row items-center justify-between bg-white px-5 py-5 active:bg-[#F8F9FA] ${
|
||||
index < menuItems.length - 1 ? "border-b border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
>
|
||||
}`}>
|
||||
<View className="flex-1 flex-row items-center">
|
||||
<Ionicons name={item.icon} size={20} color="#333" />
|
||||
<Text className="ml-3 text-base font-semibold text-[#333]">{item.name}</Text>
|
||||
@@ -128,8 +139,7 @@ export default function More() {
|
||||
<View className="mt-6 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<TouchableOpacity
|
||||
onPress={handleSignOut}
|
||||
className="flex-row items-center justify-center bg-white px-5 py-4 active:bg-red-50"
|
||||
>
|
||||
className="flex-row items-center justify-center bg-white px-5 py-4 active:bg-red-50">
|
||||
<Ionicons name="log-out-outline" size={20} color="#800000" />
|
||||
<Text className="ml-2 text-base font-medium text-[#800000]">Sign Out</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -139,10 +149,7 @@ export default function More() {
|
||||
<Text className="mt-6 px-1 text-center text-xs text-gray-400">
|
||||
The companion app is an extension of the web application.{"\n"}
|
||||
For advanced features, visit{" "}
|
||||
<Text
|
||||
className="text-gray-800"
|
||||
onPress={() => openInAppBrowser("https://app.cal.com", "Cal.com")}
|
||||
>
|
||||
<Text className="text-gray-800" onPress={() => openInAppBrowser("https://app.cal.com", "Cal.com")}>
|
||||
app.cal.com
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-context-menu-view": "1.20.0",
|
||||
"react-native-gesture-handler": "2.28.0",
|
||||
"react-native-reanimated": "4.2.0",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
@@ -1683,8 +1682,6 @@
|
||||
|
||||
"react-native": ["react-native@0.83.1", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.83.1", "@react-native/codegen": "0.83.1", "@react-native/community-cli-plugin": "0.83.1", "@react-native/gradle-plugin": "0.83.1", "@react-native/js-polyfills": "0.83.1", "@react-native/normalize-colors": "0.83.1", "@react-native/virtualized-lists": "0.83.1", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "hermes-compiler": "0.14.0", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.3", "metro-source-map": "^0.83.3", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-mL1q5HPq5cWseVhWRLl+Fwvi5z1UO+3vGOpjr+sHFwcUletPRZ5Kv+d0tUfqHmvi73/53NjlQqX1Pyn4GguUfA=="],
|
||||
|
||||
"react-native-context-menu-view": ["react-native-context-menu-view@1.20.0", "", { "peerDependencies": { "react": "^16.8.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-native": ">=0.60.0-rc.0 <1.0.x" } }, "sha512-g1EYZiPaJWjeq7GWeL9TaYSnKmL9/hiDE7Arvj8r8fLip/l+BvS+BRtblX9qk9VpQdDWyd6L6A4yL2sz6+ohQw=="],
|
||||
|
||||
"react-native-css-interop": ["react-native-css-interop@0.2.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/traverse": "^7.23.0", "@babel/types": "^7.23.0", "debug": "^4.3.7", "lightningcss": "~1.27.0", "semver": "^7.6.3" }, "peerDependencies": { "react": ">=18", "react-native": "*", "react-native-reanimated": ">=3.6.2", "tailwindcss": "~3" } }, "sha512-B88f5rIymJXmy1sNC/MhTkb3xxBej1KkuAt7TiT9iM7oXz3RM8Bn+7GUrfR02TvSgKm4cg2XiSuLEKYfKwNsjA=="],
|
||||
|
||||
"react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-context-menu-view": "1.20.0",
|
||||
"react-native-gesture-handler": "2.28.0",
|
||||
"react-native-reanimated": "4.2.0",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
|
||||
Reference in New Issue
Block a user