From ccf8ee1cafb4d716727f4574bec8cee55d8439b2 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 20 Apr 2026 14:23:14 +0800 Subject: [PATCH 1/7] upd: caching data Deskripsi: - update caching pada fitur utama -yg fitur divisi belom --- app/(application)/announcement/index.tsx | 104 +++++------- app/(application)/banner/index.tsx | 130 ++++++++------ app/(application)/discussion/index.tsx | 121 ++++++------- app/(application)/division/index.tsx | 113 ++++++------ app/(application)/feature.tsx | 2 + app/(application)/group/index.tsx | 63 +++---- app/(application)/home.tsx | 45 +++-- app/(application)/member/index.tsx | 125 +++++++------- app/(application)/notification.tsx | 114 +++++++------ app/(application)/position/index.tsx | 66 +++---- app/(application)/project/index.tsx | 189 +++++++-------------- app/_layout.tsx | 9 +- bun.lock | 16 ++ components/auth/viewLogin.tsx | 1 + components/division/headerDivisionList.tsx | 2 +- components/home/carouselHome2.tsx | 60 ++++--- components/home/chartDokumenHome.tsx | 58 +++---- components/home/chartProgresHome.tsx | 39 ++--- components/home/discussionHome.tsx | 41 ++--- components/home/divisionHome.tsx | 41 ++--- components/home/eventHome.tsx | 38 ++--- components/home/projectHome.tsx | 61 +++---- ios/Desa.xcodeproj/project.pbxproj | 4 +- ios/Podfile.lock | 26 +-- package.json | 4 + providers/AuthProvider.tsx | 3 +- providers/QueryProvider.tsx | 58 +++++++ 27 files changed, 767 insertions(+), 766 deletions(-) create mode 100644 providers/QueryProvider.tsx diff --git a/app/(application)/announcement/index.tsx b/app/(application)/announcement/index.tsx index 83cedb4..c62c9d7 100644 --- a/app/(application)/announcement/index.tsx +++ b/app/(application)/announcement/index.tsx @@ -2,14 +2,14 @@ import BorderBottomItem from "@/components/borderBottomItem"; import InputSearch from "@/components/inputSearch"; import SkeletonContent from "@/components/skeletonContent"; import Text from '@/components/Text'; -import { ColorsStatus } from "@/constants/ColorsStatus"; import Styles from "@/constants/Styles"; import { apiGetAnnouncement } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { MaterialIcons } from "@expo/vector-icons"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { router } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -20,68 +20,60 @@ type Props = { createdAt: string } - export default function Announcement() { const { token, decryptToken } = useAuthSession() const { colors } = useTheme(); - const [data, setData] = useState([]) const [search, setSearch] = useState('') const update = useSelector((state: any) => state.announcementUpdate) - const [loading, setLoading] = useState(true) const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) - const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setWaiting(true) - setLoading(loading) - setPage(thisPage) + // TanStack Query Infinite Query + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch, + isRefetching + } = useInfiniteQuery({ + queryKey: ['announcements', search], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetAnnouncement({ user: hasil, search: search, page: thisPage }) - if (thisPage == 1) { - setData(response.data) - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - } catch (error) { - console.error(error) - } finally { - setLoading(false) - setWaiting(false) - } - } + const response = await apiGetAnnouncement({ + user: hasil, + search: search, + page: pageParam + }) + return response.data + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.length > 0 ? allPages.length + 1 : undefined + }, + }) + // Trigger refetch when Redux state 'update' changes useEffect(() => { - handleLoad(false, 1) - }, [update]) + refetch() + }, [update, refetch]) - useEffect(() => { - handleLoad(true, 1) - }, [search]) + // Flatten data from pages + const flattenedData = useMemo(() => { + return data?.pages.flat() || [] + }, [data]) const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - }; - - const handleRefresh = async () => { - setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); - setRefreshing(false) + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } }; const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - createdAt: data[index].createdAt, + id: flattenedData[index].id, + title: flattenedData[index].title, + desc: flattenedData[index].desc, + createdAt: flattenedData[index].createdAt, }) return ( @@ -91,18 +83,18 @@ export default function Announcement() { { - loading ? + isLoading && !flattenedData.length ? arrSkeleton.map((item, index) => { return ( ) }) : - data.length > 0 + flattenedData.length > 0 ? data.length} + data={flattenedData} + getItemCount={() => flattenedData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -112,9 +104,7 @@ export default function Announcement() { borderType="bottom" bgColor="transparent" icon={ - // - // } title={item.title} desc={item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')} @@ -122,14 +112,14 @@ export default function Announcement() { /> ) }} - keyExtractor={(item, index) => String(index)} + keyExtractor={(item, index) => String(item.id || index)} onEndReached={loadMoreData} onEndReachedThreshold={0.5} showsVerticalScrollIndicator={false} refreshControl={ } diff --git a/app/(application)/banner/index.tsx b/app/(application)/banner/index.tsx index dc6ce62..c5560ab 100644 --- a/app/(application)/banner/index.tsx +++ b/app/(application)/banner/index.tsx @@ -1,3 +1,4 @@ +import styles from "@/components/AppHeader" import AppHeader from "@/components/AppHeader" import HeaderRightBannerList from "@/components/banner/headerBannerList" import BorderBottomItem from "@/components/borderBottomItem" @@ -5,6 +6,7 @@ import DrawerBottom from "@/components/drawerBottom" import MenuItemRow from "@/components/menuItemRow" import ModalConfirmation from "@/components/ModalConfirmation" import ModalLoading from "@/components/modalLoading" +import Skeleton from "@/components/skeleton" import Text from "@/components/Text" import { ConstEnv } from "@/constants/ConstEnv" import Styles from "@/constants/Styles" @@ -13,11 +15,12 @@ import { setEntities } from "@/lib/bannerSlice" import { useAuthSession } from "@/providers/AuthProvider" import { useTheme } from "@/providers/ThemeProvider" import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import * as FileSystem from 'expo-file-system' import { startActivityAsync } from 'expo-intent-launcher' import { router, Stack } from "expo-router" import * as Sharing from 'expo-sharing' -import { useState } from "react" +import { useEffect, useState } from "react" import { Alert, Image, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native" import ImageViewing from 'react-native-image-viewing' import * as mime from 'react-native-mime-types' @@ -43,36 +46,51 @@ export default function BannerList() { const [loadingOpen, setLoadingOpen] = useState(false) const [viewImg, setViewImg] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) + const queryClient = useQueryClient() - const handleDeleteEntity = async () => { - try { - const hasil = await decryptToken(String(token?.current)); - const deletedEntity = await apiDeleteBanner({ user: hasil }, dataId); - if (deletedEntity.success) { - Toast.show({ type: 'small', text1: 'Berhasil menghapus data', }) - apiGetBanner({ user: hasil }).then((data) => - dispatch(setEntities(data.data)) - ); - } else { - Toast.show({ type: 'small', text1: 'Gagal menghapus data', }) - } - } catch (error: any) { - console.error(error); - const message = error?.response?.data?.message || "Gagal menghapus data" + // 1. Fetching logic with useQuery + const { data: bannersRes, isLoading } = useQuery({ + queryKey: ['banners'], + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const response = await apiGetBanner({ user: hasil }) + return response.data || [] + }, + enabled: !!token?.current, + staleTime: 0, + }) - Toast.show({ type: 'small', text1: message }) - } finally { - setModal(false) + // Sync results with Redux + useEffect(() => { + if (bannersRes) { + dispatch(setEntities(bannersRes)) } + }, [bannersRes, dispatch]) + + // 2. Deletion logic with useMutation + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + const hasil = await decryptToken(String(token?.current)) + return await apiDeleteBanner({ user: hasil }, id) + }, + onSuccess: () => { + Toast.show({ type: 'small', text1: 'Berhasil menghapus data' }) + queryClient.invalidateQueries({ queryKey: ['banners'] }) + }, + onError: (error: any) => { + const message = error?.response?.data?.message || "Gagal menghapus data" + Toast.show({ type: 'small', text1: message }) + } + }) + + const handleDeleteEntity = () => { + deleteMutation.mutate(dataId) + setModal(false) }; const handleRefresh = async () => { setRefreshing(true) - const hasil = await decryptToken(String(token?.current)); - apiGetBanner({ user: hasil }).then((data) => - dispatch(setEntities(data.data)) - ); - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['banners'] }) setRefreshing(false) }; @@ -140,36 +158,40 @@ export default function BannerList() { } style={[Styles.h100, { backgroundColor: colors.background }]} > - { - entities.length > 0 - ? - - {entities.map((index: any, key: number) => ( - { - setDataId(index.id) - setSelectFile(index) - setModal(true) - }} - borderType="all" - icon={ - - } - title={index.title} - /> - ))} - - : - - Tidak ada data - - } - - + + { + isLoading ? ( + <> + + + + + ) : + entities.length > 0 ? + entities.map((index: any, key: number) => ( + { + setDataId(index.id) + setSelectFile(index) + setModal(true) + }} + borderType="all" + icon={ + + } + title={index.title} + /> + )) + : + + Tidak ada data + + } + setModal(false)} title="Menu"> diff --git a/app/(application)/discussion/index.tsx b/app/(application)/discussion/index.tsx index 4dbee12..14b521d 100644 --- a/app/(application)/discussion/index.tsx +++ b/app/(application)/discussion/index.tsx @@ -11,8 +11,9 @@ import { apiGetDiscussionGeneral } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { AntDesign, Feather, Ionicons, MaterialIcons } from "@expo/vector-icons"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -32,70 +33,76 @@ export default function Discussion() { const { colors } = useTheme(); const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>() const [search, setSearch] = useState('') - const [nameGroup, setNameGroup] = useState('') - const [data, setData] = useState([]) const update = useSelector((state: any) => state.discussionGeneralDetailUpdate) - const [loading, setLoading] = useState(true) - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [status, setStatus] = useState<'true' | 'false'>('true') - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) + const queryClient = useQueryClient() + const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setWaiting(true) - setLoading(loading) - setPage(thisPage) + // TanStack Query for Discussions with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['discussions', { status, search, group }], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetDiscussionGeneral({ user: hasil, active: status, search: search, group: String(group), page: thisPage }) - if (thisPage == 1) { - setData(response.data) - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - setNameGroup(response.filter.name) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - setWaiting(false) - } - } + const response = await apiGetDiscussionGeneral({ + user: hasil, + active: status, + search: search, + group: String(group), + page: pageParam + }) + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) + // Get nameGroup from the first available page + const nameGroup = useMemo(() => { + return data?.pages[0]?.filter?.name || ""; + }, [data]) + + // Refetch when manual update state changes useEffect(() => { - handleLoad(false, 1) - }, [update]) - - useEffect(() => { - handleLoad(true, 1) - }, [status, search, group]) - - - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - }; + refetch() + }, [update, refetch]) const handleRefresh = async () => { setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['discussions'] }) setRefreshing(false) }; + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; + + const arrSkeleton = [0, 1, 2, 3, 4] + const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - status: data[index].status, - total_komentar: data[index].total_komentar, - createdAt: data[index].createdAt, + id: flatData[index]?.id, + title: flatData[index]?.title, + desc: flatData[index]?.desc, + status: flatData[index]?.status, + total_komentar: flatData[index]?.total_komentar, + createdAt: flatData[index]?.createdAt, }) return ( @@ -132,18 +139,18 @@ export default function Discussion() { { - loading ? + isLoading ? arrSkeleton.map((item: any, i: number) => { return ( ) }) : - data.length > 0 + flatData.length > 0 ? data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -153,16 +160,14 @@ export default function Discussion() { onPress={() => { router.push(`/discussion/${item.id}`) }} borderType="bottom" icon={ - // - // } title={item.title} subtitle={ status != "false" && } rightTopInfo={item.createdAt} - desc={item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')} + desc={item.desc?.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')} leftBottomInfo={ diff --git a/app/(application)/division/index.tsx b/app/(application)/division/index.tsx index 5537d86..40bbc82 100644 --- a/app/(application)/division/index.tsx +++ b/app/(application)/division/index.tsx @@ -17,8 +17,9 @@ import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Pressable, RefreshControl, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -40,23 +41,23 @@ export default function ListDivision() { const { token, decryptToken } = useAuthSession() const { colors } = useTheme(); const [search, setSearch] = useState("") - const [nameGroup, setNameGroup] = useState("") - const [data, setData] = useState([]) - // ... state same ... + const queryClient = useQueryClient() + const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') + const [category, setCategory] = useState<'divisi-saya' | 'semua'>(cat == 'semua' ? 'semua' : 'divisi-saya') const update = useSelector((state: any) => state.divisionUpdate) - const arrSkeleton = Array.from({ length: 3 }, (_, index) => index) - const [loading, setLoading] = useState(false) - const [status, setStatus] = useState<'true' | 'false'>('true') - const [category, setCategory] = useState<'divisi-saya' | 'semua'>('divisi-saya') - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setWaiting(true) - setLoading(loading) - setPage(thisPage) + // TanStack Query for Divisions with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['divisions', { status, search, group, category }], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)); const response = await apiGetDivision({ user: hasil, @@ -64,54 +65,52 @@ export default function ListDivision() { search: search, group: String(group), kategori: category, - page: thisPage + page: pageParam }); + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) - if (response.success) { - if (thisPage == 1) { - setData(response.data); - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]); - } else { - return; - } - setNameGroup(response.filter.name); - } - } catch (error) { - console.error(error); - } finally { - setLoading(false) - setWaiting(false) - } - } - + // Refetch when manual update state changes useEffect(() => { - handleLoad(false, 1); - }, [update]); + refetch() + }, [update, refetch]) - useEffect(() => { - handleLoad(true, 1); - }, [status, search, group, category]); + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - }; + // Get nameGroup from the first available page + const nameGroup = useMemo(() => { + return data?.pages[0]?.filter?.name || ""; + }, [data]) const handleRefresh = async () => { setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['divisions'] }) setRefreshing(false) }; + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; + + const arrSkeleton = [0, 1, 2] + const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - name: data[index].name, - desc: data[index].desc, - jumlah_member: data[index].jumlah_member, + id: flatData[index]?.id, + name: flatData[index]?.name, + desc: flatData[index]?.desc, + jumlah_member: flatData[index]?.jumlah_member, }) @@ -206,7 +205,7 @@ export default function ListDivision() { { - loading ? + isLoading ? isList ? arrSkeleton.map((item, index) => ( @@ -216,7 +215,7 @@ export default function ListDivision() { )) : - data.length == 0 ? ( + flatData.length == 0 ? ( Tidak ada data @@ -224,9 +223,9 @@ export default function ListDivision() { isList ? ( data.length} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -260,9 +259,9 @@ export default function ListDivision() { ) : ( data.length} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( diff --git a/app/(application)/feature.tsx b/app/(application)/feature.tsx index 00b2311..23cfba6 100644 --- a/app/(application)/feature.tsx +++ b/app/(application)/feature.tsx @@ -11,6 +11,8 @@ export default function Feature() { const entityUser = useSelector((state: any) => state.user) const { colors } = useTheme(); + console.log({entityUser}) + return ( ([]) const [search, setSearch] = useState('') - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [loading, setLoading] = useState(true) const [showDeleteModal, setShowDeleteModal] = useState(false) const [status, setStatus] = useState<'true' | 'false'>('true') const [loadingSubmit, setLoadingSubmit] = useState(false) const [idChoose, setIdChoose] = useState('') const [activeChoose, setActiveChoose] = useState(true) const [titleChoose, setTitleChoose] = useState('') + const queryClient = useQueryClient() const [refreshing, setRefreshing] = useState(false) const dispatch = useDispatch() @@ -49,12 +48,38 @@ export default function Index() { title: false, }); + // TanStack Query for Groups + const { + data: queryData, + isLoading, + refetch + } = useQuery({ + queryKey: ['groups', { status, search }], + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const response = await apiGetGroup({ + user: hasil, + active: status, + search: search + }) + return response; + }, + enabled: !!token?.current, + staleTime: 0, + }) + + const data = useMemo(() => queryData?.data || [], [queryData]) + + useEffect(() => { + refetch() + }, [update, refetch]) async function handleEdit() { try { setLoadingSubmit(true) const hasil = await decryptToken(String(token?.current)) const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose) + await queryClient.invalidateQueries({ queryKey: ['groups'] }) dispatch(setUpdateGroup(!update)) } catch (error) { console.error(error) @@ -71,6 +96,7 @@ export default function Index() { try { const hasil = await decryptToken(String(token?.current)) const response = await apiDeleteGroup({ user: hasil, isActive: activeChoose }, idChoose) + await queryClient.invalidateQueries({ queryKey: ['groups'] }) dispatch(setUpdateGroup(!update)) } catch (error) { console.error(error) @@ -80,32 +106,9 @@ export default function Index() { } } - async function handleLoad(loading: boolean) { - try { - setLoading(loading) - const hasil = await decryptToken(String(token?.current)) - const response = await apiGetGroup({ user: hasil, active: status, search: search }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - - } - } - - useEffect(() => { - handleLoad(false) - }, [update]) - - useEffect(() => { - handleLoad(true) - }, [status, search]) - const handleRefresh = async () => { setRefreshing(true) - handleLoad(false) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['groups'] }) setRefreshing(false) }; @@ -129,6 +132,8 @@ export default function Index() { + const arrSkeleton = [0, 1, 2, 3, 4] + return ( @@ -152,7 +157,7 @@ export default function Index() { { - loading ? + isLoading ? arrSkeleton.map((item, index) => { return ( diff --git a/app/(application)/home.tsx b/app/(application)/home.tsx index 2c8e1d9..b45163c 100644 --- a/app/(application)/home.tsx +++ b/app/(application)/home.tsx @@ -12,6 +12,7 @@ import { apiGetProfile } from "@/lib/api"; import { setEntities } from "@/lib/entitiesSlice"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { LinearGradient } from "expo-linear-gradient"; import { Stack } from "expo-router"; import { useEffect, useState } from "react"; @@ -23,28 +24,46 @@ import { useDispatch, useSelector } from "react-redux"; export default function Home() { const entities = useSelector((state: any) => state.entities) const dispatch = useDispatch() + const queryClient = useQueryClient() const { token, decryptToken, signOut } = useAuthSession() const { colors } = useTheme(); const insets = useSafeAreaInsets() const [refreshing, setRefreshing] = useState(false) - useEffect(() => { - handleUserLogin() - }, [dispatch]); + const { data: profile, isError } = useQuery({ + queryKey: ['profile'], + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const data = await apiGetProfile({ id: hasil }) + return data.data + }, + enabled: !!token?.current, + staleTime: 0, // Ensure it refetches every time the component mounts + }) - async function handleUserLogin() { - const hasil = await decryptToken(String(token?.current)) - apiGetProfile({ id: hasil }) - .then((data) => dispatch(setEntities(data.data))) - .catch((error) => { - signOut() - }); - } + // Sync to Redux for global usage + useEffect(() => { + if (profile) { + dispatch(setEntities(profile)) + } + }, [profile, dispatch]) + + // Auto Sign Out if profile fetch fails (e.g. invalid/expired token) + useEffect(() => { + if (isError) { + signOut() + } + }, [isError, signOut]) const handleRefresh = async () => { setRefreshing(true) - handleUserLogin() - await new Promise(resolve => setTimeout(resolve, 2000)); + // Invalidate all queries related to the home screen + await queryClient.invalidateQueries({ queryKey: ['profile'] }) + await queryClient.invalidateQueries({ queryKey: ['banners'] }) + await queryClient.invalidateQueries({ queryKey: ['homeData'] }) + + // Artificial delay to show refresh indicator if sync is too fast + await new Promise(resolve => setTimeout(resolve, 1000)); setRefreshing(false) }; diff --git a/app/(application)/member/index.tsx b/app/(application)/member/index.tsx index cab1d5c..1a9db89 100644 --- a/app/(application)/member/index.tsx +++ b/app/(application)/member/index.tsx @@ -12,8 +12,9 @@ import { apiGetUser } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { AntDesign, Feather } from "@expo/vector-icons"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -37,73 +38,81 @@ export default function Index() { const entityUser = useSelector((state: any) => state.user) const { colors } = useTheme(); const [search, setSearch] = useState('') - const [nameGroup, setNameGroup] = useState('') - const [data, setData] = useState([]) const update = useSelector((state: any) => state.memberUpdate) - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [loading, setLoading] = useState(true) - const [status, setStatus] = useState<'true' | 'false'>('true') - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) + const queryClient = useQueryClient() + const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setWaiting(true) - setLoading(loading) - setPage(thisPage) + // TanStack Query for Members with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['members', { status, search, group }], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetUser({ user: hasil, active: status, search, group: String(group), page: thisPage }) - if (thisPage == 1) { - setData(response.data) - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - setNameGroup(response.filter.name) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - setWaiting(false) - } - } + const response = await apiGetUser({ + user: hasil, + active: status, + search, + group: String(group), + page: pageParam + }) + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - }; + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) + // Get nameGroup from the first available page + const nameGroup = useMemo(() => { + return data?.pages[0]?.filter?.name || ""; + }, [data]) + + // Refetch when manual update state changes useEffect(() => { - handleLoad(false, 1) - }, [update]) - - useEffect(() => { - handleLoad(true, 1) - }, [group, search, status]) + refetch() + }, [update, refetch]) const handleRefresh = async () => { setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['members'] }) setRefreshing(false) }; + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; + + const arrSkeleton = [0, 1, 2, 3, 4] + const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - name: data[index].name, - nik: data[index].nik, - email: data[index].email, - phone: data[index].phone, - gender: data[index].gender, - position: data[index].position, - group: data[index].group, - img: data[index].img, - isActive: data[index].isActive, - role: data[index].role, + id: flatData[index]?.id, + name: flatData[index]?.name, + nik: flatData[index]?.nik, + email: flatData[index]?.email, + phone: flatData[index]?.phone, + gender: flatData[index]?.gender, + position: flatData[index]?.position, + group: flatData[index]?.group, + img: flatData[index]?.img, + isActive: flatData[index]?.isActive, + role: flatData[index]?.role, }); return ( @@ -136,18 +145,18 @@ export default function Index() { { - loading ? + isLoading ? arrSkeleton.map((item, index) => { return ( ) }) : - data.length > 0 + flatData.length > 0 ? data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( diff --git a/app/(application)/notification.tsx b/app/(application)/notification.tsx index 0ef9ea7..c985a6f 100644 --- a/app/(application)/notification.tsx +++ b/app/(application)/notification.tsx @@ -1,4 +1,3 @@ -import BorderBottomItem from "@/components/borderBottomItem"; import BorderBottomItemVertical from "@/components/borderBottomItemVertical"; import SkeletonTwoItem from "@/components/skeletonTwoItem"; import Text from "@/components/Text"; @@ -10,7 +9,8 @@ import { pushToPage } from "@/lib/pushToPage"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { Feather } from "@expo/vector-icons"; -import { useEffect, useState } from "react"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, SafeAreaView, View, VirtualizedList } from "react-native"; import { useDispatch, useSelector } from "react-redux"; @@ -27,64 +27,61 @@ type Props = { export default function Notification() { const { token, decryptToken } = useAuthSession() const { colors } = useTheme(); - const [loading, setLoading] = useState(false) - const [data, setData] = useState([]) - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) + const queryClient = useQueryClient() const dispatch = useDispatch() const updateNotification = useSelector((state: any) => state.notificationUpdate) const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setLoading(loading) - setPage(thisPage) - setWaiting(true) + // TanStack Query for Notifications with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['notifications'], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetNotification({ user: hasil, page: thisPage }) - if (thisPage == 1) { - setData(response.data) - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - } catch (error) { - console.error(error) - } finally { - setLoading(false) - setWaiting(false) - } - } + const response = await apiGetNotification({ user: hasil, page: pageParam }) + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); + // Refetch when manual update state changes + useEffect(() => { + refetch() + }, [updateNotification, refetch]) + + const handleRefresh = async () => { + setRefreshing(true) + await queryClient.invalidateQueries({ queryKey: ['notifications'] }) + setRefreshing(false) }; - useEffect(() => { - handleLoad(true, 1) - }, []) - - - const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - category: data[index].category, - idContent: data[index].idContent, - isRead: data[index].isRead, - createdAt: data[index].createdAt, - }); + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; async function handleReadNotification(id: string, category: string, idContent: string) { try { const hasil = await decryptToken(String(token?.current)) const response = await apiReadOneNotification({ user: hasil, id: id }) + await queryClient.invalidateQueries({ queryKey: ['notifications'] }) pushToPage(category, idContent) dispatch(setUpdateNotification(!updateNotification)) } catch (error) { @@ -92,28 +89,33 @@ export default function Notification() { } } - const handleRefresh = async () => { - setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); - setRefreshing(false) - }; + const arrSkeleton = [0, 1, 2, 3, 4] + + const getItem = (_data: unknown, index: number): Props => ({ + id: flatData[index]?.id, + title: flatData[index]?.title, + desc: flatData[index]?.desc, + category: flatData[index]?.category, + idContent: flatData[index]?.idContent, + isRead: flatData[index]?.isRead, + createdAt: flatData[index]?.createdAt, + }); return ( { - loading ? + isLoading ? arrSkeleton.map((item, index) => { return ( ) }) : - data.length > 0 ? + flatData.length > 0 ? data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( diff --git a/app/(application)/position/index.tsx b/app/(application)/position/index.tsx index 47fe3a9..18a76b2 100644 --- a/app/(application)/position/index.tsx +++ b/app/(application)/position/index.tsx @@ -16,8 +16,9 @@ import { setUpdatePosition } from "@/lib/positionSlice"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, View, VirtualizedList } from "react-native"; import Toast from "react-native-toast-message"; import { useDispatch, useSelector } from "react-redux"; @@ -31,51 +32,53 @@ type Props = { } export default function Index() { - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [loading, setLoading] = useState(true) const { token, decryptToken } = useAuthSession() const { colors } = useTheme() - const [status, setStatus] = useState<'true' | 'false'>('true') - const entityUser = useSelector((state: any) => state.user) const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>() + const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') + const entityUser = useSelector((state: any) => state.user) const [isModal, setModal] = useState(false) const [isVisibleEdit, setVisibleEdit] = useState(false) - const [data, setData] = useState([]) const [search, setSearch] = useState('') - const [nameGroup, setNameGroup] = useState('') const [loadingSubmit, setLoadingSubmit] = useState(false) const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' }) const [error, setError] = useState({ name: false, }); + const queryClient = useQueryClient() const [refreshing, setRefreshing] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const dispatch = useDispatch() const update = useSelector((state: any) => state.positionUpdate) - async function handleLoad(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Positions + const { + data: queryData, + isLoading, + refetch + } = useQuery({ + queryKey: ['positions', { status, search, group }], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetPosition({ user: hasil, active: status, search: search, group: String(group) }) - setData(response.data) - setNameGroup(response.filter.name) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } + const response = await apiGetPosition({ + user: hasil, + active: status, + search: search, + group: String(group) + }) + return response; + }, + enabled: !!token?.current, + staleTime: 0, + }) + + const data = useMemo(() => queryData?.data || [], [queryData]) + const nameGroup = useMemo(() => queryData?.filter?.name || "", [queryData]) useEffect(() => { - handleLoad(false) - }, [update]) - - - useEffect(() => { - handleLoad(true) - }, [status, search, group]) + refetch() + }, [update, refetch]) function handleChooseData(id: string, name: string, active: boolean, group: string) { @@ -88,7 +91,7 @@ export default function Index() { const hasil = await decryptToken(String(token?.current)) const response = await apiDeletePosition({ user: hasil, isActive: chooseData.active }, chooseData.id) dispatch(setUpdatePosition(!update)) - } catch (error : any ) { + } catch (error: any) { console.error(error); const message = error?.response?.data?.message || "Gagal menghapus data" @@ -110,7 +113,7 @@ export default function Index() { } else { Toast.show({ type: 'small', text1: response.message, }) } - } catch (error : any ) { + } catch (error: any) { console.error(error); const message = error?.response?.data?.message || "Gagal mengubah data" @@ -138,10 +141,11 @@ export default function Index() { handleEdit() } + const arrSkeleton = [0, 1, 2, 3, 4] + const handleRefresh = async () => { setRefreshing(true) - handleLoad(false) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['positions'] }) setRefreshing(false) }; @@ -184,7 +188,7 @@ export default function Index() { { - loading ? + isLoading ? arrSkeleton.map((item, index) => { return ( diff --git a/app/(application)/project/index.tsx b/app/(application)/project/index.tsx index b23639e..f283d04 100644 --- a/app/(application)/project/index.tsx +++ b/app/(application)/project/index.tsx @@ -18,8 +18,9 @@ import { Ionicons, MaterialCommunityIcons, } from "@expo/vector-icons"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -40,28 +41,29 @@ export default function ListProject() { cat?: string; year?: string; }>(); - const [statusFix, setStatusFix] = useState<'0' | '1' | '2' | '3'>('0') + const [statusFix, setStatusFix] = useState<'0' | '1' | '2' | '3'>( + (status == '1' || status == '2' || status == '3') ? status : '0' + ) const { token, decryptToken } = useAuthSession(); const { colors } = useTheme(); const entityUser = useSelector((state: any) => state.user) const [search, setSearch] = useState("") - const [nameGroup, setNameGroup] = useState("") - // ... state same ... - const [isYear, setYear] = useState("") - const [data, setData] = useState([]) const [isList, setList] = useState(false) const update = useSelector((state: any) => state.projectUpdate) - const [loading, setLoading] = useState(true) - const arrSkeleton = Array.from({ length: 3 }, (_, index) => index) - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) + const queryClient = useQueryClient() const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setLoading(loading) - setWaiting(true) - setPage(thisPage) + // TanStack Query for Projects with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['projects', { statusFix, search, group, cat, year }], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)); const response = await apiGetProject({ user: hasil, @@ -69,60 +71,55 @@ export default function ListProject() { search: search, group: String(group), kategori: String(cat), - page: thisPage, + page: pageParam, year: String(year) }); + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) - if (response.success) { - setNameGroup(response.filter.name); - setYear(response.tahun) - if (thisPage == 1) { - setData(response.data); - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - } - } catch (error) { - console.error(error); - } finally { - setLoading(false) - setWaiting(false) - } - } - + // Refetch when manual update state changes useEffect(() => { - handleLoad(false, 1); - }, [update.data]); + refetch() + }, [update.data, refetch]) + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) - useEffect(() => { - handleLoad(true, 1); - }, [statusFix, search, group, cat, year]); - - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - } + // Get metadata from the first available page + const nameGroup = useMemo(() => data?.pages[0]?.filter?.name || "", [data]) + const isYear = useMemo(() => data?.pages[0]?.tahun || "", [data]) const handleRefresh = async () => { setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['projects'] }) setRefreshing(false) - } + }; + + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; + + const arrSkeleton = [0, 1, 2] const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - status: data[index].status, - member: data[index].member, - progress: data[index].progress, - createdAt: data[index].createdAt, + id: flatData[index]?.id, + title: flatData[index]?.title, + desc: flatData[index]?.desc, + status: flatData[index]?.status, + member: flatData[index]?.member, + progress: flatData[index]?.progress, + createdAt: flatData[index]?.createdAt, }) return ( @@ -205,7 +202,6 @@ export default function ListProject() { { - // entityUser.role != 'cosupadmin' && entityUser.role != 'admin' && Filter : { @@ -218,18 +214,13 @@ export default function ListProject() { : '' } - {/* { - (entityUser.role == 'user' || entityUser.role == 'coadmin') - ? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? : - : '' - } */} } { - loading ? + isLoading ? isList ? arrSkeleton.map((item, index) => ( @@ -239,13 +230,13 @@ export default function ListProject() { )) : - data.length > 0 + flatData.length > 0 ? isList ? ( data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -279,35 +270,12 @@ export default function ListProject() { /> } /> - {/* { - data.map((item, index) => { - return ( - { router.push(`/project/${item.id}`); }} - borderType="bottom" - icon={ - - - - } - title={item.title} - /> - ); - }) - } */} ) : ( data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -359,43 +327,6 @@ export default function ListProject() { /> } /> - {/* {data.map((item, index) => { - return ( - { - router.push(`/project/${item.id}`); - }} - content="page" - title={item.title} - headerColor="primary" - > - - - - {item.createdAt} - - - - - ); - })} */} ) : diff --git a/app/_layout.tsx b/app/_layout.tsx index 5d0acb0..cb20548 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,5 +1,6 @@ import AuthProvider from '@/providers/AuthProvider'; import ThemeProvider, { useTheme } from '@/providers/ThemeProvider'; +import QueryProvider from '@/providers/QueryProvider'; import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; @@ -48,9 +49,11 @@ export default function RootLayout() { - - - + + + + + diff --git a/bun.lock b/bun.lock index 1bf2725..2140a31 100644 --- a/bun.lock +++ b/bun.lock @@ -11,12 +11,16 @@ "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-community/cli": "^19.1.0", "@react-native-community/datetimepicker": "8.4.1", + "@react-native-community/netinfo": "^12.0.1", "@react-native-firebase/app": "^22.4.0", "@react-native-firebase/database": "^22.4.0", "@react-native-firebase/messaging": "^22.2.1", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", "@reduxjs/toolkit": "^2.7.0", + "@tanstack/query-async-storage-persister": "^5.99.2", + "@tanstack/react-query": "^5.99.2", + "@tanstack/react-query-persist-client": "^5.99.2", "@types/formidable": "^3.4.5", "axios": "^1.8.4", "crypto-es": "^2.1.0", @@ -566,6 +570,8 @@ "@react-native-community/datetimepicker": ["@react-native-community/datetimepicker@8.4.1", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": ">=52.0.0", "react": "*", "react-native": "*", "react-native-windows": "*" }, "optionalPeers": ["expo", "react-native-windows"] }, "sha512-DrK+CUS5fZnz8dhzBezirkzQTcNDdaXer3oDLh0z4nc2tbdIdnzwvXCvi8IEOIvleoc9L95xS5tKUl0/Xv71Mg=="], + "@react-native-community/netinfo": ["@react-native-community/netinfo@12.0.1", "", { "peerDependencies": { "react": "*", "react-native": ">=0.59" } }, "sha512-P/3caXIvfYSJG8AWJVefukg+ZGRPs+M4Lp3pNJtgcTYoJxCjWrKQGNnCkj/Cz//zWa/avGed0i/wzm0T8vV2IQ=="], + "@react-native-firebase/app": ["@react-native-firebase/app@22.4.0", "", { "dependencies": { "firebase": "11.10.0" }, "peerDependencies": { "expo": ">=47.0.0", "react": "*", "react-native": "*" }, "optionalPeers": ["expo"] }, "sha512-mW49qYioddRZjCRiF4XMpt7pyPoh84pqU2obnFY0pWD9K0aFRv6+BfLBYrsAFY3xqA5cqf0uj+Nne0vrvmuAyw=="], "@react-native-firebase/database": ["@react-native-firebase/database@22.4.0", "", { "peerDependencies": { "@react-native-firebase/app": "22.4.0" } }, "sha512-iY+676RTwntRqq0CqcbGhidaegt/a6eKaoLTXeAxvtPYQaYXQL1fCDuZKfoy6uBfNsAGDxx2z4jYJuU+kOv4pA=="], @@ -624,6 +630,16 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.99.2", "", { "dependencies": { "@tanstack/query-core": "5.99.2", "@tanstack/query-persist-client-core": "5.99.2" } }, "sha512-FIr13Zv7GiMZGrdxoxOuzolT4xfyLrKWVBMfTZLMGJTc9IceFu2RT+EfH+j5jcKfvjB4T2no3qWSPGHxYmKKWg=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.99.2", "", {}, "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA=="], + + "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.99.2", "", { "dependencies": { "@tanstack/query-core": "5.99.2" } }, "sha512-YYuLGBDGCsUbfN2LuYrfkRCpg1vOUZnK2bn4j7zAZv+m1B4CnLAv58Z3A43d5Cruxvld5udYFeYXw9F6g/pZcQ=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.99.2", "", { "dependencies": { "@tanstack/query-core": "5.99.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA=="], + + "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.99.2", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.99.2" }, "peerDependencies": { "@tanstack/react-query": "^5.99.2", "react": "^18 || ^19" } }, "sha512-7+y5+kpaR26X2gdaEv0yQSFLZjqXz4Kn7wqzuYDQrb203b9MlYS3baML1M9hJTiLgi4QGGF2eJDdW8lHAazUow=="], + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], diff --git a/components/auth/viewLogin.tsx b/components/auth/viewLogin.tsx index ea358a2..02bd624 100644 --- a/components/auth/viewLogin.tsx +++ b/components/auth/viewLogin.tsx @@ -29,6 +29,7 @@ export default function ViewLogin({ onValidate }: Props) { setLoadingLogin(true) const response = await apiCheckPhoneLogin({ phone: `62${phone}` }) if (response.success) { + console.log({ response }) if (response.isWithoutOTP) { const encrypted = await encryptToken(response.id) signIn(encrypted) diff --git a/components/division/headerDivisionList.tsx b/components/division/headerDivisionList.tsx index d5d31b1..0b2f12d 100644 --- a/components/division/headerDivisionList.tsx +++ b/components/division/headerDivisionList.tsx @@ -33,7 +33,7 @@ export default function HeaderRightDivisionList() { }} /> { - (entityUser.role == "userRole" || entityUser.role == "developer") && + (entityUser.role == "supadmin" || entityUser.role == "developer") && } title="Filter" diff --git a/components/home/carouselHome2.tsx b/components/home/carouselHome2.tsx index 901cb45..a73ac74 100644 --- a/components/home/carouselHome2.tsx +++ b/components/home/carouselHome2.tsx @@ -5,7 +5,8 @@ import { setEntities } from "@/lib/bannerSlice"; import { setEntityUser } from "@/lib/userSlice"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; -import { AntDesign, Feather, FontAwesome5, Ionicons, MaterialCommunityIcons, MaterialIcons, } from "@expo/vector-icons"; +import { Feather, Ionicons } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; import React, { useEffect } from "react"; import { Dimensions, Image, View } from "react-native"; @@ -23,37 +24,44 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean }) const progress = useSharedValue(0); const dispatch = useDispatch() const entities = useSelector((state: any) => state.banner) - const entityUser = useSelector((state: any) => state.user) - async function handleBannerView() { - const hasil = await decryptToken(String(token?.current)) - apiGetBanner({ user: hasil }).then((data) => { - if (data.data.length > 0) { - dispatch(setEntities(data.data)) - } else { - dispatch(setEntities([])) - } - }) - } + // Query for Banners + const { data: banners } = useQuery({ + queryKey: ['banners'], + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const data = await apiGetBanner({ user: hasil }) + return data.data || [] + }, + enabled: !!token?.current, + staleTime: 0, + }) - async function handleUser() { - const hasil = await decryptToken(String(token?.current)) - const response = await apiGetProfile({ id: hasil }) - dispatch(setEntityUser({ role: response.data.idUserRole, admin: false })) - } + // Query for Profile (Role Check) + const { data: profile } = useQuery({ + queryKey: ['profile'], // Shares same key as Home.tsx + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const data = await apiGetProfile({ id: hasil }) + return data.data + }, + enabled: !!token?.current, + staleTime: 0, + }) + // Sync Banners to Redux useEffect(() => { - if (refreshing) - handleBannerView() - }, [refreshing]); + if (banners) { + dispatch(setEntities(banners)) + } + }, [banners, dispatch]) + // Sync User Role to Redux useEffect(() => { - handleBannerView() - }, [dispatch]); - - useEffect(() => { - handleUser() - }, []); + if (profile) { + dispatch(setEntityUser({ role: profile.idUserRole, admin: false })) + } + }, [profile, dispatch]) return ( ([]) - const [maxValue, setMaxValue] = useState(5) - const [chartKey, setChartKey] = useState(0) - const barData = [ - { value: 23, label: 'Gambar', frontColor: '#fac858' }, - { value: 12, label: 'Dokumen', frontColor: '#92cc76' }, - ]; const width = Dimensions.get("window").width; - - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Document Chart data + const { data: chartData = [], isLoading } = useQuery({ + queryKey: ['homeData', 'dokumen'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "dokumen", user: hasil }) - const maxVal = response.data.reduce((max: number, obj: { value: number; }) => Math.max(max, Number(obj.value)), 0); - const roundUp = maxVal > 0 ? Math.ceil(maxVal / 10) * 10 : 10; - setMaxValue(roundUp) - const convertedArray = response.data.map((item: { color: any; label: any; value: any; }) => ({ + return response.data.map((item: { color: any; label: any; value: any; }) => ({ frontColor: item.color, label: item.label, value: Number(item.value) - })); - setData(convertedArray) - setChartKey((prev: number) => prev + 1) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } + })) as Props + }, + enabled: !!token?.current, + staleTime: 0, + }) - useEffect(() => { - if (refreshing) { - handleData(false) - } - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + // Derived state for maxValue + const maxValue = useMemo(() => { + const maxVal = chartData.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), 0); + return maxVal > 0 ? Math.ceil(maxVal / 10) * 10 : 10; + }, [chartData]); return ( JUMLAH DOKUMEN { - loading ? + isLoading ? : } - ) } \ No newline at end of file diff --git a/components/home/chartProgresHome.tsx b/components/home/chartProgresHome.tsx index f4a6c5f..78c58f3 100644 --- a/components/home/chartProgresHome.tsx +++ b/components/home/chartProgresHome.tsx @@ -2,7 +2,7 @@ import Styles from "@/constants/Styles"; import { apiGetDataHome } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; -import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { View } from "react-native"; import { PieChart } from "react-native-gifted-charts"; import Skeleton from "../skeleton"; @@ -17,45 +17,32 @@ type Props = { export default function ChartProgresHome({ refreshing }: { refreshing: boolean }) { const { decryptToken, token } = useAuthSession() const { colors } = useTheme(); - const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Progress Chart data + const { data: chartData = [], isLoading } = useQuery({ + queryKey: ['homeData', 'progress'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "progress", user: hasil }) - const convertedArray = response.data.map((item: { color: any; text: any; value: any; }) => ({ + return response.data.map((item: { color: any; text: any; value: any; }) => ({ color: item.color, text: item.text, value: Number(item.value) - })); - setData(convertedArray) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + })) as Props + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( PROGRES KEGIATAN { - loading ? + isLoading ? : <> ([]) - const [loading, setLoading] = useState(true) const { colors } = useTheme(); - - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Discussion data + const { data: homeDiscussions = [], isLoading } = useQuery({ + queryKey: ['homeData', 'discussion'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "discussion", user: hasil }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + return response.data as Props[] + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( Diskusi { - loading ? + isLoading ? <> : - data.length > 0 ? - data.map((item, index) => { + homeDiscussions.length > 0 ? + homeDiscussions.map((item: Props, index: number) => { return ( { router.push(`/division/${item.idDivision}/discussion/${item.id}`) }} /> ) diff --git a/components/home/divisionHome.tsx b/components/home/divisionHome.tsx index 137509a..4f342ef 100644 --- a/components/home/divisionHome.tsx +++ b/components/home/divisionHome.tsx @@ -3,8 +3,9 @@ import { apiGetDataHome } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { Feather } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { Dimensions, Pressable, View } from "react-native"; import { ICarouselInstance } from "react-native-reanimated-carousel"; import Skeleton from "../skeleton"; @@ -21,45 +22,31 @@ export default function DivisionHome({ refreshing }: { refreshing: boolean }) { const { colors } = useTheme(); const ref = React.useRef(null) const width = Dimensions.get("window").width - const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) const arrSkeleton = Array.from({ length: 2 }, (_, index) => index) - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Division data + const { data: homeDivisions = [], isLoading } = useQuery({ + queryKey: ['homeData', 'division'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "division", user: hasil }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); - - + return response.data as Props[] + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( Divisi Teraktif { - loading ? + isLoading ? arrSkeleton.map((item, index) => ( )) : - data.length > 0 ? - data.map((item, index) => ( + homeDivisions.length > 0 ? + homeDivisions.map((item, index) => ( { router.push(`/division/${item.id}`) }}> diff --git a/components/home/eventHome.tsx b/components/home/eventHome.tsx index 6e0927a..7848a0f 100644 --- a/components/home/eventHome.tsx +++ b/components/home/eventHome.tsx @@ -2,8 +2,8 @@ import Styles from "@/constants/Styles"; import { apiGetDataHome } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; +import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; -import { useEffect, useState } from "react"; import { View } from "react-native"; import EventItem from "../eventItem"; import Skeleton from "../skeleton"; @@ -26,30 +26,18 @@ type Props = { export default function EventHome({ refreshing }: { refreshing: boolean }) { const { decryptToken, token } = useAuthSession() const { colors } = useTheme(); - const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Event data + const { data: homeEvents = [], isLoading } = useQuery({ + queryKey: ['homeData', 'event'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "event", user: hasil }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + return response.data as Props[] + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( @@ -57,14 +45,14 @@ export default function EventHome({ refreshing }: { refreshing: boolean }) { { - loading ? + isLoading ? <> : - data.length > 0 ? - data.map((item, index) => { + homeEvents.length > 0 ? + homeEvents.map((item: Props, index: number) => { return ( { router.push(`/division/${item.idDivision}/calendar/${item.id}`) }} title={item.title} user={item.user_name} jamAwal={item.timeStart} jamAkhir={item.timeEnd} /> ) diff --git a/components/home/projectHome.tsx b/components/home/projectHome.tsx index ad42093..debd327 100644 --- a/components/home/projectHome.tsx +++ b/components/home/projectHome.tsx @@ -2,8 +2,9 @@ import Styles from "@/constants/Styles"; import { apiGetDataHome } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; +import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { Dimensions, View } from "react-native"; import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel"; import LabelStatus from "../labelStatus"; @@ -25,45 +26,33 @@ export default function ProjectHome({ refreshing }: { refreshing: boolean }) { const { decryptToken, token } = useAuthSession() const ref = React.useRef(null); const width = Dimensions.get("window").width; - const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) const { colors } = useTheme(); - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Projects data + const { data: homeProjects = [], isLoading } = useQuery({ + queryKey: ['homeData', 'kegiatan'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "kegiatan", user: hasil }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + return response.data as Props[] + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( Kegiatan Terupdate { - loading ? () + isLoading ? () : - data.length > 0 ? + homeProjects.length > 0 ? ( - { router.push(`/project/${data[index].id}`) }} title={data[index].title} headerColor="primary"> - + { router.push(`/project/${homeProjects[index].id}`) }} title={homeProjects[index].title} headerColor="primary"> + - {data[index].createdAt} + {homeProjects[index].createdAt} diff --git a/ios/Desa.xcodeproj/project.pbxproj b/ios/Desa.xcodeproj/project.pbxproj index b1cad5d..942742d 100644 --- a/ios/Desa.xcodeproj/project.pbxproj +++ b/ios/Desa.xcodeproj/project.pbxproj @@ -394,7 +394,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app; - PRODUCT_NAME = "Desa"; + PRODUCT_NAME = Desa; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -429,7 +429,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app; - PRODUCT_NAME = "Desa"; + PRODUCT_NAME = Desa; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7b1b0a8..c1a059c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,7 +3,7 @@ PODS: - DoubleConversion (1.1.6) - EXApplication (6.1.5): - ExpoModulesCore - - EXConstants (17.1.7): + - EXConstants (17.1.8): - ExpoModulesCore - EXImageLoader (5.1.0): - ExpoModulesCore @@ -11,9 +11,9 @@ PODS: - EXJSONUtils (0.15.0) - EXManifests (0.16.6): - ExpoModulesCore - - EXNotifications (0.31.4): + - EXNotifications (0.31.5): - ExpoModulesCore - - Expo (53.0.20): + - Expo (53.0.27): - DoubleConversion - ExpoModulesCore - glog @@ -282,7 +282,7 @@ PODS: - ExpoModulesCore - ExpoHaptics (14.1.4): - ExpoModulesCore - - ExpoHead (5.1.4): + - ExpoHead (5.1.11): - ExpoModulesCore - ExpoImagePicker (16.1.4): - ExpoModulesCore @@ -324,7 +324,7 @@ PODS: - ExpoModulesCore - ExpoSymbols (0.4.5): - ExpoModulesCore - - ExpoSystemUI (5.0.10): + - ExpoSystemUI (5.0.11): - ExpoModulesCore - ExpoWebBrowser (14.2.0): - ExpoModulesCore @@ -1703,6 +1703,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-netinfo (12.0.1): + - React-Core - react-native-render-html (6.3.4): - React-Core - react-native-safe-area-context (5.4.0): @@ -2296,6 +2298,7 @@ DEPENDENCIES: - react-native-blob-util (from `../node_modules/react-native-blob-util`) - react-native-date-picker (from `../node_modules/react-native-date-picker`) - react-native-image-picker (from `../node_modules/react-native-image-picker`) + - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-webview (from `../node_modules/react-native-webview`) @@ -2505,6 +2508,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-date-picker" react-native-image-picker: :path: "../node_modules/react-native-image-picker" + react-native-netinfo: + :path: "../node_modules/@react-native-community/netinfo" react-native-render-html: :path: "../node_modules/react-native-render-html" react-native-safe-area-context: @@ -2600,12 +2605,12 @@ SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EXApplication: 1e06972201838375ca1ec1ba34d586a98a5dc718 - EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 + EXConstants: d3d551cb154718f5161c4247304e96aa59f6cca7 EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd EXManifests: 691a779b04e4f2c96da46fb9bef4f86174fefcb5 - EXNotifications: be5e949edf1d60b70e77178b81aa505298fadd07 - Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea + EXNotifications: 6770976336aacdc7dc7aed7b538dd8f7ad2c55e8 + Expo: 052536aae777d5156739c960afd6aa54881df42a expo-dev-client: 9b1e78baf0dd87b005f035d180bbb07c05917fad expo-dev-launcher: 2f95084d36be3d9106790bea7a933a0d34210646 expo-dev-menu: 1456232a68c883078b61c02b7fa5b01d8a5ab840 @@ -2618,7 +2623,7 @@ SPEC CHECKSUMS: ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63 ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e ExpoHaptics: 0ff6e0d83cd891178a306e548da1450249d54500 - ExpoHead: a7b66cbaeeb51f4a85338d335a0f5467e29a2c90 + ExpoHead: cfc12096c9a68cbe25de93a8bfc4781c7689467e ExpoImagePicker: 0963da31800c906e01c03e25d7c849f16ebf02a2 ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7 ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d @@ -2628,7 +2633,7 @@ SPEC CHECKSUMS: ExpoSharing: b0377be82430d07398c6a4cd60b5a15696accbd3 ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385 ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859 - ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2 + ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92 EXUpdatesInterface: 7ff005b7af94ee63fa452ea7bb95d7a8ff40277a fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 @@ -2683,6 +2688,7 @@ SPEC CHECKSUMS: react-native-blob-util: 45eb0e23b243b48955d231414ca5ee4da2439968 react-native-date-picker: 2eca217a8fb09c517f5bb6b23978718c6cec59ec react-native-image-picker: 0c4a539c4e67fbe3977916cd2c8d0e4c67f00a8c + react-native-netinfo: bed7e7b8f68e22e0862a77d7df28d31faa66375d react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1 react-native-webview: 3df1192782174d1bd23f6a0f5a4fec3cdcca9954 diff --git a/package.json b/package.json index c108c70..5ddc145 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,16 @@ "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-community/cli": "^19.1.0", "@react-native-community/datetimepicker": "8.4.1", + "@react-native-community/netinfo": "^12.0.1", "@react-native-firebase/app": "^22.4.0", "@react-native-firebase/database": "^22.4.0", "@react-native-firebase/messaging": "^22.2.1", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", "@reduxjs/toolkit": "^2.7.0", + "@tanstack/query-async-storage-persister": "^5.99.2", + "@tanstack/react-query": "^5.99.2", + "@tanstack/react-query-persist-client": "^5.99.2", "@types/formidable": "^3.4.5", "axios": "^1.8.4", "crypto-es": "^2.1.0", diff --git a/providers/AuthProvider.tsx b/providers/AuthProvider.tsx index e9d4c02..0d49e97 100644 --- a/providers/AuthProvider.tsx +++ b/providers/AuthProvider.tsx @@ -52,12 +52,11 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea const signIn = useCallback(async (token: string) => { const hasil = await decryptToken(String(token)) - // const permission = await requestPermission() const permissionStorage = await AsyncStorage.getItem('@notification_permission') if (permissionStorage === "true") { const tokenDevice = await getToken() try { - const register = await apiRegisteredToken({ user: hasil, token: String(tokenDevice) }) + await apiRegisteredToken({ user: hasil, token: String(tokenDevice) }) } catch (error) { console.error(error) } finally { diff --git a/providers/QueryProvider.tsx b/providers/QueryProvider.tsx new file mode 100644 index 0000000..654a51c --- /dev/null +++ b/providers/QueryProvider.tsx @@ -0,0 +1,58 @@ +import React, { useEffect } from 'react'; +import { QueryClient, QueryClientProvider, focusManager, onlineManager } from '@tanstack/react-query'; +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import NetInfo from '@react-native-community/netinfo'; +import { AppState, Platform, AppStateStatus } from 'react-native'; + +// 1. Configure the QueryClient +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Data is considered stale after 5 minutes + staleTime: 5 * 60 * 1000, + // Keep unused data in cache for 24 hours + gcTime: 24 * 60 * 60 * 1000, + // Retry failed queries 2 times + retry: 2, + }, + }, +}); + +// 2. Configure the AsyncStorage persister +const asyncStoragePersister = createAsyncStoragePersister({ + storage: AsyncStorage, + // Key used to store cache in AsyncStorage + key: 'OFFLINE_CACHE', +}); + +// 3. Configure the Online Manager for NetInfo +onlineManager.setEventListener((setOnline) => { + return NetInfo.addEventListener((state) => { + setOnline(!!state.isConnected); + }); +}); + +// 4. Configure the Focus Manager for AppState +function onAppStateChange(status: AppStateStatus) { + if (Platform.OS !== 'web') { + focusManager.setFocused(status === 'active'); + } +} + +export default function QueryProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + const subscription = AppState.addEventListener('change', onAppStateChange); + return () => subscription.remove(); + }, []); + + return ( + + {children} + + ); +} -- 2.49.1 From 5dac451754613206814531ccc7632310729ee07d Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 20 Apr 2026 14:37:36 +0800 Subject: [PATCH 2/7] fix: grafik jumlah dokumen --- components/home/chartDokumenHome.tsx | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/components/home/chartDokumenHome.tsx b/components/home/chartDokumenHome.tsx index 60ac76c..fa87790 100644 --- a/components/home/chartDokumenHome.tsx +++ b/components/home/chartDokumenHome.tsx @@ -21,16 +21,24 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean } const width = Dimensions.get("window").width; // TanStack Query for Document Chart data - const { data: chartData = [], isLoading } = useQuery({ + const { data: chartData = [], isLoading, isFetching } = useQuery({ queryKey: ['homeData', 'dokumen'], queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "dokumen", user: hasil }) - return response.data.map((item: { color: any; label: any; value: any; }) => ({ - frontColor: item.color, - label: item.label, - value: Number(item.value) - })) as Props + return response.data.map((item: { color: any; label: any; value: any; }) => { + const val = Number(item.value) || 0; + return { + frontColor: val > 0 ? (item.color || '#fac858') : 'transparent', + label: item.label, + value: val, + topLabelComponent: () => ( + + {val > 0 ? val : ""} + + ) + } + }) as Props }, enabled: !!token?.current, staleTime: 0, @@ -39,19 +47,23 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean } // Derived state for maxValue const maxValue = useMemo(() => { const maxVal = chartData.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), 0); - return maxVal > 0 ? Math.ceil(maxVal / 10) * 10 : 10; + // Adjust maxValue and intervals based on the data + if (maxVal === 0) return 10; + if (maxVal < 5) return 5; + return Math.ceil(maxVal / 10) * 10; }, [chartData]); return ( JUMLAH DOKUMEN { - isLoading ? + isLoading || (refreshing && isFetching) ? : { - return ( - - {item.value} - - ); - }} /> } -- 2.49.1 From 8b8ea61a137f86dc1eefd412b0d0cdfe2608b9b8 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 20 Apr 2026 16:27:44 +0800 Subject: [PATCH 3/7] fix: grafik bar --- components/division/reportChartDocument.tsx | 46 ++++++++++++------- components/division/reportChartEvent.tsx | 50 ++++++++++++--------- components/home/chartDokumenHome.tsx | 18 +++++--- 3 files changed, 72 insertions(+), 42 deletions(-) diff --git a/components/division/reportChartDocument.tsx b/components/division/reportChartDocument.tsx index 9cb0957..80beea6 100644 --- a/components/division/reportChartDocument.tsx +++ b/components/division/reportChartDocument.tsx @@ -2,40 +2,54 @@ import Styles from "@/constants/Styles"; import { Dimensions, View } from "react-native"; import { BarChart } from "react-native-gifted-charts"; import { useTheme } from "@/providers/ThemeProvider"; +import { useMemo } from "react"; import Text from "../Text"; export default function ReportChartDocument({ data }: { data: { label: string; value: number; }[] }) { const { colors } = useTheme(); - const maxValue = Math.max(...data.map(i => i.value)) const width = Dimensions.get("window").width; + + const maxValue = useMemo(() => { + const maxVal = data.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), 0); + if (maxVal === 0) return 10; + if (maxVal < 5) return 5; + return Math.ceil(maxVal / 10) * 10; + }, [data]); + + const barData = useMemo(() => { + return data.map(item => ({ + ...item, + frontColor: item.value > 0 ? "#fac858" : "transparent", + topLabelComponent: () => ( + + + {item.value > 0 ? item.value : ""} + + + ) + })) + }, [data, colors.text]); return ( JUMLAH DOKUMEN { - return ( - - {item.value} - - ); - }} /> ) diff --git a/components/division/reportChartEvent.tsx b/components/division/reportChartEvent.tsx index ec3398e..c9f9235 100644 --- a/components/division/reportChartEvent.tsx +++ b/components/division/reportChartEvent.tsx @@ -2,44 +2,54 @@ import Styles from "@/constants/Styles"; import { Dimensions, View } from "react-native"; import { BarChart } from "react-native-gifted-charts"; import { useTheme } from "@/providers/ThemeProvider"; +import { useMemo } from "react"; import Text from "../Text"; export default function ReportChartEvent({ data }: { data: { label: string; value: number; }[] }) { const { colors } = useTheme(); const width = Dimensions.get("window").width; - const maxValue = Math.max(...data.map(i => i.value)) - const barData = [ - { value: 23, label: 'Akan Datang', }, - { value: 12, label: 'Selesai' }, - ]; + + const maxValue = useMemo(() => { + const maxVal = data.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), 0); + if (maxVal === 0) return 10; + if (maxVal < 5) return 5; + return Math.ceil(maxVal / 10) * 10; + }, [data]); + + const barData = useMemo(() => { + return data.map(item => ({ + ...item, + frontColor: item.value > 0 ? "#177AD5" : "transparent", + topLabelComponent: () => ( + + + {item.value > 0 ? item.value : ""} + + + ) + })) + }, [data, colors.text]); return ( ACARA DIVISI { - return ( - - {item.value} - - ); - }} /> ) diff --git a/components/home/chartDokumenHome.tsx b/components/home/chartDokumenHome.tsx index fa87790..c36d7ed 100644 --- a/components/home/chartDokumenHome.tsx +++ b/components/home/chartDokumenHome.tsx @@ -32,11 +32,6 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean } frontColor: val > 0 ? (item.color || '#fac858') : 'transparent', label: item.label, value: val, - topLabelComponent: () => ( - - {val > 0 ? val : ""} - - ) } }) as Props }, @@ -52,6 +47,17 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean } if (maxVal < 5) return 5; return Math.ceil(maxVal / 10) * 10; }, [chartData]); + + const barData = useMemo(() => { + return chartData.map(item => ({ + ...item, + topLabelComponent: () => ( + + {item.value > 0 ? item.value : ""} + + ) + })) + }, [chartData, colors.text]); return ( @@ -65,7 +71,7 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean } showYAxisIndices noOfSections={maxValue < 5 ? (maxValue === 0 ? 4 : maxValue) : 4} maxValue={maxValue} - data={chartData} + data={barData} isAnimated width={width - 140} barWidth={width * 0.25} -- 2.49.1 From 47cb146c5a20a5401088056ab051d14ad57fa918 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 20 Apr 2026 17:05:40 +0800 Subject: [PATCH 4/7] upd: claude --- CLAUDE.md | 72 +++++++++++++++ __tests__/ErrorBoundary-test.tsx | 77 ++++++++++++++++ app/_layout.tsx | 22 +++-- app/verification.tsx | 77 ---------------- bun.lock | 151 +++++++++++++++++++++++++++---- components/ErrorBoundary.tsx | 74 +++++++++++++++ package.json | 3 +- 7 files changed, 370 insertions(+), 106 deletions(-) create mode 100644 CLAUDE.md create mode 100644 __tests__/ErrorBoundary-test.tsx delete mode 100644 app/verification.tsx create mode 100644 components/ErrorBoundary.tsx diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2a00ce8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Desa+** is a React Native (Expo) mobile app for village administration — managing announcements, projects, discussions, members, divisions, and documents. Primary platforms are Android and iOS. + +## Commands + +```bash +npm run start # Start Expo dev server +npm run android # Run on Android +npm run ios # Run on iOS +npm run lint # Expo lint +npm run test # Jest tests +npm run build:android # Production Android build via EAS (bumps version first) +``` + +Run a single test file: +```bash +bunx jest path/to/test.tsx --no-coverage +``` + +> Project uses **Bun** as the package manager (`bun.lock` present). Use `bun add` / `bunx` instead of `npm install` / `npx`. + +## Architecture + +### Routing (Expo Router — file-based) + +- `app/index.tsx` — Login/splash (public); OTP verification is handled inline via `components/auth/viewVerification.tsx` (not a separate route) +- `app/(application)/` — All authenticated screens; Expo Router enforces auth guard here +- Deep-link navigation is handled by `lib/pushToPage.ts`, which maps notification payloads to routes + +### State Management (three layers) + +1. **Context** (`providers/`) — Auth (token encryption/decryption via CryptoES.AES), Theme (light/dark, persisted to AsyncStorage), and React Query client +2. **Redux Toolkit** (`lib/store.ts` + slices) — Feature-level state for CRUD operations. Slices follow a naming pattern: `*Slice.ts` for read state, `*Update.ts`/`*Create.ts` for mutation state +3. **TanStack React Query** — All server data fetching; configured with 5-min stale time, 24-hour cache retention, 2 retries, and AsyncStorage persistence for offline support + +### API Layer (`lib/api.ts`) + +Single 773-line file defining 50+ Axios-based endpoints. The Axios instance reads `baseURL` from `Constants.expoConfig.extra.URL_API` (set in `.env` via `app.config.js`). Authentication uses Bearer tokens in headers. File uploads use `FormData` with `multipart/form-data`. + +Three separate backend services are integrated: +- **REST API** (axios) — main business logic +- **WhatsApp server** — OTP delivery (separate token in `.env`) +- **Firebase** — real-time database (`lib/firebaseDatabase.ts`) and push notifications (`lib/useNotification.ts`, `lib/registerForPushNotificationsAsync.ts`) + +### Providers Initialization Order + +`app/_layout.tsx` wraps the app in: `ErrorBoundary` → `NotifierWrapper` → `ThemeProvider` → `QueryProvider` → `AuthProvider` → navigation stack. Redux `store` is provided inside `app/(application)/_layout.tsx`, not at the root. + +### Error Boundary + +`components/ErrorBoundary.tsx` is a class component (required by React) wrapping the entire app. It uses React Native's built-in `Text` — **do not replace it with the custom `components/Text.tsx`** as that pulls in `ThemeProvider` → `AsyncStorage`, which breaks Jest tests. + +Tests for ErrorBoundary live in `__tests__/ErrorBoundary-test.tsx` and use `@testing-library/react-native`. + +## Key Conventions + +**Imports:** Use `@/` alias (maps to project root, configured in `tsconfig.json`). Never use relative paths like `../../`. + +**Utility functions:** Prefixed with `fun_` (e.g., `lib/fun_stringToDate.ts`, `lib/fun_validateName.ts`). + +**Styling:** Use theme-aware colors from `useTheme()` hook. Global `StyleSheet` definitions live in `constants/Styles.ts`. Color tokens are in `constants/Colors.ts` with explicit `light`/`dark` variants. + +**Component structure:** Feature-specific subdirectories under `components/` (e.g., `components/announcement/`) typically contain a header component alongside list/card components for that feature. + +**Environment config:** All env vars are declared in `.env`, exposed through `app.config.js` `extra` field, and accessed via `Constants.expoConfig.extra.*` or the `constants/ConstEnv.ts` wrapper. + +**EAS builds:** Profiles are `development`, `preview`, and `production` in `eas.json`. Production builds auto-increment the app version via the `bump` script. diff --git a/__tests__/ErrorBoundary-test.tsx b/__tests__/ErrorBoundary-test.tsx new file mode 100644 index 0000000..820db69 --- /dev/null +++ b/__tests__/ErrorBoundary-test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import ErrorBoundary from '@/components/ErrorBoundary'; + +// Komponen yang sengaja throw error saat render +const BrokenComponent = () => { + throw new Error('Test error boundary!'); +}; + +// Komponen normal +const NormalComponent = () => <>; + +// Suppress React's error boundary console output selama test + +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterEach(() => { + (console.error as jest.Mock).mockRestore(); +}); + +describe('ErrorBoundary', () => { + it('merender children dengan normal jika tidak ada error', () => { + // Tidak boleh throw dan tidak menampilkan teks error + const { queryByText } = render( + + + + ); + expect(queryByText('Terjadi Kesalahan')).toBeNull(); + }); + + it('menampilkan UI fallback ketika child throw error', () => { + const { getByText } = render( + + + + ); + expect(getByText('Terjadi Kesalahan')).toBeTruthy(); + }); + + it('menampilkan pesan error yang dilempar', () => { + const { getByText } = render( + + + + ); + expect(getByText('Test error boundary!')).toBeTruthy(); + }); + + it('merender custom fallback jika prop fallback diberikan', () => { + const { getByText } = render( + }> + + + ); + // Custom fallback fragment kosong — pastikan teks default tidak muncul + expect(() => getByText('Terjadi Kesalahan')).toThrow(); + }); + + it('mereset error state saat tombol Coba Lagi ditekan', () => { + const { getByText } = render( + + + + ); + + const button = getByText('Coba Lagi'); + expect(button).toBeTruthy(); + + // Tekan tombol reset — hasError kembali false, BrokenComponent throw lagi + // sehingga fallback muncul kembali (membuktikan reset berjalan) + fireEvent.press(button); + expect(getByText('Terjadi Kesalahan')).toBeTruthy(); + }); +}); diff --git a/app/_layout.tsx b/app/_layout.tsx index cb20548..4960e59 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,6 +1,7 @@ import AuthProvider from '@/providers/AuthProvider'; import ThemeProvider, { useTheme } from '@/providers/ThemeProvider'; import QueryProvider from '@/providers/QueryProvider'; +import ErrorBoundary from '@/components/ErrorBoundary'; import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; @@ -22,7 +23,6 @@ function AppStack() { <> - @@ -47,15 +47,17 @@ export default function RootLayout() { return ( - - - - - - - - - + + + + + + + + + + + ); } diff --git a/app/verification.tsx b/app/verification.tsx deleted file mode 100644 index fa07add..0000000 --- a/app/verification.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { ButtonForm } from "@/components/buttonForm"; -import Text from '@/components/Text'; -import { ConstEnv } from "@/constants/ConstEnv"; -import Styles from "@/constants/Styles"; -import { useAuthSession } from "@/providers/AuthProvider"; -import { useTheme } from "@/providers/ThemeProvider"; -import CryptoES from "crypto-es"; -import React, { useState } from "react"; -import { Image, View } from "react-native"; -import { CodeField, Cursor, useBlurOnFulfill, useClearByFocusCell, } from 'react-native-confirmation-code-field'; - -export default function Index() { - const [value, setValue] = useState(''); - const ref = useBlurOnFulfill({ value, cellCount: 4 }); - const [props, getCellOnLayoutHandler] = useClearByFocusCell({ - value, - setValue, - }); - const { colors } = useTheme(); - - const { signIn } = useAuthSession(); - const login = (): void => { - // WARNING: This is a hardcoded bypass for development purposes. - // It should be removed or secured before production release. - if (__DEV__) { - const random: string = 'contohLoginMobileDarmasaba'; - var mytexttoEncryption = "contohLoginMobileDarmasaba" - const encrypted = CryptoES.AES.encrypt(mytexttoEncryption, ConstEnv.pass_encrypt).toString(); - signIn(encrypted); - } else { - console.warn("Bypass login disabled in production."); - } - } - return ( - - - - {/* PERBEKEL DARMASABA */} - - - Verifikasi Nomor Telepon - Masukkan kode yang kami kirimkan melalui WhatsApp - +628980185458 - - ( - - {symbol || (isFocused ? : null)} - - )} - /> - { router.push("/home") }} - onPress={login} - /> - - Tidak Menerima kode verifikasi? Kirim Ulang - - - ); -} diff --git a/bun.lock b/bun.lock index 2140a31..952f9ff 100644 --- a/bun.lock +++ b/bun.lock @@ -88,13 +88,14 @@ "devDependencies": { "@babel/core": "^7.25.2", "@react-native-community/cli-platform-ios": "^18.0.0", + "@testing-library/react-native": "^13.3.3", "@types/crypto-js": "^4.2.2", "@types/jest": "^29.5.12", "@types/react": "~19.0.10", "@types/react-test-renderer": "^18.3.0", "jest": "^29.2.1", "jest-expo": "~53.0.5", - "react-test-renderer": "18.3.1", + "react-test-renderer": "19.0.0", "typescript": "^5.3.3", }, }, @@ -460,6 +461,8 @@ "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="], + "@jest/diff-sequences": ["@jest/diff-sequences@30.3.0", "", {}, "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA=="], + "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], "@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="], @@ -468,11 +471,13 @@ "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], + "@jest/get-type": ["@jest/get-type@30.1.0", "", {}, "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA=="], + "@jest/globals": ["@jest/globals@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", "jest-mock": "^29.7.0" } }, "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ=="], "@jest/reporters": ["@jest/reporters@29.7.0", "", { "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg=="], - "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "@jest/schemas": ["@jest/schemas@30.0.5", "", { "dependencies": { "@sinclair/typebox": "^0.34.0" } }, "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA=="], "@jest/source-map": ["@jest/source-map@29.6.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" } }, "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw=="], @@ -620,7 +625,7 @@ "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], - "@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], @@ -640,6 +645,8 @@ "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.99.2", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.99.2" }, "peerDependencies": { "@tanstack/react-query": "^5.99.2", "react": "^18 || ^19" } }, "sha512-7+y5+kpaR26X2gdaEv0yQSFLZjqXz4Kn7wqzuYDQrb203b9MlYS3baML1M9hJTiLgi4QGGF2eJDdW8lHAazUow=="], + "@testing-library/react-native": ["@testing-library/react-native@13.3.3", "", { "dependencies": { "jest-matcher-utils": "^30.0.5", "picocolors": "^1.1.1", "pretty-format": "^30.0.5", "redent": "^3.0.0" }, "peerDependencies": { "jest": ">=29.0.0", "react": ">=18.2.0", "react-native": ">=0.71", "react-test-renderer": ">=18.2.0" }, "optionalPeers": ["jest"] }, "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg=="], + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -1246,6 +1253,8 @@ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -1326,7 +1335,7 @@ "jest-config": ["jest-config@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-circus": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-runner": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "ts-node": ">=9.0.0" }, "optionalPeers": ["@types/node", "ts-node"] }, "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ=="], - "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + "jest-diff": ["jest-diff@30.3.0", "", { "dependencies": { "@jest/diff-sequences": "30.3.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "pretty-format": "30.3.0" } }, "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ=="], "jest-docblock": ["jest-docblock@29.7.0", "", { "dependencies": { "detect-newline": "^3.0.0" } }, "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g=="], @@ -1344,7 +1353,7 @@ "jest-leak-detector": ["jest-leak-detector@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw=="], - "jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + "jest-matcher-utils": ["jest-matcher-utils@30.3.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.3.0", "pretty-format": "30.3.0" } }, "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA=="], "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], @@ -1512,6 +1521,8 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "minimatch": ["minimatch@9.0.8", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1632,7 +1643,7 @@ "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], - "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "pretty-format": ["pretty-format@30.3.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ=="], "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], @@ -1684,7 +1695,7 @@ "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], - "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], "react-native": ["react-native@0.79.5", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.79.5", "@react-native/codegen": "0.79.5", "@react-native/community-cli-plugin": "0.79.5", "@react-native/gradle-plugin": "0.79.5", "@react-native/js-polyfills": "0.79.5", "@react-native/normalize-colors": "0.79.5", "@react-native/virtualized-lists": "0.79.5", "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.25.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.82.0", "metro-source-map": "^0.82.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.25.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.0.0", "react": "^19.0.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-jVihwsE4mWEHZ9HkO1J2eUZSwHyDByZOqthwnGrVZCh6kTQBCm4v8dicsyDa6p0fpWNE5KicTcpX/XXl0ASJFg=="], @@ -1744,12 +1755,12 @@ "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - "react-shallow-renderer": ["react-shallow-renderer@16.15.0", "", { "dependencies": { "object-assign": "^4.1.1", "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA=="], - - "react-test-renderer": ["react-test-renderer@18.3.1", "", { "dependencies": { "react-is": "^18.3.1", "react-shallow-renderer": "^16.15.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA=="], + "react-test-renderer": ["react-test-renderer@19.0.0", "", { "dependencies": { "react-is": "^19.0.0", "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-oX5u9rOQlHzqrE/64CNr0HB0uWxkCQmZNSfozlYvwE71TLVgeZxVf0IjouGEr1v7r1kcDifdAJBeOhdhxsG/DA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], @@ -1806,7 +1817,7 @@ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -1904,6 +1915,8 @@ "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], @@ -2086,6 +2099,8 @@ "@expo/cli/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], + "@expo/cli/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "@expo/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@expo/cli/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], @@ -2128,10 +2143,14 @@ "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "@jest/core/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "@jest/reporters/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@jest/reporters/string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], + "@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "@react-native-community/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@react-native-community/cli-config-apple/@react-native-community/cli-tools": ["@react-native-community/cli-tools@18.0.1", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-WxWFXwfYhHR2eYiB4lkHZVC/PmIkRWeVHBQKmn0h1mecr3GrHYO4BzW1jpD5Xt6XZ9jojQ9wE5xrCqXjiMSAIQ=="], @@ -2162,7 +2181,7 @@ "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], - "@react-navigation/core/react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], + "@types/jest/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "@types/react-test-renderer/@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="], @@ -2206,6 +2225,8 @@ "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "expect/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], @@ -2232,18 +2253,36 @@ "istanbul-lib-instrument/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "jest-circus/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + + "jest-circus/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "jest-config/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "jest-expo/react-test-renderer": ["react-test-renderer@19.0.0", "", { "dependencies": { "react-is": "^19.0.0", "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-oX5u9rOQlHzqrE/64CNr0HB0uWxkCQmZNSfozlYvwE71TLVgeZxVf0IjouGEr1v7r1kcDifdAJBeOhdhxsG/DA=="], + "jest-config/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-each/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-leak-detector/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], "jest-runtime/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "jest-snapshot/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + + "jest-snapshot/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + + "jest-snapshot/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "jest-snapshot/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "jest-watch-select-projects/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], "jest-watch-typeahead/ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], @@ -2296,21 +2335,21 @@ "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - "react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], - "react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.5", "", {}, "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ=="], "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "react-native/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + "react-native/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "react-native/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -2374,6 +2413,10 @@ "@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + "@expo/cli/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@expo/cli/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "@expo/package-manager/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], @@ -2392,10 +2435,16 @@ "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "@jest/core/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/core/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "@jest/reporters/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "@jest/reporters/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + "@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "@react-native-community/cli-config-apple/@react-native-community/cli-tools/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@react-native-community/cli-doctor/@react-native-community/cli-platform-apple/@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@19.1.2", "", { "dependencies": { "@react-native-community/cli-tools": "19.1.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-91upuYMLgEtJE6foWQFgGDpT3ZDTc5bX6rMY5cJMqiAE5svgh1q0kbbpRuv/ptBYzcxLplL7wZWpA77TlJdm9A=="], @@ -2422,6 +2471,10 @@ "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "@types/jest/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@types/jest/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "ansi-fragments/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], @@ -2434,16 +2487,46 @@ "css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + "expect/jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + + "expect/jest-matcher-utils/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "jest-circus/jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + + "jest-circus/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "jest-circus/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "jest-config/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "jest-expo/react-test-renderer/react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], + "jest-config/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "jest-expo/react-test-renderer/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + "jest-config/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-each/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "jest-each/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-leak-detector/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "jest-leak-detector/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-runtime/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "jest-snapshot/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "jest-snapshot/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-validate/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "jest-watch-select-projects/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-watch-typeahead/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -2480,6 +2563,10 @@ "react-native/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "react-native/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "react-native/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -2504,6 +2591,8 @@ "@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + "@expo/cli/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], @@ -2516,6 +2605,8 @@ "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "@jest/core/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "@jest/reporters/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "@react-native-community/cli-server-api/pretty-format/@jest/types/@types/yargs": ["@types/yargs@15.0.20", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg=="], @@ -2526,12 +2617,32 @@ "@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "@types/jest/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "css-select/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "expect/jest-matcher-utils/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "expect/jest-matcher-utils/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-circus/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "jest-config/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "jest-config/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + + "jest-each/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + + "jest-leak-detector/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + + "jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "jest-runtime/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "jest-snapshot/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + + "jest-validate/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "logkitty/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "logkitty/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -2546,6 +2657,8 @@ "react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "react-native/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -2574,6 +2687,8 @@ "@react-native/codegen/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "expect/jest-matcher-utils/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + "jest-config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "jest-runtime/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx new file mode 100644 index 0000000..841a956 --- /dev/null +++ b/components/ErrorBoundary.tsx @@ -0,0 +1,74 @@ +import React, { Component, ReactNode } from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +type Props = { + children: ReactNode; + fallback?: ReactNode; +}; + +type State = { + hasError: boolean; + error: Error | null; +}; + +export default class ErrorBoundary extends Component { + state: State = { hasError: false, error: null }; + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + handleReset = () => { + this.setState({ hasError: false, error: null }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) return this.props.fallback; + return ( + + Terjadi Kesalahan + + {this.state.error?.message ?? 'Kesalahan tidak diketahui'} + + + Coba Lagi + + + ); + } + return this.props.children; + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 24, + backgroundColor: '#f7f7f7', + }, + title: { + fontSize: 18, + fontWeight: '600', + marginBottom: 8, + color: '#11181C', + }, + message: { + fontSize: 14, + color: '#707887', + textAlign: 'center', + marginBottom: 24, + }, + button: { + backgroundColor: '#19345E', + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + buttonText: { + color: '#fff', + fontWeight: '600', + }, +}); diff --git a/package.json b/package.json index 5ddc145..bb9c889 100644 --- a/package.json +++ b/package.json @@ -101,13 +101,14 @@ "devDependencies": { "@babel/core": "^7.25.2", "@react-native-community/cli-platform-ios": "^18.0.0", + "@testing-library/react-native": "^13.3.3", "@types/crypto-js": "^4.2.2", "@types/jest": "^29.5.12", "@types/react": "~19.0.10", "@types/react-test-renderer": "^18.3.0", "jest": "^29.2.1", "jest-expo": "~53.0.5", - "react-test-renderer": "18.3.1", + "react-test-renderer": "19.0.0", "typescript": "^5.3.3" }, "private": true, -- 2.49.1 From de5ad545a7e74050f5f5ee9d4f6d41869316b810 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 20 Apr 2026 17:07:49 +0800 Subject: [PATCH 5/7] upd: hapus console.log --- app/(application)/feature.tsx | 2 -- components/auth/viewLogin.tsx | 1 - 2 files changed, 3 deletions(-) diff --git a/app/(application)/feature.tsx b/app/(application)/feature.tsx index 23cfba6..00b2311 100644 --- a/app/(application)/feature.tsx +++ b/app/(application)/feature.tsx @@ -11,8 +11,6 @@ export default function Feature() { const entityUser = useSelector((state: any) => state.user) const { colors } = useTheme(); - console.log({entityUser}) - return ( Date: Mon, 20 Apr 2026 17:12:41 +0800 Subject: [PATCH 6/7] fix: variable yg tidak terpakai --- app/(application)/_layout.tsx | 2 +- providers/AuthProvider.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/(application)/_layout.tsx b/app/(application)/_layout.tsx index 3b32f68..9f939c6 100644 --- a/app/(application)/_layout.tsx +++ b/app/(application)/_layout.tsx @@ -108,7 +108,7 @@ export default function RootLayout() { try { if (title != "Komentar Baru") { const hasil = await decryptToken(String(token?.current)) - const response = await apiReadOneNotification({ user: hasil, id: id }) + await apiReadOneNotification({ user: hasil, id: id }) } pushToPage(category, idContent) } catch (error) { diff --git a/providers/AuthProvider.tsx b/providers/AuthProvider.tsx index 0d49e97..1087f55 100644 --- a/providers/AuthProvider.tsx +++ b/providers/AuthProvider.tsx @@ -66,7 +66,7 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea return true } } else { - const register = await apiRegisteredToken({ user: hasil, token: "" }) + await apiRegisteredToken({ user: hasil, token: "" }) await AsyncStorage.setItem('@token', token); tokenRef.current = token; router.replace('/home') @@ -78,7 +78,7 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea const hasil = await decryptToken(String(tokenRef.current)) // if (Platform.OS === 'android') { const token = await getToken() - const response = await apiUnregisteredToken({ user: hasil, token: String(token) }) + await apiUnregisteredToken({ user: hasil, token: String(token) }) // }else{ // const response = await apiUnregisteredToken({ user: hasil, token: "" }) // } -- 2.49.1 From b7165c59901b7b4a88688be6eb5af6725dff7a2e Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 20 Apr 2026 17:45:19 +0800 Subject: [PATCH 7/7] hapus komen --- app/(application)/_layout.tsx | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/app/(application)/_layout.tsx b/app/(application)/_layout.tsx index 9f939c6..30f5cab 100644 --- a/app/(application)/_layout.tsx +++ b/app/(application)/_layout.tsx @@ -106,7 +106,7 @@ export default function RootLayout() { async function handleReadNotification(id: string, category: string, idContent: string, title: string) { try { - if (title != "Komentar Baru") { + if (title !== "Komentar Baru") { const hasil = await decryptToken(String(token?.current)) await apiReadOneNotification({ user: hasil, id: id }) } @@ -203,8 +203,6 @@ export default function RootLayout() { { router.back() }} />, headerTitle: 'Notifikasi', headerTitleAlign: 'center', header: () => ( @@ -213,10 +211,8 @@ export default function RootLayout() { }} /> { router.back() }} />, title: 'Pengaturan', headerTitleAlign: 'center', - // headerRight: () => header: () => ( { router.back() }} />, title: 'Anggota', headerTitleAlign: 'center', - // headerRight: () => header: () => ( { router.back() }} />, title: 'Diskusi Umum', headerTitleAlign: 'center', - // headerRight: () => header: () => ( { router.back() }} />, title: 'Kegiatan', headerTitleAlign: 'center', - // headerRight: () => header: () => ( { router.back() }} />, title: 'Divisi', headerTitleAlign: 'center', - // headerRight: () => header: () => ( { router.back() }} />, headerTitle: 'Lembaga Desa', headerTitleAlign: 'center', - // headerRight: () => header: () => ( { router.back() }} />, headerTitle: 'Jabatan', headerTitleAlign: 'center', - // headerRight: () => header: () => ( { router.back() }} />, headerTitle: 'Pengumuman', headerTitleAlign: 'center', - // headerRight: () => header: () => (