Compare commits

...

34 Commits

Author SHA1 Message Date
e2ffef1085 Merge pull request 'amalia/23-apr-26' (#41) from amalia/23-apr-26 into join
Reviewed-on: #41
2026-04-23 17:32:41 +08:00
cb2a57ee8e feat: tambah versi aplikasi di bagian bawah halaman setting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 17:29:38 +08:00
f3b677f847 fix: ganti pesan error boundary dengan teks yang lebih ramah pengguna
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 11:26:33 +08:00
6ffe599ad0 docs: pecah CLAUDE.md jadi file terpisah di docs/
Pindahkan konten architecture dan conventions ke docs/ARCHITECTURE.md
dan docs/CONVENTIONS.md, lalu referensikan via @path di CLAUDE.md
agar file tetap ramping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 11:19:49 +08:00
4a92def490 Merge pull request 'amalia/22-apr-26' (#40) from amalia/22-apr-26 into join
Reviewed-on: #40
2026-04-22 17:30:00 +08:00
0bad792ce8 feat: tambah pengecekan villageIsActive saat desa dinonaktifkan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 16:42:59 +08:00
6c9623954c feat: tambah pengecekan isActive dan bersihkan cache saat logout
- Tampilkan Alert jika admin menonaktifkan akun user yang sedang login
- Clear React Query cache saat signOut agar data akun lama tidak bocor ke sesi berikutnya

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 15:17:21 +08:00
f39d5a4c85 Merge pull request 'feat: tambah error logger ke monitoring dashboard dengan offline queue' (#39) from amalia/21-apr-26 into join
Reviewed-on: #39
2026-04-21 17:32:08 +08:00
6021d17b5a feat: tambah error logger ke monitoring dashboard dengan offline queue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 15:25:10 +08:00
2d86a77a48 Merge pull request 'amalia/20-apr-26' (#38) from amalia/20-apr-26 into join
Reviewed-on: #38
2026-04-20 17:49:34 +08:00
b7165c5990 hapus komen 2026-04-20 17:45:19 +08:00
7dc51bd2b9 fix: variable yg tidak terpakai 2026-04-20 17:12:41 +08:00
de5ad545a7 upd: hapus console.log 2026-04-20 17:07:49 +08:00
47cb146c5a upd: claude 2026-04-20 17:05:40 +08:00
8b8ea61a13 fix: grafik bar 2026-04-20 16:27:44 +08:00
5dac451754 fix: grafik jumlah dokumen 2026-04-20 14:37:36 +08:00
ccf8ee1caf upd: caching data
Deskripsi:
- update caching pada fitur utama -yg fitur divisi belom
2026-04-20 14:23:14 +08:00
887e787a99 Merge pull request 'amalia/17-apr-26' (#37) from amalia/17-apr-26 into join
Reviewed-on: #37
2026-04-17 17:40:04 +08:00
772551a917 upd: page kosong atau data udah di hapus pada pengumuman 2026-04-17 16:46:49 +08:00
555b9e4037 upd: fix laopran grafik divisi 2026-04-17 15:26:06 +08:00
d4b4db4251 upd: fix: jumlah dokumen grafik pada home 2026-04-17 15:01:14 +08:00
17d92cba25 Merge pull request 'amalia/10-mar-26' (#36) from amalia/10-mar-26 into join
Reviewed-on: #36
2026-03-10 16:30:52 +08:00
e1b62be6da upd:build number 2026-03-10 15:31:57 +08:00
b2b125c410 upd: build eas 2026-03-10 15:19:40 +08:00
1cfecbbdd5 upd : border di android 2026-03-10 11:09:19 +08:00
21006e8eee upd: gambar caraousel 2026-03-10 11:02:26 +08:00
91231d60e4 upd: md 2026-03-09 16:43:04 +08:00
7174e27be1 upd: version dan prebuild 2026-03-09 14:17:36 +08:00
9d4b931aa6 Merge pull request 'amalia/05-mar-26' (#35) from amalia/05-mar-26 into join
Reviewed-on: #35
2026-03-05 16:19:03 +08:00
166d8f1c16 upd: notifikasi
Deskripsi:
- update notifikasi android

No Issues
2026-03-05 14:38:45 +08:00
7060a2d165 upd: otp
Deskripsi:
- link otp baru

NOIssues
2026-03-05 14:15:36 +08:00
d6217aecf1 Merge pull request 'upd: notifikasi' (#34) from amalia/04-mar-26 into join
Reviewed-on: #34
2026-03-04 16:39:05 +08:00
608381673f upd: notifikasi
Deskripsi:
- notifikasi saat allowed device
- ios dan android

No Issues
2026-03-04 16:35:59 +08:00
3cc7f76346 Merge pull request 'amalia/03-mar-26' (#33) from amalia/03-mar-26 into join
Reviewed-on: #33
2026-03-03 16:46:19 +08:00
68 changed files with 1589 additions and 1056 deletions

33
CLAUDE.md Normal file
View File

@@ -0,0 +1,33 @@
# 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
See @docs/ARCHITECTURE.md
## Key Conventions
See @docs/CONVENTIONS.md

View File

@@ -1,6 +1,6 @@
# Desa+ # Desa+
Desa+ adalah aplikasi mobile berbasis React Native yang dikembangkan dengan Expo untuk membantu pengelolaan dan komunikasi di lingkungan desa/kelurahan. Aplikasi ini menyediakan berbagai fitur untuk memudahkan administrasi desa, komunikasi antar warga, dan pengelolaan informasi penting. Desa+ (Desa Plus) adalah aplikasi mobile berbasis React Native yang dikembangkan dengan Expo untuk membantu pengelolaan dan komunikasi di lingkungan desa/kelurahan. Aplikasi ini menyediakan berbagai fitur untuk memudahkan administrasi desa, komunikasi antar warga, dan pengelolaan informasi penting.
## Fitur Utama ## Fitur Utama

View File

@@ -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(
<ErrorBoundary>
<NormalComponent />
</ErrorBoundary>
);
expect(queryByText('Terjadi Kesalahan')).toBeNull();
});
it('menampilkan UI fallback ketika child throw error', () => {
const { getByText } = render(
<ErrorBoundary>
<BrokenComponent />
</ErrorBoundary>
);
expect(getByText('Terjadi Kesalahan')).toBeTruthy();
});
it('menampilkan pesan error yang dilempar', () => {
const { getByText } = render(
<ErrorBoundary>
<BrokenComponent />
</ErrorBoundary>
);
expect(getByText('Test error boundary!')).toBeTruthy();
});
it('merender custom fallback jika prop fallback diberikan', () => {
const { getByText } = render(
<ErrorBoundary fallback={<></>}>
<BrokenComponent />
</ErrorBoundary>
);
// 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(
<ErrorBoundary>
<BrokenComponent />
</ErrorBoundary>
);
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();
});
});

View File

@@ -92,8 +92,8 @@ android {
applicationId 'mobiledarmasaba.app' applicationId 'mobiledarmasaba.app'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 6 versionCode 16
versionName "1.0.2" versionName "2.1.0"
} }
signingConfigs { signingConfigs {
debug { debug {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,9 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#ffffff</item> <item name="android:statusBarColor">#ffffff</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
</style> </style>
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen"> <style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_background</item> <item name="windowSplashScreenBackground">@color/splashscreen_background</item>

View File

@@ -14,7 +14,7 @@ export default {
ios: { ios: {
supportsTablet: true, supportsTablet: true,
bundleIdentifier: "mobiledarmasaba.app", bundleIdentifier: "mobiledarmasaba.app",
buildNumber: "8", buildNumber: "9",
infoPlist: { infoPlist: {
ITSAppUsesNonExemptEncryption: false, ITSAppUsesNonExemptEncryption: false,
CFBundleDisplayName: "Desa+" CFBundleDisplayName: "Desa+"
@@ -85,6 +85,8 @@ export default {
FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET, FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID, FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID, FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
URL_MONITORING: process.env.URL_MONITORING,
KEY_API_MONITORING: process.env.KEY_API_MONITORING,
} }
} }
}; };

View File

@@ -106,9 +106,9 @@ export default function RootLayout() {
async function handleReadNotification(id: string, category: string, idContent: string, title: string) { async function handleReadNotification(id: string, category: string, idContent: string, title: string) {
try { try {
if (title != "Komentar Baru") { if (title !== "Komentar Baru") {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiReadOneNotification({ user: hasil, id: id }) await apiReadOneNotification({ user: hasil, id: id })
} }
pushToPage(category, idContent) pushToPage(category, idContent)
} catch (error) { } catch (error) {
@@ -203,8 +203,6 @@ export default function RootLayout() {
<Stack.Screen name="feature" options={{ title: 'Fitur' }} /> <Stack.Screen name="feature" options={{ title: 'Fitur' }} />
<Stack.Screen name="search" options={{ title: 'Pencarian' }} /> <Stack.Screen name="search" options={{ title: 'Pencarian' }} />
<Stack.Screen name="notification" options={{ <Stack.Screen name="notification" options={{
title: 'Notifikasi',
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Notifikasi', headerTitle: 'Notifikasi',
headerTitleAlign: 'center', headerTitleAlign: 'center',
header: () => ( header: () => (
@@ -213,10 +211,8 @@ export default function RootLayout() {
}} /> }} />
<Stack.Screen name="profile" options={{ title: 'Profile' }} /> <Stack.Screen name="profile" options={{ title: 'Profile' }} />
<Stack.Screen name="setting/index" options={{ <Stack.Screen name="setting/index" options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Pengaturan', title: 'Pengaturan',
headerTitleAlign: 'center', headerTitleAlign: 'center',
// headerRight: () => <HeaderRightProjectList />
header: () => ( header: () => (
<AppHeader title="Pengaturan" <AppHeader title="Pengaturan"
showBack={true} showBack={true}
@@ -225,10 +221,8 @@ export default function RootLayout() {
) )
}} /> }} />
<Stack.Screen name="member/index" options={{ <Stack.Screen name="member/index" options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Anggota', title: 'Anggota',
headerTitleAlign: 'center', headerTitleAlign: 'center',
// headerRight: () => <HeaderMemberList />
header: () => ( header: () => (
<AppHeader title="Anggota" <AppHeader title="Anggota"
showBack={true} showBack={true}
@@ -238,10 +232,8 @@ export default function RootLayout() {
) )
}} /> }} />
<Stack.Screen name="discussion/index" options={{ <Stack.Screen name="discussion/index" options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Diskusi Umum', title: 'Diskusi Umum',
headerTitleAlign: 'center', headerTitleAlign: 'center',
// headerRight: () => <HeaderDiscussionGeneral />
header: () => ( header: () => (
<AppHeader <AppHeader
title="Diskusi Umum" title="Diskusi Umum"
@@ -252,10 +244,8 @@ export default function RootLayout() {
) )
}} /> }} />
<Stack.Screen name="project/index" options={{ <Stack.Screen name="project/index" options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Kegiatan', title: 'Kegiatan',
headerTitleAlign: 'center', headerTitleAlign: 'center',
// headerRight: () => <HeaderRightProjectList />
header: () => ( header: () => (
<AppHeader title="Kegiatan" <AppHeader title="Kegiatan"
showBack={true} showBack={true}
@@ -265,10 +255,8 @@ export default function RootLayout() {
) )
}} /> }} />
<Stack.Screen name="division/index" options={{ <Stack.Screen name="division/index" options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
title: 'Divisi', title: 'Divisi',
headerTitleAlign: 'center', headerTitleAlign: 'center',
// headerRight: () => <HeaderRightDivisionList />
header: () => ( header: () => (
<AppHeader title="Divisi" <AppHeader title="Divisi"
showBack={true} showBack={true}
@@ -279,10 +267,8 @@ export default function RootLayout() {
}} /> }} />
<Stack.Screen name="division/[id]/(fitur-division)" options={{ headerShown: false }} /> <Stack.Screen name="division/[id]/(fitur-division)" options={{ headerShown: false }} />
<Stack.Screen name="group/index" options={{ <Stack.Screen name="group/index" options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Lembaga Desa', headerTitle: 'Lembaga Desa',
headerTitleAlign: 'center', headerTitleAlign: 'center',
// headerRight: () => <HeaderRightGroupList />
header: () => ( header: () => (
<AppHeader title="Lembaga Desa" <AppHeader title="Lembaga Desa"
showBack={true} showBack={true}
@@ -292,10 +278,8 @@ export default function RootLayout() {
) )
}} /> }} />
<Stack.Screen name="position/index" options={{ <Stack.Screen name="position/index" options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Jabatan', headerTitle: 'Jabatan',
headerTitleAlign: 'center', headerTitleAlign: 'center',
// headerRight: () => <HeaderRightPositionList />
header: () => ( header: () => (
<AppHeader title="Jabatan" <AppHeader title="Jabatan"
showBack={true} showBack={true}
@@ -306,10 +290,8 @@ export default function RootLayout() {
}} /> }} />
<Stack.Screen name="announcement/index" <Stack.Screen name="announcement/index"
options={{ options={{
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Pengumuman', headerTitle: 'Pengumuman',
headerTitleAlign: 'center', headerTitleAlign: 'center',
// headerRight: () => <HeaderRightAnnouncementList />
header: () => ( header: () => (
<AppHeader title="Pengumuman" <AppHeader title="Pengumuman"
showBack={true} showBack={true}

View File

@@ -3,6 +3,7 @@ import AppHeader from "@/components/AppHeader";
import BorderBottomItem from "@/components/borderBottomItem"; import BorderBottomItem from "@/components/borderBottomItem";
import Skeleton from "@/components/skeleton"; import Skeleton from "@/components/skeleton";
import Text from '@/components/Text'; import Text from '@/components/Text';
import ErrorView from "@/components/ErrorView";
import { ConstEnv } from "@/constants/ConstEnv"; import { ConstEnv } from "@/constants/ConstEnv";
import { isImageFile } from "@/constants/FileExtensions"; import { isImageFile } from "@/constants/FileExtensions";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
@@ -65,6 +66,7 @@ export default function DetailAnnouncement() {
const [loadingOpen, setLoadingOpen] = useState(false) const [loadingOpen, setLoadingOpen] = useState(false)
const [preview, setPreview] = useState(false) const [preview, setPreview] = useState(false)
const [chooseFile, setChooseFile] = useState<FileData>() const [chooseFile, setChooseFile] = useState<FileData>()
const [isError, setIsError] = useState(false)
/** /**
* Opens the image preview modal for the selected image file * Opens the image preview modal for the selected image file
@@ -79,6 +81,7 @@ export default function DetailAnnouncement() {
async function handleLoad(loading: boolean) { async function handleLoad(loading: boolean) {
try { try {
setIsError(false)
setLoading(loading) setLoading(loading)
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil }) const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
@@ -87,10 +90,12 @@ export default function DetailAnnouncement() {
setDataMember(response.member) setDataMember(response.member)
setDataFile(response.file) setDataFile(response.file)
} else { } else {
setIsError(true)
Toast.show({ type: 'small', text1: response.message }) Toast.show({ type: 'small', text1: response.message })
} }
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
setIsError(true)
const message = error?.response?.data?.message || "Gagal mengambil data" const message = error?.response?.data?.message || "Gagal mengambil data"
Toast.show({ type: 'small', text1: message }) Toast.show({ type: 'small', text1: message })
@@ -206,104 +211,110 @@ export default function DetailAnnouncement() {
/> />
} }
> >
<View style={[Styles.p15, Styles.mb50]}> {isError && !loading ? (
<View style={[Styles.wrapPaper, Styles.borderAll, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}> <View style={[Styles.mv50]}>
{ <ErrorView />
loading ?
<View>
<View style={[Styles.rowOnly]}>
<Skeleton width={30} height={30} borderRadius={10} />
<View style={[Styles.flex1, Styles.ph05]}>
<Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
</View>
</View>
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
</View>
:
<>
<View style={[Styles.rowOnly, Styles.alignStart]}>
<MaterialIcons name="campaign" size={25} color={colors.text} style={[Styles.mr05]} />
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
</View>
<View style={[Styles.mt10]}>
{
hasHtmlTags(data?.desc) ?
<RenderHTML
contentWidth={contentWidth}
source={{ html: data?.desc }}
baseStyle={{ color: colors.text }}
/>
:
<Text>{data?.desc}</Text>
}
</View>
</>
}
</View> </View>
{ ) : (
dataFile.length > 0 && ( <View style={[Styles.p15, Styles.mb50]}>
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}> <View style={[Styles.wrapPaper, Styles.borderAll, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
<View style={[Styles.mb05]}> {
<Text style={[Styles.textDefaultSemiBold]}>File</Text> loading ?
</View> <View>
{dataFile.map((item, index) => ( <View style={[Styles.rowOnly]}>
<BorderBottomItem <Skeleton width={30} height={30} borderRadius={10} />
key={`${item.id}-${index}`} <View style={[Styles.flex1, Styles.ph05]}>
borderType={index === dataFile.length - 1 ? 'none' : 'bottom'} <Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
icon={<MaterialCommunityIcons </View>
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
size={25}
color={colors.text}
/>}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => {
isImageFile(item.extension) ?
handleChooseFile(item)
: openFile(item)
}}
/>
))}
</View>
)
}
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
{
loading ?
arrSkeleton.map((item, index) => {
return (
<View key={index}>
<Skeleton width={30} widthType="percent" height={10} borderRadius={10} />
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
</View> </View>
) <Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
}) <Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
: <Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
Object.keys(dataMember).map((v: any, i: any) => { </View>
return ( :
<View key={i} style={[Styles.mb05]}> <>
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text> <View style={[Styles.rowOnly, Styles.alignStart]}>
<MaterialIcons name="campaign" size={25} color={colors.text} style={[Styles.mr05]} />
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
</View>
<View style={[Styles.mt10]}>
{ {
dataMember[v].map((item: any, x: any) => { hasHtmlTags(data?.desc) ?
return ( <RenderHTML
<View key={x} style={[Styles.rowItemsCenter, Styles.w90]}> contentWidth={contentWidth}
<Entypo name="dot-single" size={24} color={colors.text} /> source={{ html: data?.desc }}
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{item.division}</Text> baseStyle={{ color: colors.text }}
</View> />
) :
}) <Text>{data?.desc}</Text>
} }
</View> </View>
) </>
}) }
</View>
{
dataFile.length > 0 && (
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
<View style={[Styles.mb05]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
</View>
{dataFile.map((item, index) => (
<BorderBottomItem
key={`${item.id}-${index}`}
borderType={index === dataFile.length - 1 ? 'none' : 'bottom'}
icon={<MaterialCommunityIcons
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
size={25}
color={colors.text}
/>}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => {
isImageFile(item.extension) ?
handleChooseFile(item)
: openFile(item)
}}
/>
))}
</View>
)
} }
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
{
loading ?
arrSkeleton.map((item, index) => {
return (
<View key={index}>
<Skeleton width={30} widthType="percent" height={10} borderRadius={10} />
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
</View>
)
})
:
Object.keys(dataMember).map((v: any, i: any) => {
return (
<View key={i} style={[Styles.mb05]}>
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text>
{
dataMember[v].map((item: any, x: any) => {
return (
<View key={x} style={[Styles.rowItemsCenter, Styles.w90]}>
<Entypo name="dot-single" size={24} color={colors.text} />
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{item.division}</Text>
</View>
)
})
}
</View>
)
})
}
</View>
</View> </View>
</View> )}
</ScrollView> </ScrollView>
<ImageViewing <ImageViewing

View File

@@ -2,14 +2,14 @@ import BorderBottomItem from "@/components/borderBottomItem";
import InputSearch from "@/components/inputSearch"; import InputSearch from "@/components/inputSearch";
import SkeletonContent from "@/components/skeletonContent"; import SkeletonContent from "@/components/skeletonContent";
import Text from '@/components/Text'; import Text from '@/components/Text';
import { ColorsStatus } from "@/constants/ColorsStatus";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
import { apiGetAnnouncement } from "@/lib/api"; import { apiGetAnnouncement } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { MaterialIcons } from "@expo/vector-icons"; import { MaterialIcons } from "@expo/vector-icons";
import { useInfiniteQuery } from "@tanstack/react-query";
import { router } from "expo-router"; import { router } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { RefreshControl, View, VirtualizedList } from "react-native"; import { RefreshControl, View, VirtualizedList } from "react-native";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@@ -20,68 +20,60 @@ type Props = {
createdAt: string createdAt: string
} }
export default function Announcement() { export default function Announcement() {
const { token, decryptToken } = useAuthSession() const { token, decryptToken } = useAuthSession()
const { colors } = useTheme(); const { colors } = useTheme();
const [data, setData] = useState<Props[]>([])
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const update = useSelector((state: any) => state.announcementUpdate) const update = useSelector((state: any) => state.announcementUpdate)
const [loading, setLoading] = useState(true)
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) 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) { // TanStack Query Infinite Query
try { const {
setWaiting(true) data,
setLoading(loading) fetchNextPage,
setPage(thisPage) hasNextPage,
isFetchingNextPage,
isLoading,
refetch,
isRefetching
} = useInfiniteQuery({
queryKey: ['announcements', search],
queryFn: async ({ pageParam = 1 }) => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetAnnouncement({ user: hasil, search: search, page: thisPage }) const response = await apiGetAnnouncement({
if (thisPage == 1) { user: hasil,
setData(response.data) search: search,
} else if (thisPage > 1 && response.data.length > 0) { page: pageParam
setData([...data, ...response.data]) })
} else { return response.data
return; },
} initialPageParam: 1,
} catch (error) { getNextPageParam: (lastPage, allPages) => {
console.error(error) return lastPage.length > 0 ? allPages.length + 1 : undefined
} finally { },
setLoading(false) })
setWaiting(false)
}
}
// Trigger refetch when Redux state 'update' changes
useEffect(() => { useEffect(() => {
handleLoad(false, 1) refetch()
}, [update]) }, [update, refetch])
useEffect(() => { // Flatten data from pages
handleLoad(true, 1) const flattenedData = useMemo(() => {
}, [search]) return data?.pages.flat() || []
}, [data])
const loadMoreData = () => { const loadMoreData = () => {
if (waiting) return if (hasNextPage && !isFetchingNextPage) {
setTimeout(() => { fetchNextPage()
handleLoad(false, page + 1) }
}, 1000);
};
const handleRefresh = async () => {
setRefreshing(true)
handleLoad(false, 1)
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false)
}; };
const getItem = (_data: unknown, index: number): Props => ({ const getItem = (_data: unknown, index: number): Props => ({
id: data[index].id, id: flattenedData[index].id,
title: data[index].title, title: flattenedData[index].title,
desc: data[index].desc, desc: flattenedData[index].desc,
createdAt: data[index].createdAt, createdAt: flattenedData[index].createdAt,
}) })
return ( return (
@@ -91,18 +83,18 @@ export default function Announcement() {
</View> </View>
<View style={[Styles.flex2, Styles.mt05]}> <View style={[Styles.flex2, Styles.mt05]}>
{ {
loading ? isLoading && !flattenedData.length ?
arrSkeleton.map((item, index) => { arrSkeleton.map((item, index) => {
return ( return (
<SkeletonContent key={index} /> <SkeletonContent key={index} />
) )
}) })
: :
data.length > 0 flattenedData.length > 0
? ?
<VirtualizedList <VirtualizedList
data={data} data={flattenedData}
getItemCount={() => data.length} getItemCount={() => flattenedData.length}
getItem={getItem} getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => { renderItem={({ item, index }: { item: Props, index: number }) => {
return ( return (
@@ -112,9 +104,7 @@ export default function Announcement() {
borderType="bottom" borderType="bottom"
bgColor="transparent" bgColor="transparent"
icon={ icon={
// <View style={[Styles.iconContent]}>
<MaterialIcons name="campaign" size={25} color={colors.text} /> <MaterialIcons name="campaign" size={25} color={colors.text} />
// </View>
} }
title={item.title} title={item.title}
desc={item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')} 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} onEndReached={loadMoreData}
onEndReachedThreshold={0.5} onEndReachedThreshold={0.5}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={refreshing} refreshing={isRefetching && !isFetchingNextPage}
onRefresh={handleRefresh} onRefresh={refetch}
tintColor={colors.icon} tintColor={colors.icon}
/> />
} }

View File

@@ -1,3 +1,4 @@
import styles from "@/components/AppHeader"
import AppHeader from "@/components/AppHeader" import AppHeader from "@/components/AppHeader"
import HeaderRightBannerList from "@/components/banner/headerBannerList" import HeaderRightBannerList from "@/components/banner/headerBannerList"
import BorderBottomItem from "@/components/borderBottomItem" import BorderBottomItem from "@/components/borderBottomItem"
@@ -5,6 +6,7 @@ import DrawerBottom from "@/components/drawerBottom"
import MenuItemRow from "@/components/menuItemRow" import MenuItemRow from "@/components/menuItemRow"
import ModalConfirmation from "@/components/ModalConfirmation" import ModalConfirmation from "@/components/ModalConfirmation"
import ModalLoading from "@/components/modalLoading" import ModalLoading from "@/components/modalLoading"
import Skeleton from "@/components/skeleton"
import Text from "@/components/Text" import Text from "@/components/Text"
import { ConstEnv } from "@/constants/ConstEnv" import { ConstEnv } from "@/constants/ConstEnv"
import Styles from "@/constants/Styles" import Styles from "@/constants/Styles"
@@ -13,11 +15,12 @@ import { setEntities } from "@/lib/bannerSlice"
import { useAuthSession } from "@/providers/AuthProvider" import { useAuthSession } from "@/providers/AuthProvider"
import { useTheme } from "@/providers/ThemeProvider" import { useTheme } from "@/providers/ThemeProvider"
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons" import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import * as FileSystem from 'expo-file-system' import * as FileSystem from 'expo-file-system'
import { startActivityAsync } from 'expo-intent-launcher' import { startActivityAsync } from 'expo-intent-launcher'
import { router, Stack } from "expo-router" import { router, Stack } from "expo-router"
import * as Sharing from 'expo-sharing' 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 { Alert, Image, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
import ImageViewing from 'react-native-image-viewing' import ImageViewing from 'react-native-image-viewing'
import * as mime from 'react-native-mime-types' import * as mime from 'react-native-mime-types'
@@ -43,36 +46,51 @@ export default function BannerList() {
const [loadingOpen, setLoadingOpen] = useState(false) const [loadingOpen, setLoadingOpen] = useState(false)
const [viewImg, setViewImg] = useState(false) const [viewImg, setViewImg] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false)
const queryClient = useQueryClient()
const handleDeleteEntity = async () => { // 1. Fetching logic with useQuery
try { const { data: bannersRes, isLoading } = useQuery({
const hasil = await decryptToken(String(token?.current)); queryKey: ['banners'],
const deletedEntity = await apiDeleteBanner({ user: hasil }, dataId); queryFn: async () => {
if (deletedEntity.success) { const hasil = await decryptToken(String(token?.current))
Toast.show({ type: 'small', text1: 'Berhasil menghapus data', }) const response = await apiGetBanner({ user: hasil })
apiGetBanner({ user: hasil }).then((data) => return response.data || []
dispatch(setEntities(data.data)) },
); enabled: !!token?.current,
} else { staleTime: 0,
Toast.show({ type: 'small', text1: 'Gagal menghapus data', }) })
}
} catch (error: any) {
console.error(error);
const message = error?.response?.data?.message || "Gagal menghapus data"
Toast.show({ type: 'small', text1: message }) // Sync results with Redux
} finally { useEffect(() => {
setModal(false) 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 () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
const hasil = await decryptToken(String(token?.current)); await queryClient.invalidateQueries({ queryKey: ['banners'] })
apiGetBanner({ user: hasil }).then((data) =>
dispatch(setEntities(data.data))
);
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false) setRefreshing(false)
}; };
@@ -140,36 +158,40 @@ export default function BannerList() {
} }
style={[Styles.h100, { backgroundColor: colors.background }]} style={[Styles.h100, { backgroundColor: colors.background }]}
> >
{ <View style={[Styles.p15, Styles.mb100]}>
entities.length > 0 {
? isLoading ? (
<View style={[Styles.p15, Styles.mb100]}> <>
{entities.map((index: any, key: number) => ( <Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
<BorderBottomItem <Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
key={key} <Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
onPress={() => { </>
setDataId(index.id) ) :
setSelectFile(index) entities.length > 0 ?
setModal(true) entities.map((index: any, key: number) => (
}} <BorderBottomItem
borderType="all" key={key}
icon={ onPress={() => {
<Image setDataId(index.id)
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }} setSelectFile(index)
style={[Styles.imgListBanner]} setModal(true)
/> }}
} borderType="all"
title={index.title} icon={
/> <Image
))} source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
</View> style={[Styles.imgListBanner]}
: />
<View style={[Styles.p15, Styles.mb100]}> }
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text> title={index.title}
</View> />
} ))
:
<View style={[Styles.p15, Styles.mb100]}>
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text>
</View>
}
</View>
</ScrollView> </ScrollView>
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu"> <DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu">

View File

@@ -11,8 +11,9 @@ import { apiGetDiscussionGeneral } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather, Ionicons, MaterialIcons } from "@expo/vector-icons"; import { AntDesign, Feather, Ionicons, MaterialIcons } from "@expo/vector-icons";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router"; 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 { RefreshControl, View, VirtualizedList } from "react-native";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@@ -32,70 +33,76 @@ export default function Discussion() {
const { colors } = useTheme(); const { colors } = useTheme();
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>() const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [nameGroup, setNameGroup] = useState('')
const [data, setData] = useState<Props[]>([])
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate) const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
const [loading, setLoading] = useState(true) const queryClient = useQueryClient()
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
const [status, setStatus] = useState<'true' | 'false'>('true')
const [page, setPage] = useState(1)
const [waiting, setWaiting] = useState(false)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
async function handleLoad(loading: boolean, thisPage: number) { // TanStack Query for Discussions with Infinite Scroll
try { const {
setWaiting(true) data,
setLoading(loading) fetchNextPage,
setPage(thisPage) hasNextPage,
isFetchingNextPage,
isLoading,
refetch
} = useInfiniteQuery({
queryKey: ['discussions', { status, search, group }],
queryFn: async ({ pageParam = 1 }) => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetDiscussionGeneral({ user: hasil, active: status, search: search, group: String(group), page: thisPage }) const response = await apiGetDiscussionGeneral({
if (thisPage == 1) { user: hasil,
setData(response.data) active: status,
} else if (thisPage > 1 && response.data.length > 0) { search: search,
setData([...data, ...response.data]) group: String(group),
} else { page: pageParam
return; })
} return response;
setNameGroup(response.filter.name) },
} catch (error) { initialPageParam: 1,
console.error(error) getNextPageParam: (lastPage, allPages) => {
} finally { return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
setLoading(false) },
setWaiting(false) 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(() => { useEffect(() => {
handleLoad(false, 1) refetch()
}, [update]) }, [update, refetch])
useEffect(() => {
handleLoad(true, 1)
}, [status, search, group])
const loadMoreData = () => {
if (waiting) return
setTimeout(() => {
handleLoad(false, page + 1)
}, 1000);
};
const handleRefresh = async () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
handleLoad(false, 1) await queryClient.invalidateQueries({ queryKey: ['discussions'] })
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false) setRefreshing(false)
}; };
const loadMoreData = () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
};
const arrSkeleton = [0, 1, 2, 3, 4]
const getItem = (_data: unknown, index: number): Props => ({ const getItem = (_data: unknown, index: number): Props => ({
id: data[index].id, id: flatData[index]?.id,
title: data[index].title, title: flatData[index]?.title,
desc: data[index].desc, desc: flatData[index]?.desc,
status: data[index].status, status: flatData[index]?.status,
total_komentar: data[index].total_komentar, total_komentar: flatData[index]?.total_komentar,
createdAt: data[index].createdAt, createdAt: flatData[index]?.createdAt,
}) })
return ( return (
@@ -132,18 +139,18 @@ export default function Discussion() {
</View> </View>
<View style={[Styles.flex2, Styles.mt05]}> <View style={[Styles.flex2, Styles.mt05]}>
{ {
loading ? isLoading ?
arrSkeleton.map((item: any, i: number) => { arrSkeleton.map((item: any, i: number) => {
return ( return (
<SkeletonContent key={i} /> <SkeletonContent key={i} />
) )
}) })
: :
data.length > 0 flatData.length > 0
? ?
<VirtualizedList <VirtualizedList
data={data} data={flatData}
getItemCount={() => data.length} getItemCount={() => flatData.length}
getItem={getItem} getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => { renderItem={({ item, index }: { item: Props, index: number }) => {
return ( return (
@@ -153,16 +160,14 @@ export default function Discussion() {
onPress={() => { router.push(`/discussion/${item.id}`) }} onPress={() => { router.push(`/discussion/${item.id}`) }}
borderType="bottom" borderType="bottom"
icon={ icon={
// <View style={[Styles.iconContent]}>
<MaterialIcons name="chat" size={25} color={colors.text} /> <MaterialIcons name="chat" size={25} color={colors.text} />
// </View>
} }
title={item.title} title={item.title}
subtitle={ subtitle={
status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" /> status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
} }
rightTopInfo={item.createdAt} rightTopInfo={item.createdAt}
desc={item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')} desc={item.desc?.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
leftBottomInfo={ leftBottomInfo={
<View style={[Styles.rowItemsCenter]}> <View style={[Styles.rowItemsCenter]}>
<Ionicons name="chatbox-ellipses-outline" size={18} color={colors.dimmed} style={Styles.mr05} /> <Ionicons name="chatbox-ellipses-outline" size={18} color={colors.dimmed} style={Styles.mr05} />

View File

@@ -17,8 +17,9 @@ import {
Ionicons, Ionicons,
MaterialCommunityIcons MaterialCommunityIcons
} from "@expo/vector-icons"; } from "@expo/vector-icons";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router"; 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 { Pressable, RefreshControl, View, VirtualizedList } from "react-native";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@@ -40,23 +41,23 @@ export default function ListDivision() {
const { token, decryptToken } = useAuthSession() const { token, decryptToken } = useAuthSession()
const { colors } = useTheme(); const { colors } = useTheme();
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [nameGroup, setNameGroup] = useState("") const queryClient = useQueryClient()
const [data, setData] = useState<Props[]>([]) const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
// ... state same ... const [category, setCategory] = useState<'divisi-saya' | 'semua'>(cat == 'semua' ? 'semua' : 'divisi-saya')
const update = useSelector((state: any) => state.divisionUpdate) 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) const [refreshing, setRefreshing] = useState(false)
async function handleLoad(loading: boolean, thisPage: number) { // TanStack Query for Divisions with Infinite Scroll
try { const {
setWaiting(true) data,
setLoading(loading) fetchNextPage,
setPage(thisPage) hasNextPage,
isFetchingNextPage,
isLoading,
refetch
} = useInfiniteQuery({
queryKey: ['divisions', { status, search, group, category }],
queryFn: async ({ pageParam = 1 }) => {
const hasil = await decryptToken(String(token?.current)); const hasil = await decryptToken(String(token?.current));
const response = await apiGetDivision({ const response = await apiGetDivision({
user: hasil, user: hasil,
@@ -64,54 +65,52 @@ export default function ListDivision() {
search: search, search: search,
group: String(group), group: String(group),
kategori: category, 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) { // Refetch when manual update state changes
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)
}
}
useEffect(() => { useEffect(() => {
handleLoad(false, 1); refetch()
}, [update]); }, [update, refetch])
useEffect(() => { // Flatten pages into a single data array
handleLoad(true, 1); const flatData = useMemo(() => {
}, [status, search, group, category]); return data?.pages.flatMap(page => page.data) || [];
}, [data])
const loadMoreData = () => { // Get nameGroup from the first available page
if (waiting) return const nameGroup = useMemo(() => {
setTimeout(() => { return data?.pages[0]?.filter?.name || "";
handleLoad(false, page + 1) }, [data])
}, 1000);
};
const handleRefresh = async () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
handleLoad(false, 1) await queryClient.invalidateQueries({ queryKey: ['divisions'] })
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false) setRefreshing(false)
}; };
const loadMoreData = () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
};
const arrSkeleton = [0, 1, 2]
const getItem = (_data: unknown, index: number): Props => ({ const getItem = (_data: unknown, index: number): Props => ({
id: data[index].id, id: flatData[index]?.id,
name: data[index].name, name: flatData[index]?.name,
desc: data[index].desc, desc: flatData[index]?.desc,
jumlah_member: data[index].jumlah_member, jumlah_member: flatData[index]?.jumlah_member,
}) })
@@ -206,7 +205,7 @@ export default function ListDivision() {
</View> </View>
<View style={[{ flex: 2 }, Styles.mt10]}> <View style={[{ flex: 2 }, Styles.mt10]}>
{ {
loading ? isLoading ?
isList ? isList ?
arrSkeleton.map((item, index) => ( arrSkeleton.map((item, index) => (
<SkeletonTwoItem key={index} /> <SkeletonTwoItem key={index} />
@@ -216,7 +215,7 @@ export default function ListDivision() {
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} /> <Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
)) ))
: :
data.length == 0 ? ( flatData.length == 0 ? (
<View style={[Styles.mt15]}> <View style={[Styles.mt15]}>
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada data</Text> <Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada data</Text>
</View> </View>
@@ -224,9 +223,9 @@ export default function ListDivision() {
isList ? ( isList ? (
<View style={[Styles.h100]}> <View style={[Styles.h100]}>
<VirtualizedList <VirtualizedList
data={data} data={flatData}
style={[{ paddingBottom: 100 }]} style={[{ paddingBottom: 100 }]}
getItemCount={() => data.length} getItemCount={() => flatData.length}
getItem={getItem} getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => { renderItem={({ item, index }: { item: Props, index: number }) => {
return ( return (
@@ -260,9 +259,9 @@ export default function ListDivision() {
) : ( ) : (
<View style={[Styles.h100]}> <View style={[Styles.h100]}>
<VirtualizedList <VirtualizedList
data={data} data={flatData}
style={[{ paddingBottom: 100 }]} style={[{ paddingBottom: 100 }]}
getItemCount={() => data.length} getItemCount={() => flatData.length}
getItem={getItem} getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => { renderItem={({ item, index }: { item: Props, index: number }) => {
return ( return (

View File

@@ -15,7 +15,8 @@ import { setUpdateGroup } from "@/lib/groupSlice";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { AntDesign, Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
import { RefreshControl, View, VirtualizedList } from "react-native"; import { RefreshControl, View, VirtualizedList } from "react-native";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@@ -31,16 +32,14 @@ export default function Index() {
const { colors } = useTheme(); const { colors } = useTheme();
const [isModal, setModal] = useState(false) const [isModal, setModal] = useState(false)
const [isVisibleEdit, setVisibleEdit] = useState(false) const [isVisibleEdit, setVisibleEdit] = useState(false)
const [data, setData] = useState<Props[]>([])
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
const [loading, setLoading] = useState(true)
const [showDeleteModal, setShowDeleteModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false)
const [status, setStatus] = useState<'true' | 'false'>('true') const [status, setStatus] = useState<'true' | 'false'>('true')
const [loadingSubmit, setLoadingSubmit] = useState(false) const [loadingSubmit, setLoadingSubmit] = useState(false)
const [idChoose, setIdChoose] = useState('') const [idChoose, setIdChoose] = useState('')
const [activeChoose, setActiveChoose] = useState(true) const [activeChoose, setActiveChoose] = useState(true)
const [titleChoose, setTitleChoose] = useState('') const [titleChoose, setTitleChoose] = useState('')
const queryClient = useQueryClient()
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const dispatch = useDispatch() const dispatch = useDispatch()
@@ -49,12 +48,38 @@ export default function Index() {
title: false, 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() { async function handleEdit() {
try { try {
setLoadingSubmit(true) setLoadingSubmit(true)
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose) const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose)
await queryClient.invalidateQueries({ queryKey: ['groups'] })
dispatch(setUpdateGroup(!update)) dispatch(setUpdateGroup(!update))
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -71,6 +96,7 @@ export default function Index() {
try { try {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiDeleteGroup({ user: hasil, isActive: activeChoose }, idChoose) const response = await apiDeleteGroup({ user: hasil, isActive: activeChoose }, idChoose)
await queryClient.invalidateQueries({ queryKey: ['groups'] })
dispatch(setUpdateGroup(!update)) dispatch(setUpdateGroup(!update))
} catch (error) { } catch (error) {
console.error(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 () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
handleLoad(false) await queryClient.invalidateQueries({ queryKey: ['groups'] })
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false) setRefreshing(false)
}; };
@@ -129,6 +132,8 @@ export default function Index() {
const arrSkeleton = [0, 1, 2, 3, 4]
return ( return (
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}> <View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
<View style={[Styles.mb10]}> <View style={[Styles.mb10]}>
@@ -152,7 +157,7 @@ export default function Index() {
</View> </View>
<View style={[{ flex: 2 }, Styles.mt10]}> <View style={[{ flex: 2 }, Styles.mt10]}>
{ {
loading ? isLoading ?
arrSkeleton.map((item, index) => { arrSkeleton.map((item, index) => {
return ( return (
<SkeletonTwoItem key={index} /> <SkeletonTwoItem key={index} />

View File

@@ -12,10 +12,11 @@ import { apiGetProfile } from "@/lib/api";
import { setEntities } from "@/lib/entitiesSlice"; import { setEntities } from "@/lib/entitiesSlice";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Dimensions, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"; import { Alert, Dimensions, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@@ -23,28 +24,66 @@ import { useDispatch, useSelector } from "react-redux";
export default function Home() { export default function Home() {
const entities = useSelector((state: any) => state.entities) const entities = useSelector((state: any) => state.entities)
const dispatch = useDispatch() const dispatch = useDispatch()
const queryClient = useQueryClient()
const { token, decryptToken, signOut } = useAuthSession() const { token, decryptToken, signOut } = useAuthSession()
const { colors } = useTheme(); const { colors } = useTheme();
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
useEffect(() => { const { data: profile, isError } = useQuery({
handleUserLogin() queryKey: ['profile'],
}, [dispatch]); 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() { // Sync to Redux for global usage
const hasil = await decryptToken(String(token?.current)) useEffect(() => {
apiGetProfile({ id: hasil }) if (profile) {
.then((data) => dispatch(setEntities(data.data))) dispatch(setEntities(profile))
.catch((error) => { }
signOut() }, [profile, dispatch])
});
} // Auto Sign Out if profile fetch fails (e.g. invalid/expired token)
useEffect(() => {
if (isError) {
signOut()
}
}, [isError, signOut])
useEffect(() => {
if (profile && profile.isActive === false) {
Alert.alert(
'Akun Dinonaktifkan',
'Akun kamu telah dinonaktifkan. Silahkan hubungi admin untuk informasi lebih lanjut.',
[{ text: 'OK', onPress: signOut }]
)
}
}, [profile, signOut])
useEffect(() => {
if (profile && profile.villageIsActive === false) {
Alert.alert(
'Desa Dinonaktifkan',
'Desa kamu saat ini telah dinonaktifkan. Silahkan hubungi pengelola sistem untuk informasi lebih lanjut.',
[{ text: 'OK', onPress: signOut }]
)
}
}, [profile, signOut])
const handleRefresh = async () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
handleUserLogin() // Invalidate all queries related to the home screen
await new Promise(resolve => setTimeout(resolve, 2000)); 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) setRefreshing(false)
}; };

View File

@@ -12,8 +12,9 @@ import { apiGetUser } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather } from "@expo/vector-icons"; import { AntDesign, Feather } from "@expo/vector-icons";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router"; 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 { RefreshControl, View, VirtualizedList } from "react-native";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@@ -37,73 +38,81 @@ export default function Index() {
const entityUser = useSelector((state: any) => state.user) const entityUser = useSelector((state: any) => state.user)
const { colors } = useTheme(); const { colors } = useTheme();
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [nameGroup, setNameGroup] = useState('')
const [data, setData] = useState<Props[]>([])
const update = useSelector((state: any) => state.memberUpdate) const update = useSelector((state: any) => state.memberUpdate)
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) const queryClient = useQueryClient()
const [loading, setLoading] = useState(true) const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
const [status, setStatus] = useState<'true' | 'false'>('true')
const [page, setPage] = useState(1)
const [waiting, setWaiting] = useState(false)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
async function handleLoad(loading: boolean, thisPage: number) { // TanStack Query for Members with Infinite Scroll
try { const {
setWaiting(true) data,
setLoading(loading) fetchNextPage,
setPage(thisPage) hasNextPage,
isFetchingNextPage,
isLoading,
refetch
} = useInfiniteQuery({
queryKey: ['members', { status, search, group }],
queryFn: async ({ pageParam = 1 }) => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetUser({ user: hasil, active: status, search, group: String(group), page: thisPage }) const response = await apiGetUser({
if (thisPage == 1) { user: hasil,
setData(response.data) active: status,
} else if (thisPage > 1 && response.data.length > 0) { search,
setData([...data, ...response.data]) group: String(group),
} else { page: pageParam
return; })
} return response;
setNameGroup(response.filter.name) },
} catch (error) { initialPageParam: 1,
console.error(error) getNextPageParam: (lastPage, allPages) => {
} finally { return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
setLoading(false) },
setWaiting(false) enabled: !!token?.current,
} staleTime: 0,
} })
const loadMoreData = () => { // Flatten pages into a single data array
if (waiting) return const flatData = useMemo(() => {
setTimeout(() => { return data?.pages.flatMap(page => page.data) || [];
handleLoad(false, page + 1) }, [data])
}, 1000);
};
// Get nameGroup from the first available page
const nameGroup = useMemo(() => {
return data?.pages[0]?.filter?.name || "";
}, [data])
// Refetch when manual update state changes
useEffect(() => { useEffect(() => {
handleLoad(false, 1) refetch()
}, [update]) }, [update, refetch])
useEffect(() => {
handleLoad(true, 1)
}, [group, search, status])
const handleRefresh = async () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
handleLoad(false, 1) await queryClient.invalidateQueries({ queryKey: ['members'] })
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false) setRefreshing(false)
}; };
const loadMoreData = () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
};
const arrSkeleton = [0, 1, 2, 3, 4]
const getItem = (_data: unknown, index: number): Props => ({ const getItem = (_data: unknown, index: number): Props => ({
id: data[index].id, id: flatData[index]?.id,
name: data[index].name, name: flatData[index]?.name,
nik: data[index].nik, nik: flatData[index]?.nik,
email: data[index].email, email: flatData[index]?.email,
phone: data[index].phone, phone: flatData[index]?.phone,
gender: data[index].gender, gender: flatData[index]?.gender,
position: data[index].position, position: flatData[index]?.position,
group: data[index].group, group: flatData[index]?.group,
img: data[index].img, img: flatData[index]?.img,
isActive: data[index].isActive, isActive: flatData[index]?.isActive,
role: data[index].role, role: flatData[index]?.role,
}); });
return ( return (
@@ -136,18 +145,18 @@ export default function Index() {
</View> </View>
<View style={[{ flex: 2 }, Styles.mt10]}> <View style={[{ flex: 2 }, Styles.mt10]}>
{ {
loading ? isLoading ?
arrSkeleton.map((item, index) => { arrSkeleton.map((item, index) => {
return ( return (
<SkeletonTwoItem key={index} /> <SkeletonTwoItem key={index} />
) )
}) })
: :
data.length > 0 flatData.length > 0
? ?
<VirtualizedList <VirtualizedList
data={data} data={flatData}
getItemCount={() => data.length} getItemCount={() => flatData.length}
getItem={getItem} getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => { renderItem={({ item, index }: { item: Props, index: number }) => {
return ( return (

View File

@@ -1,4 +1,3 @@
import BorderBottomItem from "@/components/borderBottomItem";
import BorderBottomItemVertical from "@/components/borderBottomItemVertical"; import BorderBottomItemVertical from "@/components/borderBottomItemVertical";
import SkeletonTwoItem from "@/components/skeletonTwoItem"; import SkeletonTwoItem from "@/components/skeletonTwoItem";
import Text from "@/components/Text"; import Text from "@/components/Text";
@@ -10,7 +9,8 @@ import { pushToPage } from "@/lib/pushToPage";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { Feather } from "@expo/vector-icons"; 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 { RefreshControl, SafeAreaView, View, VirtualizedList } from "react-native";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@@ -27,64 +27,61 @@ type Props = {
export default function Notification() { export default function Notification() {
const { token, decryptToken } = useAuthSession() const { token, decryptToken } = useAuthSession()
const { colors } = useTheme(); const { colors } = useTheme();
const [loading, setLoading] = useState(false) const queryClient = useQueryClient()
const [data, setData] = useState<Props[]>([])
const [page, setPage] = useState(1)
const [waiting, setWaiting] = useState(false)
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
const dispatch = useDispatch() const dispatch = useDispatch()
const updateNotification = useSelector((state: any) => state.notificationUpdate) const updateNotification = useSelector((state: any) => state.notificationUpdate)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
async function handleLoad(loading: boolean, thisPage: number) { // TanStack Query for Notifications with Infinite Scroll
try { const {
setLoading(loading) data,
setPage(thisPage) fetchNextPage,
setWaiting(true) hasNextPage,
isFetchingNextPage,
isLoading,
refetch
} = useInfiniteQuery({
queryKey: ['notifications'],
queryFn: async ({ pageParam = 1 }) => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetNotification({ user: hasil, page: thisPage }) const response = await apiGetNotification({ user: hasil, page: pageParam })
if (thisPage == 1) { return response;
setData(response.data) },
} else if (thisPage > 1 && response.data.length > 0) { initialPageParam: 1,
setData([...data, ...response.data]) getNextPageParam: (lastPage, allPages) => {
} else { return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
return; },
} enabled: !!token?.current,
} catch (error) { staleTime: 0,
console.error(error) })
} finally {
setLoading(false)
setWaiting(false)
}
}
// Flatten pages into a single data array
const flatData = useMemo(() => {
return data?.pages.flatMap(page => page.data) || [];
}, [data])
const loadMoreData = () => { // Refetch when manual update state changes
if (waiting) return useEffect(() => {
setTimeout(() => { refetch()
handleLoad(false, page + 1) }, [updateNotification, refetch])
}, 1000);
const handleRefresh = async () => {
setRefreshing(true)
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
setRefreshing(false)
}; };
useEffect(() => { const loadMoreData = () => {
handleLoad(true, 1) if (hasNextPage && !isFetchingNextPage) {
}, []) fetchNextPage()
}
};
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,
});
async function handleReadNotification(id: string, category: string, idContent: string) { async function handleReadNotification(id: string, category: string, idContent: string) {
try { try {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiReadOneNotification({ user: hasil, id: id }) const response = await apiReadOneNotification({ user: hasil, id: id })
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
pushToPage(category, idContent) pushToPage(category, idContent)
dispatch(setUpdateNotification(!updateNotification)) dispatch(setUpdateNotification(!updateNotification))
} catch (error) { } catch (error) {
@@ -92,28 +89,33 @@ export default function Notification() {
} }
} }
const handleRefresh = async () => { const arrSkeleton = [0, 1, 2, 3, 4]
setRefreshing(true)
handleLoad(false, 1) const getItem = (_data: unknown, index: number): Props => ({
await new Promise(resolve => setTimeout(resolve, 2000)); id: flatData[index]?.id,
setRefreshing(false) 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 ( return (
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}> <SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
<View style={[Styles.p15]}> <View style={[Styles.p15]}>
{ {
loading ? isLoading ?
arrSkeleton.map((item, index) => { arrSkeleton.map((item, index) => {
return ( return (
<SkeletonTwoItem key={index} /> <SkeletonTwoItem key={index} />
) )
}) })
: :
data.length > 0 ? flatData.length > 0 ?
<VirtualizedList <VirtualizedList
data={data} data={flatData}
getItemCount={() => data.length} getItemCount={() => flatData.length}
getItem={getItem} getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => { renderItem={({ item, index }: { item: Props, index: number }) => {
return ( return (

View File

@@ -16,8 +16,9 @@ import { setUpdatePosition } from "@/lib/positionSlice";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons"; import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { RefreshControl, View, VirtualizedList } from "react-native"; import { RefreshControl, View, VirtualizedList } from "react-native";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@@ -31,51 +32,53 @@ type Props = {
} }
export default function Index() { export default function Index() {
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
const [loading, setLoading] = useState(true)
const { token, decryptToken } = useAuthSession() const { token, decryptToken } = useAuthSession()
const { colors } = useTheme() 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 { 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 [isModal, setModal] = useState(false)
const [isVisibleEdit, setVisibleEdit] = useState(false) const [isVisibleEdit, setVisibleEdit] = useState(false)
const [data, setData] = useState<Props[]>([])
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [nameGroup, setNameGroup] = useState('')
const [loadingSubmit, setLoadingSubmit] = useState(false) const [loadingSubmit, setLoadingSubmit] = useState(false)
const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' }) const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' })
const [error, setError] = useState({ const [error, setError] = useState({
name: false, name: false,
}); });
const queryClient = useQueryClient()
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false)
const dispatch = useDispatch() const dispatch = useDispatch()
const update = useSelector((state: any) => state.positionUpdate) const update = useSelector((state: any) => state.positionUpdate)
async function handleLoad(loading: boolean) { // TanStack Query for Positions
try { const {
setLoading(loading) data: queryData,
isLoading,
refetch
} = useQuery({
queryKey: ['positions', { status, search, group }],
queryFn: async () => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetPosition({ user: hasil, active: status, search: search, group: String(group) }) const response = await apiGetPosition({
setData(response.data) user: hasil,
setNameGroup(response.filter.name) active: status,
} catch (error) { search: search,
console.error(error) group: String(group)
} finally { })
setLoading(false) return response;
} },
} enabled: !!token?.current,
staleTime: 0,
})
const data = useMemo(() => queryData?.data || [], [queryData])
const nameGroup = useMemo(() => queryData?.filter?.name || "", [queryData])
useEffect(() => { useEffect(() => {
handleLoad(false) refetch()
}, [update]) }, [update, refetch])
useEffect(() => {
handleLoad(true)
}, [status, search, group])
function handleChooseData(id: string, name: string, active: boolean, group: string) { 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 hasil = await decryptToken(String(token?.current))
const response = await apiDeletePosition({ user: hasil, isActive: chooseData.active }, chooseData.id) const response = await apiDeletePosition({ user: hasil, isActive: chooseData.active }, chooseData.id)
dispatch(setUpdatePosition(!update)) dispatch(setUpdatePosition(!update))
} catch (error : any ) { } catch (error: any) {
console.error(error); console.error(error);
const message = error?.response?.data?.message || "Gagal menghapus data" const message = error?.response?.data?.message || "Gagal menghapus data"
@@ -110,7 +113,7 @@ export default function Index() {
} else { } else {
Toast.show({ type: 'small', text1: response.message, }) Toast.show({ type: 'small', text1: response.message, })
} }
} catch (error : any ) { } catch (error: any) {
console.error(error); console.error(error);
const message = error?.response?.data?.message || "Gagal mengubah data" const message = error?.response?.data?.message || "Gagal mengubah data"
@@ -138,10 +141,11 @@ export default function Index() {
handleEdit() handleEdit()
} }
const arrSkeleton = [0, 1, 2, 3, 4]
const handleRefresh = async () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
handleLoad(false) await queryClient.invalidateQueries({ queryKey: ['positions'] })
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false) setRefreshing(false)
}; };
@@ -184,7 +188,7 @@ export default function Index() {
</View> </View>
<View style={[Styles.flex2, Styles.mt10]}> <View style={[Styles.flex2, Styles.mt10]}>
{ {
loading ? isLoading ?
arrSkeleton.map((item, index) => { arrSkeleton.map((item, index) => {
return ( return (
<SkeletonTwoItem key={index} /> <SkeletonTwoItem key={index} />

View File

@@ -18,8 +18,9 @@ import {
Ionicons, Ionicons,
MaterialCommunityIcons, MaterialCommunityIcons,
} from "@expo/vector-icons"; } from "@expo/vector-icons";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router"; 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 { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@@ -40,28 +41,29 @@ export default function ListProject() {
cat?: string; cat?: string;
year?: 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 { token, decryptToken } = useAuthSession();
const { colors } = useTheme(); const { colors } = useTheme();
const entityUser = useSelector((state: any) => state.user) const entityUser = useSelector((state: any) => state.user)
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [nameGroup, setNameGroup] = useState("")
// ... state same ...
const [isYear, setYear] = useState("")
const [data, setData] = useState<Props[]>([])
const [isList, setList] = useState(false) const [isList, setList] = useState(false)
const update = useSelector((state: any) => state.projectUpdate) const update = useSelector((state: any) => state.projectUpdate)
const [loading, setLoading] = useState(true) const queryClient = useQueryClient()
const arrSkeleton = Array.from({ length: 3 }, (_, index) => index)
const [page, setPage] = useState(1)
const [waiting, setWaiting] = useState(false)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
async function handleLoad(loading: boolean, thisPage: number) { // TanStack Query for Projects with Infinite Scroll
try { const {
setLoading(loading) data,
setWaiting(true) fetchNextPage,
setPage(thisPage) hasNextPage,
isFetchingNextPage,
isLoading,
refetch
} = useInfiniteQuery({
queryKey: ['projects', { statusFix, search, group, cat, year }],
queryFn: async ({ pageParam = 1 }) => {
const hasil = await decryptToken(String(token?.current)); const hasil = await decryptToken(String(token?.current));
const response = await apiGetProject({ const response = await apiGetProject({
user: hasil, user: hasil,
@@ -69,60 +71,55 @@ export default function ListProject() {
search: search, search: search,
group: String(group), group: String(group),
kategori: String(cat), kategori: String(cat),
page: thisPage, page: pageParam,
year: String(year) 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) { // Refetch when manual update state changes
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)
}
}
useEffect(() => { useEffect(() => {
handleLoad(false, 1); refetch()
}, [update.data]); }, [update.data, refetch])
// Flatten pages into a single data array
const flatData = useMemo(() => {
return data?.pages.flatMap(page => page.data) || [];
}, [data])
useEffect(() => { // Get metadata from the first available page
handleLoad(true, 1); const nameGroup = useMemo(() => data?.pages[0]?.filter?.name || "", [data])
}, [statusFix, search, group, cat, year]); const isYear = useMemo(() => data?.pages[0]?.tahun || "", [data])
const loadMoreData = () => {
if (waiting) return
setTimeout(() => {
handleLoad(false, page + 1)
}, 1000);
}
const handleRefresh = async () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
handleLoad(false, 1) await queryClient.invalidateQueries({ queryKey: ['projects'] })
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false) setRefreshing(false)
} };
const loadMoreData = () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
};
const arrSkeleton = [0, 1, 2]
const getItem = (_data: unknown, index: number): Props => ({ const getItem = (_data: unknown, index: number): Props => ({
id: data[index].id, id: flatData[index]?.id,
title: data[index].title, title: flatData[index]?.title,
desc: data[index].desc, desc: flatData[index]?.desc,
status: data[index].status, status: flatData[index]?.status,
member: data[index].member, member: flatData[index]?.member,
progress: data[index].progress, progress: flatData[index]?.progress,
createdAt: data[index].createdAt, createdAt: flatData[index]?.createdAt,
}) })
return ( return (
@@ -205,7 +202,6 @@ export default function ListProject() {
</View> </View>
<View style={[Styles.mt10]}> <View style={[Styles.mt10]}>
{ {
// entityUser.role != 'cosupadmin' && entityUser.role != 'admin' &&
<View style={[Styles.rowOnly]}> <View style={[Styles.rowOnly]}>
<Text style={[Styles.mr05]}>Filter :</Text> <Text style={[Styles.mr05]}>Filter :</Text>
{ {
@@ -218,18 +214,13 @@ export default function ListProject() {
: '' : ''
} }
<LabelStatus size="small" category="secondary" text={isYear} style={[Styles.mr05]} /> <LabelStatus size="small" category="secondary" text={isYear} style={[Styles.mr05]} />
{/* {
(entityUser.role == 'user' || entityUser.role == 'coadmin')
? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? <LabelStatus size="small" category="primary" text="Kegiatan Saya" /> : <LabelStatus size="small" category="primary" text="Semua Kegiatan" />
: ''
} */}
</View> </View>
} }
</View> </View>
</View> </View>
<View style={[Styles.flex2, Styles.mt10]}> <View style={[Styles.flex2, Styles.mt10]}>
{ {
loading ? isLoading ?
isList ? isList ?
arrSkeleton.map((item, index) => ( arrSkeleton.map((item, index) => (
<SkeletonTwoItem key={index} /> <SkeletonTwoItem key={index} />
@@ -239,13 +230,13 @@ export default function ListProject() {
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} /> <Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
)) ))
: :
data.length > 0 flatData.length > 0
? ?
isList ? ( isList ? (
<View style={[Styles.h100]}> <View style={[Styles.h100]}>
<VirtualizedList <VirtualizedList
data={data} data={flatData}
getItemCount={() => data.length} getItemCount={() => flatData.length}
getItem={getItem} getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => { renderItem={({ item, index }: { item: Props, index: number }) => {
return ( return (
@@ -279,35 +270,12 @@ export default function ListProject() {
/> />
} }
/> />
{/* {
data.map((item, index) => {
return (
<BorderBottomItem
key={index}
onPress={() => { router.push(`/project/${item.id}`); }}
borderType="bottom"
icon={
<View
style={[Styles.iconContent, ColorsStatus.lightGreen]}
>
<AntDesign
name="areachart"
size={25}
color={"#384288"}
/>
</View>
}
title={item.title}
/>
);
})
} */}
</View> </View>
) : ( ) : (
<View style={[Styles.h100]}> <View style={[Styles.h100]}>
<VirtualizedList <VirtualizedList
data={data} data={flatData}
getItemCount={() => data.length} getItemCount={() => flatData.length}
getItem={getItem} getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => { renderItem={({ item, index }: { item: Props, index: number }) => {
return ( return (
@@ -359,43 +327,6 @@ export default function ListProject() {
/> />
} }
/> />
{/* {data.map((item, index) => {
return (
<PaperGridContent
key={index}
onPress={() => {
router.push(`/project/${item.id}`);
}}
content="page"
title={item.title}
headerColor="primary"
>
<ProgressBar value={item.progress} category="list" />
<View style={[Styles.rowSpaceBetween]}>
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>
{item.createdAt}
</Text>
<LabelStatus
size="default"
category={
item.status === 0 ? 'primary' :
item.status === 1 ? 'warning' :
item.status === 2 ? 'success' :
item.status === 3 ? 'error' :
'primary'
}
text={
item.status === 0 ? 'SEGERA' :
item.status === 1 ? 'DIKERJAKAN' :
item.status === 2 ? 'SELESAI' :
item.status === 3 ? 'DIBATALKAN' :
'SEGERA'
}
/>
</View>
</PaperGridContent>
);
})} */}
</View> </View>
) )
: :

View File

@@ -9,6 +9,7 @@ import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { Feather, Ionicons } from "@expo/vector-icons"; import { Feather, Ionicons } from "@expo/vector-icons";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import Constants from "expo-constants";
import { router } from "expo-router"; import { router } from "expo-router";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { AppState, AppStateStatus, Pressable, View } from "react-native"; import { AppState, AppStateStatus, Pressable, View } from "react-native";
@@ -33,6 +34,7 @@ export default function ListSetting() {
const registerToken = async () => { const registerToken = async () => {
try { try {
await AsyncStorage.setItem('@notification_permission', "true");
const token = await getToken(); const token = await getToken();
if (token) { if (token) {
await apiRegisteredToken({ user: entities.id, token, category: "register" }); await apiRegisteredToken({ user: entities.id, token, category: "register" });
@@ -44,9 +46,10 @@ export default function ListSetting() {
const unregisterToken = async () => { const unregisterToken = async () => {
try { try {
await AsyncStorage.setItem('@notification_permission', "false");
const token = await getToken(); const token = await getToken();
if (token) { if (token) {
await apiUnregisteredToken({ user: entities.id, token }); await apiUnregisteredToken({ user: entities.id, token, category: "unregister" });
} }
} catch (error) { } catch (error) {
console.warn('Error unregistering token:', error); console.warn('Error unregistering token:', error);
@@ -113,10 +116,8 @@ export default function ListSetting() {
// OS Permission is granted, perform in-app toggle // OS Permission is granted, perform in-app toggle
const targetState = !isNotificationEnabled; const targetState = !isNotificationEnabled;
if (targetState) { if (targetState) {
await AsyncStorage.setItem('@notification_permission', "true");
await registerToken(); await registerToken();
} else { } else {
await AsyncStorage.setItem('@notification_permission', "false");
await unregisterToken(); await unregisterToken();
} }
// UI will be updated by checkNotif (triggered by state change or manually here) // UI will be updated by checkNotif (triggered by state change or manually here)
@@ -196,6 +197,10 @@ export default function ListSetting() {
<ThemeOption label="Gelap" value="dark" icon="moon-outline" /> <ThemeOption label="Gelap" value="dark" icon="moon-outline" />
<ThemeOption label="Sistem" value="system" icon="phone-portrait-outline" /> <ThemeOption label="Sistem" value="system" icon="phone-portrait-outline" />
</DrawerBottom> </DrawerBottom>
<Text style={{ color: colors.icon, textAlign: 'center', marginTop: 'auto', fontSize: 12 }}>
Versi {Constants.expoConfig?.version}
</Text>
</View> </View>
) )
} }

View File

@@ -1,10 +1,14 @@
import AuthProvider from '@/providers/AuthProvider'; import AuthProvider from '@/providers/AuthProvider';
import ThemeProvider, { useTheme } from '@/providers/ThemeProvider'; import ThemeProvider, { useTheme } from '@/providers/ThemeProvider';
import QueryProvider from '@/providers/QueryProvider';
import ErrorBoundary from '@/components/ErrorBoundary';
import { flushErrorQueue } from '@/lib/errorLogger';
import { useFonts } from 'expo-font'; import { useFonts } from 'expo-font';
import { Stack } from 'expo-router'; import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen'; import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { AppState } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { NotifierWrapper } from 'react-native-notifier'; import { NotifierWrapper } from 'react-native-notifier';
import 'react-native-reanimated'; import 'react-native-reanimated';
@@ -21,7 +25,6 @@ function AppStack() {
<> <>
<Stack screenOptions={{ contentStyle: { backgroundColor: colors.header } }}> <Stack screenOptions={{ contentStyle: { backgroundColor: colors.header } }}>
<Stack.Screen name="index" options={{ headerShown: false }} /> <Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="verification" options={{ headerShown: false }} />
<Stack.Screen name="(application)" options={{ headerShown: false }} /> <Stack.Screen name="(application)" options={{ headerShown: false }} />
</Stack> </Stack>
<StatusBar style="auto" /> <StatusBar style="auto" />
@@ -37,22 +40,34 @@ export default function RootLayout() {
useEffect(() => { useEffect(() => {
if (loaded) { if (loaded) {
SplashScreen.hideAsync(); SplashScreen.hideAsync();
flushErrorQueue();
} }
}, [loaded]); }, [loaded]);
useEffect(() => {
const sub = AppState.addEventListener('change', (state) => {
if (state === 'active') flushErrorQueue();
});
return () => sub.remove();
}, []);
if (!loaded) { if (!loaded) {
return null; return null;
} }
return ( return (
<GestureHandlerRootView style={Styles.flex1}> <GestureHandlerRootView style={Styles.flex1}>
<NotifierWrapper> <ErrorBoundary>
<ThemeProvider> <NotifierWrapper>
<AuthProvider> <ThemeProvider>
<AppStack /> <QueryProvider>
</AuthProvider> <AuthProvider>
</ThemeProvider> <AppStack />
</NotifierWrapper> </AuthProvider>
</QueryProvider>
</ThemeProvider>
</NotifierWrapper>
</ErrorBoundary>
</GestureHandlerRootView> </GestureHandlerRootView>
); );
} }

View File

@@ -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 (
<View style={[Styles.wrapLogin, { backgroundColor: colors.background }]} >
<View style={[Styles.rowItemsCenter, Styles.mv50]}>
<Image
source={require("../assets/images/logo.png")}
style={[{ width: 300, height: 150 }]}
width={270}
height={110}
/>
{/* <Text style={[Styles.textSubtitle]}>PERBEKEL DARMASABA</Text> */}
</View>
<View style={[Styles.mb30]}>
<Text style={[Styles.textDefaultSemiBold]}>Verifikasi Nomor Telepon</Text>
<Text style={[Styles.textMediumNormal]}>Masukkan kode yang kami kirimkan melalui WhatsApp</Text>
<Text style={[Styles.textMediumSemiBold]}>+628980185458</Text>
</View>
<CodeField
ref={ref}
{...props}
value={value}
rootStyle={{ width: '80%', alignSelf: 'center' }}
onChangeText={setValue}
cellCount={4}
keyboardType="number-pad"
renderCell={({ index, symbol, isFocused }) => (
<Text
key={index}
style={[Styles.verificationCell, isFocused && Styles.verificationFocusCell, { borderColor: isFocused ? colors.tint : colors.icon, color: colors.text }]}
onLayout={getCellOnLayoutHandler(index)}>
{symbol || (isFocused ? <Cursor /> : null)}
</Text>
)}
/>
<ButtonForm
text="SUBMIT"
// onPress={() => { router.push("/home") }}
onPress={login}
/>
<Text style={[Styles.textInformation, Styles.mt05, Styles.cDefault, { textAlign: 'center', color: colors.tint }]}>
Tidak Menerima kode verifikasi? Kirim Ulang
</Text>
</View>
);
}

167
bun.lock
View File

@@ -11,12 +11,16 @@
"@react-native-clipboard/clipboard": "^1.16.3", "@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/cli": "^19.1.0", "@react-native-community/cli": "^19.1.0",
"@react-native-community/datetimepicker": "8.4.1", "@react-native-community/datetimepicker": "8.4.1",
"@react-native-community/netinfo": "^12.0.1",
"@react-native-firebase/app": "^22.4.0", "@react-native-firebase/app": "^22.4.0",
"@react-native-firebase/database": "^22.4.0", "@react-native-firebase/database": "^22.4.0",
"@react-native-firebase/messaging": "^22.2.1", "@react-native-firebase/messaging": "^22.2.1",
"@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14", "@react-navigation/native": "^7.0.14",
"@reduxjs/toolkit": "^2.7.0", "@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", "@types/formidable": "^3.4.5",
"axios": "^1.8.4", "axios": "^1.8.4",
"crypto-es": "^2.1.0", "crypto-es": "^2.1.0",
@@ -84,13 +88,14 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@react-native-community/cli-platform-ios": "^18.0.0", "@react-native-community/cli-platform-ios": "^18.0.0",
"@testing-library/react-native": "^13.3.3",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/react": "~19.0.10", "@types/react": "~19.0.10",
"@types/react-test-renderer": "^18.3.0", "@types/react-test-renderer": "^18.3.0",
"jest": "^29.2.1", "jest": "^29.2.1",
"jest-expo": "~53.0.5", "jest-expo": "~53.0.5",
"react-test-renderer": "18.3.1", "react-test-renderer": "19.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
}, },
}, },
@@ -456,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/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/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=="], "@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="],
@@ -464,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/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/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/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=="], "@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=="],
@@ -566,6 +575,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/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/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=="], "@react-native-firebase/database": ["@react-native-firebase/database@22.4.0", "", { "peerDependencies": { "@react-native-firebase/app": "22.4.0" } }, "sha512-iY+676RTwntRqq0CqcbGhidaegt/a6eKaoLTXeAxvtPYQaYXQL1fCDuZKfoy6uBfNsAGDxx2z4jYJuU+kOv4pA=="],
@@ -614,7 +625,7 @@
"@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], "@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=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="],
@@ -624,6 +635,18 @@
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], "@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=="],
"@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=="], "@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=="], "@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=="],
@@ -1230,6 +1253,8 @@
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "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=="], "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=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@@ -1310,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-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=="], "jest-docblock": ["jest-docblock@29.7.0", "", { "dependencies": { "detect-newline": "^3.0.0" } }, "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g=="],
@@ -1328,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-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=="], "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=="],
@@ -1496,6 +1521,8 @@
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "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=="], "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=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -1616,7 +1643,7 @@
"pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], "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=="], "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="],
@@ -1668,7 +1695,7 @@
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], "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=="], "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=="],
@@ -1728,12 +1755,12 @@
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "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@19.0.0", "", { "dependencies": { "react-is": "^19.0.0", "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-oX5u9rOQlHzqrE/64CNr0HB0uWxkCQmZNSfozlYvwE71TLVgeZxVf0IjouGEr1v7r1kcDifdAJBeOhdhxsG/DA=="],
"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=="],
"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=="], "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": ["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=="], "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
@@ -1790,7 +1817,7 @@
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], "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=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -1888,6 +1915,8 @@
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], "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=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
@@ -2070,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/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/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=="], "@expo/cli/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="],
@@ -2112,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=="], "@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/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/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/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=="], "@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=="],
@@ -2146,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-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=="], "@types/react-test-renderer/@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="],
@@ -2190,6 +2225,8 @@
"error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "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-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=="], "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
@@ -2216,18 +2253,36 @@
"istanbul-lib-instrument/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "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-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-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-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-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-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-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=="], "jest-watch-typeahead/ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="],
@@ -2280,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=="], "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=="], "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=="], "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-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/@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/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/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=="], "react-native/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -2358,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/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/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=="], "@expo/package-manager/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="],
@@ -2376,10 +2435,16 @@
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "@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/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/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-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=="], "@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=="],
@@ -2406,6 +2471,10 @@
"@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "@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=="], "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=="], "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
@@ -2418,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=="], "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=="], "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-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-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-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=="], "jest-watch-typeahead/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
@@ -2464,6 +2563,10 @@
"react-native/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "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=="], "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=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -2488,6 +2591,8 @@
"@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], "@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/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=="], "@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -2500,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=="], "@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=="], "@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=="], "@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=="],
@@ -2510,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=="], "@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=="], "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/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-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/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=="], "logkitty/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
@@ -2530,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/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=="], "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=="], "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
@@ -2558,6 +2687,8 @@
"@react-native/codegen/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@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-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=="], "jest-runtime/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],

View File

@@ -0,0 +1,79 @@
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<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error): void {
const { logError } = require('@/lib/errorLogger');
logError(error.message, error);
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<View style={styles.container}>
<Text style={styles.title}>Terjadi Kesalahan</Text>
<Text style={styles.message}>
Silahkan coba lagi beberapa saat lagi atau hubungi admin untuk bantuan.
</Text>
<TouchableOpacity style={styles.button} onPress={this.handleReset}>
<Text style={styles.buttonText}>Coba Lagi</Text>
</TouchableOpacity>
</View>
);
}
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',
},
});

42
components/ErrorView.tsx Normal file
View File

@@ -0,0 +1,42 @@
import Text from '@/components/Text';
import Styles from '@/constants/Styles';
import { useTheme } from '@/providers/ThemeProvider';
import { MaterialCommunityIcons } from "@expo/vector-icons";
import React from 'react';
import { View } from 'react-native';
interface ErrorViewProps {
title?: string;
message?: string;
icon?: keyof typeof MaterialCommunityIcons.glyphMap;
}
/**
* ErrorView component to display error or empty states.
* Used when data is not found, deleted, or an error occurs during fetching.
*/
export default function ErrorView({
title = "Terjadi Kesalahan",
message = "Data tidak ditemukan atau sudah dihapus.",
icon = "alert-circle-outline"
}: ErrorViewProps) {
const { colors } = useTheme();
return (
<View style={[Styles.flex1, Styles.contentItemCenter, Styles.ph20]}>
<View style={[Styles.mb10]}>
<MaterialCommunityIcons
name={icon}
size={40}
color={colors.dimmed}
/>
</View>
<Text style={[Styles.textDefaultSemiBold, { color: colors.text, textAlign: 'center' }]}>
{title}
</Text>
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, textAlign: 'center', marginTop: 4 }]}>
{message}
</Text>
</View>
);
}

View File

@@ -45,7 +45,7 @@ export default function ViewLogin({ onValidate }: Props) {
} else { } else {
return Toast.show({ type: 'small', text1: response.message, position: 'bottom' }) return Toast.show({ type: 'small', text1: response.message, position: 'bottom' })
} }
} catch (error : any ) { } catch (error: any) {
console.error(error); console.error(error);
const message = error?.response?.data?.message || "Gagal login" const message = error?.response?.data?.message || "Gagal login"

View File

@@ -33,7 +33,7 @@ export default function HeaderRightDivisionList() {
}} }}
/> />
{ {
(entityUser.role == "userRole" || entityUser.role == "developer") && (entityUser.role == "supadmin" || entityUser.role == "developer") &&
<MenuItemRow <MenuItemRow
icon={<AntDesign name="filter" color={colors.text} size={25} />} icon={<AntDesign name="filter" color={colors.text} size={25} />}
title="Filter" title="Filter"

View File

@@ -2,44 +2,54 @@ import Styles from "@/constants/Styles";
import { Dimensions, View } from "react-native"; import { Dimensions, View } from "react-native";
import { BarChart } from "react-native-gifted-charts"; import { BarChart } from "react-native-gifted-charts";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { useMemo } from "react";
import Text from "../Text"; import Text from "../Text";
export default function ReportChartDocument({ data }: { data: { label: string; value: number; }[] }) { export default function ReportChartDocument({ data }: { data: { label: string; value: number; }[] }) {
const { colors } = useTheme(); const { colors } = useTheme();
const maxValue = Math.max(...data.map(i => i.value))
const barData = [
{ value: 23, label: 'Gambar', },
{ value: 12, label: 'Dokumen' },
];
const width = Dimensions.get("window").width; 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: () => (
<View style={{ marginBottom: 5 }}>
<View
style={{
alignItems: 'center',
justifyContent: 'center',
width: width * 0.25
}}>
<Text style={{ color: colors.text, fontSize: 12 }}>{item.value > 0 ? item.value : ""}</Text>
</View>
</View>
)
}))
}, [data, colors.text]);
return ( return (
<View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.background }]}> <View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.background }]}>
<Text style={[Styles.textSubtitle, Styles.mv15]}>JUMLAH DOKUMEN</Text> <Text style={[Styles.textSubtitle, Styles.mv15]}>JUMLAH DOKUMEN</Text>
<BarChart <BarChart
key={JSON.stringify(data)}
xAxisLabelTextStyle={{ color: colors.text }} xAxisLabelTextStyle={{ color: colors.text }}
yAxisTextStyle={{ color: colors.text }} yAxisTextStyle={{ color: colors.text }}
showFractionalValues={false} showFractionalValues={false}
showYAxisIndices showYAxisIndices
noOfSections={maxValue < 5 ? 2 : 4} noOfSections={maxValue < 5 ? (maxValue === 0 ? 4 : maxValue) : 4}
maxValue={maxValue} maxValue={maxValue}
data={data} data={barData}
isAnimated isAnimated
width={width - 140} width={width - 140}
barWidth={width * 0.25} barWidth={width * 0.25}
frontColor="#fac858"
renderTooltip={(item: any, index: any) => {
return (
<View
style={{
alignItems: 'center',
justifyContent: 'center',
width: width * 0.25
}}>
<Text style={{ color: colors.text }}>{item.value}</Text>
</View>
);
}}
/> />
</View> </View>
) )

