Dokumentasi lengkap API DuitKu untuk pengembangan aplikasi mobile dengan React Native Expo.
API menggunakan Laravel Sanctum dengan Bearer Token authentication.
http://localhost:8000/api/v1
{
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer {token}'
}Semua response mengikuti format standar:
{
"status": "success",
"message": "Optional message",
"data": { ... },
"meta": { ... } // untuk pagination
}{
"status": "error",
"message": "Error description"
}{
"message": "The given data was invalid.",
"errors": {
"field_name": ["Error message"]
}
}Registrasi user baru.
Request Body:
{
"name": "John Doe",
"email": "john@example.com",
"password": "password123",
"password_confirmation": "password123"
}Response (201):
{
"status": "success",
"message": "Registration successful",
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"token": "1|abc123xyz..."
}
}React Native Usage:
const register = async (name, email, password) => {
const response = await fetch(`${API_URL}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
name,
email,
password,
password_confirmation: password,
}),
});
const data = await response.json();
if (data.status === 'success') {
// Save token to AsyncStorage
await AsyncStorage.setItem('token', data.data.token);
await AsyncStorage.setItem('user', JSON.stringify(data.data.user));
}
return data;
};Login dan dapatkan token.
Request Body:
{
"email": "john@example.com",
"password": "password123"
}Response (200):
{
"status": "success",
"message": "Login berhasil",
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"token": "1|abc123xyz..."
}
}Error Response (401):
{
"status": "error",
"message": "Email atau password salah"
}Logout (revoke current token).
Response (200):
{
"status": "success",
"message": "Logout berhasil"
}Get data user yang login.
Response (200):
{
"status": "success",
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
}Get semua data dashboard sekaligus (stats, recent transactions, budget overview).
Response (200):
{
"status": "success",
"data": {
"stats": {
"income": 10000000,
"expense": 5000000,
"balance": 5000000,
"count": 25,
"income_change": 15.5,
"expense_change": -10.2
},
"recent_transactions": [...],
"budget_overview": [...],
"chart_data": {...}
}
}Get statistics saja.
Query Parameters:
| Parameter | Type | Description | Default |
|---|---|---|---|
| period | string | week, month, year | month |
Response (200):
{
"status": "success",
"data": {
"income": 10000000,
"expense": 5000000,
"balance": 5000000,
"count": 25
}
}Get semua transaksi dengan filter dan pagination.
Query Parameters:
| Parameter | Type | Description | Default |
|---|---|---|---|
| type | string | income / expense | - |
| category_id | integer | Filter by category | - |
| period | string | today, week, month, 30days, all | all |
| search | string | Search di description | - |
| per_page | integer | Items per page | 15 |
| page | integer | Page number | 1 |
Response (200):
{
"status": "success",
"data": [
{
"id": 1,
"type": "expense",
"amount": 150000,
"formatted_amount": "Rp 150.000",
"description": "Makan siang",
"notes": "Di restoran favorit",
"date": "2025-12-12",
"formatted_date": "12 Des 2025",
"category": {
"id": 1,
"name": "Makanan",
"icon": "🍔",
"color": "from-orange-500 to-red-500"
}
}
],
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 15,
"total": 75
}
}React Native Usage:
const getTransactions = async (filters = {}) => {
const token = await AsyncStorage.getItem('token');
const params = new URLSearchParams(filters).toString();
const response = await fetch(`${API_URL}/transactions?${params}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
return await response.json();
};
// Example usage
const data = await getTransactions({
type: 'expense',
period: 'month',
per_page: 20,
});Buat transaksi baru.
Request Body:
{
"type": "expense",
"amount": 150000,
"description": "Makan siang",
"category_id": 1,
"date": "2025-12-12",
"notes": "Di restoran favorit"
}Response (201):
{
"status": "success",
"message": "Transaksi berhasil ditambahkan",
"data": {
"id": 1,
"type": "expense",
"amount": 150000,
"description": "Makan siang",
"date": "2025-12-12",
"category_id": 1
}
}Get single transaction.
Response (200):
{
"status": "success",
"data": {
"id": 1,
"type": "expense",
"amount": 150000,
"formatted_amount": "Rp 150.000",
"description": "Makan siang",
"notes": "Di restoran",
"date": "2025-12-12",
"formatted_date": "12 Des 2025",
"category": {...}
}
}Update transaksi.
Request Body (partial update allowed):
{
"amount": 175000,
"description": "Makan siang update"
}Hapus transaksi.
Response (200):
{
"status": "success",
"message": "Transaksi berhasil dihapus"
}Get transaction statistics.
Query Parameters:
| Parameter | Type | Default |
|---|---|---|
| period | string | month |
Response (200):
{
"status": "success",
"data": {
"income": 10000000,
"expense": 5000000,
"balance": 5000000,
"count": 25
}
}Get semua kategori.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| type | string | income / expense |
Response (200):
{
"status": "success",
"data": [
{
"id": 1,
"name": "Makanan",
"type": "expense",
"type_label": "Pengeluaran",
"icon": "🍔",
"color": "from-orange-500 to-red-500",
"transaction_count": 15,
"total_amount": 750000
}
]
}Buat kategori baru.
Request Body:
{
"name": "Makanan",
"type": "expense",
"icon": "🍔",
"color": "from-orange-500 to-red-500"
}Update kategori.
Hapus kategori.
Get spending/income breakdown by category.
Query Parameters:
| Parameter | Type | Default |
|---|---|---|
| type | string | expense |
| year | integer | current year |
| month | integer | current month |
Response (200):
{
"status": "success",
"data": {
"total": 5000000,
"items": [
{
"id": 1,
"name": "Makanan",
"icon": "🍔",
"color": "from-orange-500 to-red-500",
"spent": 750000,
"percentage": 15
}
]
}
}Get semua budget dengan info spending.
Response (200):
{
"status": "success",
"data": [
{
"id": 1,
"category": {
"id": 1,
"name": "Makanan",
"icon": "🍔",
"color": "from-orange-500 to-red-500"
},
"amount": 2000000,
"formatted_amount": "Rp 2.000.000",
"spent": 750000,
"formatted_spent": "Rp 750.000",
"remaining": 1250000,
"formatted_remaining": "Rp 1.250.000",
"percentage": 37.5,
"period": "monthly",
"period_label": "Bulanan",
"start_date": "2025-12-01",
"alert_threshold": 80,
"is_exceeded": false,
"is_over_threshold": false
}
]
}Buat budget baru.
Request Body:
{
"category_id": 1,
"amount": 2000000,
"period": "monthly",
"start_date": "2025-12-01",
"alert_threshold": 80
}Period options: weekly, monthly, yearly
Update budget.
Request Body:
{
"amount": 2500000,
"alert_threshold": 75
}Hapus budget.
Get budget summary.
Response (200):
{
"status": "success",
"data": {
"total_budget": 5000000,
"total_spent": 2500000,
"total_remaining": 2500000,
"budget_count": 4,
"exceeded_count": 1,
"available_categories": [
{"id": 5, "name": "Hiburan", "icon": "🎮"}
]
}
}Get laporan lengkap.
Query Parameters:
| Parameter | Type | Default |
|---|---|---|
| year | integer | current year |
| month | integer | current month |
Response (200):
{
"status": "success",
"data": {
"stats": {...},
"category_breakdown": {...},
"daily_trend": [...],
"income_vs_expense": [...],
"available_months": [
{"year": 2025, "month": 12, "label": "Desember 2025"}
]
}
}Get monthly statistics.
Get category breakdown untuk report.
Get daily spending trend.
Response (200):
{
"status": "success",
"data": [
{"day": 1, "income": 0, "expense": 150000},
{"day": 2, "income": 10000000, "expense": 50000},
...
]
}import AsyncStorage from '@react-native-async-storage/async-storage';
const API_URL = 'http://10.0.2.2:8000/api/v1'; // Android Emulator
// const API_URL = 'http://localhost:8000/api/v1'; // iOS Simulator
class ApiService {
async getToken() {
return await AsyncStorage.getItem('token');
}
async request(endpoint, options = {}) {
const token = await this.getToken();
const config = {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
...options.headers,
},
...options,
};
if (options.body && typeof options.body === 'object') {
config.body = JSON.stringify(options.body);
}
const response = await fetch(`${API_URL}${endpoint}`, config);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Request failed');
}
return data;
}
// Auth
async login(email, password) {
const data = await this.request('/auth/login', {
method: 'POST',
body: { email, password },
});
await AsyncStorage.setItem('token', data.data.token);
await AsyncStorage.setItem('user', JSON.stringify(data.data.user));
return data;
}
async logout() {
await this.request('/auth/logout', { method: 'POST' });
await AsyncStorage.multiRemove(['token', 'user']);
}
// Dashboard
async getDashboard() {
return await this.request('/dashboard');
}
// Transactions
async getTransactions(params = {}) {
const query = new URLSearchParams(params).toString();
return await this.request(`/transactions?${query}`);
}
async createTransaction(data) {
return await this.request('/transactions', {
method: 'POST',
body: data,
});
}
async deleteTransaction(id) {
return await this.request(`/transactions/${id}`, {
method: 'DELETE',
});
}
// Categories
async getCategories(type = null) {
const query = type ? `?type=${type}` : '';
return await this.request(`/categories${query}`);
}
// Budgets
async getBudgets() {
return await this.request('/budgets');
}
// Reports
async getReports(year, month) {
return await this.request(`/reports?year=${year}&month=${month}`);
}
}
export default new ApiService();import api from '../services/api';
// In your component
const [transactions, setTransactions] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadTransactions();
}, []);
const loadTransactions = async () => {
try {
const response = await api.getTransactions({ period: 'month' });
setTransactions(response.data);
} catch (error) {
Alert.alert('Error', error.message);
} finally {
setLoading(false);
}
};
const handleDelete = async (id) => {
try {
await api.deleteTransaction(id);
setTransactions(prev => prev.filter(t => t.id !== id));
} catch (error) {
Alert.alert('Error', error.message);
}
};-
Base URL untuk Emulator:
- Android:
http://10.0.2.2:8000/api/v1 - iOS:
http://localhost:8000/api/v1 - Real device: Gunakan IP address laptop (contoh:
http://192.168.1.100:8000/api/v1)
- Android:
-
Token Storage: Simpan token di AsyncStorage setelah login
-
Error Handling: Semua error response memiliki format yang sama dengan
status: "error" -
Pagination: Gunakan
meta.last_pageuntuk load more functionality -
Date Format: Selalu gunakan format
Y-m-d(contoh:2025-12-12) -
Amount: Kirim sebagai number, tidak perlu format currency
Aplikasi DuitKu menggunakan tema iOS 26 Liquid Glass yang menampilkan:
- Frosted glass effect dengan blur dan transparansi
- Minimalist & clean UI dengan spacing yang lega
- Soft shadows dan border halus
- Premium feel dengan gradients dan animasi halus
- Dark mode as default dengan opsi light mode
const darkColors = {
// Backgrounds
bgBase: '#000000', // App background
bgElevated: 'rgba(28, 28, 30, 0.8)', // Cards, modals
glassBg: 'rgba(255, 255, 255, 0.08)', // Glass effect
glassBgHeavy: 'rgba(255, 255, 255, 0.12)', // Heavier glass
// Text
textPrimary: '#FFFFFF',
textSecondary: 'rgba(255, 255, 255, 0.7)',
textTertiary: 'rgba(255, 255, 255, 0.5)',
// Accent
accentColor: '#0A84FF', // Primary blue
accentSecondary: '#5E5CE6', // Purple
// System Colors
systemGreen: '#30D158', // Income, success
systemRed: '#FF453A', // Expense, error
systemOrange: '#FF9F0A', // Warning
// Borders & Fills
separator: 'rgba(255, 255, 255, 0.08)',
fillPrimary: 'rgba(120, 120, 128, 0.36)',
glassBorder: 'rgba(255, 255, 255, 0.1)',
};const lightColors = {
// Backgrounds
bgBase: '#F2F2F7',
bgElevated: 'rgba(255, 255, 255, 0.8)',
glassBg: 'rgba(255, 255, 255, 0.6)',
glassBgHeavy: 'rgba(255, 255, 255, 0.75)',
// Text
textPrimary: '#000000',
textSecondary: 'rgba(0, 0, 0, 0.6)',
textTertiary: 'rgba(0, 0, 0, 0.4)',
// Accent
accentColor: '#007AFF',
accentSecondary: '#5856D6',
// System Colors
systemGreen: '#34C759',
systemRed: '#FF3B30',
systemOrange: '#FF9500',
// Borders & Fills
separator: 'rgba(0, 0, 0, 0.08)',
fillPrimary: 'rgba(120, 120, 128, 0.2)',
glassBorder: 'rgba(0, 0, 0, 0.06)',
};const typography = {
// Font Family - Use system fonts
fontFamily: Platform.select({
ios: 'System',
android: 'Roboto',
}),
// Sizes
title: {
fontSize: 32,
fontWeight: '700',
letterSpacing: -0.5,
},
headline: {
fontSize: 17,
fontWeight: '600',
letterSpacing: -0.2,
},
body: {
fontSize: 16,
fontWeight: '400',
letterSpacing: -0.2,
},
caption: {
fontSize: 13,
fontWeight: '400',
color: 'textTertiary',
},
stat: {
fontSize: 36,
fontWeight: '700',
letterSpacing: -0.5,
},
};const glassCard = {
backgroundColor: colors.glassBg,
borderRadius: 20,
borderWidth: 1,
borderColor: colors.glassBorder,
padding: 16,
// Note: Use expo-blur for blur effect in React Native
// Or use react-native-blur
};// Primary Button
const primaryButton = {
backgroundColor: colors.accentColor,
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
};
// Secondary Button (Glass)
const secondaryButton = {
backgroundColor: colors.glassBg,
borderWidth: 1,
borderColor: colors.glassBorder,
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 14,
};const inputField = {
backgroundColor: colors.glassBg,
borderWidth: 1,
borderColor: colors.glassBorder,
borderRadius: 14,
paddingVertical: 14,
paddingHorizontal: 16,
fontSize: 16,
color: colors.textPrimary,
};
// Focused state
const inputFieldFocused = {
borderColor: colors.accentColor,
// Add shadow/glow effect
};┌─────────────────────────────────────┐
│ [≡] Dashboard [🌙] [👤 ▾] │ ← Navbar
├─────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ │ ← Stats Grid
│ │ Income │ │ Expense │ │
│ │ Rp 10jt │ │ Rp 5jt │ │
│ │ +15.5% │ │ -10.2% │ │
│ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Balance │ │ Trans. │ │
│ │ Rp 5jt │ │ 25 │ │
│ └─────────┘ └─────────┘ │
│ │
│ ┌─────────────────────────────────┐│ ← Chart Card
│ │ Grafik Keuangan [7 Hari ▾] ││
│ │ ││
│ │ ░░ ▓▓▓▓ ││
│ │ ░░░░ ▓▓▓▓▓▓ ░░░ ││
│ │ ░░░░░░ ▓▓▓▓▓▓▓▓ ░░░░ ░ ││
│ │ Sat Sun Mon Tue Wed Thu Fri ││
│ └─────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────┐│ ← Recent Transactions
│ │ Transaksi Terbaru [Lihat >] ││
│ ├─────────────────────────────────┤│
│ │ 🍔 Makan siang - Rp 150.000 ││
│ │ 💰 Gaji + Rp 10.000.000│
│ └─────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────┐│ ← Budget Overview
│ │ Quick Actions ││
│ │ [+ Pemasukan] [- Pengeluaran] ││
│ └─────────────────────────────────┘│
│ │
└─────────────────────────────────────┘
│ 🏠 💰 📁 💵 📊 │ ← Bottom Tab
└─────────────────────────────────────┘
📱 App
├── 🔐 Auth Stack (Unauthenticated)
│ ├── Login Screen
│ └── Register Screen
│
└── 🏠 Main Tab Navigator (Authenticated)
├── Dashboard Tab
│ └── Dashboard Screen
│
├── Transaksi Tab
│ ├── Transaction List Screen
│ ├── Add Transaction Modal
│ └── Transaction Detail Screen
│
├── Kategori Tab
│ ├── Category List Screen
│ └── Add/Edit Category Modal
│
├── Budget Tab
│ ├── Budget List Screen
│ └── Add Budget Modal
│
└── Laporan Tab
└── Report Screen (Charts & Stats)
Kategori menggunakan emoji sebagai icon:
- 🍔 Makanan
- 🚗 Transport
- 🛒 Belanja
- 🎮 Hiburan
- 💊 Kesehatan
- 📄 Tagihan
- 💰 Gaji
- 🎁 Bonus
- 💼 Freelance
- 📈 Investasi
- Fade in untuk loading content
- Scale on press untuk buttons (0.98 scale)
- Smooth transitions untuk theme switching
- Pull to refresh untuk lists
- Skeleton loading untuk data fetching
- Use Expo untuk kemudahan development
- expo-blur atau @react-native-community/blur untuk glassmorphism
- expo-linear-gradient untuk gradient backgrounds
- react-native-reanimated untuk animasi smooth
- nativewind atau styled-components untuk styling
- react-query atau SWR untuk data fetching/caching
- zustand atau context untuk state management
// contexts/ThemeContext.js
import React, { createContext, useState, useContext } from 'react';
const ThemeContext = createContext();
export const themes = {
dark: {
bgBase: '#000000',
bgElevated: 'rgba(28, 28, 30, 0.8)',
glassBg: 'rgba(255, 255, 255, 0.08)',
textPrimary: '#FFFFFF',
textSecondary: 'rgba(255, 255, 255, 0.7)',
accentColor: '#0A84FF',
systemGreen: '#30D158',
systemRed: '#FF453A',
// ... more colors
},
light: {
bgBase: '#F2F2F7',
bgElevated: 'rgba(255, 255, 255, 0.8)',
glassBg: 'rgba(255, 255, 255, 0.6)',
textPrimary: '#000000',
textSecondary: 'rgba(0, 0, 0, 0.6)',
accentColor: '#007AFF',
systemGreen: '#34C759',
systemRed: '#FF3B30',
// ... more colors
},
};
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark');
const toggleTheme = () => {
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
};
return (
<ThemeContext.Provider value={{
theme,
colors: themes[theme],
toggleTheme
}}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);// utils/currency.js
export const formatCurrency = (amount) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
};
// Usage: formatCurrency(1500000) => "Rp 1.500.000"// utils/date.js
import { format, formatDistanceToNow } from 'date-fns';
import { id } from 'date-fns/locale';
export const formatDate = (date) => {
return format(new Date(date), 'd MMM yyyy', { locale: id });
};
export const formatRelativeDate = (date) => {
return formatDistanceToNow(new Date(date), {
addSuffix: true,
locale: id
});
};
// Usage:
// formatDate('2025-12-12') => "12 Des 2025"
// formatRelativeDate('2025-12-12') => "2 hari yang lalu"