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