View File

@@ -2,44 +2,54 @@ import Styles from "@/constants/Styles";
import { Dimensions, View } from "react-native"; import { Dimensions, View } from "react-native";
import { BarChart } from "react-native-gifted-charts"; import { BarChart } from "react-native-gifted-charts";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { useMemo } from "react";
import Text from "../Text"; import Text from "../Text";
export default function ReportChartEvent({ data }: { data: { label: string; value: number; }[] }) { export default function ReportChartEvent({ data }: { data: { label: string; value: number; }[] }) {
const { colors } = useTheme(); const { colors } = useTheme();
const width = Dimensions.get("window").width; const width = Dimensions.get("window").width;
const maxValue = Math.max(...data.map(i => i.value))
const barData = [ const maxValue = useMemo(() => {
{ value: 23, label: 'Akan Datang', }, const maxVal = data.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), 0);
{ value: 12, label: 'Selesai' }, 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: () => (
<View style={{ marginBottom: 5 }}>
<View
style={{
alignItems: 'center',
justifyContent: 'center',
width: width * 0.25
}}>
<Text style={{ color: colors.text, fontSize: 12 }}>{item.value > 0 ? item.value : ""}</Text>
</View>
</View>
)
}))
}, [data, colors.text]);
return ( return (
<View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.background }]}> <View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.background }]}>
<Text style={[Styles.textSubtitle, Styles.mv15]}>ACARA DIVISI</Text> <Text style={[Styles.textSubtitle, Styles.mv15]}>ACARA DIVISI</Text>
<BarChart <BarChart
key={JSON.stringify(data)}
xAxisLabelTextStyle={{ color: colors.text }} xAxisLabelTextStyle={{ color: colors.text }}
yAxisTextStyle={{ color: colors.text }} yAxisTextStyle={{ color: colors.text }}
showFractionalValues={false} showFractionalValues={false}
showYAxisIndices showYAxisIndices
noOfSections={maxValue < 5 ? 2 : 4} noOfSections={maxValue < 5 ? (maxValue === 0 ? 4 : maxValue) : 4}
maxValue={maxValue} maxValue={maxValue}
frontColor="#177AD5" data={barData}
data={data}
isAnimated isAnimated
width={width - 140} width={width - 140}
barWidth={width * 0.25} barWidth={width * 0.25}
renderTooltip={(item: any, index: any) => {
return (
<View
style={{
alignItems: 'center',
justifyContent: 'center',
width: width * 0.25
}}>
<Text style={{ color: colors.text }}>{item.value}</Text>
</View>
);
}}
/> />
</View> </View>
) )

