Compare commits

...

8 Commits

Author SHA1 Message Date
868b712fbb upd: notifikasi
Deskripsi:
- belom selesai notifikasi

No Issues
2026-03-03 16:44:02 +08:00
a53b99b39d version 2026-03-03 10:57:55 +08:00
25d521f013 Merge pull request 'amalia/26-feb-26' (#32) from amalia/26-feb-26 into join
Reviewed-on: #32
2026-02-26 17:48:06 +08:00
aee0823cb1 upd: fiksasi 2026-02-26 17:42:33 +08:00
2a0e1f4c1f upd: bun lock 2026-02-26 14:54:51 +08:00
ef08c821fa Merge pull request 'upd: fiksasi' (#30) from amalia/25-feb-26 into join
Reviewed-on: #30
2026-02-25 16:09:31 +08:00
fd5d582092 upd: fiksasi
Deskripsi:
-tampilan

No Issues
2026-02-25 16:07:17 +08:00
7729dc38f8 Merge pull request 'amalia/24-feb-26' (#29) from amalia/24-feb-26 into join
Reviewed-on: #29
2026-02-24 18:01:29 +08:00
19 changed files with 2697 additions and 105 deletions

View File

@@ -4,7 +4,7 @@ export default {
expo: {
name: "Desa+",
slug: "mobile-darmasaba",
version: "2.0.5", // Versi aplikasi (App Store)
version: "2.1.0", // Versi aplikasi (App Store)
jsEngine: "jsc",
orientation: "portrait",
icon: "./assets/images/logo-icon-small.png",
@@ -14,7 +14,7 @@ export default {
ios: {
supportsTablet: true,
bundleIdentifier: "mobiledarmasaba.app",
buildNumber: "7",
buildNumber: "8",
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
CFBundleDisplayName: "Desa+"
@@ -23,7 +23,7 @@ export default {
},
android: {
package: "mobiledarmasaba.app",
versionCode: 15,
versionCode: 16,
adaptiveIcon: {
foregroundImage: "./assets/images/logo-icon-small.png",
backgroundColor: "#ffffff"

View File

@@ -252,7 +252,7 @@ export default function DetailAnnouncement() {
{dataFile.map((item, index) => (
<BorderBottomItem
key={`${item.id}-${index}`}
borderType="bottom"
borderType={index === dataFile.length - 1 ? 'none' : 'bottom'}
icon={<MaterialCommunityIcons
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
size={25}

View File

@@ -78,7 +78,6 @@ export default function CreateAnnouncement() {
async function handleCreate() {
try {
setLoading(true)
console.log('jalan')
const hasil = await decryptToken(String(token?.current))
const fd = new FormData()
@@ -91,7 +90,7 @@ export default function CreateAnnouncement() {
}
fd.append("data", JSON.stringify(
{ user: 'apaya', groups: divisionMember, ...dataForm }
{ user: hasil, groups: divisionMember, ...dataForm }
))
const response = await apiCreateAnnouncement(fd)

View File

@@ -241,7 +241,6 @@ export default function ListDivision() {
</View>
}
title={item.name}
titleWeight="normal"
/>
)
}}

View File

@@ -319,6 +319,7 @@ export default function ListProject() {
content="page"
title={item.title}
headerColor="primary"
titleTail={2}
>
<ProgressBar value={item.progress} category="list" />
<View style={[Styles.rowSpaceBetween]}>

View File

@@ -3,13 +3,14 @@ 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 { apiGetCheckToken, apiRegisteredToken, apiUnregisteredToken } from "@/lib/api";
import { checkPermission, getToken, openSettings } from "@/lib/useNotification";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { Feather, Ionicons } from "@expo/vector-icons";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { router } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { AppState, AppStateStatus, Pressable, View } from "react-native";
import { useSelector } from "react-redux";
@@ -28,12 +29,13 @@ export default function ListSetting() {
const [showLogoutModal, setShowLogoutModal] = useState(false)
const [showThemeModal, setShowThemeModal] = useState(false)
const prevOsPermission = useRef<boolean | undefined>(undefined);
const registerToken = async () => {
try {
const token = await getToken();
if (token) {
await apiRegisteredToken({ user: entities.id, token });
await apiRegisteredToken({ user: entities.id, token, category: "register" });
}
} catch (error) {
console.warn('Error registering token:', error);
@@ -52,15 +54,31 @@ export default function ListSetting() {
};
const checkNotif = useCallback(async () => {
const status = await checkPermission();
setIsNotificationEnabled((prev) => {
if (prev === false && status === true) {
registerToken();
} else if (prev === true && status === false) {
unregisterToken();
const osPermission = await checkPermission();
// Jika dari tidak diijinkan sistem kemudian diijinkan (setelah balik dari pengaturan device)
if (prevOsPermission.current === false && osPermission === true) {
await registerToken();
}
prevOsPermission.current = osPermission;
if (!osPermission) {
setIsNotificationEnabled(false);
return;
}
try {
const token = await getToken();
if (token) {
const response = await apiGetCheckToken({ user: entities.id, token });
setIsNotificationEnabled(!!response.data);
} else {
setIsNotificationEnabled(false);
}
return !!status;
});
} catch (error) {
console.warn('Error checking token status:', error);
setIsNotificationEnabled(false);
}
}, [entities.id]);
useEffect(() => {
@@ -78,10 +96,12 @@ export default function ListSetting() {
}, [checkNotif]);
const handleToggleNotif = async () => {
if (isNotificationEnabled) {
const osPermission = await checkPermission();
if (!osPermission) {
setModalConfig({
title: "Matikan Notifikasi?",
message: "Anda akan diarahkan ke pengaturan sistem untuk mematikan notifikasi.",
title: "Aktifkan Notifikasi?",
message: "Izin notifikasi tidak diberikan. Buka pengaturan sistem untuk mengaktifkannya?",
confirmText: "Buka Pengaturan",
onConfirm: () => {
setModalVisible(false);
@@ -90,22 +110,17 @@ export default function ListSetting() {
});
setModalVisible(true);
} else {
const granted = await requestPermission();
if (granted) {
setIsNotificationEnabled(true);
registerToken();
// OS Permission is granted, perform in-app toggle
const targetState = !isNotificationEnabled;
if (targetState) {
await AsyncStorage.setItem('@notification_permission', "true");
await 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);
await AsyncStorage.setItem('@notification_permission', "false");
await unregisterToken();
}
// UI will be updated by checkNotif (triggered by state change or manually here)
setIsNotificationEnabled(targetState);
}
};

2587
bun.lock Normal file

File diff suppressed because it is too large Load Diff

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,6 +1,5 @@
import Styles from '@/constants/Styles';
import { useTheme } from "@/providers/ThemeProvider";
import { useRouter } from 'expo-router';
import { Platform, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import ButtonBackHeader from './buttonBackHeader';
@@ -15,13 +14,12 @@ type Props = {
export default function AppHeader({ title, right, showBack = true, onPressLeft, left }: Props) {
const insets = useSafeAreaInsets();
const router = useRouter();
const { colors } = useTheme();
return (
<View style={[Styles.headerContainer, Platform.OS === 'ios' ? Styles.pb05 : Styles.pb13, { backgroundColor: colors.header, paddingTop: Platform.OS === 'ios' ? insets.top : 10 }]}>
<View style={Styles.headerApp}>
<View style={[Styles.rowItemsCenter]}>
<View style={[Styles.rowItemsCenter, Styles.flex1]}>
{showBack ? (
<ButtonBackHeader onPress={onPressLeft} />
) :
@@ -30,7 +28,9 @@ export default function AppHeader({ title, right, showBack = true, onPressLeft,
<View style={Styles.headerSide} />
)}
<Text style={[Styles.headerTitle, Styles.ml05]}>{title}</Text>
<Text style={[Styles.headerTitle, Styles.ml05, Styles.flex1, Styles.mr10]} numberOfLines={1} ellipsizeMode="tail">
{title ? title.charAt(0).toUpperCase() + title.slice(1) : ""}
</Text>
</View>
<View style={Styles.headerSide}>{right}</View>
</View>

View File

@@ -48,7 +48,7 @@ export default function BorderBottomItem({ title, subtitle, icon, desc, onPress,
{icon}
<View style={[Styles.rowSpaceBetween, Styles.flex1]}>
<View style={[Styles.ml10, Styles.flex1, Styles.mr10]}>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title}</Text>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title ? title.charAt(0).toUpperCase() + title.slice(1) : ""}</Text>
{
subtitle &&
typeof subtitle == "string"

View File

@@ -18,7 +18,7 @@ export default function DiscussionItem({ title, user, date, onPress }: Props) {
<View style={[Styles.rowItemsCenter, Styles.mb10]}>
<Ionicons name="chatbox-ellipses-outline" size={22} color={colors.text} style={Styles.mr10} />
<View style={[{ flex: 1 }]}>
<Text style={{ fontWeight: 'bold' }} numberOfLines={1} ellipsizeMode="tail">{title}</Text>
<Text style={{ fontWeight: 'bold' }} numberOfLines={1} ellipsizeMode="tail">{title?.charAt(0).toUpperCase() + title?.slice(1)}</Text>
</View>
</View>
<View style={[Styles.rowSpaceBetween]}>

View File

@@ -18,7 +18,7 @@ type Props = {
export default function ItemFile({ category, checked, dateTime, title, onChecked, onPress, canChecked }: Props) {
const { colors } = useTheme();
return (
<View style={[Styles.wrapItemBorderBottom, { borderColor: colors.background }]}>
<View style={[Styles.wrapItemBorderBottom, { borderColor: colors.icon + '20' }]}>
<View style={[Styles.rowItemsCenter]}>
<Pressable onPress={onPress}>
{
@@ -56,8 +56,8 @@ export default function ItemFile({ category, checked, dateTime, title, onChecked
<Pressable onPress={onChecked}>
{
checked
? <MaterialCommunityIcons name="checkbox-marked-circle" size={25} color={colors.text} />
: <MaterialCommunityIcons name="checkbox-blank-circle-outline" size={25} color={colors.icon} />
? <MaterialCommunityIcons name="checkbox-marked-circle" size={25} color={colors.icon} />
: <MaterialCommunityIcons name="checkbox-blank-circle-outline" size={25} color={colors.icon + '90'} />
}
</Pressable>

View File

@@ -63,7 +63,7 @@ export default function DivisionHome({ refreshing }: { refreshing: boolean }) {
<Pressable style={[Styles.wrapPaper, Styles.mb05, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]} key={index} onPress={() => { router.push(`/division/${item.id}`) }}>
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
<View>
<Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text>
<Text style={[Styles.textDefaultSemiBold]}>{item.name?.charAt(0).toUpperCase() + item.name?.slice(1)}</Text>
<Text style={[Styles.textDefault]}>{item.jumlah} Kegiatan</Text>
</View>
<Feather name="chevron-right" size={20} color={colors.text} />

View File

@@ -1,6 +1,7 @@
import Styles from "@/constants/Styles";
import { apiGetDataHome } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { router } from "expo-router";
import React, { useEffect, useState } from "react";
import { Dimensions, View } from "react-native";
@@ -10,7 +11,6 @@ import PaperGridContent from "../paperGridContent";
import ProgressBar from "../progressBar";
import Skeleton from "../skeleton";
import Text from "../Text";
import { useTheme } from "@/providers/ThemeProvider";
type Props = {
id: string
@@ -52,7 +52,7 @@ export default function ProjectHome({ refreshing }: { refreshing: boolean }) {
}, []);
return (
<View style={[Styles.mb15]}>
<View style={[Styles.mb05]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mb10]}>Kegiatan Terupdate</Text>
{
loading ? (<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />)
@@ -62,7 +62,7 @@ export default function ProjectHome({ refreshing }: { refreshing: boolean }) {
ref={ref}
style={{ width: "100%" }}
width={width * 0.8}
height={235}
height={220}
data={data}
loop={false}
autoPlay={false}

View File

@@ -22,7 +22,7 @@ export default function PaperGridContent({ content, children, title, headerColor
const bgSource = activeTheme === 'light' ? bgLight : bgDark;
return (
<Pressable onPress={onPress}>
<View style={[content == 'carousel' ? Styles.wrapGridCaraousel : Styles.wrapGridContent]}>
<View style={[content == 'carousel' ? Styles.wrapGridCaraousel : Styles.wrapGridContent, { backgroundColor: colors.card }]}>
{
headerColor == 'warning' ? (
<View style={[Styles.headerPaperGrid, ColorsStatus.warning]}>
@@ -35,13 +35,12 @@ export default function PaperGridContent({ content, children, title, headerColor
imageStyle={{ borderTopLeftRadius: 5, borderTopRightRadius: 5 }}
style={[Styles.headerPaperGrid, { backgroundColor: colors.primary }]}
>
<Text numberOfLines={titleTail ? titleTail : undefined} style={[Styles.textSubtitle, Styles.cWhite, { textAlign: 'center' }]}>{title}</Text>
<Text numberOfLines={titleTail ? titleTail : undefined} style={[Styles.textSubtitle, Styles.cWhite, { textAlign: 'center' }]}>{title.charAt(0).toUpperCase() + title.slice(1)}</Text>
</ImageBackground>
)
}
<View style={[
contentPosition && contentPosition == 'top' ? Styles.contentPaperGrid2 : Styles.contentPaperGrid,
{ backgroundColor: colors.card },
height ? { height: height } : {}
]}>
{children}

View File

@@ -392,10 +392,10 @@ const Styles = StyleSheet.create({
},
wrapGridContent: {
shadowColor: '#171717',
shadowOffset: { width: 0, height: 4 },
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.1,
shadowRadius: 10,
elevation: 5,
shadowRadius: 5,
elevation: 2,
borderRadius: 5,
marginBottom: 15
},
@@ -403,12 +403,13 @@ const Styles = StyleSheet.create({
width: '95%',
height: 200,
shadowColor: '#171717',
shadowOffset: { width: 0, height: 4 },
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 5,
shadowRadius: 5,
elevation: 2,
borderRadius: 5,
marginLeft: 5
marginLeft: 5,
display: 'flex',
},
headerPaperGrid: {
paddingVertical: 25,
@@ -418,15 +419,13 @@ const Styles = StyleSheet.create({
borderTopEndRadius: 5
},
contentPaperGrid: {
backgroundColor: 'white',
height: 150,
height: 125,
borderBottomEndRadius: 5,
borderBottomStartRadius: 5,
paddingHorizontal: 20,
justifyContent: 'space-evenly'
},
contentPaperGrid2: {
backgroundColor: 'white',
height: 100,
borderBottomEndRadius: 5,
borderBottomStartRadius: 5,
@@ -455,8 +454,8 @@ const Styles = StyleSheet.create({
shadowColor: '#171717',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 5,
shadowRadius: 5,
elevation: 2,
},
noShadow: {
shadowColor: 'transparent',
@@ -469,8 +468,8 @@ const Styles = StyleSheet.create({
shadowColor: '#171717',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 5,
shadowRadius: 5,
elevation: 2,
},
contentItemCenter: {
justifyContent: 'center',
@@ -749,10 +748,10 @@ const Styles = StyleSheet.create({
},
wrapHomeCarousel: {
shadowColor: '#171717',
shadowOffset: { width: 0, height: 5 },
shadowOpacity: 0.2,
shadowRadius: 5,
elevation: 50,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 5,
},
modalOverlay: {
flex: 1,
@@ -764,7 +763,7 @@ const Styles = StyleSheet.create({
width: '80%',
borderRadius: 14,
overflow: 'hidden',
elevation: 5,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,

View File

@@ -740,7 +740,7 @@ export const apiShareDocument = async (data: { dataDivision: any[], dataItem: an
return response.data;
};
export const apiRegisteredToken = async (data: { user: string, token: string }) => {
export const apiRegisteredToken = async (data: { user: string, token: string, category?: string }) => {
const response = await api.post(`/mobile/auth-token`, data)
return response.data;
};
@@ -750,6 +750,11 @@ export const apiUnregisteredToken = async (data: { user: string, token: string }
return response.data;
};
export const apiGetCheckToken = async (data: { user: string, token: string }) => {
const response = await api.post(`mobile/auth-token/check`, data);
return response.data;
};
export const apiGetNotification = async ({ user, page }: { user: string, page?: number }) => {
const response = await api.get(`mobile/home/notification?user=${user}&page=${page}`);
return response.data;

View File

@@ -1,3 +1,5 @@
import { ConstEnv } from '@/constants/ConstEnv';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getApp, getApps, initializeApp } from '@react-native-firebase/app';
import {
getMessaging,
@@ -6,8 +8,7 @@ import {
} from '@react-native-firebase/messaging';
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';
import { Linking, PermissionsAndroid, Platform } from 'react-native';
import { ConstEnv } from '@/constants/ConstEnv';
import { Linking, Platform } from 'react-native';
const RNfirebaseConfig = {
apiKey: ConstEnv.firebase.apiKey,
@@ -39,13 +40,15 @@ 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';
// Cek status permission sekarang
const { status } = await Notifications.getPermissionsAsync();
if (status === 'granted') {
return true;
}
if (status === 'denied') {
return false;
}
} catch (err) {
console.warn('Error checking notification permissions:', err);
@@ -63,21 +66,9 @@ export const openSettings = () => {
export const requestPermission = async () => {
try {
if (Platform.OS === 'android') {
const cek = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);
if (!cek) {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
return true;
} else if (Platform.OS === 'ios') {
const { status } = await Notifications.requestPermissionsAsync();
return status === 'granted';
}
const { status: newStatus } = await Notifications.requestPermissionsAsync();
await AsyncStorage.setItem('@notification_permission', newStatus === 'granted' ? "true" : "false");
return newStatus === 'granted';
} catch (err) {
console.warn('Error requesting notification permissions:', err);
}

View File

@@ -1,6 +1,6 @@
import { ConstEnv } from '@/constants/ConstEnv';
import { apiRegisteredToken, apiUnregisteredToken } from '@/lib/api';
import { getToken, requestPermission } from '@/lib/useNotification';
import { getToken } from '@/lib/useNotification';
import AsyncStorage from '@react-native-async-storage/async-storage';
import CryptoES from "crypto-es";
import { router } from "expo-router";
@@ -52,16 +52,12 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
const signIn = useCallback(async (token: string) => {
const hasil = await decryptToken(String(token))
const permission = await requestPermission()
if (permission) {
// const permission = await requestPermission()
const permissionStorage = await AsyncStorage.getItem('@notification_permission')
if (permissionStorage === "true") {
const tokenDevice = await getToken()
try {
// if (Platform.OS === 'android') {
const tokenDevice = await getToken()
const register = await apiRegisteredToken({ user: hasil, token: String(tokenDevice) })
// }else{
// const tokenDevice = await getToken()
// const register = await apiRegisteredToken({ user: hasil, token: String(tokenDevice) })
// }
} catch (error) {
console.error(error)
} finally {
@@ -71,6 +67,7 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
return true
}
} else {
const register = await apiRegisteredToken({ user: hasil, token: "" })
await AsyncStorage.setItem('@token', token);
tokenRef.current = token;
router.replace('/home')