diff --git a/app/(application)/_layout.tsx b/app/(application)/_layout.tsx
index 2795722..5f02572 100644
--- a/app/(application)/_layout.tsx
+++ b/app/(application)/_layout.tsx
@@ -9,16 +9,18 @@ import HeaderRightPositionList from "@/components/position/headerRightPositionLi
import HeaderRightProjectList from "@/components/project/headerProjectList";
import Text from "@/components/Text";
import ToastCustom from "@/components/toastCustom";
-import { apiReadOneNotification } from "@/lib/api";
+import ModalUpdateMaintenance from "@/components/ModalUpdateMaintenance";
+import { apiGetVersion, apiReadOneNotification } from "@/lib/api";
import { pushToPage } from "@/lib/pushToPage";
import store from "@/lib/store";
import { useAuthSession } from "@/providers/AuthProvider";
import AsyncStorage from "@react-native-async-storage/async-storage";
+import Constants from "expo-constants";
import { getApp } from "@react-native-firebase/app";
import { getMessaging, onMessage } from "@react-native-firebase/messaging";
import { Redirect, router, Stack, usePathname } from "expo-router";
-import { StatusBar } from 'expo-status-bar';
-import { useEffect } from "react";
+import { StatusBar } from 'expo-status-bar';
+import { useEffect, useState } from "react";
import { Easing, Notifier, NotifierComponents } from 'react-native-notifier';
import { Provider } from "react-redux";
import { useTheme } from "@/providers/ThemeProvider";
@@ -27,6 +29,81 @@ export default function RootLayout() {
const { token, decryptToken, isLoading } = useAuthSession()
const pathname = usePathname()
const { colors } = useTheme()
+ const [modalUpdateMaintenance, setModalUpdateMaintenance] = useState(false)
+ const [modalType, setModalType] = useState<'update' | 'maintenance'>('update')
+ const [isForceUpdate, setIsForceUpdate] = useState(false)
+ const [updateMessage, setUpdateMessage] = useState('')
+
+ const currentVersion = Constants.expoConfig?.version ?? '0.0.0'
+
+ const compareVersions = (v1: string, v2: string) => {
+ const parts1 = v1.split('.').map(Number);
+ const parts2 = v2.split('.').map(Number);
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
+ const p1 = parts1[i] || 0;
+ const p2 = parts2[i] || 0;
+ if (p1 < p2) return -1;
+ if (p1 > p2) return 1;
+ }
+ return 0;
+ };
+
+ useEffect(() => {
+ const checkVersion = async () => {
+ try {
+ const response = await apiGetVersion();
+ console.log('response',response)
+ if (response.success && response.data) {
+ const maintenance = response.data.find((item: any) => item.id === 'mobile_maintenance')?.value === 'true';
+ const latestVersion = response.data.find((item: any) => item.id === 'mobile_latest_version')?.value || '0.0.0';
+ const minVersion = response.data.find((item: any) => item.id === 'mobile_minimum_version')?.value || '0.0.0';
+ const message = response.data.find((item: any) => item.id === 'mobile_message_update')?.value || '';
+
+ if (maintenance) {
+ setModalType('maintenance');
+ setModalUpdateMaintenance(true);
+ setIsForceUpdate(true);
+ return;
+ }
+
+ if (compareVersions(currentVersion, minVersion) === -1) {
+ setModalType('update');
+ setIsForceUpdate(true);
+ setUpdateMessage(message);
+ setModalUpdateMaintenance(true);
+ } else if (compareVersions(currentVersion, latestVersion) === -1) {
+ // Check if this soft update version was already dismissed
+ const dismissedVersion = await AsyncStorage.getItem('dismissed_update_version');
+ if (dismissedVersion !== latestVersion) {
+ setModalType('update');
+ setIsForceUpdate(false);
+ setUpdateMessage(message);
+ setModalUpdateMaintenance(true);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Failed to check version:', error);
+ }
+ };
+
+ checkVersion();
+ }, [currentVersion]);
+
+ const handleDismissUpdate = async () => {
+ if (!isForceUpdate) {
+ try {
+ const response = await apiGetVersion();
+ const latestVersion = response.data.find((item: any) => item.id === 'mobile_latest_version')?.value;
+ if (latestVersion) {
+ await AsyncStorage.setItem('dismissed_update_version', latestVersion);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ setModalUpdateMaintenance(false);
+ }
+ }
async function handleReadNotification(id: string, category: string, idContent: string, title: string) {
try {
@@ -246,6 +323,13 @@ export default function RootLayout() {
+
)
}
diff --git a/components/ModalUpdateMaintenance.tsx b/components/ModalUpdateMaintenance.tsx
new file mode 100644
index 0000000..c278d98
--- /dev/null
+++ b/components/ModalUpdateMaintenance.tsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import { Modal, View, Image, TouchableOpacity, BackHandler, Platform } from 'react-native';
+import { useTheme } from '@/providers/ThemeProvider';
+import Text from './Text';
+import * as Linking from 'expo-linking';
+import Styles from '@/constants/Styles';
+
+interface ModalUpdateMaintenanceProps {
+ visible: boolean;
+ type: 'update' | 'maintenance';
+ isForceUpdate?: boolean;
+ onDismiss?: () => void;
+ appName?: string;
+ customDescription?: string;
+ androidStoreUrl?: string;
+ iosStoreUrl?: string;
+}
+
+const ModalUpdateMaintenance: React.FC = ({
+ visible,
+ type,
+ isForceUpdate = false,
+ onDismiss,
+ appName = 'Desa+',
+ customDescription,
+ androidStoreUrl = 'https://play.google.com/store/apps/details?id=mobiledarmasaba.app',
+ iosStoreUrl = 'https://apps.apple.com/id/app/desa-plus-desa/id6752375538'
+}) => {
+ const { colors } = useTheme();
+
+ const handleUpdate = () => {
+ const storeUrl = Platform.OS === 'ios' ? iosStoreUrl : androidStoreUrl;
+ Linking.openURL(storeUrl);
+ };
+
+ const handleCloseApp = () => {
+ // For maintenance mode, we might want to exit the app or just keep the modal.
+ // React Native doesn't have a built-in "exit" for iOS, but for Android:
+ if (Platform.OS === 'android') {
+ BackHandler.exitApp();
+ }
+ };
+
+ return (
+ {
+ if (!isForceUpdate && type === 'update') {
+ onDismiss?.();
+ }
+ }}
+ >
+
+ {/* Background decorative circles could be added here if we had SVGs or images */}
+
+
+
+
+
+
+
+
+ {type === 'update' ? 'Update Tersedia' : 'Perbaikan'}
+
+
+ {customDescription ? customDescription :
+ (type === 'update'
+ ? `Versi terbaru dari ${appName} tersedia di Store. Silakan buka Store untuk menginstalnya.`
+ : 'Aplikasi saat ini sedang dalam pemeliharaan untuk peningkatan sistem. Silakan coba kembali beberapa saat lagi.')}
+
+
+
+
+ {type === 'update' ? (
+ <>
+
+
+ Update
+
+
+
+ {!isForceUpdate && (
+
+ Nanti
+
+ )}
+ >
+ ) : (
+ <>>
+ //
+ //
+ // {Platform.OS === 'android' ? 'Close App' : 'Please check back later'}
+ //
+ //
+ )}
+
+
+
+
+ );
+};
+
+export default ModalUpdateMaintenance;
diff --git a/components/auth/viewLogin.tsx b/components/auth/viewLogin.tsx
index 8173817..64bf6f2 100644
--- a/components/auth/viewLogin.tsx
+++ b/components/auth/viewLogin.tsx
@@ -22,7 +22,7 @@ export default function ViewLogin({ onValidate }: Props) {
const [disableLogin, setDisableLogin] = useState(true)
const [phone, setPhone] = useState('')
const { signIn, encryptToken } = useAuthSession();
- const { colors, theme } = useTheme();
+ const { colors, activeTheme } = useTheme();
const handleCheckPhone = async () => {
try {
@@ -52,11 +52,11 @@ export default function ViewLogin({ onValidate }: Props) {
return (
-
+
diff --git a/components/auth/viewVerification.tsx b/components/auth/viewVerification.tsx
index 0dc96bf..e9a213d 100644
--- a/components/auth/viewVerification.tsx
+++ b/components/auth/viewVerification.tsx
@@ -21,7 +21,7 @@ export default function ViewVerification({ phone, otp }: Props) {
const [value, setValue] = useState('');
const [otpFix, setOtpFix] = useState(otp)
const { signIn, encryptToken } = useAuthSession();
- const { colors, theme } = useTheme();
+ const { colors, activeTheme } = useTheme();
const login = async () => {
const valueUser = await AsyncStorage.getItem('user');
@@ -59,11 +59,11 @@ export default function ViewVerification({ phone, otp }: Props) {
return (
-
+
diff --git a/components/eventItem.tsx b/components/eventItem.tsx
index e780786..10117cc 100644
--- a/components/eventItem.tsx
+++ b/components/eventItem.tsx
@@ -13,20 +13,20 @@ type Props = {
}
export default function EventItem({ category, title, user, jamAwal, jamAkhir, onPress }: Props) {
- const { theme, colors } = useTheme();
+ const { activeTheme, colors } = useTheme();
const getBackgroundColor = (cat: 'purple' | 'orange') => {
- if (theme === 'dark') {
+ if (activeTheme === 'dark') {
return cat === 'orange' ? '#547792' : '#1D546D';
}
return cat === 'orange' ? '#D6E6F2' : '#A9B5DF';
};
const getStickColor = (cat: 'purple' | 'orange') => {
- if (theme === 'dark') {
+ if (activeTheme === 'dark') {
return cat === 'orange' ? '#94B4C1' : '#5F9598';
}
- return cat === 'orange' ? '#F5F5F5' : '#7886C7' ;
+ return cat === 'orange' ? '#F5F5F5' : '#7886C7';
};
return (
diff --git a/constants/Styles.ts b/constants/Styles.ts
index 5895d8f..759d4f9 100644
--- a/constants/Styles.ts
+++ b/constants/Styles.ts
@@ -885,7 +885,87 @@ const Styles = StyleSheet.create({
borderRadius: 5,
alignItems: 'center',
gap: 10,
- }
+ },
+ modalUpdateContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 30,
+ overflow: 'hidden',
+ },
+ modalUpdateDecorativeCircle1: {
+ position: 'absolute',
+ width: 300,
+ height: 300,
+ borderRadius: 150,
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
+ top: -50,
+ right: -50,
+ },
+ modalUpdateDecorativeCircle2: {
+ position: 'absolute',
+ width: 200,
+ height: 200,
+ borderRadius: 100,
+ backgroundColor: 'rgba(255, 255, 255, 0.03)',
+ bottom: -30,
+ left: -30,
+ },
+ modalUpdateContent: {
+ width: '100%',
+ alignItems: 'flex-start',
+ zIndex: 1,
+ },
+ modalUpdateLogo: {
+ width: 200,
+ height: 100,
+ marginBottom: 40,
+ alignSelf: 'center',
+ },
+ modalUpdateTextContainer: {
+ marginBottom: 40,
+ },
+ modalUpdateTitle: {
+ fontSize: 32,
+ fontWeight: 'bold',
+ color: 'white',
+ marginBottom: 20,
+ lineHeight: 38,
+ },
+ modalUpdateDescription: {
+ fontSize: 16,
+ color: 'white',
+ lineHeight: 24,
+ },
+ modalUpdateButtonContainer: {
+ width: '100%',
+ alignItems: 'center',
+ },
+ modalUpdatePrimaryButton: {
+ width: '100%',
+ paddingVertical: 15,
+ borderRadius: 12,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 15,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.2,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ modalUpdatePrimaryButtonText: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ modalUpdateSecondaryButton: {
+ paddingVertical: 10,
+ },
+ modalUpdateSecondaryButtonText: {
+ fontSize: 16,
+ color: 'white',
+ fontWeight: '500',
+ },
})
export default Styles;
\ No newline at end of file
diff --git a/lib/api.ts b/lib/api.ts
index 9ce5413..ad33ea3 100644
--- a/lib/api.ts
+++ b/lib/api.ts
@@ -758,4 +758,9 @@ export const apiGetNotification = async ({ user, page }: { user: string, page?:
export const apiReadOneNotification = async (data: { user: string, id: string }) => {
const response = await api.put(`/mobile/home/notification`, data)
return response.data;
+};
+
+export const apiGetVersion = async () => {
+ const response = await api.get(`mobile/version`);
+ return response.data;
};
\ No newline at end of file