View File

@@ -5,7 +5,8 @@ import { setEntities } from "@/lib/bannerSlice";
import { setEntityUser } from "@/lib/userSlice"; import { setEntityUser } from "@/lib/userSlice";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; 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 { router } from "expo-router";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Dimensions, Image, View } from "react-native"; import { Dimensions, Image, View } from "react-native";
@@ -23,37 +24,44 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean })
const progress = useSharedValue<number>(0); const progress = useSharedValue<number>(0);
const dispatch = useDispatch() const dispatch = useDispatch()
const entities = useSelector((state: any) => state.banner) const entities = useSelector((state: any) => state.banner)
const entityUser = useSelector((state: any) => state.user)
async function handleBannerView() { // Query for Banners
const hasil = await decryptToken(String(token?.current)) const { data: banners } = useQuery({
apiGetBanner({ user: hasil }).then((data) => { queryKey: ['banners'],
if (data.data.length > 0) { queryFn: async () => {
dispatch(setEntities(data.data)) const hasil = await decryptToken(String(token?.current))
} else { const data = await apiGetBanner({ user: hasil })
dispatch(setEntities([])) return data.data || []
} },
}) enabled: !!token?.current,
} staleTime: 0,
})
async function handleUser() { // Query for Profile (Role Check)
const hasil = await decryptToken(String(token?.current)) const { data: profile } = useQuery({
const response = await apiGetProfile({ id: hasil }) queryKey: ['profile'], // Shares same key as Home.tsx
dispatch(setEntityUser({ role: response.data.idUserRole, admin: false })) 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(() => { useEffect(() => {
if (refreshing) if (banners) {
handleBannerView() dispatch(setEntities(banners))
}, [refreshing]); }
}, [banners, dispatch])
// Sync User Role to Redux
useEffect(() => { useEffect(() => {
handleBannerView() if (profile) {
}, [dispatch]); dispatch(setEntityUser({ role: profile.idUserRole, admin: false }))
}
useEffect(() => { }, [profile, dispatch])
handleUser()
}, []);
return ( return (
<View <View
@@ -92,7 +100,7 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean })
backgroundColor: colors.primary backgroundColor: colors.primary
}, },
]} ]}
resizeMode="cover" resizeMode="stretch"
/> />
)} )}
/> />

