From 64aaafa2bee2fe14afd92ba6e3fe161a1e93d782 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Wed, 18 Feb 2026 15:45:45 +0800 Subject: [PATCH] upd: setting Deskripsi: - buat halaman setting - isinya edit profile, ganti tema aplikasi, nonaktifkan notifikasi, sign out No Issues --- app/(application)/_layout.tsx | 12 ++ app/(application)/edit-profile.tsx | 48 ++++--- app/(application)/profile.tsx | 92 +------------- app/(application)/setting/index.tsx | 186 ++++++++++++++++++++++++++++ components/buttonSetting.tsx | 33 +++++ lib/useNotification.ts | 27 +++- 6 files changed, 294 insertions(+), 104 deletions(-) create mode 100644 app/(application)/setting/index.tsx create mode 100644 components/buttonSetting.tsx diff --git a/app/(application)/_layout.tsx b/app/(application)/_layout.tsx index 57cced1..5d77e94 100644 --- a/app/(application)/_layout.tsx +++ b/app/(application)/_layout.tsx @@ -112,6 +112,18 @@ export default function RootLayout() { ) }} /> + { router.back() }} />, + title: 'Pengaturan', + headerTitleAlign: 'center', + // headerRight: () => + header: () => ( + router.back()} + /> + ) + }} /> { router.back() }} />, title: 'Anggota', diff --git a/app/(application)/edit-profile.tsx b/app/(application)/edit-profile.tsx index 6b32dda..3157aa4 100644 --- a/app/(application)/edit-profile.tsx +++ b/app/(application)/edit-profile.tsx @@ -1,4 +1,4 @@ -import ButtonBackHeader from "@/components/buttonBackHeader"; +import AppHeader from "@/components/AppHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import { InputForm } from "@/components/inputForm"; import ModalSelect from "@/components/modalSelect"; @@ -219,24 +219,40 @@ export default function EditProfile() { ( - { - router.back(); - }} - /> - ), + // headerLeft: () => ( + // { + // router.back(); + // }} + // /> + // ), headerTitle: "Edit Profile", headerTitleAlign: "center", - headerRight: () => ( - { - handleEdit() - }} + header: () => ( + router.back()} + right={ + { + handleEdit() + }} + /> + } /> - ), + ) + // headerRight: () => ( + // { + // handleEdit() + // }} + // /> + // ), }} /> state.entities) const [error, setError] = useState(false) const [preview, setPreview] = useState(false) - const [showThemeModal, setShowThemeModal] = useState(false) - const [showLogoutModal, setShowLogoutModal] = useState(false) - - const ThemeOption = ({ label, value, icon }: { label: string, value: 'light' | 'dark' | 'system', icon: string }) => ( - { - setTheme(value); - setShowThemeModal(false); - }} - > - - - {label} - - {theme === value && } - - ); return ( @@ -54,9 +33,9 @@ export default function Profile() { onPressLeft={() => router.back()} right={ } + item={} onPress={() => { - setShowLogoutModal(true) + router.push('/setting') }} /> } @@ -81,31 +60,8 @@ export default function Profile() { {entities.role} - - Tampilan - - - setShowThemeModal(true)} - style={[Styles.wrapItemBorderAll, { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderColor: colors.icon + '40', backgroundColor: colors.background }]} - > - - - Tema Aplikasi - - - - {theme === 'light' ? 'Terang' : theme === 'dark' ? 'Gelap' : 'Sistem'} - - - - - - + Informasi - { - entities.idUserRole != "developer" && { router.push('/edit-profile') }} style={[Styles.textLink]}>Edit - } {/* Note: ItemDetailMember might need updates to support dynamic colors if it uses default text colors */} @@ -118,29 +74,6 @@ export default function Profile() { - setShowThemeModal(false)} - > - setShowThemeModal(false)}> - - - Pilih Tema - setShowThemeModal(false)}> - - - - - - - - - - - - setPreview(false)} doubleTapToZoomEnabled /> - - { - setShowLogoutModal(false) - signOut() - }} - onCancel={() => setShowLogoutModal(false)} - confirmText="Keluar" - cancelText="Batal" - /> ) } \ No newline at end of file diff --git a/app/(application)/setting/index.tsx b/app/(application)/setting/index.tsx new file mode 100644 index 0000000..0a25d8e --- /dev/null +++ b/app/(application)/setting/index.tsx @@ -0,0 +1,186 @@ +import ModalConfirmation from "@/components/ModalConfirmation"; +import Text from "@/components/Text"; +import ButtonSetting from "@/components/buttonSetting"; +import DrawerBottom from "@/components/drawerBottom"; +import Styles from "@/constants/Styles"; +import { apiRegisteredToken, apiUnregisteredToken } from "@/lib/api"; +import { checkPermission, getToken, openSettings, requestPermission } from "@/lib/useNotification"; +import { useAuthSession } from "@/providers/AuthProvider"; +import { useTheme } from "@/providers/ThemeProvider"; +import { Feather, Ionicons } from "@expo/vector-icons"; +import { router } from "expo-router"; +import { useCallback, useEffect, useState } from "react"; +import { AppState, AppStateStatus, Pressable, View } from "react-native"; +import { useSelector } from "react-redux"; + +export default function ListSetting() { + const { theme, setTheme, colors } = useTheme() + const { signOut } = useAuthSession() + const [isNotificationEnabled, setIsNotificationEnabled] = useState(null); + const entities = useSelector((state: any) => state.entities) + const [modalVisible, setModalVisible] = useState(false); + const [modalConfig, setModalConfig] = useState({ + title: '', + message: '', + confirmText: 'Buka Pengaturan', + onConfirm: () => { } + }); + + const [showLogoutModal, setShowLogoutModal] = useState(false) + const [showThemeModal, setShowThemeModal] = useState(false) + + const registerToken = async () => { + try { + const token = await getToken(); + if (token) { + await apiRegisteredToken({ user: entities.id, token }); + } + } catch (error) { + console.warn('Error registering token:', error); + } + }; + + const unregisterToken = async () => { + try { + const token = await getToken(); + if (token) { + await apiUnregisteredToken({ user: entities.id, token }); + } + } catch (error) { + console.warn('Error unregistering token:', error); + } + }; + + const checkNotif = useCallback(async () => { + const status = await checkPermission(); + setIsNotificationEnabled((prev) => { + if (prev === false && status === true) { + registerToken(); + } else if (prev === true && status === false) { + unregisterToken(); + } + return !!status; + }); + }, [entities.id]); + + useEffect(() => { + checkNotif(); + + const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => { + if (nextAppState === 'active') { + checkNotif(); + } + }); + + return () => { + subscription.remove(); + }; + }, [checkNotif]); + + const handleToggleNotif = async () => { + if (isNotificationEnabled) { + setModalConfig({ + title: "Matikan Notifikasi?", + message: "Anda akan diarahkan ke pengaturan sistem untuk mematikan notifikasi.", + confirmText: "Buka Pengaturan", + onConfirm: () => { + setModalVisible(false); + openSettings(); + } + }); + setModalVisible(true); + } else { + const granted = await requestPermission(); + if (granted) { + setIsNotificationEnabled(true); + registerToken(); + } else { + setModalConfig({ + title: "Aktifkan Notifikasi?", + message: "Izin notifikasi tidak diberikan. Buka pengaturan sistem untuk mengaktifkannya?", + confirmText: "Buka Pengaturan", + onConfirm: () => { + setModalVisible(false); + openSettings(); + } + }); + setModalVisible(true); + } + } + }; + + const ThemeOption = ({ label, value, icon }: { label: string, value: 'light' | 'dark' | 'system', icon: string }) => ( + { + setTheme(value); + }} + > + + + {label} + + {theme === value && } + + ); + + return ( + + + { + entities.idUserRole != "developer" && + } + onPress={() => { router.push('/edit-profile') }} + /> + } + } + onPress={() => setShowThemeModal(true)} + value={theme === 'light' ? 'Terang' : theme === 'dark' ? 'Gelap' : 'Sistem'} + /> + } + onPress={handleToggleNotif} + value={isNotificationEnabled === null ? 'Memuat...' : isNotificationEnabled ? 'Aktif' : 'Nonaktif'} + /> + } + onPress={() => setShowLogoutModal(true)} + borderBottom={false} + /> + + + setModalVisible(false)} + /> + + { + setShowLogoutModal(false) + signOut() + }} + onCancel={() => setShowLogoutModal(false)} + confirmText="Keluar" + cancelText="Batal" + /> + + + + + + + ) +} \ No newline at end of file diff --git a/components/buttonSetting.tsx b/components/buttonSetting.tsx new file mode 100644 index 0000000..419b227 --- /dev/null +++ b/components/buttonSetting.tsx @@ -0,0 +1,33 @@ +import Styles from "@/constants/Styles"; +import { useTheme } from "@/providers/ThemeProvider"; +import { ReactNode } from "react"; +import { Pressable, View } from "react-native"; +import Text from "./Text"; + +type Props = { + title: string + onPress?: () => void, + icon?: ReactNode, + borderBottom?: boolean + value?: string +} + +export default function ButtonSetting({ title, onPress, icon, borderBottom = true, value }: Props) { + const { colors } = useTheme(); + return ( + + + + {icon} + {title} + + {value && {value}} + + + ) + +} \ No newline at end of file diff --git a/lib/useNotification.ts b/lib/useNotification.ts index 4af6e44..7a9ac7c 100644 --- a/lib/useNotification.ts +++ b/lib/useNotification.ts @@ -6,8 +6,7 @@ import { } from '@react-native-firebase/messaging'; import * as Notifications from 'expo-notifications'; import { useEffect } from 'react'; -import { PermissionsAndroid, Platform } from 'react-native'; - +import { Linking, PermissionsAndroid, Platform } from 'react-native'; import { ConstEnv } from '@/constants/ConstEnv'; const RNfirebaseConfig = { @@ -38,6 +37,30 @@ const initializeFirebase = async () => { } }; +export const checkPermission = async () => { + try { + if (Platform.OS === 'android') { + return await PermissionsAndroid.check( + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS + ); + } else if (Platform.OS === 'ios') { + const { status } = await Notifications.getPermissionsAsync(); + return status === 'granted'; + } + } catch (err) { + console.warn('Error checking notification permissions:', err); + return false; + } +}; + +export const openSettings = () => { + if (Platform.OS === 'ios') { + Linking.openURL('app-settings:'); + } else { + Linking.openSettings(); + } +}; + export const requestPermission = async () => { try { if (Platform.OS === 'android') {