From 42f245f37cb0a97242197d568c770b63482e451e Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 12 Jan 2026 14:12:24 +0800 Subject: [PATCH 1/2] fix: kode otp Deskripsi: - fix ganti wa jenna untuk mengirim kode otp No Issues --- app.config.js | 3 ++- lib/api.ts | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app.config.js b/app.config.js index 26d1684..f8aba53 100644 --- a/app.config.js +++ b/app.config.js @@ -77,7 +77,8 @@ export default { URL_OTP: process.env.URL_OTP, URL_STORAGE: process.env.URL_STORAGE, URL_FIREBASE_DB: process.env.URL_FIREBASE_DB, - PASS_ENC: process.env.PASS_ENC + PASS_ENC: process.env.PASS_ENC, + WA_SERVER_TOKEN: process.env.WA_SERVER_TOKEN, } } }; diff --git a/lib/api.ts b/lib/api.ts index 7a92ff1..f23b005 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -11,7 +11,20 @@ export const apiCheckPhoneLogin = async (body: { phone: string }) => { } export const apiSendOtp = async (body: { phone: string, otp: number }) => { - const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*Desa%2B*%0AMasukkan%20kode%20ini%20*${encodeURIComponent(body.otp)}*%20pada%20aplikasi%20Desa%2B%20anda.%20Jangan%20berikan%20pada%20siapapun.`) + const message = "Desa+\nMasukkan kode ini " + body.otp + " pada aplikasi Desa+ anda. Jangan berikan pada siapapun." + const textFix = encodeURIComponent(message) + // const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*Desa%2B*%0AMasukkan%20kode%20ini%20*${encodeURIComponent(body.otp)}*%20pada%20aplikasi%20Desa%2B%20anda.%20Jangan%20berikan%20pada%20siapapun.`) + const res = await fetch( + `${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=${textFix}`, + { + cache: "no-cache", + headers: { + Authorization: `Bearer ${Constants.expoConfig?.extra?.WA_SERVER_TOKEN}`, + }, + } + ); + + return res.status } From ca3d0d9d1944c7a720981b14fe98676bed567d7a Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 12 Jan 2026 15:52:48 +0800 Subject: [PATCH 2/2] upd: req client Deskripsi: - tampilan tambah file saat tambah data pengumuman - tampilan list file pada halaman detail pengumuman - tampilan tambah file saat edit data pengumuman No Issues --- app/(application)/announcement/[id].tsx | 65 +++++++++++++++++++- app/(application)/announcement/create.tsx | 59 +++++++++++++++++- app/(application)/announcement/edit/[id].tsx | 58 ++++++++++++++++- 3 files changed, 177 insertions(+), 5 deletions(-) diff --git a/app/(application)/announcement/[id].tsx b/app/(application)/announcement/[id].tsx index b9be05b..7530a6b 100644 --- a/app/(application)/announcement/[id].tsx +++ b/app/(application)/announcement/[id].tsx @@ -1,14 +1,20 @@ import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail"; +import BorderBottomItem from "@/components/borderBottomItem"; import ButtonBackHeader from "@/components/buttonBackHeader"; import Skeleton from "@/components/skeleton"; import Text from '@/components/Text'; +import { ConstEnv } from "@/constants/ConstEnv"; import Styles from "@/constants/Styles"; import { apiGetAnnouncementOne } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; -import { Entypo, MaterialIcons } from "@expo/vector-icons"; +import { Entypo, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"; +import * as FileSystem from 'expo-file-system'; +import { startActivityAsync } from 'expo-intent-launcher'; import { router, Stack, useLocalSearchParams } from "expo-router"; +import * as Sharing from 'expo-sharing'; import React, { useEffect, useState } from "react"; -import { Dimensions, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"; +import { Alert, Dimensions, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"; +import * as mime from 'react-native-mime-types'; import RenderHTML from 'react-native-render-html'; import Toast from "react-native-toast-message"; import { useSelector } from "react-redux"; @@ -24,12 +30,15 @@ export default function DetailAnnouncement() { const { token, decryptToken } = useAuthSession() const [data, setData] = useState({ id: '', title: '', desc: '' }) const [dataMember, setDataMember] = useState({}) + const [dataFile, setDataFile] = useState<{ idStorage: string; name: string; extension: string }[]>([]) const update = useSelector((state: any) => state.announcementUpdate) const entityUser = useSelector((state: any) => state.user) const contentWidth = Dimensions.get('window').width const [loading, setLoading] = useState(true) const arrSkeleton = Array.from({ length: 2 }, (_, index) => index) const [refreshing, setRefreshing] = useState(false) + const [loadingOpen, setLoadingOpen] = useState(false) + async function handleLoad(loading: boolean) { try { @@ -70,6 +79,37 @@ export default function DetailAnnouncement() { setRefreshing(false) }; + const openFile = (item: { idStorage: string; name: string; extension: string }) => { + if (Platform.OS == 'android') setLoadingOpen(true) + let remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage; + const fileName = item.name + '.' + item.extension; + let localPath = `${FileSystem.documentDirectory}/${fileName}`; + const mimeType = mime.lookup(fileName) + + FileSystem.downloadAsync(remoteUrl, localPath).then(async ({ uri }) => { + const contentURL = await FileSystem.getContentUriAsync(uri); + setLoadingOpen(false) + try { + if (Platform.OS == 'android') { + await startActivityAsync( + 'android.intent.action.VIEW', + { + data: contentURL, + flags: 1, + type: mimeType as string, + } + ); + } else if (Platform.OS == 'ios') { + Sharing.shareAsync(localPath); + } + } catch (error) { + Alert.alert('INFO', 'Gagal membuka file, tidak ada aplikasi yang dapat membuka file ini'); + } finally { + if (Platform.OS == 'android') setLoadingOpen(false) + } + }); + }; + return ( - + { + dataFile.length > 0 && ( + + + File + + {dataFile.map((item, index) => ( + } + title={item.name} + titleWeight="normal" + onPress={() => { openFile({ idStorage: item.idStorage, name: item.name, extension: item.extension }) }} + /> + ))} + + ) + } + { loading ? arrSkeleton.map((item, index) => { diff --git a/app/(application)/announcement/create.tsx b/app/(application)/announcement/create.tsx index 152e9cc..31cda46 100644 --- a/app/(application)/announcement/create.tsx +++ b/app/(application)/announcement/create.tsx @@ -1,14 +1,18 @@ +import BorderBottomItem from "@/components/borderBottomItem"; import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import ButtonSelect from "@/components/buttonSelect"; +import DrawerBottom from "@/components/drawerBottom"; import { InputForm } from "@/components/inputForm"; +import MenuItemRow from "@/components/menuItemRow"; import ModalSelectMultiple from "@/components/modalSelectMultiple"; import Text from "@/components/Text"; import Styles from "@/constants/Styles"; import { setUpdateAnnouncement } from "@/lib/announcementUpdate"; import { apiCreateAnnouncement } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; -import { Entypo } from "@expo/vector-icons"; +import { Entypo, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import * as DocumentPicker from "expo-document-picker"; import { router, Stack } from "expo-router"; import { useEffect, useState } from "react"; import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; @@ -23,6 +27,9 @@ export default function CreateAnnouncement() { const [modalDivisi, setModalDivisi] = useState(false); const [divisionMember, setDivisionMember] = useState([]) const [loading, setLoading] = useState(false) + const [fileForm, setFileForm] = useState([]) + const [isModalFile, setModalFile] = useState(false) + const [indexDelFile, setIndexDelFile] = useState(0) const [dataForm, setDataForm] = useState({ title: "", desc: "", @@ -84,6 +91,25 @@ export default function CreateAnnouncement() { } } + const pickDocumentAsync = async () => { + let result = await DocumentPicker.getDocumentAsync({ + type: ["*/*"], + multiple: true + }); + if (!result.canceled) { + for (let i = 0; i < result.assets?.length; i++) { + if (result.assets[i].uri) { + setFileForm((prev) => [...prev, result.assets[i]]) + } + } + } + }; + + function deleteFile(index: number) { + setFileForm([...fileForm.filter((val, i) => i !== index)]) + setModalFile(false) + } + return ( validationForm("desc", val)} multiline /> + + { + fileForm.length > 0 + && + + File + { + fileForm.map((item, index) => ( + 1 ? "bottom" : "none"} + icon={} + title={item.name} + titleWeight="normal" + onPress={() => { setIndexDelFile(index); setModalFile(true) }} + /> + )) + } + + } + { @@ -178,6 +225,16 @@ export default function CreateAnnouncement() { setModalDivisi(false) }} /> + + + + } + title="Hapus" + onPress={() => { deleteFile(indexDelFile) }} + /> + + ); } diff --git a/app/(application)/announcement/edit/[id].tsx b/app/(application)/announcement/edit/[id].tsx index 42e55ac..d43f314 100644 --- a/app/(application)/announcement/edit/[id].tsx +++ b/app/(application)/announcement/edit/[id].tsx @@ -1,14 +1,18 @@ +import BorderBottomItem from "@/components/borderBottomItem"; import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import ButtonSelect from "@/components/buttonSelect"; +import DrawerBottom from "@/components/drawerBottom"; import { InputForm } from "@/components/inputForm"; +import MenuItemRow from "@/components/menuItemRow"; import ModalSelectMultiple from "@/components/modalSelectMultiple"; import Text from '@/components/Text'; import Styles from "@/constants/Styles"; import { setUpdateAnnouncement } from "@/lib/announcementUpdate"; import { apiEditAnnouncement, apiGetAnnouncementOne } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; -import { Entypo } from "@expo/vector-icons"; +import { Entypo, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import * as DocumentPicker from "expo-document-picker"; import { router, Stack, useLocalSearchParams } from "expo-router"; import { useEffect, useState } from "react"; import { SafeAreaView, ScrollView, View } from "react-native"; @@ -33,6 +37,9 @@ export default function EditAnnouncement() { const [modalDivisi, setModalDivisi] = useState(false); const [disableBtn, setDisableBtn] = useState(true); const [dataMember, setDataMember] = useState([]); + const [fileForm, setFileForm] = useState([]) + const [indexDelFile, setIndexDelFile] = useState(0) + const [isModalFile, setModalFile] = useState(false) const [loading, setLoading] = useState(false) const [dataForm, setDataForm] = useState({ title: "", @@ -127,6 +134,25 @@ export default function EditAnnouncement() { } } + const pickDocumentAsync = async () => { + let result = await DocumentPicker.getDocumentAsync({ + type: ["*/*"], + multiple: true + }); + if (!result.canceled) { + for (let i = 0; i < result.assets?.length; i++) { + if (result.assets[i].uri) { + setFileForm((prev) => [...prev, result.assets[i]]) + } + } + } + }; + + function deleteFile(index: number) { + setFileForm([...fileForm.filter((val, i) => i !== index)]) + setModalFile(false) + } + return ( + + { + fileForm.length > 0 + && + + File + { + fileForm.map((item, index) => ( + 1 ? "bottom" : "none"} + icon={} + title={item.name} + titleWeight="normal" + onPress={() => { setIndexDelFile(index); setModalFile(true) }} + /> + )) + } + + } { @@ -223,6 +269,16 @@ export default function EditAnnouncement() { }} value={dataMember} /> + + + + } + title="Hapus" + onPress={() => { deleteFile(indexDelFile) }} + /> + + ); }