View File

@@ -2,7 +2,8 @@ import Styles from "@/constants/Styles";
import { apiGetDataHome } from "@/lib/api"; import { apiGetDataHome } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { Dimensions, View } from "react-native"; import { Dimensions, View } from "react-native";
import { BarChart } from "react-native-gifted-charts"; import { BarChart } from "react-native-gifted-charts";
import Skeleton from "../skeleton"; import Skeleton from "../skeleton";
@@ -15,80 +16,69 @@ type Props = {
}[] }[]
export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }) { export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }) {
const [loading, setLoading] = useState(true)
const { decryptToken, token } = useAuthSession() const { decryptToken, token } = useAuthSession()
const { colors } = useTheme(); const { colors } = useTheme();
const [data, setData] = useState<Props>([])
const [maxValue, setMaxValue] = useState(5)
const barData = [
{ value: 23, label: 'Gambar', frontColor: '#fac858' },
{ value: 12, label: 'Dokumen', frontColor: '#92cc76' },
];
const width = Dimensions.get("window").width; const width = Dimensions.get("window").width;
// TanStack Query for Document Chart data
async function handleData(loading: boolean) { const { data: chartData = [], isLoading, isFetching } = useQuery({
try { queryKey: ['homeData', 'dokumen'],
setLoading(loading) queryFn: async () => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetDataHome({ cat: "dokumen", user: hasil }) const response = await apiGetDataHome({ cat: "dokumen", user: hasil })
const maxValue = response.data.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), -Infinity); return response.data.map((item: { color: any; label: any; value: any; }) => {
const roundUp = Math.ceil(maxValue / 10) * 10 const val = Number(item.value) || 0;
setMaxValue(roundUp) return {
const convertedArray = response.data.map((item: { color: any; label: any; value: any; }) => ({ frontColor: val > 0 ? (item.color || '#fac858') : 'transparent',
frontColor: item.color, label: item.label,
label: item.label, value: val,
value: Number(item.value) }
})); }) as Props
setData(convertedArray) },
} catch (error) { enabled: !!token?.current,
console.error(error) staleTime: 0,
} finally { })
setLoading(false)
}
}
useEffect(() => { // Derived state for maxValue
if (refreshing) const maxValue = useMemo(() => {
handleData(false) const maxVal = chartData.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), 0);
}, [refreshing]); // Adjust maxValue and intervals based on the data
if (maxVal === 0) return 10;
useEffect(() => { if (maxVal < 5) return 5;
handleData(true) return Math.ceil(maxVal / 10) * 10;
}, []); }, [chartData]);
const barData = useMemo(() => {
return chartData.map(item => ({
...item,
topLabelComponent: () => (
<View style={{ marginBottom: 5 }}>
<Text style={{ color: colors.text, fontSize: 12 }}>{item.value > 0 ? item.value : ""}</Text>
</View>
)
}))
}, [chartData, colors.text]);
return ( return (
<View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}> <View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
<Text style={[Styles.textSubtitle, Styles.mv15]}>JUMLAH DOKUMEN</Text> <Text style={[Styles.textSubtitle, Styles.mv15]}>JUMLAH DOKUMEN</Text>
{ {
loading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" /> isLoading || (refreshing && isFetching) ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" />
: :
<BarChart <BarChart
key={JSON.stringify(chartData)}
showFractionalValues={false} showFractionalValues={false}
showYAxisIndices showYAxisIndices
noOfSections={4} noOfSections={maxValue < 5 ? (maxValue === 0 ? 4 : maxValue) : 4}
maxValue={maxValue} maxValue={maxValue}
data={data} data={barData}
isAnimated isAnimated
width={width - 140} width={width - 140}
barWidth={width * 0.25} barWidth={width * 0.25}
yAxisTextStyle={{ color: colors.text }} yAxisTextStyle={{ color: colors.text }}
xAxisLabelTextStyle={{ color: colors.text }} xAxisLabelTextStyle={{ color: colors.text }}
renderTooltip={(item: any, index: any) => {
return (
<View
style={{
alignItems: 'center',
justifyContent: 'center',
width: width * 0.25
}}>
<Text>{item.value}</Text>
</View>
);
}}
/> />
} }
</View> </View>
) )
} }

