Compare commits
10 Commits
amalia/21-
...
join
| Author | SHA1 | Date | |
|---|---|---|---|
| e2ffef1085 | |||
| cb2a57ee8e | |||
| f3b677f847 | |||
| 6ffe599ad0 | |||
| 4a92def490 | |||
| 0bad792ce8 | |||
| 6c9623954c | |||
| f39d5a4c85 | |||
| 2d86a77a48 | |||
| 887e787a99 |
43
CLAUDE.md
43
CLAUDE.md
@@ -26,47 +26,8 @@ bunx jest path/to/test.tsx --no-coverage
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Routing (Expo Router — file-based)
|
See @docs/ARCHITECTURE.md
|
||||||
|
|
||||||
- `app/index.tsx` — Login/splash (public); OTP verification is handled inline via `components/auth/viewVerification.tsx` (not a separate route)
|
|
||||||
- `app/(application)/` — All authenticated screens; Expo Router enforces auth guard here
|
|
||||||
- Deep-link navigation is handled by `lib/pushToPage.ts`, which maps notification payloads to routes
|
|
||||||
|
|
||||||
### State Management (three layers)
|
|
||||||
|
|
||||||
1. **Context** (`providers/`) — Auth (token encryption/decryption via CryptoES.AES), Theme (light/dark, persisted to AsyncStorage), and React Query client
|
|
||||||
2. **Redux Toolkit** (`lib/store.ts` + slices) — Feature-level state for CRUD operations. Slices follow a naming pattern: `*Slice.ts` for read state, `*Update.ts`/`*Create.ts` for mutation state
|
|
||||||
3. **TanStack React Query** — All server data fetching; configured with 5-min stale time, 24-hour cache retention, 2 retries, and AsyncStorage persistence for offline support
|
|
||||||
|
|
||||||
### API Layer (`lib/api.ts`)
|
|
||||||
|
|
||||||
Single 773-line file defining 50+ Axios-based endpoints. The Axios instance reads `baseURL` from `Constants.expoConfig.extra.URL_API` (set in `.env` via `app.config.js`). Authentication uses Bearer tokens in headers. File uploads use `FormData` with `multipart/form-data`.
|
|
||||||
|
|
||||||
Three separate backend services are integrated:
|
|
||||||
- **REST API** (axios) — main business logic
|
|
||||||
- **WhatsApp server** — OTP delivery (separate token in `.env`)
|
|
||||||
- **Firebase** — real-time database (`lib/firebaseDatabase.ts`) and push notifications (`lib/useNotification.ts`, `lib/registerForPushNotificationsAsync.ts`)
|
|
||||||
|
|
||||||
### Providers Initialization Order
|
|
||||||
|
|
||||||
`app/_layout.tsx` wraps the app in: `ErrorBoundary` → `NotifierWrapper` → `ThemeProvider` → `QueryProvider` → `AuthProvider` → navigation stack. Redux `store` is provided inside `app/(application)/_layout.tsx`, not at the root.
|
|
||||||
|
|
||||||
### Error Boundary
|
|
||||||
|
|
||||||
`components/ErrorBoundary.tsx` is a class component (required by React) wrapping the entire app. It uses React Native's built-in `Text` — **do not replace it with the custom `components/Text.tsx`** as that pulls in `ThemeProvider` → `AsyncStorage`, which breaks Jest tests.
|
|
||||||
|
|
||||||
Tests for ErrorBoundary live in `__tests__/ErrorBoundary-test.tsx` and use `@testing-library/react-native`.
|
|
||||||
|
|
||||||
## Key Conventions
|
## Key Conventions
|
||||||
|
|
||||||
**Imports:** Use `@/` alias (maps to project root, configured in `tsconfig.json`). Never use relative paths like `../../`.
|
See @docs/CONVENTIONS.md
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ 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";
|
||||||
|
|
||||||
@@ -55,6 +55,26 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}, [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)
|
||||||
// Invalidate all queries related to the home screen
|
// Invalidate all queries related to the home screen
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ export default class ErrorBoundary extends Component<Props, State> {
|
|||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>Terjadi Kesalahan</Text>
|
<Text style={styles.title}>Terjadi Kesalahan</Text>
|
||||||
<Text style={styles.message}>
|
<Text style={styles.message}>
|
||||||
{this.state.error?.message ?? 'Kesalahan tidak diketahui'}
|
Silahkan coba lagi beberapa saat lagi atau hubungi admin untuk bantuan.
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity style={styles.button} onPress={this.handleReset}>
|
<TouchableOpacity style={styles.button} onPress={this.handleReset}>
|
||||||
<Text style={styles.buttonText}>Coba Lagi</Text>
|
<Text style={styles.buttonText}>Coba Lagi</Text>
|
||||||
|
|||||||
32
docs/ARCHITECTURE.md
Normal file
32
docs/ARCHITECTURE.md
Normal 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
13
docs/CONVENTIONS.md
Normal 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.
|
||||||
@@ -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> => {
|
||||||
@@ -87,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('/');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user