Kantongku: notifikasi pengingat catat harian (toggle + pilih jam)

This commit is contained in:
Bro
2026-08-16 16:34:38 +07:00
parent 8ea2475c71
commit 0b269621b2
5 changed files with 216 additions and 16 deletions
+10
View File
@@ -5,6 +5,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
import { initDatabase } from './src/db/database';
import { checkRecurringDue, getSetting } from './src/db/repositories';
import { scheduleDailyReminder } from './src/utils/notifications';
import { RootNavigator } from './src/navigation';
import { colors } from './src/theme/colors';
@@ -27,6 +28,15 @@ export default function App() {
const onboarded = await getSetting('onboarded');
const userName = await getSetting('user_name');
setInitialRoute(onboarded === '1' && userName ? 'Main' : 'Onboarding');
// Reschedule pengingat harian kalau aktif
try {
if ((await getSetting('reminder_enabled')) === '1') {
const h = Number(await getSetting('reminder_hour'));
await scheduleDailyReminder(Number.isFinite(h) ? h : 20, 0);
}
} catch {
// abaikan
}
setReady(true);
SplashScreen.hideAsync().catch(() => {});
})
+47 -13
View File
@@ -17,6 +17,7 @@
"expo-document-picker": "~57.0.1",
"expo-file-system": "~57.0.4",
"expo-linear-gradient": "~57.0.1",
"expo-notifications": "~57.0.11",
"expo-sharing": "~57.0.12",
"expo-splash-screen": "~57.0.6",
"expo-sqlite": "~57.0.1",
@@ -2134,6 +2135,12 @@
"@babel/plugin-syntax-flow": "^7.12.1"
}
},
"node_modules/badgin": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -2951,6 +2958,28 @@
}
}
},
"node_modules/expo-application": {
"version": "57.0.2",
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-57.0.2.tgz",
"integrity": "sha512-q31YwcXyymviAmdrtDfAg3Dld4VMxLCNAfgMHip7vZpPX4lzF/AfwsywqBJUAjQnJknzaNAkzqvMVjO5XmKYDA==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-constants": {
"version": "57.0.11",
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.11.tgz",
"integrity": "sha512-gpSZ13Nfx0BOc9ALjG1SMhks5csb8Z6ncEVdhDgv0NRrmJcHPRvyUlWaKjRjzwW730XrQ6PIXFRcnNmAxOLj5A==",
"license": "MIT",
"dependencies": {
"@expo/env": "~2.4.2"
},
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-document-picker": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-57.0.1.tgz",
@@ -3040,6 +3069,24 @@
"react-native": "*"
}
},
"node_modules/expo-notifications": {
"version": "57.0.11",
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-57.0.11.tgz",
"integrity": "sha512-hQz01re2eee9X4GyQ73TVohhDDq1vAe6wXDMFrLMdbcPtiPuvlr1EfNlT12zDwsc14n0KchaFnpETWk+fwUbTw==",
"license": "MIT",
"dependencies": {
"@expo/image-utils": "^0.11.4",
"abort-controller": "^3.0.0",
"badgin": "^1.1.5",
"expo-application": "~57.0.2",
"expo-constants": "~57.0.11"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-server": {
"version": "57.0.3",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.3.tgz",
@@ -3407,19 +3454,6 @@
"react-native": "*"
}
},
"node_modules/expo/node_modules/expo-constants": {
"version": "57.0.11",
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.11.tgz",
"integrity": "sha512-gpSZ13Nfx0BOc9ALjG1SMhks5csb8Z6ncEVdhDgv0NRrmJcHPRvyUlWaKjRjzwW730XrQ6PIXFRcnNmAxOLj5A==",
"license": "MIT",
"dependencies": {
"@expo/env": "~2.4.2"
},
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo/node_modules/expo-keep-awake": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.1.tgz",
+1
View File
@@ -12,6 +12,7 @@
"expo-document-picker": "~57.0.1",
"expo-file-system": "~57.0.4",
"expo-linear-gradient": "~57.0.1",
"expo-notifications": "~57.0.11",
"expo-sharing": "~57.0.12",
"expo-splash-screen": "~57.0.6",
"expo-sqlite": "~57.0.1",
+124 -3
View File
@@ -9,6 +9,7 @@ import {
Pressable,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
View,
@@ -17,11 +18,17 @@ import { File, Paths } from 'expo-file-system';
import * as Sharing from 'expo-sharing';
import { colors } from '../theme/colors';
import { getDb } from '../db/database';
import {
cancelReminder,
ensureNotificationPermission,
scheduleDailyReminder,
} from '../utils/notifications';
import {
deleteCategory,
deleteRecurring,
getCategories,
getRecurring,
getSetting,
getSettingNumber,
setSetting,
} from '../db/repositories';
@@ -77,12 +84,26 @@ export function SettingsScreen() {
}>
>([]);
const [categories, setCategories] = useState<Category[]>([]);
const [reminderEnabled, setReminderEnabled] = useState(false);
const [reminderHour, setReminderHour] = useState(20);
const loadSettings = useCallback(async () => {
const [p, en, hr] = await Promise.all([
getSettingNumber('auto_save_percent', 10),
getSetting('reminder_enabled'),
getSetting('reminder_hour'),
]);
setPercent(p);
setReminderEnabled(en === '1');
const h = Number(hr);
setReminderHour(Number.isFinite(h) && h >= 0 && h <= 23 ? h : 20);
}, []);
useFocusEffect(
useCallback(() => {
let active = true;
getSettingNumber('auto_save_percent', 10).then((p) => {
if (active) setPercent(p);
loadSettings().then(() => {
if (!active) return;
});
getRecurring().then((r) => {
if (active) setRecurring(r);
@@ -93,9 +114,38 @@ export function SettingsScreen() {
return () => {
active = false;
};
}, [])
}, [loadSettings])
);
const handleToggleReminder = async (enabled: boolean) => {
if (enabled) {
const ok = await ensureNotificationPermission();
if (!ok) {
Alert.alert(
'Izin notifikasi ditolak',
'Aktifkan izin notifikasi di pengaturan HP untuk memakai pengingat.'
);
return;
}
await scheduleDailyReminder(reminderHour, 0);
await setSetting('reminder_enabled', '1');
await setSetting('reminder_hour', String(reminderHour));
setReminderEnabled(true);
} else {
await cancelReminder();
await setSetting('reminder_enabled', '0');
setReminderEnabled(false);
}
};
const handlePickHour = async (hour: number) => {
setReminderHour(hour);
await setSetting('reminder_hour', String(hour));
if (reminderEnabled) {
await scheduleDailyReminder(hour, 0);
}
};
const handleDeleteCategory = (category: Category) => {
Alert.alert(
'Hapus kategori?',
@@ -307,6 +357,53 @@ export function SettingsScreen() {
</View>
</LinearGradient>
{/* Pengingat */}
<Text style={styles.sectionTitle}>Pengingat</Text>
<View style={[styles.card, styles.dataCard]}>
<View style={styles.reminderRow}>
<View style={styles.dataIconCircle}>
<Ionicons name="notifications" size={18} color={colors.primaryDark} />
</View>
<View style={styles.dataInfo}>
<Text style={styles.dataTitle}>Pengingat catat harian</Text>
<Text style={styles.dataHint}>
{reminderEnabled
? `Aktif setiap hari jam ${reminderHour}:00`
: 'Ingatkan saya untuk mencatat setiap hari'}
</Text>
</View>
<Switch
value={reminderEnabled}
onValueChange={handleToggleReminder}
trackColor={{ true: colors.primary, false: colors.border }}
thumbColor={colors.white}
/>
</View>
{reminderEnabled && (
<View style={styles.reminderHours}>
{[19, 20, 21, 22].map((h) => (
<Pressable
key={h}
style={[
styles.hourChip,
reminderHour === h && styles.hourChipActive,
]}
onPress={() => handlePickHour(h)}
>
<Text
style={[
styles.hourChipText,
reminderHour === h && { color: colors.white, fontWeight: '700' },
]}
>
{h}:00
</Text>
</Pressable>
))}
</View>
)}
</View>
{/* Transaksi Berulang */}
<Text style={styles.sectionTitle}>Transaksi Berulang</Text>
<View style={[styles.card, styles.dataCard]}>
@@ -649,6 +746,30 @@ const styles = StyleSheet.create({
gap: 12,
paddingVertical: 8,
},
reminderRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
reminderHours: {
flexDirection: 'row',
gap: 8,
marginTop: 12,
paddingLeft: 50,
},
hourChip: {
backgroundColor: colors.background,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 10,
paddingHorizontal: 14,
paddingVertical: 8,
},
hourChipActive: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
hourChipText: { fontSize: 13, color: colors.textMuted, fontWeight: '600' },
dataRow: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 6 },
dataIconCircle: {
width: 44,
+34
View File
@@ -0,0 +1,34 @@
import * as Notifications from 'expo-notifications';
export async function ensureNotificationPermission(): Promise<boolean> {
try {
const { status } = await Notifications.getPermissionsAsync();
if (status === 'granted') return true;
const req = await Notifications.requestPermissionsAsync();
return req.status === 'granted';
} catch {
return false;
}
}
export async function scheduleDailyReminder(
hour: number,
minute: number
): Promise<void> {
await Notifications.cancelAllScheduledNotificationsAsync();
await Notifications.scheduleNotificationAsync({
content: {
title: 'Kantongku 👛',
body: 'Belum catat hari ini? 5 detik aja, kak — biar saldo & tabunganmu tetap kejaga!',
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour,
minute,
},
});
}
export async function cancelReminder(): Promise<void> {
await Notifications.cancelAllScheduledNotificationsAsync();
}