View File

@@ -2,7 +2,7 @@ import Styles from "@/constants/Styles";
import { apiGetDataHome } from "@/lib/api"; import { apiGetDataHome } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query";
import { View } from "react-native"; import { View } from "react-native";
import { PieChart } from "react-native-gifted-charts"; import { PieChart } from "react-native-gifted-charts";
import Skeleton from "../skeleton"; import Skeleton from "../skeleton";
@@ -17,45 +17,32 @@ type Props = {
export default function ChartProgresHome({ refreshing }: { refreshing: boolean }) { export default function ChartProgresHome({ refreshing }: { refreshing: boolean }) {
const { decryptToken, token } = useAuthSession() const { decryptToken, token } = useAuthSession()
const { colors } = useTheme(); const { colors } = useTheme();
const [data, setData] = useState<Props>([])
const [loading, setLoading] = useState(true)
async function handleData(loading: boolean) { // TanStack Query for Progress Chart data
try { const { data: chartData = [], isLoading } = useQuery({
setLoading(loading) queryKey: ['homeData', 'progress'],
queryFn: async () => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetDataHome({ cat: "progress", user: hasil }) 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, color: item.color,
text: item.text, text: item.text,
value: Number(item.value) value: Number(item.value)
})); })) as Props
setData(convertedArray) },
} catch (error) { enabled: !!token?.current,
console.error(error) staleTime: 0,
} finally { })
setLoading(false)
}
}
useEffect(() => {
if (refreshing)
handleData(false)
}, [refreshing]);
useEffect(() => {
handleData(true)
}, []);
return ( return (
<View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}> <View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
<Text style={[Styles.textSubtitle, Styles.mv15]}>PROGRES KEGIATAN</Text> <Text style={[Styles.textSubtitle, Styles.mv15]}>PROGRES KEGIATAN</Text>
{ {
loading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" /> isLoading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" />
: :
<> <>
<PieChart <PieChart
data={data} data={chartData}
showText showText
showValuesAsTooltipText showValuesAsTooltipText
textColor={'black'} textColor={'black'}

View File

@@ -1,13 +1,13 @@
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
import { apiGetDataHome } from "@/lib/api"; import { apiGetDataHome } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router"; import { router } from "expo-router";
import { useEffect, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import DiscussionItem from "../discussionItem"; import DiscussionItem from "../discussionItem";
import Skeleton from "../skeleton"; import Skeleton from "../skeleton";
import Text from "../Text"; import Text from "../Text";
import { useTheme } from "@/providers/ThemeProvider";
type Props = { type Props = {
id: string id: string
@@ -20,46 +20,33 @@ type Props = {
export default function DisccussionHome({ refreshing }: { refreshing: boolean }) { export default function DisccussionHome({ refreshing }: { refreshing: boolean }) {
const { decryptToken, token } = useAuthSession() const { decryptToken, token } = useAuthSession()
const [data, setData] = useState<Props[]>([])
const [loading, setLoading] = useState(true)
const { colors } = useTheme(); const { colors } = useTheme();
// TanStack Query for Discussion data
async function handleData(loading: boolean) { const { data: homeDiscussions = [], isLoading } = useQuery({
try { queryKey: ['homeData', 'discussion'],
setLoading(loading) queryFn: async () => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetDataHome({ cat: "discussion", user: hasil }) const response = await apiGetDataHome({ cat: "discussion", user: hasil })
setData(response.data) return response.data as Props[]
} catch (error) { },
console.error(error) enabled: !!token?.current,
} finally { staleTime: 0,
setLoading(false) })
}
}
useEffect(() => {
if (refreshing)
handleData(false)
}, [refreshing]);
useEffect(() => {
handleData(true)
}, []);
return ( return (
<View style={[Styles.mb15]}> <View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv10]}>Diskusi</Text> <Text style={[Styles.textDefaultSemiBold, Styles.mv10]}>Diskusi</Text>
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.icon + '20' }, Styles.p0]}> <View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.icon + '20' }, Styles.p0]}>
{ {
loading ? isLoading ?
<> <>
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" /> <Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" /> <Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
</> </>
: :
data.length > 0 ? homeDiscussions.length > 0 ?
data.map((item, index) => { homeDiscussions.map((item: Props, index: number) => {
return ( return (
<DiscussionItem key={index} title={item.desc} user={item.user} date={item.date} onPress={() => { router.push(`/division/${item.idDivision}/discussion/${item.id}`) }} /> <DiscussionItem key={index} title={item.desc} user={item.user} date={item.date} onPress={() => { router.push(`/division/${item.idDivision}/discussion/${item.id}`) }} />
) )

View File

@@ -3,8 +3,9 @@ import { apiGetDataHome } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router"; import { router } from "expo-router";
import React, { useEffect, useState } from "react"; import React from "react";
import { Dimensions, Pressable, View } from "react-native"; import { Dimensions, Pressable, View } from "react-native";
import { ICarouselInstance } from "react-native-reanimated-carousel"; import { ICarouselInstance } from "react-native-reanimated-carousel";
import Skeleton from "../skeleton"; import Skeleton from "../skeleton";
@@ -21,45 +22,31 @@ export default function DivisionHome({ refreshing }: { refreshing: boolean }) {
const { colors } = useTheme(); const { colors } = useTheme();
const ref = React.useRef<ICarouselInstance>(null) const ref = React.useRef<ICarouselInstance>(null)
const width = Dimensions.get("window").width const width = Dimensions.get("window").width
const [data, setData] = useState<Props[]>([])
const [loading, setLoading] = useState(true)
const arrSkeleton = Array.from({ length: 2 }, (_, index) => index) const arrSkeleton = Array.from({ length: 2 }, (_, index) => index)
async function handleData(loading: boolean) { // TanStack Query for Division data
try { const { data: homeDivisions = [], isLoading } = useQuery({
setLoading(loading) queryKey: ['homeData', 'division'],
queryFn: async () => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetDataHome({ cat: "division", user: hasil }) const response = await apiGetDataHome({ cat: "division", user: hasil })
setData(response.data) return response.data as Props[]
} catch (error) { },
console.error(error) enabled: !!token?.current,
} finally { staleTime: 0,
setLoading(false) })
}
}
useEffect(() => {
if (refreshing)
handleData(false)
}, [refreshing]);
useEffect(() => {
handleData(true)
}, []);
return ( return (
<View style={[Styles.mb15]}> <View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mb10]}>Divisi Teraktif</Text> <Text style={[Styles.textDefaultSemiBold, Styles.mb10]}>Divisi Teraktif</Text>
{ {
loading ? isLoading ?
arrSkeleton.map((item, index) => ( arrSkeleton.map((item, index) => (
<Skeleton key={index} width={100} height={80} borderRadius={10} widthType="percent" /> <Skeleton key={index} width={100} height={80} borderRadius={10} widthType="percent" />
)) ))
: :
data.length > 0 ? homeDivisions.length > 0 ?
data.map((item, index) => ( homeDivisions.map((item, index) => (
<Pressable style={[Styles.wrapPaper, Styles.mb05, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]} key={index} onPress={() => { router.push(`/division/${item.id}`) }}> <Pressable style={[Styles.wrapPaper, Styles.mb05, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]} key={index} onPress={() => { router.push(`/division/${item.id}`) }}>
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}> <View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
<View> <View>

View File

@@ -2,8 +2,8 @@ import Styles from "@/constants/Styles";
import { apiGetDataHome } from "@/lib/api"; import { apiGetDataHome } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router"; import { router } from "expo-router";
import { useEffect, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import EventItem from "../eventItem"; import EventItem from "../eventItem";
import Skeleton from "../skeleton"; import Skeleton from "../skeleton";
@@ -26,30 +26,18 @@ type Props = {
export default function EventHome({ refreshing }: { refreshing: boolean }) { export default function EventHome({ refreshing }: { refreshing: boolean }) {
const { decryptToken, token } = useAuthSession() const { decryptToken, token } = useAuthSession()
const { colors } = useTheme(); const { colors } = useTheme();
const [data, setData] = useState<Props[]>([])
const [loading, setLoading] = useState(true)
async function handleData(loading: boolean) { // TanStack Query for Event data
try { const { data: homeEvents = [], isLoading } = useQuery({
setLoading(loading) queryKey: ['homeData', 'event'],
queryFn: async () => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetDataHome({ cat: "event", user: hasil }) const response = await apiGetDataHome({ cat: "event", user: hasil })
setData(response.data) return response.data as Props[]
} catch (error) { },
console.error(error) enabled: !!token?.current,
} finally { staleTime: 0,
setLoading(false) })
}
}
useEffect(() => {
if (refreshing)
handleData(false)
}, [refreshing]);
useEffect(() => {
handleData(true)
}, []);
return ( return (
<View style={[Styles.mb15]}> <View style={[Styles.mb15]}>
@@ -57,14 +45,14 @@ export default function EventHome({ refreshing }: { refreshing: boolean }) {
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}> <View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
{ {
loading ? isLoading ?
<> <>
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" /> <Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" /> <Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
</> </>
: :
data.length > 0 ? homeEvents.length > 0 ?
data.map((item, index) => { homeEvents.map((item: Props, index: number) => {
return ( return (
<EventItem key={index} category={index % 2 == 0 ? 'purple' : 'orange'} onPress={() => { router.push(`/division/${item.idDivision}/calendar/${item.id}`) }} title={item.title} user={item.user_name} jamAwal={item.timeStart} jamAkhir={item.timeEnd} /> <EventItem key={index} category={index % 2 == 0 ? 'purple' : 'orange'} onPress={() => { router.push(`/division/${item.idDivision}/calendar/${item.id}`) }} title={item.title} user={item.user_name} jamAwal={item.timeStart} jamAkhir={item.timeEnd} />
) )

View File

@@ -2,8 +2,9 @@ import Styles from "@/constants/Styles";
import { apiGetDataHome } from "@/lib/api"; import { apiGetDataHome } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider"; import { useTheme } from "@/providers/ThemeProvider";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router"; import { router } from "expo-router";
import React, { useEffect, useState } from "react"; import React from "react";
import { Dimensions, View } from "react-native"; import { Dimensions, View } from "react-native";
import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel"; import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel";
import LabelStatus from "../labelStatus"; import LabelStatus from "../labelStatus";
@@ -25,45 +26,33 @@ export default function ProjectHome({ refreshing }: { refreshing: boolean }) {
const { decryptToken, token } = useAuthSession() const { decryptToken, token } = useAuthSession()
const ref = React.useRef<ICarouselInstance>(null); const ref = React.useRef<ICarouselInstance>(null);
const width = Dimensions.get("window").width; const width = Dimensions.get("window").width;
const [data, setData] = useState<Props[]>([])
const [loading, setLoading] = useState(true)
const { colors } = useTheme(); const { colors } = useTheme();
async function handleData(loading: boolean) { // TanStack Query for Projects data
try { const { data: homeProjects = [], isLoading } = useQuery({
setLoading(loading) queryKey: ['homeData', 'kegiatan'],
queryFn: async () => {
const hasil = await decryptToken(String(token?.current)) const hasil = await decryptToken(String(token?.current))
const response = await apiGetDataHome({ cat: "kegiatan", user: hasil }) const response = await apiGetDataHome({ cat: "kegiatan", user: hasil })
setData(response.data) return response.data as Props[]
} catch (error) { },
console.error(error) enabled: !!token?.current,
} finally { staleTime: 0,
setLoading(false) })
}
}
useEffect(() => {
if (refreshing)
handleData(false)
}, [refreshing]);
useEffect(() => {
handleData(true)
}, []);
return ( return (
<View style={[Styles.mb05]}> <View style={[Styles.mb05]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mb10]}>Kegiatan Terupdate</Text> <Text style={[Styles.textDefaultSemiBold, Styles.mb10]}>Kegiatan Terupdate</Text>
{ {
loading ? (<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />) isLoading ? (<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />)
: :
data.length > 0 ? homeProjects.length > 0 ?
<Carousel <Carousel
ref={ref} ref={ref}
style={{ width: "100%" }} style={{ width: "100%" }}
width={width * 0.8} width={width * 0.8}
height={220} height={220}
data={data} data={homeProjects}
loop={false} loop={false}
autoPlay={false} autoPlay={false}
autoPlayReverse={false} autoPlayReverse={false}
@@ -71,24 +60,24 @@ export default function ProjectHome({ refreshing }: { refreshing: boolean }) {
snapEnabled={true} snapEnabled={true}
vertical={false} vertical={false}
renderItem={({ index }) => ( renderItem={({ index }) => (
<PaperGridContent titleTail={1} content="carousel" onPress={() => { router.push(`/project/${data[index].id}`) }} title={data[index].title} headerColor="primary"> <PaperGridContent titleTail={1} content="carousel" onPress={() => { router.push(`/project/${homeProjects[index].id}`) }} title={homeProjects[index].title} headerColor="primary">
<ProgressBar value={data[index].progress} category="carousel" /> <ProgressBar value={homeProjects[index].progress} category="carousel" />
<View style={[Styles.rowSpaceBetween]}> <View style={[Styles.rowSpaceBetween]}>
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>{data[index].createdAt}</Text> <Text style={[Styles.textDefault, { color: colors.dimmed }]}>{homeProjects[index].createdAt}</Text>
<LabelStatus <LabelStatus
size="default" size="default"
category={ category={
data[index].status === 0 ? 'secondary' : homeProjects[index].status === 0 ? 'secondary' :
data[index].status === 1 ? 'warning' : homeProjects[index].status === 1 ? 'warning' :
data[index].status === 2 ? 'success' : homeProjects[index].status === 2 ? 'success' :
data[index].status === 3 ? 'error' : homeProjects[index].status === 3 ? 'error' :
'secondary' 'secondary'
} }
text={ text={
data[index].status === 0 ? 'SEGERA' : homeProjects[index].status === 0 ? 'SEGERA' :
data[index].status === 1 ? 'DIKERJAKAN' : homeProjects[index].status === 1 ? 'DIKERJAKAN' :
data[index].status === 2 ? 'SELESAI' : homeProjects[index].status === 2 ? 'SELESAI' :
data[index].status === 3 ? 'DIBATALKAN' : homeProjects[index].status === 3 ? 'DIBATALKAN' :
'SEGERA' 'SEGERA'
} }
/> />

View File

@@ -3,6 +3,8 @@ import Constants from 'expo-constants';
export const ConstEnv = { export const ConstEnv = {
url_storage: Constants?.expoConfig?.extra?.URL_STORAGE, url_storage: Constants?.expoConfig?.extra?.URL_STORAGE,
pass_encrypt: Constants?.expoConfig?.extra?.PASS_ENC, pass_encrypt: Constants?.expoConfig?.extra?.PASS_ENC,
url_monitoring: Constants?.expoConfig?.extra?.URL_MONITORING,
key_api_monitoring: Constants?.expoConfig?.extra?.KEY_API_MONITORING,
firebase: { firebase: {
apiKey: Constants?.expoConfig?.extra?.FIREBASE_API_KEY, apiKey: Constants?.expoConfig?.extra?.FIREBASE_API_KEY,
authDomain: Constants?.expoConfig?.extra?.FIREBASE_AUTH_DOMAIN, authDomain: Constants?.expoConfig?.extra?.FIREBASE_AUTH_DOMAIN,

View File

@@ -490,17 +490,17 @@ const Styles = StyleSheet.create({
wrapItemDiscussion: { wrapItemDiscussion: {
padding: 15, padding: 15,
borderRadius: 5, borderRadius: 5,
borderColor: '#d6d8f6', // borderColor: '#d6d8f6',
borderBottomWidth: 1, borderBottomWidth: 1,
}, },
wrapItemBorderBottom: { wrapItemBorderBottom: {
padding: 10, padding: 10,
borderColor: '#d6d8f6', // borderColor: '#d6d8f6',
borderBottomWidth: 1, borderBottomWidth: 1,
}, },
wrapItemBorderAll: { wrapItemBorderAll: {
padding: 10, padding: 10,
borderColor: '#d6d8f6', // borderColor: '#d6d8f6',
borderWidth: 1, borderWidth: 1,
borderRadius: 5, borderRadius: 5,
marginBottom: 5 marginBottom: 5

32
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,32 @@
# 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 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`.

13
docs/CONVENTIONS.md Normal file
View File

@@ -0,0 +1,13 @@
# 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.

View File

@@ -28,7 +28,8 @@
"buildType": "app-bundle" "buildType": "app-bundle"
}, },
"ios": { "ios": {
"simulator": false "simulator": false,
"image": "latest"
} }
} }
}, },

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.0.5</string> <string>2.1.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -39,7 +39,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>7</string> <string>9</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@@ -3,7 +3,7 @@ PODS:
- DoubleConversion (1.1.6) - DoubleConversion (1.1.6)
- EXApplication (6.1.5): - EXApplication (6.1.5):
- ExpoModulesCore - ExpoModulesCore
- EXConstants (17.1.7): - EXConstants (17.1.8):
- ExpoModulesCore - ExpoModulesCore
- EXImageLoader (5.1.0): - EXImageLoader (5.1.0):
- ExpoModulesCore - ExpoModulesCore
@@ -11,9 +11,9 @@ PODS:
- EXJSONUtils (0.15.0) - EXJSONUtils (0.15.0)
- EXManifests (0.16.6): - EXManifests (0.16.6):
- ExpoModulesCore - ExpoModulesCore
- EXNotifications (0.31.4): - EXNotifications (0.31.5):
- ExpoModulesCore - ExpoModulesCore
- Expo (53.0.20): - Expo (53.0.27):
- DoubleConversion - DoubleConversion
- ExpoModulesCore - ExpoModulesCore
- glog - glog
@@ -282,7 +282,7 @@ PODS:
- ExpoModulesCore - ExpoModulesCore
- ExpoHaptics (14.1.4): - ExpoHaptics (14.1.4):
- ExpoModulesCore - ExpoModulesCore
- ExpoHead (5.1.4): - ExpoHead (5.1.11):
- ExpoModulesCore - ExpoModulesCore
- ExpoImagePicker (16.1.4): - ExpoImagePicker (16.1.4):
- ExpoModulesCore - ExpoModulesCore
@@ -324,7 +324,7 @@ PODS:
- ExpoModulesCore - ExpoModulesCore
- ExpoSymbols (0.4.5): - ExpoSymbols (0.4.5):
- ExpoModulesCore - ExpoModulesCore
- ExpoSystemUI (5.0.10): - ExpoSystemUI (5.0.11):
- ExpoModulesCore - ExpoModulesCore
- ExpoWebBrowser (14.2.0): - ExpoWebBrowser (14.2.0):
- ExpoModulesCore - ExpoModulesCore
@@ -1703,6 +1703,8 @@ PODS:
- ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- Yoga - Yoga
- react-native-netinfo (12.0.1):
- React-Core
- react-native-render-html (6.3.4): - react-native-render-html (6.3.4):
- React-Core - React-Core
- react-native-safe-area-context (5.4.0): - 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-blob-util (from `../node_modules/react-native-blob-util`)
- react-native-date-picker (from `../node_modules/react-native-date-picker`) - react-native-date-picker (from `../node_modules/react-native-date-picker`)
- react-native-image-picker (from `../node_modules/react-native-image-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-render-html (from `../node_modules/react-native-render-html`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-webview (from `../node_modules/react-native-webview`) - react-native-webview (from `../node_modules/react-native-webview`)
@@ -2505,6 +2508,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-date-picker" :path: "../node_modules/react-native-date-picker"
react-native-image-picker: react-native-image-picker:
:path: "../node_modules/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: react-native-render-html:
:path: "../node_modules/react-native-render-html" :path: "../node_modules/react-native-render-html"
react-native-safe-area-context: react-native-safe-area-context:
@@ -2600,12 +2605,12 @@ SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
EXApplication: 1e06972201838375ca1ec1ba34d586a98a5dc718 EXApplication: 1e06972201838375ca1ec1ba34d586a98a5dc718
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 EXConstants: d3d551cb154718f5161c4247304e96aa59f6cca7
EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee
EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd
EXManifests: 691a779b04e4f2c96da46fb9bef4f86174fefcb5 EXManifests: 691a779b04e4f2c96da46fb9bef4f86174fefcb5
EXNotifications: be5e949edf1d60b70e77178b81aa505298fadd07 EXNotifications: 6770976336aacdc7dc7aed7b538dd8f7ad2c55e8
Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea Expo: 052536aae777d5156739c960afd6aa54881df42a
expo-dev-client: 9b1e78baf0dd87b005f035d180bbb07c05917fad expo-dev-client: 9b1e78baf0dd87b005f035d180bbb07c05917fad
expo-dev-launcher: 2f95084d36be3d9106790bea7a933a0d34210646 expo-dev-launcher: 2f95084d36be3d9106790bea7a933a0d34210646
expo-dev-menu: 1456232a68c883078b61c02b7fa5b01d8a5ab840 expo-dev-menu: 1456232a68c883078b61c02b7fa5b01d8a5ab840
@@ -2618,7 +2623,7 @@ SPEC CHECKSUMS:
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63 ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
ExpoHaptics: 0ff6e0d83cd891178a306e548da1450249d54500 ExpoHaptics: 0ff6e0d83cd891178a306e548da1450249d54500
ExpoHead: a7b66cbaeeb51f4a85338d335a0f5467e29a2c90 ExpoHead: cfc12096c9a68cbe25de93a8bfc4781c7689467e
ExpoImagePicker: 0963da31800c906e01c03e25d7c849f16ebf02a2 ExpoImagePicker: 0963da31800c906e01c03e25d7c849f16ebf02a2
ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7 ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7
ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d
@@ -2628,7 +2633,7 @@ SPEC CHECKSUMS:
ExpoSharing: b0377be82430d07398c6a4cd60b5a15696accbd3 ExpoSharing: b0377be82430d07398c6a4cd60b5a15696accbd3
ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385 ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385
ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859 ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859
ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2 ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92 ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
EXUpdatesInterface: 7ff005b7af94ee63fa452ea7bb95d7a8ff40277a EXUpdatesInterface: 7ff005b7af94ee63fa452ea7bb95d7a8ff40277a
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
@@ -2683,6 +2688,7 @@ SPEC CHECKSUMS:
react-native-blob-util: 45eb0e23b243b48955d231414ca5ee4da2439968 react-native-blob-util: 45eb0e23b243b48955d231414ca5ee4da2439968
react-native-date-picker: 2eca217a8fb09c517f5bb6b23978718c6cec59ec react-native-date-picker: 2eca217a8fb09c517f5bb6b23978718c6cec59ec
react-native-image-picker: 0c4a539c4e67fbe3977916cd2c8d0e4c67f00a8c react-native-image-picker: 0c4a539c4e67fbe3977916cd2c8d0e4c67f00a8c
react-native-netinfo: bed7e7b8f68e22e0862a77d7df28d31faa66375d
react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd
react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1 react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1
react-native-webview: 3df1192782174d1bd23f6a0f5a4fec3cdcca9954 react-native-webview: 3df1192782174d1bd23f6a0f5a4fec3cdcca9954

View File

@@ -1,10 +1,22 @@
import axios from 'axios'; import axios, { AxiosError } from 'axios';
import Constants from 'expo-constants'; import Constants from 'expo-constants';
import { logError } from '@/lib/errorLogger';
const api = axios.create({ const api = axios.create({
baseURL: Constants?.expoConfig?.extra?.URL_API baseURL: Constants?.expoConfig?.extra?.URL_API
}); });
api.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
const status = error.response?.status;
const url = error.config?.url ?? 'unknown endpoint';
const description = `API error ${status ?? 'network'} on ${url}`;
logError(description, error);
return Promise.reject(error);
}
);
export const apiCheckPhoneLogin = async (body: { phone: string }) => { export const apiCheckPhoneLogin = async (body: { phone: string }) => {
const response = await api.post('/auth/login', body) const response = await api.post('/auth/login', body)
return response.data; return response.data;
@@ -13,18 +25,21 @@ export const apiCheckPhoneLogin = async (body: { phone: string }) => {
export const apiSendOtp = async (body: { phone: string, otp: number }) => { export const apiSendOtp = async (body: { phone: string, otp: number }) => {
const message = "Desa+\nMasukkan kode ini " + body.otp + " pada aplikasi Desa+ anda. Jangan berikan pada siapapun." const message = "Desa+\nMasukkan kode ini " + body.otp + " pada aplikasi Desa+ anda. Jangan berikan pada siapapun."
const textFix = encodeURIComponent(message) const textFix = encodeURIComponent(message)
// const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*Desa%2B*%0AMasukkan%20kode%20ini%20*${encodeURIComponent(body.otp)}*%20pada%20aplikasi%20Desa%2B%20anda.%20Jangan%20berikan%20pada%20siapapun.`)
const res = await fetch( const res = await fetch(
`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=${textFix}`, `${Constants.expoConfig?.extra?.URL_OTP}/api/wa/send-text`,
{ {
cache: "no-cache", method: 'POST',
headers: { headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Constants.expoConfig?.extra?.WA_SERVER_TOKEN}`, Authorization: `Bearer ${Constants.expoConfig?.extra?.WA_SERVER_TOKEN}`,
}, },
body: JSON.stringify({
number: body.phone,
text: message
})
} }
); );
return res.status return res.status
} }
@@ -745,7 +760,7 @@ export const apiRegisteredToken = async (data: { user: string, token: string, ca
return response.data; return response.data;
}; };
export const apiUnregisteredToken = async (data: { user: string, token: string }) => { export const apiUnregisteredToken = async (data: { user: string, token: string, category?: string }) => {
const response = await api.put(`/mobile/auth-token`, data) const response = await api.put(`/mobile/auth-token`, data)
return response.data; return response.data;
}; };

100
lib/errorLogger.ts Normal file
View File

@@ -0,0 +1,100 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';
import Constants from 'expo-constants';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
import { ConstEnv } from '@/constants/ConstEnv';
const QUEUE_KEY = 'error_log_queue';
const APP_NAME = 'desa-plus';
type ErrorPayload = {
affectedVersion: string;
device: string;
os: string;
description: string;
app: string;
source: 'SYSTEM';
stackTrace: string;
};
function buildPayload(description: string, stackTrace?: string): ErrorPayload {
const platformName = Platform.OS === 'ios' ? 'iOS' : 'Android';
return {
affectedVersion: Constants.expoConfig?.version ?? 'unknown',
device: Device.modelName ?? 'unknown',
os: `${platformName} ${Device.osVersion ?? ''}`.trim(),
description,
app: APP_NAME,
source: 'SYSTEM',
stackTrace: stackTrace ?? '',
};
}
async function sendToMonitoring(payload: ErrorPayload): Promise<boolean> {
try {
const res = await fetch(`${ConstEnv.url_monitoring}/api/bugs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': ConstEnv.key_api_monitoring ?? '',
},
body: JSON.stringify(payload),
});
return res.ok;
} catch (error) {
console.log(error);
return false;
}
}
async function enqueue(payload: ErrorPayload): Promise<void> {
try {
const raw = await AsyncStorage.getItem(QUEUE_KEY);
const queue: ErrorPayload[] = raw ? JSON.parse(raw) : [];
queue.push(payload);
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
} catch {}
}
export async function flushErrorQueue(): Promise<void> {
try {
const state = await NetInfo.fetch();
if (!state.isConnected) return;
const raw = await AsyncStorage.getItem(QUEUE_KEY);
if (!raw) return;
const queue: ErrorPayload[] = JSON.parse(raw);
if (queue.length === 0) return;
const failed: ErrorPayload[] = [];
for (const payload of queue) {
const ok = await sendToMonitoring(payload);
if (!ok) failed.push(payload);
}
if (failed.length > 0) {
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(failed));
} else {
await AsyncStorage.removeItem(QUEUE_KEY);
}
} catch {}
}
export async function logError(description: string, error?: Error | unknown): Promise<void> {
const stackTrace = error instanceof Error ? (error.stack ?? error.message) : String(error ?? '');
const payload = buildPayload(description, stackTrace);
try {
const state = await NetInfo.fetch();
if (state.isConnected) {
const ok = await sendToMonitoring(payload);
if (!ok) await enqueue(payload);
} else {
await enqueue(payload);
}
} catch {
await enqueue(payload);
}
}

View File

@@ -66,11 +66,48 @@ export const openSettings = () => {
export const requestPermission = async () => { export const requestPermission = async () => {
try { try {
const { status: newStatus } = await Notifications.requestPermissionsAsync(); const existing = await AsyncStorage.getItem('@notification_permission');
await AsyncStorage.setItem('@notification_permission', newStatus === 'granted' ? "true" : "false"); const { status: currentStatus, canAskAgain } = await Notifications.getPermissionsAsync();
return newStatus === 'granted';
// Jika baru pertama kali (fresh install / storage belum ada)
if (existing === null) {
// Cek apakah OS memungkinkan untuk memunculkan popup
if (currentStatus === 'undetermined' || (currentStatus === 'denied' && canAskAgain)) {
const { status: newStatus } = await Notifications.requestPermissionsAsync();
await AsyncStorage.setItem('@notification_permission', newStatus === 'granted' ? 'true' : 'false');
return newStatus === 'granted';
}
// Jika OS sudah granted (sudah diijinkan entah lewat mana), simpan true
if (currentStatus === 'granted') {
await AsyncStorage.setItem('@notification_permission', 'true');
return true;
}
// Jika OS denied dan tidak bisa minta lagi (disables)
if (currentStatus === 'denied' && !canAskAgain) {
await AsyncStorage.setItem('@notification_permission', 'false');
return false;
}
}
const osPermission = await checkPermission();
// Jika sudah pernah di-request sebelumnya (storage sudah ada)
// Selalu sinkronkan jika OS menyatakan Denied
if (osPermission === false) {
await AsyncStorage.setItem('@notification_permission', 'false');
return false;
}
// Jika OS Granted, ikuti nilai tersimpan di storage (menghargai in-app toggle user)
if (osPermission === true) {
return existing === 'true';
}
return false;
} catch (err) { } catch (err) {
console.warn('Error requesting notification permissions:', err); console.warn('Error requesting notification permissions:', err);
return false;
} }
}; };

View File

@@ -24,12 +24,16 @@
"@react-native-clipboard/clipboard": "^1.16.3", "@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/cli": "^19.1.0", "@react-native-community/cli": "^19.1.0",
"@react-native-community/datetimepicker": "8.4.1", "@react-native-community/datetimepicker": "8.4.1",
"@react-native-community/netinfo": "^12.0.1",
"@react-native-firebase/app": "^22.4.0", "@react-native-firebase/app": "^22.4.0",
"@react-native-firebase/database": "^22.4.0", "@react-native-firebase/database": "^22.4.0",
"@react-native-firebase/messaging": "^22.2.1", "@react-native-firebase/messaging": "^22.2.1",
"@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14", "@react-navigation/native": "^7.0.14",
"@reduxjs/toolkit": "^2.7.0", "@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", "@types/formidable": "^3.4.5",
"axios": "^1.8.4", "axios": "^1.8.4",
"crypto-es": "^2.1.0", "crypto-es": "^2.1.0",
@@ -97,13 +101,14 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@react-native-community/cli-platform-ios": "^18.0.0", "@react-native-community/cli-platform-ios": "^18.0.0",
"@testing-library/react-native": "^13.3.3",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/react": "~19.0.10", "@types/react": "~19.0.10",
"@types/react-test-renderer": "^18.3.0", "@types/react-test-renderer": "^18.3.0",
"jest": "^29.2.1", "jest": "^29.2.1",
"jest-expo": "~53.0.5", "jest-expo": "~53.0.5",
"react-test-renderer": "18.3.1", "react-test-renderer": "19.0.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"private": true, "private": true,

View File

@@ -2,6 +2,7 @@ import { ConstEnv } from '@/constants/ConstEnv';
import { apiRegisteredToken, apiUnregisteredToken } from '@/lib/api'; import { apiRegisteredToken, apiUnregisteredToken } from '@/lib/api';
import { getToken } from '@/lib/useNotification'; import { getToken } from '@/lib/useNotification';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { useQueryClient } from '@tanstack/react-query';
import CryptoES from "crypto-es"; import CryptoES from "crypto-es";
import { router } from "expo-router"; import { router } from "expo-router";
import { createContext, MutableRefObject, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { createContext, MutableRefObject, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react';
@@ -30,6 +31,7 @@ export function useAuthSession() {
export default function AuthProvider({ children }: { children: ReactNode }): ReactNode { export default function AuthProvider({ children }: { children: ReactNode }): ReactNode {
const tokenRef = useRef<string | null>(null); const tokenRef = useRef<string | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const queryClient = useQueryClient();
useEffect(() => { useEffect(() => {
(async (): Promise<void> => { (async (): Promise<void> => {
@@ -52,12 +54,11 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
const signIn = useCallback(async (token: string) => { const signIn = useCallback(async (token: string) => {
const hasil = await decryptToken(String(token)) const hasil = await decryptToken(String(token))
// const permission = await requestPermission()
const permissionStorage = await AsyncStorage.getItem('@notification_permission') const permissionStorage = await AsyncStorage.getItem('@notification_permission')
if (permissionStorage === "true") { if (permissionStorage === "true") {
const tokenDevice = await getToken() const tokenDevice = await getToken()
try { try {
const register = await apiRegisteredToken({ user: hasil, token: String(tokenDevice) }) await apiRegisteredToken({ user: hasil, token: String(tokenDevice) })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {
@@ -67,7 +68,7 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
return true return true
} }
} else { } else {
const register = await apiRegisteredToken({ user: hasil, token: "" }) await apiRegisteredToken({ user: hasil, token: "" })
await AsyncStorage.setItem('@token', token); await AsyncStorage.setItem('@token', token);
tokenRef.current = token; tokenRef.current = token;
router.replace('/home') router.replace('/home')
@@ -79,7 +80,7 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
const hasil = await decryptToken(String(tokenRef.current)) const hasil = await decryptToken(String(tokenRef.current))
// if (Platform.OS === 'android') { // if (Platform.OS === 'android') {
const token = await getToken() const token = await getToken()
const response = await apiUnregisteredToken({ user: hasil, token: String(token) }) await apiUnregisteredToken({ user: hasil, token: String(token) })
// }else{ // }else{
// const response = await apiUnregisteredToken({ user: hasil, token: "" }) // const response = await apiUnregisteredToken({ user: hasil, token: "" })
// } // }
@@ -88,6 +89,7 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
} finally { } finally {
await AsyncStorage.setItem('@token', ''); await AsyncStorage.setItem('@token', '');
tokenRef.current = null; tokenRef.current = null;
queryClient.clear();
router.replace('/'); router.replace('/');
} }
}, []); }, []);

View File

@@ -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 (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: asyncStoragePersister }}
>
{children}
</PersistQueryClientProvider>
);
}