Compare commits
10 Commits
amalia/04-
...
amalia/10-
| Author | SHA1 | Date | |
|---|---|---|---|
| e1b62be6da | |||
| b2b125c410 | |||
| 1cfecbbdd5 | |||
| 21006e8eee | |||
| 91231d60e4 | |||
| 7174e27be1 | |||
| 9d4b931aa6 | |||
| 166d8f1c16 | |||
| 7060a2d165 | |||
| d6217aecf1 |
@@ -1,6 +1,6 @@
|
||||
# Desa+
|
||||
|
||||
Desa+ adalah aplikasi mobile berbasis React Native yang dikembangkan dengan Expo untuk membantu pengelolaan dan komunikasi di lingkungan desa/kelurahan. Aplikasi ini menyediakan berbagai fitur untuk memudahkan administrasi desa, komunikasi antar warga, dan pengelolaan informasi penting.
|
||||
Desa+ (Desa Plus) adalah aplikasi mobile berbasis React Native yang dikembangkan dengan Expo untuk membantu pengelolaan dan komunikasi di lingkungan desa/kelurahan. Aplikasi ini menyediakan berbagai fitur untuk memudahkan administrasi desa, komunikasi antar warga, dan pengelolaan informasi penting.
|
||||
|
||||
## Fitur Utama
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ android {
|
||||
applicationId 'mobiledarmasaba.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 6
|
||||
versionName "1.0.2"
|
||||
versionCode 16
|
||||
versionName "2.1.0"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 904 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,9 +1,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">#ffffff</item>
|
||||
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
ios: {
|
||||
supportsTablet: true,
|
||||
bundleIdentifier: "mobiledarmasaba.app",
|
||||
buildNumber: "8",
|
||||
buildNumber: "9",
|
||||
infoPlist: {
|
||||
ITSAppUsesNonExemptEncryption: false,
|
||||
CFBundleDisplayName: "Desa+"
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function ListSetting() {
|
||||
|
||||
const registerToken = async () => {
|
||||
try {
|
||||
await AsyncStorage.setItem('@notification_permission', "true");
|
||||
const token = await getToken();
|
||||
if (token) {
|
||||
await apiRegisteredToken({ user: entities.id, token, category: "register" });
|
||||
@@ -44,6 +45,7 @@ export default function ListSetting() {
|
||||
|
||||
const unregisterToken = async () => {
|
||||
try {
|
||||
await AsyncStorage.setItem('@notification_permission', "false");
|
||||
const token = await getToken();
|
||||
if (token) {
|
||||
await apiUnregisteredToken({ user: entities.id, token, category: "unregister" });
|
||||
@@ -113,10 +115,8 @@ export default function ListSetting() {
|
||||
// OS Permission is granted, perform in-app toggle
|
||||
const targetState = !isNotificationEnabled;
|
||||
if (targetState) {
|
||||
await AsyncStorage.setItem('@notification_permission', "true");
|
||||
await registerToken();
|
||||
} else {
|
||||
await AsyncStorage.setItem('@notification_permission', "false");
|
||||
await unregisterToken();
|
||||
}
|
||||
// UI will be updated by checkNotif (triggered by state change or manually here)
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function ViewLogin({ onValidate }: Props) {
|
||||
} else {
|
||||
return Toast.show({ type: 'small', text1: response.message, position: 'bottom' })
|
||||
}
|
||||
} catch (error : any ) {
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal login"
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean })
|
||||
backgroundColor: colors.primary
|
||||
},
|
||||
]}
|
||||
resizeMode="cover"
|
||||
resizeMode="stretch"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -490,17 +490,17 @@ const Styles = StyleSheet.create({
|
||||
wrapItemDiscussion: {
|
||||
padding: 15,
|
||||
borderRadius: 5,
|
||||
borderColor: '#d6d8f6',
|
||||
// borderColor: '#d6d8f6',
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
wrapItemBorderBottom: {
|
||||
padding: 10,
|
||||
borderColor: '#d6d8f6',
|
||||
// borderColor: '#d6d8f6',
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
wrapItemBorderAll: {
|
||||
padding: 10,
|
||||
borderColor: '#d6d8f6',
|
||||
// borderColor: '#d6d8f6',
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
marginBottom: 5
|
||||
|
||||
3
eas.json
@@ -28,7 +28,8 @@
|
||||
"buildType": "app-bundle"
|
||||
},
|
||||
"ios": {
|
||||
"simulator": false
|
||||
"simulator": false,
|
||||
"image": "latest"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app;
|
||||
PRODUCT_NAME = Desa;
|
||||
PRODUCT_NAME = "Desa";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -429,7 +429,7 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app;
|
||||
PRODUCT_NAME = Desa;
|
||||
PRODUCT_NAME = "Desa";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.0.5</string>
|
||||
<string>2.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -39,7 +39,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>7</string>
|
||||
<string>9</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
11
lib/api.ts
@@ -13,18 +13,21 @@ export const apiCheckPhoneLogin = async (body: { phone: string }) => {
|
||||
export const apiSendOtp = async (body: { phone: string, otp: number }) => {
|
||||
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}`,
|
||||
`${Constants.expoConfig?.extra?.URL_OTP}/api/wa/send-text`,
|
||||
{
|
||||
cache: "no-cache",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Constants.expoConfig?.extra?.WA_SERVER_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
number: body.phone,
|
||||
text: message
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
return res.status
|
||||
}
|
||||
|
||||
|
||||
@@ -66,32 +66,41 @@ export const openSettings = () => {
|
||||
|
||||
export const requestPermission = async () => {
|
||||
try {
|
||||
const { status: currentStatus } = await Notifications.getPermissionsAsync();
|
||||
const existing = await AsyncStorage.getItem('@notification_permission');
|
||||
const { status: currentStatus, canAskAgain } = await Notifications.getPermissionsAsync();
|
||||
|
||||
// Jika belum pernah ditentukan (undetermined), baru panggil request
|
||||
if (currentStatus === 'undetermined') {
|
||||
const { status: newStatus } = await Notifications.requestPermissionsAsync();
|
||||
await AsyncStorage.setItem('@notification_permission', newStatus === 'granted' ? 'true' : 'false');
|
||||
return newStatus === 'granted';
|
||||
// Jika baru pertama kali (fresh install / storage belum ada)
|
||||
if (existing === null) {
|
||||
// Cek apakah OS memungkinkan untuk memunculkan popup
|
||||
if (currentStatus === 'undetermined' || (currentStatus === 'denied' && canAskAgain)) {
|
||||
const { status: newStatus } = await Notifications.requestPermissionsAsync();
|
||||
await AsyncStorage.setItem('@notification_permission', newStatus === 'granted' ? 'true' : 'false');
|
||||
return newStatus === 'granted';
|
||||
}
|
||||
|
||||
// Jika OS sudah granted (sudah diijinkan entah lewat mana), simpan true
|
||||
if (currentStatus === 'granted') {
|
||||
await AsyncStorage.setItem('@notification_permission', 'true');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Jika OS denied dan tidak bisa minta lagi (disables)
|
||||
if (currentStatus === 'denied' && !canAskAgain) {
|
||||
await AsyncStorage.setItem('@notification_permission', 'false');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Jika sudah pernah ditentukan (granted/denied), update storage sesuai OS
|
||||
// Tapi jika granted, kita cek storage dlu sapa tau user pernah matiin manual di app
|
||||
const osPermission = await checkPermission();
|
||||
const existing = await AsyncStorage.getItem('@notification_permission');
|
||||
|
||||
// Jika sudah pernah di-request sebelumnya (storage sudah ada)
|
||||
// Selalu sinkronkan jika OS menyatakan Denied
|
||||
if (osPermission === false) {
|
||||
await AsyncStorage.setItem('@notification_permission', 'false');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Jika OS Granted, ikuti nilai tersimpan di storage (menghargai in-app toggle user)
|
||||
if (osPermission === true) {
|
||||
// Jika OS ijinkan, tapi di storage belum ada, set true
|
||||
if (existing === null) {
|
||||
await AsyncStorage.setItem('@notification_permission', 'true');
|
||||
return true;
|
||||
}
|
||||
// Jika OS ijinkan dan di storage ada, ikuti storage (toggle manual user)
|
||||
return existing === 'true';
|
||||
}
|
||||
|
||||
|
||||