Kantongku: notifikasi pengingat catat harian (toggle + pilih jam)
This commit is contained in:
@@ -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(() => {});
|
||||
})
|
||||
|
||||
Generated
+47
-13
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user