upd: upd version

Deskripsi:
- tampilan jika update versi terbaru atau sedang maintenance

NO Issues
This commit is contained in:
2026-02-24 15:51:29 +08:00
parent 449f6f96cc
commit 214a243e44
7 changed files with 306 additions and 16 deletions

View File

@@ -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() {
</Stack>
<StatusBar style={'light'} translucent={false} backgroundColor="black" />
<ToastCustom />
<ModalUpdateMaintenance
visible={modalUpdateMaintenance}
type={modalType}
isForceUpdate={isForceUpdate}
customDescription={updateMessage}
onDismiss={handleDismissUpdate}
/>
</Provider>
)
}

View File

@@ -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<ModalUpdateMaintenanceProps> = ({
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 (
<Modal
visible={visible}
animationType="fade"
transparent={false}
onRequestClose={() => {
if (!isForceUpdate && type === 'update') {
onDismiss?.();
}
}}
>
<View style={[Styles.modalUpdateContainer, { backgroundColor: colors.primary }]}>
{/* Background decorative circles could be added here if we had SVGs or images */}
<View style={Styles.modalUpdateDecorativeCircle1} />
<View style={Styles.modalUpdateDecorativeCircle2} />
<View style={Styles.modalUpdateContent}>
<Image
source={require('@/assets/images/logo-dark.png')}
style={Styles.modalUpdateLogo}
resizeMode="contain"
/>
<View style={Styles.modalUpdateTextContainer}>
<Text style={Styles.modalUpdateTitle}>
{type === 'update' ? 'Update Tersedia' : 'Perbaikan'}
</Text>
<Text style={[Styles.modalUpdateDescription, { opacity: 0.8 }]}>
{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.')}
</Text>
</View>
<View style={Styles.modalUpdateButtonContainer}>
{type === 'update' ? (
<>
<TouchableOpacity
style={[Styles.modalUpdatePrimaryButton, { backgroundColor: 'white' }]}
onPress={handleUpdate}
activeOpacity={0.8}
>
<Text style={[Styles.modalUpdatePrimaryButtonText, { color: colors.primary }]}>
Update
</Text>
</TouchableOpacity>
{!isForceUpdate && (
<TouchableOpacity
style={Styles.modalUpdateSecondaryButton}
onPress={onDismiss}
activeOpacity={0.7}
>
<Text style={Styles.modalUpdateSecondaryButtonText}>Nanti</Text>
</TouchableOpacity>
)}
</>
) : (
<></>
// <TouchableOpacity
// style={[Styles.modalUpdatePrimaryButton, { backgroundColor: 'white' }]}
// onPress={handleCloseApp}
// activeOpacity={0.8}
// >
// <Text style={[Styles.modalUpdatePrimaryButtonText, { color: colors.primary }]}>
// {Platform.OS === 'android' ? 'Close App' : 'Please check back later'}
// </Text>
// </TouchableOpacity>
)}
</View>
</View>
</View>
</Modal>
);
};
export default ModalUpdateMaintenance;

View File

@@ -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 (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
<StatusBar style={theme === 'dark' ? 'light' : 'dark'} translucent={false} backgroundColor={colors.background} />
<StatusBar style={activeTheme === 'dark' ? 'light' : 'dark'} translucent={false} backgroundColor={colors.background} />
<View style={[Styles.p20, Styles.h100]}>
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>
<Image
source={theme === 'dark' ? require("../../assets/images/logo-dark.png") : require("../../assets/images/logo.png")}
source={activeTheme === 'dark' ? require("../../assets/images/logo-dark.png") : require("../../assets/images/logo.png")}
style={[{ width: 300, height: 150 }]}
width={270}
height={110}
@@ -82,7 +82,7 @@ export default function ViewLogin({ onValidate }: Props) {
<View style={{ flex: 1 }} />
<View style={{ alignItems: 'center' }}>
<Image
source={theme === 'dark' ? require("../../assets/images/cogniti-dark.png") : require("../../assets/images/cogniti-light.png")}
source={activeTheme === 'dark' ? require("../../assets/images/cogniti-dark.png") : require("../../assets/images/cogniti-light.png")}
style={{ width: 86, height: 27 }}
resizeMode="contain"
/>

View File

@@ -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 (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
<StatusBar style={theme === 'dark' ? 'light' : 'dark'} translucent={false} backgroundColor={colors.background} />
<StatusBar style={activeTheme === 'dark' ? 'light' : 'dark'} translucent={false} backgroundColor={colors.background} />
<View style={[Styles.p20, Styles.h100]} >
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>
<Image
source={theme === 'dark' ? require("../../assets/images/logo-dark.png") : require("../../assets/images/logo.png")}
source={activeTheme === 'dark' ? require("../../assets/images/logo-dark.png") : require("../../assets/images/logo.png")}
style={[{ width: 300, height: 150 }]}
width={270}
height={110}
@@ -101,7 +101,7 @@ export default function ViewVerification({ phone, otp }: Props) {
<View style={{ flex: 1 }} />
<View style={[{ alignItems: 'center' }]}>
<Image
source={theme === 'dark' ? require("../../assets/images/cogniti-dark.png") : require("../../assets/images/cogniti-light.png")}
source={activeTheme === 'dark' ? require("../../assets/images/cogniti-dark.png") : require("../../assets/images/cogniti-light.png")}
style={{ width: 86, height: 27 }}
resizeMode="contain"
/>

View File

@@ -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 (

View File

@@ -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;

View File

@@ -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;
};