Compare commits

..

7 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
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
10 changed files with 2683 additions and 90 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

@@ -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,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]}>
@@ -41,7 +41,6 @@ export default function PaperGridContent({ content, children, title, headerColor
}
<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')