Homeβ€ΊZX Parentingβ€ΊZX Parentingβ€ΊApp Anak (Android)
DRAFT

App Anak (Android)

JSON

πŸ’‘ Ide & Konsep

Belum ada ide. Klik Edit untuk menambahkan.

Identitas & Konfigurasi

alamat.json

{
  "idPaket": "com.zomet.zxparentingkids",
  "versiNama": "2.5.0",
  "versiKode": 32,
  "beranda": "https://zxparenting.zomet.my.id",
  "api": "https://zxparenting.zomet.my.id/api/agent",
  "repoRilis": "djvpri/ZX-Parenting-agent"
}

{

"idPaket": "com.zomet.zxparentingkids",

"versiNama": "2.5.0",

"versiKode": 32,

"beranda": "https://zxparenting.zomet.my.id",

"api": "https://zxparenting.zomet.my.id/api/agent",

"repoRilis": "djvpri/ZX-Parenting-agent"

}


## Config build.gradle.kts
- **namespace**: `com.zxparenting.agent`
- **compileSdk**: 36
- **targetSdk**: 36 (wajib untuk app baru Okt 2026)
- **minSdk**: 24
- **Java**: 17
- **Compose**: BOM 2024.12.01
- **isMinifyEnabled**: false (no R8/ProGuard)
- **No native code** (no NDK/CMake/.so)

## Dependencies (15)
- Compose (ui, material3, material-icons-extended)
- AppCompat 1.7.0
- Retrofit 2.11.0 + Gson converter
- OkHttp (logging interceptor)
- Lifecycle (runtime-compose, viewmodel-compose)
- buildConfig = true (BuildConfig.API, BuildConfig.VERSI_NAMA, BuildConfig.VERSI_KODE)

## Rilis
- CI: `assembleDebug` per push to main
- Rilis: `workflow_dispatch` β†’ AAB + APK + GitHub Release
- Keystore: GitHub Secrets (KEYSTORE_B64, KEYSTORE_PASSWORD, KEYSTORE_ALIAS, KEYSTORE_KEY_PASSWORD)
- Tag: `git tag -f` + `push --force` (re-rilis versi sama OK)

Config build.gradle.kts

  • namespace: com.zxparenting.agent
  • compileSdk: 36
  • targetSdk: 36 (wajib untuk app baru Okt 2026)
  • minSdk: 24
  • Java: 17
  • Compose: BOM 2024.12.01
  • isMinifyEnabled: false (no R8/ProGuard)
  • No native code (no NDK/CMake/.so)

Dependencies (15)

  • Compose (ui, material3, material-icons-extended)
  • AppCompat 1.7.0
  • Retrofit 2.11.0 + Gson converter
  • OkHttp (logging interceptor)
  • Lifecycle (runtime-compose, viewmodel-compose)
  • buildConfig = true (BuildConfig.API, BuildConfig.VERSI_NAMA, BuildConfig.VERSI_KODE)

Rilis

  • CI: assembleDebug per push to main
  • Rilis: workflow_dispatch β†’ AAB + APK + GitHub Release
  • Keystore: GitHub Secrets (KEYSTORE_B64, KEYSTORE_PASSWORD, KEYSTORE_ALIAS, KEYSTORE_KEY_PASSWORD)
  • Tag: git tag -f + push --force (re-rilis versi sama OK)

Arsitektur & Struktur Kode

Overview

App Android parental control untuk HP anak. Konsumsi API /api/agent dari web ZX Parenting. Dual fungsi:

1. Agent parental control β€” foreground service ping loop, blokir layar, GPS, app blocking

2. Edu-gamification β€” login anak, kuis AI (Gemini), tugas, konten official, hadiah

File Structure (30 Kotlin files)

app/src/main/java/com/zxparenting/agent/
  MainActivity.kt          # Entry point, nav bottom bar (Beranda/Kuis/Tugas/Hadiah/Saya)
  PasangActivity.kt        # Layar pairing device ortu
  StatusActivity.kt        # Status koneksi device
  BlokirActivity.kt        # Layar blokir fullscreen (sesi habis/curfew)
  AgentService.kt          # Foreground service, ping loop 5 menit
  AksesService.kt          # Accessibility service, deteksi app depan
  AdminReceiver.kt         # Device admin receiver
  Api.kt                   # Retrofit interface (edu API: login, kuis, tugas, hadiah) β€” 359 lines
  ApiAgent.kt              # HttpURLConnection client (agent API: ping, lokasi, activity) β€” 68 lines
  Simpanan.kt              # SharedPreferences: token, JWT, profil anak
  Kebijakan.kt             # Parser aturan server + keputusan blokir
  Blokir.kt                # Status blokir aktif + daftar app diblokir
  Lokasi.kt                # GPS on-demand, lastKnownLocation
  Pemakaian.kt             # UsageStatsManager, laporan app usage
  ManajerBahasa.kt         # i18n: EN default + ID, AppCompatDelegate
  PemeriksaPembaruan.kt    # Auto-update APK dari GitHub releases
  Pemberitahuan.kt        # Notifikasi pengingat
  Ctx.kt                   # Global context helper
  DataDemo.kt              # Sample data untuk mode demo (reviewer Play Console)
  LaporError.kt            # Error reporting ke backend (POST /api/agent lapor-error)
  LayarBeranda.kt          # Feed konten official (kuis/artikel)
  LayarMasuk.kt            # Login anak (username + PIN) + Mode Demo
  LayarSaya.kt             # Profil anak, bahasa, device admin status
  LayarTugas.kt            # Daftar tugas anak
  LayarKuisAI.kt           # Buat kuis AI (Gemini on-demand)
  LayarKuisDetail.kt       # Detail kuis official
  LayarKuisSession.kt      # Sesi kuis: jawab per-soal, lock, review
  LayarHadiah.kt           # Katalog hadiah + tukar token β†’ waktu main
  tema/Palet.kt            # Color palette
  tema/WarnaAktif.kt       # Dynamic color scheme

app/src/main/java/com/zxparenting/agent/

MainActivity.kt # Entry point, nav bottom bar (Beranda/Kuis/Tugas/Hadiah/Saya)

PasangActivity.kt # Layar pairing device ortu

StatusActivity.kt # Status koneksi device

BlokirActivity.kt # Layar blokir fullscreen (sesi habis/curfew)

AgentService.kt # Foreground service, ping loop 5 menit

AksesService.kt # Accessibility service, deteksi app depan

AdminReceiver.kt # Device admin receiver

Api.kt # Retrofit interface (edu API: login, kuis, tugas, hadiah) β€” 359 lines

ApiAgent.kt # HttpURLConnection client (agent API: ping, lokasi, activity) β€” 68 lines

Simpanan.kt # SharedPreferences: token, JWT, profil anak

Kebijakan.kt # Parser aturan server + keputusan blokir

Blokir.kt # Status blokir aktif + daftar app diblokir

Lokasi.kt # GPS on-demand, lastKnownLocation

Pemakaian.kt # UsageStatsManager, laporan app usage

ManajerBahasa.kt # i18n: EN default + ID, AppCompatDelegate

PemeriksaPembaruan.kt # Auto-update APK dari GitHub releases

Pemberitahuan.kt # Notifikasi pengingat

Ctx.kt # Global context helper

DataDemo.kt # Sample data untuk mode demo (reviewer Play Console)

LaporError.kt # Error reporting ke backend (POST /api/agent lapor-error)

LayarBeranda.kt # Feed konten official (kuis/artikel)

LayarMasuk.kt # Login anak (username + PIN) + Mode Demo

LayarSaya.kt # Profil anak, bahasa, device admin status

LayarTugas.kt # Daftar tugas anak

LayarKuisAI.kt # Buat kuis AI (Gemini on-demand)

LayarKuisDetail.kt # Detail kuis official

LayarKuisSession.kt # Sesi kuis: jawab per-soal, lock, review

LayarHadiah.kt # Katalog hadiah + tukar token β†’ waktu main

tema/Palet.kt # Color palette

tema/WarnaAktif.kt # Dynamic color scheme


## Stats
- 30 Kotlin files, ~4764 LOC
- 143 strings (EN), 143 strings (ID)
- 5 tab: Beranda, Kuis, Tugas, Hadiah, Saya

Stats

  • 30 Kotlin files, ~4764 LOC
  • 143 strings (EN), 143 strings (ID)
  • 5 tab: Beranda, Kuis, Tugas, Hadiah, Saya

Manifest & Permissions

AndroidManifest.xml

<!-- Permissions -->
INTERNET
ACCESS_NETWORK_STATE
REQUEST_INSTALL_PACKAGES      <!-- auto-update APK -->
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
FOREGROUND_SERVICE
FOREGROUND_SERVICE_LOCATION
POST_NOTIFICATIONS
PACKAGE_USAGE_STATS           <!-- tools:ignore=ProtectedPermissions -->

<!-- Components -->
MainActivity                  (LAUNCHER)
StatusActivity                (exported=false)
PasangActivity                (exported=false)
BlokirActivity                (singleInstance, showOnLockScreen, excludeFromRecents)
AgentService                  (foregroundServiceType=location)
AksesService                  (BIND_ACCESSIBILITY_SERVICE)
AdminReceiver                 (BIND_DEVICE_ADMIN)
FileProvider                  (${applicationId}.fileprovider)

<!-- Permissions -->

INTERNET

ACCESS_NETWORK_STATE

REQUEST_INSTALL_PACKAGES <!-- auto-update APK -->

ACCESS_FINE_LOCATION

ACCESS_COARSE_LOCATION

FOREGROUND_SERVICE

FOREGROUND_SERVICE_LOCATION

POST_NOTIFICATIONS

PACKAGE_USAGE_STATS <!-- tools:ignore=ProtectedPermissions -->

<!-- Components -->

MainActivity (LAUNCHER)

StatusActivity (exported=false)

PasangActivity (exported=false)

BlokirActivity (singleInstance, showOnLockScreen, excludeFromRecents)

AgentService (foregroundServiceType=location)

AksesService (BIND_ACCESSIBILITY_SERVICE)

AdminReceiver (BIND_DEVICE_ADMIN)

FileProvider (${applicationId}.fileprovider)


## Permission Flow
1. User install app β†’ login anak (username + PIN)
2. Pairing device ortu (deviceToken dari dashboard)
3. Request: location, usage access, device admin, accessibility
4. AgentService start foreground β†’ ping loop 5 menit

Permission Flow

1. User install app β†’ login anak (username + PIN)

2. Pairing device ortu (deviceToken dari dashboard)

3. Request: location, usage access, device admin, accessibility

4. AgentService start foreground β†’ ping loop 5 menit

AgentService β€” Ping Loop

AgentService.kt β€” Foreground service, jantung agent.

START_STICKY (Android restart saat sistem matikan).

βœ… Dynamic Ping Interval (commit `edc2261c`, Okt 2026)

Normal: 5 menit

PERIODE_MS = 5 * 60_000L β€” default interval, tidak boros baterai.

Mendekati curfew: 1 menit

hitungPeriode() cek Kebijakan.menitKeCurfew(jam, menit, mulai, selesai):

  • Jika < 10 menit sebelum curfew mulai β†’ ping 1 menit (lag maks 1 menit)
  • Jika dalam curfew β†’ ping 1 menit (cepat deteksi selesai curfew)
  • Jika tidak ada curfew / jauh dari curfew β†’ ping 5 menit

Cross-midnight support

menitKeCurfew() support curfew lintas tengah malam (mis 21:00-05:00):

  • 20:50 β†’ 10 menit sebelum curfew β†’ ping 1 menit
  • 20:55 β†’ 5 menit β†’ ping 1 menit
  • 21:00 β†’ dalam curfew β†’ ping 1 menit
  • 04:59 β†’ masih dalam curfew β†’ ping 1 menit
  • 05:01 β†’ curfew selesai β†’ ping 5 menit

Handler.postDelayed

Menggantikan Thread.sleep loop β€” lebih responsif, tidak block thread.

Alur per ping:

1. ApiAgent.ping(context) β†’ POST /api/agent {aksi: "ping"} dengan header x-device-token

2. Simpan timestamp ping terakhir

3. Parse respons β†’ Aturan (sessionLimit, cooldown, dailyLimit, curfew, appBlokir)

4. Simpan aturan via Kebijakan.simpan()

5. Simpan daftar app diblokir via Blokir.simpanDaftarApp()

6. Catat sesi_main bila sesiTerakhir == null && !masterUnlock

7. GPS on-demand: Lokasi.kirimBilaDiminta() bila server minta

8. Evaluasi blokir: Kebijakan.evaluasi(aturan, jam, menit) β†’ tampilkan/sembunyikan BlokirActivity

9. βœ… Cek accessibility: AksesService.instance == null β†’ notifikasi + log "accessibility_off"

10. Tiap 12 ping (~1 jam): kirim laporan usage stats bila izin ada

Notifikasi foreground

Channel agent (IMPORTANCE_LOW), ongoing notification "ZX Agent active" + "Device connected to parent".

Channel alert (IMPORTANCE_HIGH) untuk notifikasi accessibility off.

API Layer β€” Dual API

Dua API client terpisah:

1. ApiAgent.kt (68 lines) β€” Agent API

  • Api.ping() β†’ JSONObject respons atau null
  • Api.kirimLokasi(lat, lng, akurasi) β†’ Boolean
  • Api.kirimAktivitas(tipe, detail?) β†’ Boolean
  • Api.laporError(screen, message, extra?) β†’ Boolean

2. Api.kt (359 lines) β€” Edu API (Retrofit)

Bearer JWT auth. Endpoint:

  • POST auth/native-anak β€” login (username + PIN) β†’ JWT + AnakInfo
  • GET anak/official β€” feed konten (kuis harian, tantangan, artikel) + officialMeta nested
  • GET anak/official/{id} β€” detail kuis + soal
  • POST anak/official/{id}/complete β€” submit jawaban β†’ skor + token reward
  • POST anak/kuis-ai/start β€” generate kuis AI (Gemini) β†’ attemptId + soal
  • POST anak/kuis-ai/{attemptId}/submit β€” submit jawaban β†’ hasil + explain
  • GET anak/tugas β€” daftar tugas anak
  • POST anak/tugas/{id}/session/start β€” mulai sesi kuis tugas
  • POST anak/tugas/session/{id}/submit β€” submit jawaban sesi
  • GET anak/tugas/session/{id}/review β€” review jawaban + pembahasan
  • GET anak/saldo β€” saldo token anak
  • GET anak/streak β€” streak harian + terbaik
  • GET anak/challenge β€” challenge hari ini + status selesai
  • GET anak/hadiah β€” katalog hadiah
  • POST anak/hadiah/{id}/tukar β€” tukar token β†’ hadiah

DTO lengkap (~25 data class)

LoginAnakReq/Res, OfficialItem/Meta/Feed, TugasAnak, Soal, OfficialDetail, CompleteReq/Res, KuisAIReq/Res, SubmitKuisReq/Res, HasilSoal, KuisSessionStartReq/Res, SoalSession, JawabanLocked, KuisSessionSubmitReq/Res, ReviewSoal, KuisSessionReviewRes, SaldoRes, StreakRes, ChallengeRes, HadiahItem, TukarReq/Res

Backend monitoring (untuk AI agent)

  • GET /api/diagnostik?token=<CRON_TOKEN> β€” health snapshot (DB, env, kuis harian, errors, tugas)
  • GET /api/error-log?token=<CRON_TOKEN> β€” error log machine-readable
  • scripts/smoke-test.sh β€” E2E smoke test (login β†’ feed β†’ saldo β†’ streak β†’ challenge β†’ tugas β†’ diagnostik)

Parental Control: Blokir & Kebijakan

Kebijakan.kt (88 lines)

Parser aturan server + evaluator blokir.

Aturan (dari server)

data class Aturan(
    sessionLimit: Int,       // menit per sesi
    cooldown: Int,           // menit jeda antar sesi
    dailyLimit: Int,         // menit total per hari
    curfewMulai: String?,    // "21:00"
    curfewSelesai: String?,  // "05:00"
    masterUnlock: Boolean,   // ortu override
    sesiTerpakaiHariIni: Int,
    sesiTerakhir: Long?,     // epoch ms
    gpsMinta: Boolean,
    appBlokir: List<String>, // daftar paket diblokir
)

data class Aturan(

sessionLimit: Int, // menit per sesi

cooldown: Int, // menit jeda antar sesi

dailyLimit: Int, // menit total per hari

curfewMulai: String?, // "21:00"

curfewSelesai: String?, // "05:00"

masterUnlock: Boolean, // ortu override

sesiTerpakaiHariIni: Int,

sesiTerakhir: Long?, // epoch ms

gpsMinta: Boolean,

appBlokir: List<String>, // daftar paket diblokir

)


### AlasanBlokir enum
`JAM_TIDUR` | `DAILY_HABIS` | `COOLDOWN`

### Kebijakan.evaluasi(aturan, jam, menit) β†’ AlasanBlokir?
- Cek curfew (support lintas tengah malam, mis 21:00-05:00)
- Cek daily limit terpakai
- Cek cooldown

## Blokir.kt (46 lines)
Status blokir di SharedPreferences (bertahan walau service dibunuh).
- `nyalakan(alasan)` / `matikan()` / `aktif()` / `alasan()`
- `simpanDaftarApp(daftar)` / `daftarApp()` β€” JSON array
- `overlayAppAktif` β€” volatile flag untuk AksesService overlay

## BlokirActivity.kt (94 lines)
Fullscreen, `singleInstance`, `showOnLockScreen`, `excludeFromRecents`.
- Tampilkan pesan blokir (bedtime/daily/cooldown)
- Tombol "Minta Izin Ortu"
- `selesaiJikaAda()` β€” static method dismiss

AlasanBlokir enum

JAM_TIDUR | DAILY_HABIS | COOLDOWN

Kebijakan.evaluasi(aturan, jam, menit) β†’ AlasanBlokir?

  • Cek curfew (support lintas tengah malam, mis 21:00-05:00)
  • Cek daily limit terpakai
  • Cek cooldown

Blokir.kt (46 lines)

Status blokir di SharedPreferences (bertahan walau service dibunuh).

  • nyalakan(alasan) / matikan() / aktif() / alasan()
  • simpanDaftarApp(daftar) / daftarApp() β€” JSON array
  • overlayAppAktif β€” volatile flag untuk AksesService overlay

BlokirActivity.kt (94 lines)

Fullscreen, singleInstance, showOnLockScreen, excludeFromRecents.

  • Tampilkan pesan blokir (bedtime/daily/cooldown)
  • Tombol "Minta Izin Ortu"
  • selesaiJikaAda() β€” static method dismiss

GPS & Lokasi

Lokasi.kt (48 lines)

GPS on-demand β€” server minta (gps.minta=true di ping) β†’ kirim posisi.

Strategi:

1. Cek izin ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION

2. Ambil lastKnownLocation dari GPS/NETWORK/PASSIVE provider β†’ pilih terbaru

3. Kirim via Api.kirimLokasi(lat, lng, akurasi)

Data minimization:

Hanya posisi terakhir dikirim, tanpa histori. Bila lokasi terlalu tua (>= 10 menit), requestSingleUpdate dengan timeout singkat.

`kirimBilaDiminta(c, minta)` β†’ "ok" | "tanpa_izin" | "gagal"

Accessibility Service β€” App Blocking

AksesService.kt (64 lines)

Fase 5: blokir per-app. Deteksi app yang pindah ke depan via TYPE_WINDOW_STATE_CHANGED.

Alur:

1. Event TYPE_WINDOW_STATE_CHANGED β†’ e.packageName

2. Skip app sendiri + launcher sistem

3. Cek Blokir.daftarApp() β€” bila paket ada di daftar β†’ tampilkan BlokirActivity overlay

4. Anak pindah ke app tidak diblokir β†’ lepas overlay (Blokir.overlayAppAktif = false)

Config (`akses_service.xml`):

accessibilityEventTypes: typeWindowStateChanged
accessibilityFeedbackType: feedbackGeneric
accessibilityFlags: flagIncludeNotImportantViews
notificationTimeout: 100

accessibilityEventTypes: typeWindowStateChanged

accessibilityFeedbackType: feedbackGeneric

accessibilityFlags: flagIncludeNotImportantViews

notificationTimeout: 100

Usage Stats β€” Laporan Pemakaian App

Pemakaian.kt (76 lines)

Laporan pemakaian app via UsageStatsManager. Butuh izin PACKAGE_USAGE_STATS (user setujui di Settings).

`ringkas(c, jam=6)` β†’ JSONObject `{paket: menit}`

  • Query events ACTIVITY_RESUMED / ACTIVITY_PAUSED
  • Hitung durasi per app (bounding antar event)
  • App masih terbuka: hitung sampai sekarang
  • Hanya app >= 1 menit

`kirim(c)` β†’ Boolean

Kirim ke server: Api.kirimAktivitas(c, "app_usage", {per_app, jam}). Dipanggil tiap 12 ping (~1 jam).

`adaIzin(c)` β†’ Boolean

Cek via AppOpsManager.unsafeCheckOpNoThrow(OPSTR_GET_USAGE_STATS)

UI Screens β€” Jetpack Compose

UI Screens β€” Jetpack Compose (5 tab)

| Screen | File | Fungsi |

|--------|------|--------|

| Beranda | LayarBeranda.kt | Feed konten official (kuis harian, tantangan, artikel) |

| Kuis AI | LayarKuisAI.kt | Buat kuis custom via Gemini (topik + level + jumlah) |

| Kuis Detail | LayarKuisDetail.kt | Detail kuis official + mulai sesi |

| Kuis Session | LayarKuisSession.kt | Sesi kuis: jawab per-soal, lock jawaban, review |

| Tugas | LayarTugas.kt | Daftar tugas anak + mulai sesi kuis tugas |

| Hadiah | LayarHadiah.kt | Katalog hadiah + tukar token β†’ waktu main |

| Saya | LayarSaya.kt | Profil anak, bahasa, device admin status |

| Login | LayarMasuk.kt | Login anak (username + PIN) + Mode Demo |

| Blokir | BlokirActivity.kt | Layar blokir fullscreen (sesi habis/curfew) |

| Pasang | PasangActivity.kt | Pairing device ortu |

| Status | StatusActivity.kt | Status koneksi device |

Theme

  • tema/Palet.kt β€” color palette
  • tema/WarnaAktif.kt β€” dynamic color scheme
  • Material 3 + Compose

i18n β€” Internasionalisasi

i18n β€” Internasionalisasi

  • res/values/strings.xml β€” 143 strings (English default)
  • res/values-in/strings.xml β€” 143 strings (Indonesian)
  • ManajerBahasa.kt β€” AppCompatDelegate.setApplicationLocales
  • Bahasa: EN (default), ID
  • Switch bahasa di LayarSaya

Auto-Update APK

PemeriksaPembaruan.kt (122 lines)

Cek GitHub releases latest, banding versi (tuple angka [major, minor, patch]), unduh APK, buka installer via FileProvider.

Versi comparison

versiNama dibandingkan sebagai tuple, BUKAN versiKode. Contoh: 1.9.0 β†’ [1, 9, 0].

Flow:

1. rilisTerbaru() β€” GET api.github.com/repos/djvpri/ZX-Parenting-agent/releases/latest

2. Banding tag vs BuildConfig.VERSI_NAMA

3. Cari asset .apk di release

4. unduh(url, ctx) β€” simpan ke getExternalFilesDir(null)/updates/zx-agent-update.apk

5. Buka installer via FileProvider.getUriForFile() + ACTION_INSTALL_PACKAGE

Permission: `REQUEST_INSTALL_PACKAGES`

Device Admin & Local Storage

AdminReceiver.kt (6 lines)

Device admin receiver β€” force-lock policy. Memungkinkan lockNow() untuk kunci layar saat sesi habis.

device_admin.xml:

<uses-policies>
    <force-lock />
</uses-policies>

<uses-policies>

<force-lock />

</uses-policies>


## Simpanan.kt (64 lines)
SharedPreferences (`zx_agent`):
- `token` β€” device pairing token (x-device-token)
- `terakhir_ping` β€” timestamp ping terakhir
- `jwt_anak` β€” JWT bearer anak (Authorization)
- `nama_anak`, `anak_id`, `umur_anak` β€” profil anak

Methods: `token()`, `simpanToken()`, `hapusToken()`, `catatPing()`, `pingTerakhir()`, `jwt()`, `simpanAnak()`, `namaAnak()`, `anakId()`, `umurAnak()`, `hapusAnak()`

Simpanan.kt (64 lines)

SharedPreferences (zx_agent):

  • token β€” device pairing token (x-device-token)
  • terakhir_ping β€” timestamp ping terakhir
  • jwt_anak β€” JWT bearer anak (Authorization)
  • nama_anak, anak_id, umur_anak β€” profil anak

Methods: token(), simpanToken(), hapusToken(), catatPing(), pingTerakhir(), jwt(), simpanAnak(), namaAnak(), anakId(), umurAnak(), hapusAnak()

CI/CD & Keystore

CI/CD

ci.yml β€” per push to main

  • assembleDebug β€” build check, no artifact

rilis.yml β€” workflow_dispatch

  • Input: versi (mis. 2.5.0)
  • Auto-bump versiKode (+1) di alamat.json
  • Build AAB (Play Store) + APK (pasang langsung)
  • Sign dengan keystore dari GitHub Secrets
  • git tag -f v{VERSI} + push --force (re-rilis OK)
  • Create GitHub Release dengan AAB + APK

Keystore

  • GitHub Secrets: KEYSTORE_B64, KEYSTORE_PASSWORD, KEYSTORE_ALIAS, KEYSTORE_KEY_PASSWORD
  • Tidak committed ke repo
  • Rotation v2.4.0+

Status terakhir

  • v2.5.0 / versiKode 32 β€” CI run #33 success
  • AAB + APK di GitHub Release v2.5.0
  • Ready upload ke Play Console

Play Console Readiness

Play Console Readiness

App Access untuk Reviewer

App butuh pairing device ortu untuk full functionality.

Demo mode tersedia: buka app β†’ tombol "Mode Demo" di layar login.

Demo menampilkan sample kuis, tugas, hadiah, dan UI tanpa device ortu.

Store Listing

  • Package: com.zomet.zxparentingkids
  • Developer account: Zomet Teknologi Indonesia (ID 6348446758161954408)
  • Status: Draf β€” 2 apps (ZX Parenting Ortu + ZX Parenting Kids)
  • targetSdk: 36 (wajib Okt 2026)
  • AAB uploaded: v3.4.1 ortu, v2.5.0 kids (versiKode 32)
  • Internal testing track: ortu committed βœ…

Service account

  • zx-parenting-billing@zxparenting.iam.gserviceaccount.com
  • Diinvite via Users & permissions β†’ Admin (all permissions) βœ…

Known issues resolved

  • targetSdk 35 β†’ 36 (Google reject "Target SDK too low: 54" misleading β€” 54=versionCode)
  • Tag conflict re-rilis β†’ fixed dengan git tag -f + push --force

Kontrak API Backend

Kontrak API Backend β€” App Anak ↔ Backend

Auth

| Method | Endpoint | Body | Response |

|--------|----------|------|----------|

| POST | /api/auth/native-anak | {username, pin} | {token: JWT, anak: {id, nama, umur}} |

Edu API (Bearer JWT)

| Method | Endpoint | Response |

|--------|----------|----------|

| GET | /api/anak/official | {items: [{id, tipe, judul, officialMeta: {tokenReward, levelKesulitan, views}, selesai}], total} |

| GET | /api/anak/official/{id} | detail kuis + soal |

| POST | /api/anak/official/{id}/complete | skor + token reward |

| POST | /api/anak/kuis/start | {attemptId, soal: [{q, options}]} |

| GET | /api/anak/tugas | [{id, judul, tokenReward, status, deadline}] |

| GET | /api/anak/saldo | {saldo: int} |

| GET | /api/anak/streak | {current, terbaik, terakhir} |

| GET | /api/anak/challenge | {topik, tokenReward, selesai} |

Agent API (x-device-token header)

| Method | Endpoint | Body |

|--------|----------|------|

| POST | /api/agent | {aksi: "ping", "activity", "lapor-error", "lokasi"} |

Monitoring (CRON_TOKEN)

| Method | Endpoint | Response |

|--------|----------|----------|

| GET | /api/diagnostik?token= | health snapshot JSON |

| GET | /api/error-log?token= | error log JSON |

Backend fixes (Okt 2026)

  • Race condition kuis harian: cek by (topik, publishAt) β€” no more duplikat
  • DTO officialMeta: nested object, bukan flat fields
  • Challenge deterministic by date
  • Endpoint cleanup duplikat: one-shot, sudah dihapus

Gap Analysis: Web vs App Android

Fitur yang ada di web dashboard anak (`/anak`) tapi BELUM di app Android

| # | Fitur | Web File | API Endpoint | App Android |

|---|-------|----------|--------------|-------------|

| 1 | Tukar Token β†’ Waktu Main | TukarToken.tsx | POST /api/anak/sesi-main | ❌ belum |

| 2 | Katalog Hadiah (minta dari ortu) | KatalogHadiah.tsx | GET/POST /api/anak/hadiah | ❌ belum |

| 3 | Quest Keluarga (status quest) | QuestAnak.tsx | GET /api/quests-saya | ❌ belum |

| 4 | Challenge Harian (progress) | ChallengeKartu.tsx | challenge API | ❌ belum |

| 5 | Pinjam Waktu (token darurat) | blueprint ekonomi-token | GET /api/pinjam | ❌ belum |

| 6 | Riwayat Token (ledger mutasi) | blueprint ekonomi-token | GET /api/token-ledger | ❌ belum |

Fitur yang sudah ada di app Android

| # | Fitur | App File | Status |

|---|-------|----------|--------|

| 1 | Login anak (PIN) | LayarMasuk.kt | βœ… |

| 2 | Feed official content | LayarBeranda.kt | βœ… |

| 3 | Kuis AI (Gemini) | LayarKuisAI.kt | βœ… |

| 4 | Detail kuis official | LayarKuisDetail.kt | βœ… |

| 5 | Sesi kuis tugas | LayarKuisSession.kt | βœ… |

| 6 | Daftar tugas | LayarTugas.kt | βœ… |

| 7 | Profil + bahasa | LayarSaya.kt | βœ… |

| 8 | Ping loop + blokir + GPS | AgentService.kt | βœ… |

| 9 | Auto-update APK | PemeriksaPembaruan.kt | βœ… |

Bottom nav sekarang: 4 tab

Beranda / Kuis AI / Tugas / Saya

Saran: tambah tab ke-5 "Hadiah"

Gabung Tukar Token + Katalog Hadiah + Quest + Pinjam Waktu + Riwayat Token. Semua API backend sudah live.

Fitur Missing #1: Tukar Token β†’ Waktu Main

Konsep

Anak tukar token (1 token = 1 menit main) β†’ waktu main aktif. Saldo berkurang, sesi main bertambah.

Web: `TukarToken.tsx` (58 lines)

  • Dropdown pilih menit: 5, 10, 15, 30, 45, 60, 90, 120
  • Tombol "Tukar" β†’ disabled jika saldo < 5
  • POST /api/anak/sesi-main {menit: N}
  • Response: {menit, saldo} β€” sukses pesan + saldo baru

API

POST /api/anak/sesi-main
Header: Authorization: Bearer <jwt>
Body: {menit: 15}
Response: {menit: 15, saldo: 305}
Error: {error: "Saldo tidak cukup"}

POST /api/anak/sesi-main

Header: Authorization: Bearer <jwt>

Body: {menit: 15}

Response: {menit: 15, saldo: 305}

Error: {error: "Saldo tidak cukup"}


## Implementasi app Android

Implementasi app Android

// Api.kt β€” tambah endpoint
@POST("anak/sesi-main")
suspend fun tukarToken(
    @Header("Authorization") bearer: String,
    @Body req: TukarTokenReq
): Response<TukarTokenRes>

data class TukarTokenReq(val menit: Int)
data class TukarTokenRes(val menit: Int, val saldo: Int)

// Api.kt β€” tambah endpoint

@POST("anak/sesi-main")

suspend fun tukarToken(

@Header("Authorization") bearer: String,

@Body req: TukarTokenReq

): Response<TukarTokenRes>

data class TukarTokenReq(val menit: Int)

data class TukarTokenRes(val menit: Int, val saldo: Int)


## UI: di dalam tab "Hadiah"
- Card "Tukar Token β†’ Waktu Main"
- Dropdown pilihan menit
- Tombol "Tukar" (hijau)
- Pesan sukses/error
- Minimal 5 token untuk tukar

UI: di dalam tab "Hadiah"

  • Card "Tukar Token β†’ Waktu Main"
  • Dropdown pilihan menit
  • Tombol "Tukar" (hijau)
  • Pesan sukses/error
  • Minimal 5 token untuk tukar

Fitur Missing #2: Katalog Hadiah

Konsep

Ortu buat hadiah (mainan, jajanan, dll) dengan harga coin. Anak lihat katalog β†’ klik "Minta" β†’ ortu approve β†’ dibelikan.

Web: `KatalogHadiah.tsx` (105 lines)

  • Collapsible card "Katalog Hadiah"
  • GET /api/anak/hadiah β†’ daftar hadiah {id, judul, deskripsi, hargaCoin, stok, sudahMinta}
  • Tombol "Minta" per item β†’ POST /api/anak/hadiah {hadiahId}
  • Status: "Sudah diminta" / "Stok habis" / "Minta"
  • Response sukses: "Permintaan terkirim ke orang tua!"

API

GET /api/anak/hadiah
Header: Authorization: Bearer <jwt>
Response: {katalog: [{id, judul, deskripsi, hargaCoin, stok, sudahMinta}]}

POST /api/anak/hadiah
Body: {hadiahId: "xxx"}
Response: {ok: true}
Error: {error: "Sudah diminta" | "Stok habis"}

GET /api/anak/hadiah

Header: Authorization: Bearer <jwt>

Response: {katalog: [{id, judul, deskripsi, hargaCoin, stok, sudahMinta}]}

POST /api/anak/hadiah

Body: {hadiahId: "xxx"}

Response: {ok: true}

Error: {error: "Sudah diminta" | "Stok habis"}


## Implementasi app Android

Implementasi app Android

@GET("anak/hadiah")
suspend fun hadiahList(@Header("Authorization") bearer: String): Response<HadiahListRes>

@POST("anak/hadiah")
suspend fun hadiahMinta(
    @Header("Authorization") bearer: String,
    @Body req: HadiahMintaReq
): Response<HadiahMintaRes>

data class HadiahItem(
    val id: String, val judul: String,
    val deskripsi: String?, val hargaCoin: Int,
    val stok: Int, val sudahMinta: Boolean
)
data class HadiahListRes(val katalog: List<HadiahItem>)
data class HadiahMintaReq(val hadiahId: String)

@GET("anak/hadiah")

suspend fun hadiahList(@Header("Authorization") bearer: String): Response<HadiahListRes>

@POST("anak/hadiah")

suspend fun hadiahMinta(

@Header("Authorization") bearer: String,

@Body req: HadiahMintaReq

): Response<HadiahMintaRes>

data class HadiahItem(

val id: String, val judul: String,

val deskripsi: String?, val hargaCoin: Int,

val stok: Int, val sudahMinta: Boolean

)

data class HadiahListRes(val katalog: List<HadiahItem>)

data class HadiahMintaReq(val hadiahId: String)


## UI: di dalam tab "Hadiah"
- Section "Katalog Hadiah"
- LazyColumn kartu hadiah
- Badge harga coin + status stok
- Tombol "Minta" per item

UI: di dalam tab "Hadiah"

  • Section "Katalog Hadiah"
  • LazyColumn kartu hadiah
  • Badge harga coin + status stok
  • Tombol "Minta" per item

Fitur Missing #3: Quest Keluarga

Konsep

Ortu buat quest keluarga (mis: "Bantu Mama bersih-bersih"). Anak lihat status quest β€” ortu tandai selesai β†’ anak dapat token reward.

Web: `QuestAnak.tsx` (71 lines)

  • GET /api/quests-saya β†’ daftar quest
  • Quest: {id, judul, deskripsi, tokenReward, deadline, anggota: [{anakId, selesai}]}
  • Anak hanya lihat status (⏳ menunggu / βœ… selesai)
  • Tidak ada aksi dari anak β€” ortu yang tandai

API

GET /api/quests-saya
Header: Authorization: Bearer <jwt>
Response: [{id, judul, deskripsi, tokenReward, deadline, anggota: [{anakId, selesai}]}]

GET /api/quests-saya

Header: Authorization: Bearer <jwt>

Response: [{id, judul, deskripsi, tokenReward, deadline, anggota: [{anakId, selesai}]}]


## Implementasi app Android

Implementasi app Android

@GET("quests-saya")
suspend fun questSaya(@Header("Authorization") bearer: String): Response<List<QuestAnak>>

data class QuestAnak(
    val id: String, val judul: String,
    val deskripsi: String?, val tokenReward: Int,
    val deadline: String?,
    val anggota: List<QuestMember>
)
data class QuestMember(val anakId: String, val selesai: Boolean)

@GET("quests-saya")

suspend fun questSaya(@Header("Authorization") bearer: String): Response<List<QuestAnak>>

data class QuestAnak(

val id: String, val judul: String,

val deskripsi: String?, val tokenReward: Int,

val deadline: String?,

val anggota: List<QuestMember>

)

data class QuestMember(val anakId: String, val selesai: Boolean)


## UI: di dalam tab "Hadiah" atau "Beranda"
- Section "Quest Keluarga"
- Card per quest: judul + token reward + deadline + status
- Hijau jika selesai, putih jika menunggu
- Pesan: "Ortu akan menandai selesai setelah dicek"

UI: di dalam tab "Hadiah" atau "Beranda"

  • Section "Quest Keluarga"
  • Card per quest: judul + token reward + deadline + status
  • Hijau jika selesai, putih jika menunggu
  • Pesan: "Ortu akan menandai selesai setelah dicek"

Fitur Missing #4: Challenge Harian

Konsep

Challenge harian per anak β€” target aktivitas (mis: "Kerjakan 3 kuis hari ini"). Progress tracking, reward token saat complete.

Web: `ChallengeKartu.tsx`

  • Menampilkan challenge aktif + progress
  • Backend: model ChallengeProgress β€” challenge harian per anak

Blueprint referensi

ChallengeProgress β€” challenge harian per anak. Terhubung dengan OfficialContent (TANTANGAN_MINGGUAN).

API (estimasi β€” perlu cek backend)

GET /api/anak/challenge
Header: Authorization: Bearer <jwt>
Response: [{id, judul, target, progress, tokenReward, selesai}]

GET /api/anak/challenge

Header: Authorization: Bearer <jwt>

Response: [{id, judul, target, progress, tokenReward, selesai}]


## Implementasi app Android

Implementasi app Android

@GET("anak/challenge")
suspend fun challengeAktif(@Header("Authorization") bearer: String): Response<List<ChallengeItem>>

data class ChallengeItem(
    val id: String, val judul: String,
    val target: Int, val progress: Int,
    val tokenReward: Int, val selesai: Boolean
)

@GET("anak/challenge")

suspend fun challengeAktif(@Header("Authorization") bearer: String): Response<List<ChallengeItem>>

data class ChallengeItem(

val id: String, val judul: String,

val target: Int, val progress: Int,

val tokenReward: Int, val selesai: Boolean

)


## UI: di tab "Beranda" (card di atas feed)
- Progress bar: progress/target
- Badge token reward
- Status: aktif / selesai

UI: di tab "Beranda" (card di atas feed)

  • Progress bar: progress/target
  • Badge token reward
  • Status: aktif / selesai

Fitur Missing #5: Pinjam Waktu

Konsep

Anak kehabisan token β†’ pinjam waktu darurat. Hutang token harus dibayar (dari reward berikutnya). Ortu lihat hutang anak.

Blueprint: `ekonomi-token`

  • PINJAM β€” pinjam waktu darurat
  • BAYAR_PINJAM β€” pembayaran pinjaman
  • App ortu: LayarPinjam.kt β€” anak pinjam token, bayar hutang

API

GET /api/pinjam?anakId=X
Header: Authorization: Bearer <jwt>
Response: {hutang: 10, riwayat: [{id, jumlah, status, createdAt}]}

POST /api/pinjam  (estimasi)
Body: {jumlah: 15}
Response: {ok: true, hutang: 15}

GET /api/pinjam?anakId=X

Header: Authorization: Bearer <jwt>

Response: {hutang: 10, riwayat: [{id, jumlah, status, createdAt}]}

POST /api/pinjam (estimasi)

Body: {jumlah: 15}

Response: {ok: true, hutang: 15}


## Implementasi app Android

Implementasi app Android

@GET("pinjam")
suspend fun pinjamList(@Header("Authorization") bearer: String): Response<PinjamRes>

data class PinjamRes(val hutang: Int, val riwayat: List<PinjamItem>)
data class PinjamItem(val id: String, val jumlah: Int, val status: String, val createdAt: String)

@GET("pinjam")

suspend fun pinjamList(@Header("Authorization") bearer: String): Response<PinjamRes>

data class PinjamRes(val hutang: Int, val riwayat: List<PinjamItem>)

data class PinjamItem(val id: String, val jumlah: Int, val status: String, val createdAt: String)


## UI: di dalam tab "Hadiah"
- Card "Pinjam Waktu Darurat"
- Tampilkan hutang aktif
- Tombol "Pinjam" (hanya jika tidak ada hutang aktif)
- Riwayat pinjam/bayar

UI: di dalam tab "Hadiah"

  • Card "Pinjam Waktu Darurat"
  • Tampilkan hutang aktif
  • Tombol "Pinjam" (hanya jika tidak ada hutang aktif)
  • Riwayat pinjam/bayar

Fitur Missing #6: Riwayat Token (Ledger)

Konsep

Anak lihat riwayat mutasi token: dari mana (kuis, tugas, challenge), untuk apa (tukar main, pinjam). Transparansi ekonomi token.

Blueprint: `ekonomi-token`

  • TokenLedger β€” mutasi: tipe, jumlah (+/-), catatan, refId
  • 10 TokenTipe: KUIS, TUGAS, CHALLENGE, FAMILY_QUEST, PINJAM, BAYAR_PINJAM, PEMAKAIAN, PENYESUAIAN, OFFICIAL_KUIS, OFFICIAL_TANTANGAN

API

GET /api/token-ledger?anakId=X
Header: Authorization: Bearer <jwt>
Response: [{id, tipe, jumlah, catatan, refId, createdAt}]

GET /api/token-ledger?anakId=X

Header: Authorization: Bearer <jwt>

Response: [{id, tipe, jumlah, catatan, refId, createdAt}]


## Implementasi app Android

Implementasi app Android

@GET("token-ledger")
suspend fun tokenLedger(@Header("Authorization") bearer: String): Response<List<LedgerItem>>

data class LedgerItem(
    val id: String, val tipe: String,
    val jumlah: Int, val catatan: String?,
    val refId: String?, val createdAt: String
)

@GET("token-ledger")

suspend fun tokenLedger(@Header("Authorization") bearer: String): Response<List<LedgerItem>>

data class LedgerItem(

val id: String, val tipe: String,

val jumlah: Int, val catatan: String?,

val refId: String?, val createdAt: String

)


## UI: di dalam tab "Hadiah"
- Section "Riwayat Token"
- LazyColumn: item per mutasi
- Hijau (+) untuk masuk, merah (-) untuk keluar
- Badge tipe (KUIS, TUGAS, PEMAKAIAN, dll)

UI: di dalam tab "Hadiah"

  • Section "Riwayat Token"
  • LazyColumn: item per mutasi
  • Hijau (+) untuk masuk, merah (-) untuk keluar
  • Badge tipe (KUIS, TUGAS, PEMAKAIAN, dll)

Usulan Tab ke-5: Hadiah

Bottom nav baru: 5 tab

1. Beranda β€” feed official + challenge harian

2. Kuis AI β€” buat kuis AI

3. Tugas β€” daftar tugas

4. Hadiah (BARU) β€” tukar token + katalog + quest + pinjam + riwayat

5. Saya β€” profil + bahasa + device admin

Layout tab "Hadiah"

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Saldo Token: 320           β”‚  ← header card (hijau)
β”‚  = 320 menit waktu main     β”‚
β”‚  Streak: 7 hari πŸ”₯          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Tukar Token β†’ Waktu Main   β”‚  ← card tukar
β”‚  [15 menit β–Ύ] [Tukar]       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Pinjam Waktu Darurat       β”‚  ← card pinjam
β”‚  Hutang: 0 token            β”‚
β”‚  [Pinjam 15 menit]          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Quest Keluarga             β”‚  ← section quest
β”‚  β–Έ Bantu Mama bersih... ⏳  β”‚
β”‚  β–Έ Baca buku 30 menit βœ…    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Katalog Hadiah             β”‚  ← section hadiah
β”‚  β–Έ Mainan Lego (50 coin)    β”‚
β”‚    [Minta]                  β”‚
β”‚  β–Έ Es Krim (20 coin)        β”‚
β”‚    [Sudah diminta]          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Riwayat Token              β”‚  ← section ledger
β”‚  +15 KUIS  | -30 PEMAKAIAN  β”‚
β”‚  +50 CHALLENGE | +25 TUGAS  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Saldo Token: 320 β”‚ ← header card (hijau)

β”‚ = 320 menit waktu main β”‚

β”‚ Streak: 7 hari πŸ”₯ β”‚

β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€

β”‚ Tukar Token β†’ Waktu Main β”‚ ← card tukar

β”‚ [15 menit β–Ύ] [Tukar] β”‚

β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€

β”‚ Pinjam Waktu Darurat β”‚ ← card pinjam

β”‚ Hutang: 0 token β”‚

β”‚ [Pinjam 15 menit] β”‚

β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€

β”‚ Quest Keluarga β”‚ ← section quest

β”‚ β–Έ Bantu Mama bersih... ⏳ β”‚

β”‚ β–Έ Baca buku 30 menit βœ… β”‚

β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€

β”‚ Katalog Hadiah β”‚ ← section hadiah

β”‚ β–Έ Mainan Lego (50 coin) β”‚

β”‚ [Minta] β”‚

β”‚ β–Έ Es Krim (20 coin) β”‚

β”‚ [Sudah diminta] β”‚

β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€

β”‚ Riwayat Token β”‚ ← section ledger

β”‚ +15 KUIS | -30 PEMAKAIAN β”‚

β”‚ +50 CHALLENGE | +25 TUGAS β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


## NavItem enum update

NavItem enum update

enum class NavItem(@StringRes labelRes: Int, icon: ImageVector) {
    Beranda(R.string.nav_home, Icons.Default.Home),
    Kuis(R.string.quiz_ai, Icons.Default.AutoAwesome),
    Tugas(R.string.task_title, Icons.Default.Task),
    Hadiah(R.string.nav_reward, Icons.Default.CardGiftcard),
    Saya(R.string.nav_profile, Icons.Default.Person),
}

enum class NavItem(@StringRes labelRes: Int, icon: ImageVector) {

Beranda(R.string.nav_home, Icons.Default.Home),

Kuis(R.string.quiz_ai, Icons.Default.AutoAwesome),

Tugas(R.string.task_title, Icons.Default.Task),

Hadiah(R.string.nav_reward, Icons.Default.CardGiftcard),

Saya(R.string.nav_profile, Icons.Default.Person),

}


## File baru
- `LayarHadiah.kt` β€” tab Hadiah (scrollable column: saldo + tukar + pinjam + quest + katalog + riwayat)
- DTO di `Api.kt`: TukarTokenReq/Res, HadiahItem/ListRes/MintaReq, QuestAnak/Member, PinjamRes/Item, LedgerItem

## API endpoints baru ( Retrofit)
- `POST anak/sesi-main` β€” tukar token
- `GET anak/hadiah` β€” katalog hadiah
- `POST anak/hadiah` β€” minta hadiah
- `GET quests-saya` β€” quest keluarga
- `GET pinjam` β€” riwayat pinjam
- `GET token-ledger` β€” riwayat token

## Strings baru (EN + ID)
- `nav_reward` = "Hadiah" / "Rewards"
- `token_balance` = "Saldo Token" / "Token Balance"
- `exchange_token` = "Tukar Token" / "Exchange Token"
- `borrow_time` = "Pinjam Waktu" / "Borrow Time"
- `family_quest` = "Quest Keluarga" / "Family Quest"
- `gift_catalog` = "Katalog Hadiah" / "Gift Catalog"
- `token_history` = "Riwayat Token" / "Token History"

File baru

  • LayarHadiah.kt β€” tab Hadiah (scrollable column: saldo + tukar + pinjam + quest + katalog + riwayat)
  • DTO di Api.kt: TukarTokenReq/Res, HadiahItem/ListRes/MintaReq, QuestAnak/Member, PinjamRes/Item, LedgerItem

API endpoints baru ( Retrofit)

  • POST anak/sesi-main β€” tukar token
  • GET anak/hadiah β€” katalog hadiah
  • POST anak/hadiah β€” minta hadiah
  • GET quests-saya β€” quest keluarga
  • GET pinjam β€” riwayat pinjam
  • GET token-ledger β€” riwayat token

Strings baru (EN + ID)

  • nav_reward = "Hadiah" / "Rewards"
  • token_balance = "Saldo Token" / "Token Balance"
  • exchange_token = "Tukar Token" / "Exchange Token"
  • borrow_time = "Pinjam Waktu" / "Borrow Time"
  • family_quest = "Quest Keluarga" / "Family Quest"
  • gift_catalog = "Katalog Hadiah" / "Gift Catalog"
  • token_history = "Riwayat Token" / "Token History"

Mapping Nyambung: App Ortu ↔ App Anak

App Ortu (sisi PARENT) vs App Anak (sisi CHILD)

App ortu = 20 layar, 5 tab + 14 sub-layar, ~40 API endpoints (v2.9.7).

App anak = 16 layar, 4 tab (v1.9.0).

Flow end-to-end yang SUDAH jalan (ortu ↔ anak):

| Fitur | App Ortu (parent side) | App Anak (child side) | API | Status |

|-------|------------------------|----------------------|-----|--------|

| Tugas | LayarTugas (tab 2) β€” buat tugas, validasi selesai/tolak | LayarTugas.kt β€” daftar tugas, kerjakan kuis | GET/POST/PUT /api/tugas | βœ… nyambung |

| Aturan Device | LayarAturan (sub 1) β€” sessionLimit, cooldown, curfew, appBlokir | AgentService.kt β€” terima & terapkan aturan dari backend | GET/PATCH /api/kebijakan | βœ… nyambung |

| Lokasi GPS | LayarLokasi (sub 2) β€” GPS toggle, geo-fence, lastSeen | Lokasi.kt β€” kirim GPS ke backend | POST /api/lokasi | βœ… nyambung |

| Laporan Aktivitas | LayarLaporan (sub 3) β€” activity log per anak, ringkasan 7 hari | Pemakaian.kt β€” kirim usage stats ke backend | POST /api/pemakaian | βœ… nyambung |

| Blokir App | LayarAturan β†’ appBlokir list | Blokir.kt β€” enforce blokir pakai UsageStatsManager | GET /api/blokir | βœ… nyambung |

| Ping/Heartbeat | (tidak ada UI β€” ortu lihat device online status) | AgentService.kt β€” ping loop 5 menit | POST /api/agent/ping | βœ… nyambung |

| Auto-update | (tidak relevan ortu) | PemeriksaPembaruan.kt β€” cek GitHub release | GitHub Releases API | βœ… jalan |

Flow yang PUTUS (ortu bisa, anak belum):

| # | Fitur | App Ortu (parent side) | App Anak (child side) | API | Status |

|---|-------|------------------------|----------------------|-----|--------|

| 1 | Katalog Hadiah | LayarMarketplace (tab 3) β€” CRUD hadiah, proses pesanan beli/tolak, saldo coin | BELUM β€” anak belum bisa lihat katalog/minta hadiah | GET/POST /api/anak/hadiah | ❌ putus |

| 2 | Quest Keluarga | LayarQuest (sub 5) β€” buat quest, tandai selesai β†’ anak dapat token | BELUM β€” anak belum bisa lihat status quest | GET /api/quests-saya | ❌ putus |

| 3 | Challenge Harian | LayarChallenges (sub 13) β€” daily challenges + claim | BELUM β€” anak belum bisa lihat progress | GET /api/anak/challenge | ❌ putus |

| 4 | Pinjam Waktu | LayarPinjam (sub 10) β€” lihat hutang anak | BELUM β€” anak belum bisa pinjam dari app | GET/POST /api/pinjam | ❌ putus |

| 5 | Riwayat Token | LayarCoin (sub 4) β€” saldo ZX Coin, ledger, topup/penyesuaian | BELUM β€” anak belum bisa lihat mutasi token | GET /api/token-ledger | ❌ putus |

| 6 | Tukar Token β†’ Waktu Main | (ortu tidak tukar β€” ortu topup) | BELUM β€” anak belum bisa tukar token β†’ sesi main | POST /api/anak/sesi-main | ❌ putus |

Flow ortu-only (tidak perlu di app anak):

  • LayarInsights (sub 9) β€” AI insights per anak (Gemini/rule-based)
  • LayarForum (sub 6) β€” forum posts, detail + komentar
  • LayarLangganan (sub 7) β€” tier, checkout Midtrans
  • LayarPesan (sub 8) β€” direct chat (anak belum butuh)
  • LayarReferral (sub 11) β€” kode referral
  • LayarLeaderboard (sub 12) β€” ranking token mingguan
  • LayarAturan β†’ masterUnlock, device admin activation

Kontrak API Lengkap: App Anak ↔ Backend (semua endpoint)

Endpoint yang sudah dikonsumsi app anak ( Retrofit `Api.kt`)

| Endpoint | Method | App File | Status |

|----------|--------|----------|--------|

| /api/agent/ping | POST | AgentService.kt | βœ… |

| /api/lokasi | POST | Lokasi.kt | βœ… |

| /api/kebijakan | GET | Kebijakan.kt | βœ… |

| /api/blokir | GET | Blokir.kt | βœ… |

| /api/pemakaian | POST | Pemakaian.kt | βœ… |

| /api/agent/edu (kuis) | GET | LayarKuisAI.kt | βœ… |

| /api/agent/edu (tugas) | GET | LayarTugas.kt | βœ… |

| /api/agent/edu/kuis/detail | GET | LayarKuisDetail.kt | βœ… |

| /api/agent/edu/kuis/sesi | POST | LayarKuisSession.kt | βœ… |

Endpoint yang HARUS ditambah ke app anak (child-side)

| Endpoint | Method | Untuk Fitur | Prioritas |

|----------|--------|-------------|-----------|

| /api/anak/sesi-main | POST | Tukar Token β†’ Waktu Main | TINGGI |

| /api/anak/hadiah | GET | Katalog Hadiah (list) | TINGGI |

| /api/anak/hadiah | POST | Katalog Hadiah (minta) | TINGGI |

| /api/quests-saya | GET | Quest Keluarga (status) | TINGGI |

| /api/anak/challenge | GET | Challenge Harian (progress) | SEDANG |

| /api/pinjam | GET | Pinjam Waktu (riwayat) | SEDANG |

| /api/pinjam | POST | Pinjam Waktu (ajukan) | SEDANG |

| /api/token-ledger | GET | Riwayat Token (mutasi) | SEDANG |

| /api/anak/saldo | GET | Saldo token real-time | TINGGI |

Endpoint ortu-only (tidak perlu di app anak)

~30 endpoint: auth ortu, CRUD anak, CRUD tugas, device control, marketplace CRUD, quest CRUD, forum, pesan, insights, langganan, referral, leaderboard, challenges CRUD

Autentikasi

  • App ortu: JWT dari login email/password atau Google Sign-In
  • App anak: PIN-based auth (6 digit), JWT dari /api/agent/login
  • Header: Authorization: Bearer <jwt> untuk semua endpoint anak

Arsitektur Paritas: Ortu MVVM vs Anak Procedural

App Ortu (v2.9.7)

  • Pattern: MVVM + Repository
  • ViewModel: ZxVm (757 lines), single UiState data class (20+ fields)
  • State: MutableStateFlow<UiState>, collectAsState()
  • API: Retrofit2, Gson, ApiZx interface (~40 endpoints)
  • Nav: manual mutableIntStateOf (tab + subLayar), no Navigation Compose
  • 5 tab + 14 sub-layar dari Beranda grid tiles

App Anak (v1.9.0)

  • Pattern: procedural Compose (no ViewModel, no Repository)
  • State: remember + mutableStateOf per layar
  • API: dual β€” ApiAgent (HttpURLConnection) + Api (Retrofit)
  • Nav: manual mutableIntStateOf (tab)
  • 4 tab (Beranda / Kuis AI / Tugas / Saya)

Gap arsitektur

App ortu lebih mature:

  • ViewModel β†’ state survive config change
  • muatSemua() β†’ 8 parallel fetch
  • Repository pattern β†’ testable

App anak procedural:

  • State tidak survive config change (rotasi)
  • Fetch per layar, tidak parallel
  • Tidak ada repository

Rekomendasi: tidak rewrite arsitektur anak sekarang

  • App anak working. Rewrite = risiko.
  • Tambah fitur missing dengan pattern yang sama (procedural + Retrofit)
  • Ponytail: upgrade ke MVVM saat app anak besar (20+ layar)
  • Tambah LayarHadiah.kt dengan pattern yang konsisten dengan LayarBeranda.kt

Prioritas Implementasi & Roadmap

Prioritas 1 β€” MVP Hadiah (quick wins, API sudah live)

| Fitur | Effort | API ready | Dampak |

|-------|--------|-----------|--------|

| Tukar Token β†’ Waktu Main | Kecil | βœ… POST /api/anak/sesi-main | Anak bisa pakai token |

| Katalog Hadiah (lihat + minta) | Kecil | βœ… GET/POST /api/anak/hadiah | Loop ortu↔anak jalan |

| Quest Keluarga (lihat status) | Kecil | βœ… GET /api/quests-saya | Anak tahu target |

| Saldo token real-time | Kecil | βœ… est. GET /api/anak/saldo | Transparansi |

Prioritas 2 β€” Engagement (medium)

| Fitur | Effort | API ready | Dampak |

|-------|--------|-----------|--------|

| Challenge Harian | Sedang | est. GET /api/anak/challenge | Gamifikasi |

| Riwayat Token (ledger) | Kecil | est. GET /api/token-ledger | Transparansi mutasi |

Prioritas 3 β€” Darurat (low priority)

| Fitur | Effort | API ready | Dampak |

|-------|--------|-----------|--------|

| Pinjam Waktu | Sedang | est. GET/POST /api/pinjam | Token darurat |

Estimasi total

  • Prioritas 1: ~200-300 lines Kotlin (1 file LayarHadiah.kt + DTO di Api.kt)
  • Prioritas 2: ~100-150 lines (section di layar existing)
  • Prioritas 3: ~80-100 lines (section di LayarHadiah.kt)

Dependency baru: TIDAK perlu

  • Retrofit sudah ada
  • Gson sudah ada
  • Compose Material3 sudah ada
  • Hanya tambah data class + API interface method + Composable

Roadmap

1. Bump targetSdk 35β†’36 (wajib Play Console)

2. Tambah LayarHadiah.kt (Prioritas 1)

3. Tambah challenge + ledger (Prioritas 2)

4. Tambah pinjam (Prioritas 3)

5. Update bottom nav 4β†’5 tab

6. Update strings.xml (EN + ID)

7. Build AAB, release v2.0.0

Fitur Kunci HP: Status & Gap Analysis

Yang SUDAH jalan

1. BlokirActivity β€” overlay fullscreen (sesi/curfew)

Saat Kebijakan.evaluasi() return JAM_TIDUR / DAILY_HABIS / COOLDOWN:

  • AgentService.proses() β†’ Blokir.nyalakan() β†’ start BlokirActivity
  • Fullscreen, back diblok, FLAG_SHOW_WHEN_LOCKED + FLAG_TURN_SCREEN_ON + FLAG_KEEP_SCREEN_ON
  • Keluar hanya saat server bilang lepas (ping berikutnya) atau masterUnlock
  • Tombol "Minta izin ortu" β†’ kirim log minta_buka

File: BlokirActivity.kt (90 lines), AgentService.kt baris 65-78, Kebijakan.kt evaluasi()

2. AksesService β€” blokir per-app (overlay)

Accessibility service deteksi TYPE_WINDOW_STATE_CHANGED:

  • Jika paket ada di appBlokir (dari server) β†’ overlay BlokirActivity menutupi app
  • Anak pindah app lain β†’ overlay lepas
  • Launcher tidak diblokir (anak bisa pilih app lain yang boleh)

File: AksesService.kt (50 lines), Blokir.kt simpanDaftarApp() / daftarApp()

3. Anti-uninstall β€” Device Admin aktif

  • device_admin.xml: <force-lock /> policy dideklarasi βœ…
  • StatusActivity.kt: ACTION_ADD_DEVICE_ADMIN β†’ ortu/teknisi aktifkan sekali saat pairing
  • Device Admin aktif = Android tolak uninstall tanpa deactivate dulu
  • Disconnect button β†’ dpm.removeActiveAdmin() + clear token

File: AdminReceiver.kt, StatusActivity.kt baris 103-107, device_admin.xml

Yang BELUM jalan β€” `lockNow()` tidak pernah dipanggil

device_admin.xml:   <force-lock />          βœ… policy ada
AdminReceiver.kt:   "lockNow() + anti-uninstall" ← hanya komentar
StatusActivity.kt:  dpm.isAdminActive()     βœ… cek status
                    dpm.removeActiveAdmin() βœ… lepas saat disconnect
                    dpm.lockNow()           ❌ TIDAK ADA β€” tidak pernah dipanggil

device_admin.xml: <force-lock /> βœ… policy ada

AdminReceiver.kt: "lockNow() + anti-uninstall" ← hanya komentar

StatusActivity.kt: dpm.isAdminActive() βœ… cek status

dpm.removeActiveAdmin() βœ… lepas saat disconnect

dpm.lockNow() ❌ TIDAK ADA β€” tidak pernah dipanggil


## Kelemahan overlay vs lockNow

| Aspek | Overlay (sekarang) | `lockNow()` (belum) |
|-------|--------------------|---------------------|
| Back button | Diblok di kode, bisa di-bypass (notification bar, recent apps) | HP benar-benar terkunci, butuh PIN/pattern |
| Force stop service | Anak bisa kill app dari Settings β†’ overlay hilang | Device Admin survive kill (system level) |
| Uninstall app | Diblok (Device Admin aktif) | Sama β€” anti-uninstall jalan |
| Notification bar | Bisa di-tarik dari atas | Tidak bisa |
| Recent apps | Bisa switch ke app lain | Layar terkunci penuh |
| Lock screen | Overlay di atas app, bukan lock screen system | Lock screen system Android |

## Konfigurasi ortu yang trigger blokir

Ortu set di `LayarAturan` (app ortu sub-layar 1):
- `sessionLimit` β€” menit per sesi (default 120)
- `cooldown` β€” menit jeda antar sesi (default 30)
- `dailyLimit` β€” menit total per hari (default 180)
- `curfewMulai` / `curfewSelesai` β€” jam tidur (mis. "21:00"-"05:00")
- `masterUnlock` β€” ortu override (buka paksa)
- `appBlokir` β€” daftar paket diblokir per-app

Backend β†’ `GET /api/agent/ping` response β†’ `aturan` object β†’ `AgentService.proses()` β†’ `Kebijakan.evaluasi()`

Kelemahan overlay vs lockNow

| Aspek | Overlay (sekarang) | lockNow() (belum) |

|-------|--------------------|---------------------|

| Back button | Diblok di kode, bisa di-bypass (notification bar, recent apps) | HP benar-benar terkunci, butuh PIN/pattern |

| Force stop service | Anak bisa kill app dari Settings β†’ overlay hilang | Device Admin survive kill (system level) |

| Uninstall app | Diblok (Device Admin aktif) | Sama β€” anti-uninstall jalan |

| Notification bar | Bisa di-tarik dari atas | Tidak bisa |

| Recent apps | Bisa switch ke app lain | Layar terkunci penuh |

| Lock screen | Overlay di atas app, bukan lock screen system | Lock screen system Android |

Konfigurasi ortu yang trigger blokir

Ortu set di LayarAturan (app ortu sub-layar 1):

  • sessionLimit β€” menit per sesi (default 120)
  • cooldown β€” menit jeda antar sesi (default 30)
  • dailyLimit β€” menit total per hari (default 180)
  • curfewMulai / curfewSelesai β€” jam tidur (mis. "21:00"-"05:00")
  • masterUnlock β€” ortu override (buka paksa)
  • appBlokir β€” daftar paket diblokir per-app

Backend β†’ GET /api/agent/ping response β†’ aturan object β†’ AgentService.proses() β†’ Kebijakan.evaluasi()

Implementasi lockNow(): 1 Baris Kode

Solusi: tambah `dpm.lockNow()` di AgentService saat blokir

Device Admin + <force-lock /> sudah siap. Tinggal panggil.

Patch: `AgentService.kt` β†’ `proses()`

// Tambah import
import android.app.admin.DevicePolicyManager
import android.content.ComponentName

// Tambah property di class AgentService
private val dpm get() = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
private val adminComp get() = ComponentName(this, AdminReceiver::class.java)
private val adminAktif get() = dpm.isAdminActive(adminComp)

// Di proses(), setelah Blokir.nyalakan(this, blokir):
if (blokir != null && !sedangBlokir) {
    Blokir.nyalakan(this, blokir)
    // Kunci layar system β€” tidak bisa di-bypass notification bar / recent apps
    if (adminAktif) {
        dpm.lockNow()
    }
    startActivity(
        Intent(this, BlokirActivity::class.java)
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
    )
}

// Tambah import

import android.app.admin.DevicePolicyManager

import android.content.ComponentName

// Tambah property di class AgentService

private val dpm get() = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager

private val adminComp get() = ComponentName(this, AdminReceiver::class.java)

private val adminAktif get() = dpm.isAdminActive(adminComp)

// Di proses(), setelah Blokir.nyalakan(this, blokir):

if (blokir != null && !sedangBlokir) {

Blokir.nyalakan(this, blokir)

// Kunci layar system β€” tidak bisa di-bypass notification bar / recent apps

if (adminAktif) {

dpm.lockNow()

}

startActivity(

Intent(this, BlokirActivity::class.java)

.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)

)

}


### Kenapa tetap pakai BlokirActivity + lockNow?

1. `lockNow()` kunci layar system β†’ HP benar terkunci, butuh PIN/pattern
2. `BlokirActivity` tetap tampil di lock screen (`FLAG_SHOW_WHEN_LOCKED`) β†’ anak lihat pesan "HP terkunci sampai jam tidur berakhir"
3. Saat anak buka HP (unlock PIN), `BlokirActivity` masih ada β†’ tetap blokir
4. Double layer: system lock + app overlay

### Anti-bypass tambahan (opsional, fase lanjut)

Kenapa tetap pakai BlokirActivity + lockNow?

1. lockNow() kunci layar system β†’ HP benar terkunci, butuh PIN/pattern

2. BlokirActivity tetap tampil di lock screen (FLAG_SHOW_WHEN_LOCKED) β†’ anak lihat pesan "HP terkunci sampai jam tidur berakhir"

3. Saat anak buka HP (unlock PIN), BlokirActivity masih ada β†’ tetap blokir

4. Double layer: system lock + app overlay

Anti-bypass tambahan (opsional, fase lanjut)

// 1. Sembunyikan notification bar β€” BlokirActivity sudah fullscreen
//    tambah di onCreate:
window.setFlags(
    WindowManager.LayoutParams.FLAG_FULLSCREEN,
    WindowManager.LayoutParams.FLAG_FULLSCREEN
)

// 2. Block recent apps β€” override onPause
override fun onPause() {
    super.onPause()
    // bila masih blokir, restart activity
    if (Blokir.aktif(this)) {
        startActivity(Intent(this, BlokirActivity::class.java)
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP))
    }
}

// 3. Device Admin anti-uninstall sudah jalan β€” tidak bisa uninstall tanpa deactivate

// 1. Sembunyikan notification bar β€” BlokirActivity sudah fullscreen

// tambah di onCreate:

window.setFlags(

WindowManager.LayoutParams.FLAG_FULLSCREEN,

WindowManager.LayoutParams.FLAG_FULLSCREEN

)

// 2. Block recent apps β€” override onPause

override fun onPause() {

super.onPause()

// bila masih blokir, restart activity

if (Blokir.aktif(this)) {

startActivity(Intent(this, BlokirActivity::class.java)

.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP))

}

}

// 3. Device Admin anti-uninstall sudah jalan β€” tidak bisa uninstall tanpa deactivate


### Limitasi `lockNow()` di Android modern

- Android 9+ (API 28+): `lockNow()` tetap jalan dengan Device Admin βœ…
- Android 14+ (API 34): Device Admin `force-lock` tidak deprecated βœ…
- Tidak butuh additional permission β€” `<force-lock />` policy sudah dideklarasi
- Tidak konflik dengan accessibility service β€” keduanya independent

## Tidak butuh dependency baru
- `DevicePolicyManager` = Android framework API
- `ComponentName` = Android framework API
- Tidak ada library tambahan

Limitasi `lockNow()` di Android modern

  • Android 9+ (API 28+): lockNow() tetap jalan dengan Device Admin βœ…
  • Android 14+ (API 34): Device Admin force-lock tidak deprecated βœ…
  • Tidak butuh additional permission β€” <force-lock /> policy sudah dideklarasi
  • Tidak konflik dengan accessibility service β€” keduanya independent

Tidak butuh dependency baru

  • DevicePolicyManager = Android framework API
  • ComponentName = Android framework API
  • Tidak ada library tambahan

Roadmap Update: lockNow + Hadiah + v2.0.0

Roadmap revisi (gabung lockNow + fitur hadiah)

Fase 1 β€” Wajib (Play Console)

1. Bump targetSdk 35β†’36 (wajib new apps Okt 2026)

2. Build AAB, release v2.0.0-beta1

Fase 2 β€” Kunci HP (1 baris, quick win)

3. Tambah dpm.lockNow() di AgentService.proses() saat blokir

4. Test: set curfew dari app ortu β†’ HP anak harus terkunci

5. Commit: feat: lockNow() saat blokir sesi/curfew

Fase 3 β€” Tab Hadiah MVP

6. Buat LayarHadiah.kt β€” saldo + tukar token + katalog hadiah + quest

7. Tambah DTO di Api.kt: TukarTokenReq/Res, HadiahItem, QuestAnak

8. Tambah endpoint Retrofit: anak/sesi-main, anak/hadiah, quests-saya

9. Update bottom nav 4β†’5 tab

10. Update strings.xml (EN + ID): nav_reward, token_balance, exchange_token

11. Commit: feat: tab Hadiah β€” tukar token + katalog + quest

Fase 4 β€” Engagement

12. Tambah challenge harian (section di Beranda)

13. Tambah riwayat token (section di Hadiah)

14. Tambah pinjam waktu (section di Hadiah)

15. Commit: feat: challenge + ledger + pinjam waktu

Fase 5 β€” Rilis

16. Build AAB v2.0.0

17. Release ke GitHub + Play Console

18. Store listing app anak

Estimasi effort

| Fase | Lines | File baru | Dependency |

|------|-------|-----------|------------|

| 1 β€” targetSdk 36 | ~5 (build.gradle) | 0 | 0 |

| 2 β€” lockNow | ~10 | 0 | 0 |

| 3 β€” Tab Hadiah | ~250-300 | 1 (LayarHadiah.kt) | 0 |

| 4 β€” Engagement | ~150 | 0 | 0 |

| Total | ~415-465 | 1 | 0 |

Versi target: v2.0.0

  • Major bump: 4β†’5 tab, lockNow, 6 fitur missing
  • Breaking: bottom nav structure change
  • MinSdk tetap 24, targetSdk 36

Verifikasi Backend Routes: 4 Endpoint Estimasi DICEK

Hasil cek kode backend (`src/app/api/`)

βœ… Child-accessible (pakai `sesiAnak()` β€” Bearer JWT role=CHILD) β€” READY

| Endpoint | Method | Auth | Response Shape | Status |

|----------|--------|------|----------------|--------|

| /api/anak/sesi-main | POST | sesiAnak βœ… | {ok, menit, saldo} | βœ… READY |

| /api/anak/hadiah | GET | sesiAnak βœ… | {katalog: [{id, judul, deskripsi, hargaCoin, stok, sudahMinta}]} | βœ… READY |

| /api/anak/hadiah | POST | sesiAnak βœ… | {ok} (201) | βœ… READY |

| /api/quests-saya | GET | sesiAnak βœ… | [{id, judul, deskripsi, tokenReward, deadline, anggota:[{anakId, selesai}]}] | βœ… READY |

| /api/anak/challenge | GET | sesiAnak βœ… | {id, judul, topik, tokenReward, selesai} | βœ… READY |

| /api/anak/streak | GET | sesiAnak βœ… | {current, terbaik, terakhir} | βœ… READY (bonus) |

❌ ORTU-ONLY (pakai `sesiOrtu()` β€” anak TIDAK BISA akses)

| Endpoint | Method | Auth | Kenapa | Status |

|----------|--------|------|--------|--------|

| /api/pinjam | GET | sesiOrtu ❌ | Cek ortuId match, bukan anak login | ❌ BLOCKED |

| /api/pinjam | POST | sesiOrtu ❌ | Config pinjam (limitPinjam, bunga) β€” ortu set | ❌ BLOCKED |

| /api/token-ledger | GET | sesiOrtu ❌ | Cek ortuId match, butuh anakId param | ❌ BLOCKED |

| /api/coin | GET | sesiOrtu ❌ | Saldo ZX Coin ORTU (bukan token anak) | ❌ BLOCKED |

❌ TIDAK ADA endpoint

| Endpoint | Status | Catatan |

|----------|--------|---------|

| /api/anak/saldo | ❌ TIDAK ADA | Saldo anak hanya dapat dari response sesi-main (setelah tukar) atau token-ledger (ortu-only) |

| Child-side pinjam | ❌ TIDAK ADA | Pinjam hanya ortu-side: lihat hutang + set config. Anak belum bisa ajukan pinjam |

| Child-side token-ledger | ❌ TIDAK ADA | Ledger hanya ortu-side. Anak belum bisa lihat riwayat sendiri |

Response Shape Correction (BP sebelumnya salah)

Challenge β€” BP bilang `{id, judul, target, progress, tokenReward, selesai}`

AKTUAL: {id, judul, topik, tokenReward, selesai}

  • Tidak ada target / progress field
  • Ada topik (string, mis. "Matematika perkalian")
  • selesai = boolean (dari ChallengeProgress)
  • Challenge = 1 per hari, deterministik dari tanggal. "Kuis Sempurna" β€” selesaikan 1 kuis dengan skor penuh

DTO Kotlin correction:

data class ChallengeItem(
    val id: String,
    val judul: String,
    val topik: String,        // BUKAN target: Int
    val tokenReward: Int,
    val selesai: Boolean
    // TIDAK ADA progress: Int
)

data class ChallengeItem(

val id: String,

val judul: String,

val topik: String, // BUKAN target: Int

val tokenReward: Int,

val selesai: Boolean

// TIDAK ADA progress: Int

)


### Pinjam β€” BP bilang anak bisa `GET/POST /api/pinjam`
**AKTUAL:** pinjam = ortu-only. POST = config (limitPinjam, bunga), bukan ajukan pinjam.
Anak TIDAK BISA pinjam dari app. Butuh endpoint baru.

### Token-ledger β€” BP bilang anak bisa `GET /api/token-ledger`
**AKTUAL:** ortu-only. Butuh `?anakId=X` + `sesiOrtu()`.
Anak TIDAK BISA lihat ledger sendiri. Butuh endpoint baru.

### Saldo β€” BP bilang `GET /api/anak/saldo` (estimasi)
**AKTUAL:** endpoint TIDAK ADA. Saldo anak hanya dari response `POST /api/anak/sesi-main` (`{ok, menit, saldo}`).
Butuh endpoint baru `GET /api/anak/saldo` atau `GET /api/anak/profil` yang return saldo + streak + nama.

## Yang perlu DIBUAT di backend (3 endpoint baru)

### 1. `GET /api/anak/saldo` β€” saldo token anak real-time

Pinjam β€” BP bilang anak bisa `GET/POST /api/pinjam`

AKTUAL: pinjam = ortu-only. POST = config (limitPinjam, bunga), bukan ajukan pinjam.

Anak TIDAK BISA pinjam dari app. Butuh endpoint baru.

Token-ledger β€” BP bilang anak bisa `GET /api/token-ledger`

AKTUAL: ortu-only. Butuh ?anakId=X + sesiOrtu().

Anak TIDAK BISA lihat ledger sendiri. Butuh endpoint baru.

Saldo β€” BP bilang `GET /api/anak/saldo` (estimasi)

AKTUAL: endpoint TIDAK ADA. Saldo anak hanya dari response POST /api/anak/sesi-main ({ok, menit, saldo}).

Butuh endpoint baru GET /api/anak/saldo atau GET /api/anak/profil yang return saldo + streak + nama.

Yang perlu DIBUAT di backend (3 endpoint baru)

1. `GET /api/anak/saldo` β€” saldo token anak real-time

// src/app/api/anak/saldo/route.ts
export async function GET() {
  const anak = await sesiAnak()
  if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
  const saldo = await prisma.tokenBalance.findUnique({
    where: { anakId: anak.id },
    select: { balance: true },
  })
  return NextResponse.json({ saldo: saldo?.balance ?? 0 })
}

// src/app/api/anak/saldo/route.ts

export async function GET() {

const anak = await sesiAnak()

if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })

const saldo = await prisma.tokenBalance.findUnique({

where: { anakId: anak.id },

select: { balance: true },

})

return NextResponse.json({ saldo: saldo?.balance ?? 0 })

}


### 2. `GET /api/anak/riwayat-token` β€” ledger anak (child-side)

2. `GET /api/anak/riwayat-token` β€” ledger anak (child-side)

// src/app/api/anak/riwayat-token/route.ts
export async function GET() {
  const anak = await sesiAnak()
  if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
  const ledger = await prisma.tokenLedger.findMany({
    where: { anakId: anak.id },
    orderBy: { createdAt: 'desc' },
    take: 50,
    select: { id: true, tipe: true, jumlah: true, catatan: true, createdAt: true },
  })
  return NextResponse.json({ ledger })
}

// src/app/api/anak/riwayat-token/route.ts

export async function GET() {

const anak = await sesiAnak()

if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })

const ledger = await prisma.tokenLedger.findMany({

where: { anakId: anak.id },

orderBy: { createdAt: 'desc' },

take: 50,

select: { id: true, tipe: true, jumlah: true, catatan: true, createdAt: true },

})

return NextResponse.json({ ledger })

}


### 3. `POST /api/anak/pinjam` β€” anak ajukan pinjam (child-side)

3. `POST /api/anak/pinjam` β€” anak ajukan pinjam (child-side)

// src/app/api/anak/pinjam/route.ts
export async function POST(req: Request) {
  const anak = await sesiAnak()
  if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
  const { jumlah } = await req.json()
  // cek hutang aktif, cek limit, catat ledger PINJAM
  // ...
}

// src/app/api/anak/pinjam/route.ts

export async function POST(req: Request) {

const anak = await sesiAnak()

if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })

const { jumlah } = await req.json()

// cek hutang aktif, cek limit, catat ledger PINJAM

// ...

}

Catatan: logic pinjam anak butuh cek limit config (disimpan ortu via `/api/pinjam` POST sebagai catatan di ledger). Perlu parse catatan `pinjam-config:limit=X,bunga=Y`.

## Impact ke Roadmap

| Fitur | Status Sebelum | Status Sekarang | Aksi |
|-------|----------------|-----------------|------|
| Tukar Token | API ready | βœ… CONFIRMED ready | Langsung kode |
| Katalog Hadiah | API ready | βœ… CONFIRMED ready | Langsung kode |
| Quest Keluarga | API ready | βœ… CONFIRMED ready | Langsung kode |
| Challenge Harian | API "estimasi" | βœ… EXISTS, shape beda | Fix DTO, langsung kode |
| Streak | (tidak ada di BP) | βœ… BONUS β€” ada endpoint | Tambah ke UI |
| Saldo real-time | API "estimasi" | ❌ TIDAK ADA endpoint | Buat backend dulu |
| Pinjam Waktu | API "estimasi" | ❌ ORTU-ONLY, no child endpoint | Buat backend dulu |
| Riwayat Token | API "estimasi" | ❌ ORTU-ONLY, no child endpoint | Buat backend dulu |

## Revisi prioritas
- **Prioritas 1 (langsung kode app):** Tukar Token, Katalog Hadiah, Quest, Challenge, Streak, Saldo (dari sesi-main response)
- **Prioritas 2 (butuh backend baru dulu):** Saldo endpoint, Riwayat Token endpoint, Pinjam endpoint
- Backend = ~60 lines TypeScript (3 route files). App anak tetap ~465 lines.

Catatan: logic pinjam anak butuh cek limit config (disimpan ortu via /api/pinjam POST sebagai catatan di ledger). Perlu parse catatan pinjam-config:limit=X,bunga=Y.

Impact ke Roadmap

| Fitur | Status Sebelum | Status Sekarang | Aksi |

|-------|----------------|-----------------|------|

| Tukar Token | API ready | βœ… CONFIRMED ready | Langsung kode |

| Katalog Hadiah | API ready | βœ… CONFIRMED ready | Langsung kode |

| Quest Keluarga | API ready | βœ… CONFIRMED ready | Langsung kode |

| Challenge Harian | API "estimasi" | βœ… EXISTS, shape beda | Fix DTO, langsung kode |

| Streak | (tidak ada di BP) | βœ… BONUS β€” ada endpoint | Tambah ke UI |

| Saldo real-time | API "estimasi" | ❌ TIDAK ADA endpoint | Buat backend dulu |

| Pinjam Waktu | API "estimasi" | ❌ ORTU-ONLY, no child endpoint | Buat backend dulu |

| Riwayat Token | API "estimasi" | ❌ ORTU-ONLY, no child endpoint | Buat backend dulu |

Revisi prioritas

  • Prioritas 1 (langsung kode app): Tukar Token, Katalog Hadiah, Quest, Challenge, Streak, Saldo (dari sesi-main response)
  • Prioritas 2 (butuh backend baru dulu): Saldo endpoint, Riwayat Token endpoint, Pinjam endpoint
  • Backend = ~60 lines TypeScript (3 route files). App anak tetap ~465 lines.

Tech Debt: Dual API Client (ApiAgent HttpURLConnection + Api Retrofit)

Masalah

App anak punya dua API client pattern berbeda:

1. ApiAgent.kt (68 lines) β€” HttpURLConnection manual

  • Dipakai untuk: ping, lokasi, activity (agent API)
  • Header: x-device-token
  • Manual JSON parse dengan JSONObject
  • Tidak ada error handling typed, tidak ada retry, tidak ada timeout config

2. Api.kt (256 lines) β€” Retrofit2 + Gson

  • Dipakai untuk: login, kuis, tugas, official content (edu API)
  • Header: Authorization: Bearer <jwt>
  • Typed data class, suspend function, Retrofit interface
  • Proper error handling via Response<T>

Kenapa tech debt

  • Tidak konsisten β€” developer baru harus belajar 2 pattern
  • Susah maintain β€” bug fix di satu client tidak otomatis apply ke lainnya
  • Testability β€” Retrofit mockable, HttpURLConnection tidak
  • Duplicate logic β€” base URL, timeout, logging interceptor di-maintain terpisah

Kenapa terjadi

ApiAgent dibuat duluan (fase 1 β€” ping loop simple). Api (Retrofit) ditambah belakangan (fase 2 β€” edu API). Tidak pernah di-unify.

Rekomendasi: JANGAN unify sekarang

  • App anak working. Refactor = risiko break agent service.
  • Aturan: endpoint baru (hadiah, quest, challenge, dll) β†’ semua ke Retrofit Api.kt. JANGAN tambah ke ApiAgent.
  • Ponytail: unify ApiAgent β†’ Retrofit saat app anak 20+ layar atau saat butuh retry/timeout proper.
  • Estimasi unify: ~1 hari kerja (rewrite ApiAgent 6 method ke Retrofit + test)

Aturan untuk roadmap

  • LayarHadiah.kt semua endpoint baru β†’ Retrofit Api.kt (Bearer JWT, sesiAnak)
  • ApiAgent tetap untuk agent API (ping, lokasi, activity) β€” tidak diubah
  • Dokumentasikan di komentar ApiAgent: "DEPRECATED β€” jangan tambah endpoint baru ke sini"

Risiko: Ping 5 Menit = Lag Blokir

Masalah (ORIGINAL)

AgentService ping tiap 5 menit (PERIODE_MS = 5 * 60_000L). Blokir dievaluasi hanya saat ping response diterima.

Skenario lag

1. Ortu set curfew 21:00 dari app ortu β†’ tersimpan di DB

2. Ping terakhir 20:55 β†’ response dievaluasi β†’ belum curfew

3. Ping berikutnya 21:00 β†’ response dievaluasi β†’ curfew aktif β†’ blokir

4. Lag: sampai 5 menit antara curfew mulai dan blokir aktif

βœ… FIX DONE (commit `edc2261c`, Okt 2026)

Blueprint Opsi B implemented: dynamic ping interval.

Implementasi

  • Kebijakan.menitKeCurfew(jam, menit, mulai, selesai) β€” hitung menit sampai curfew mulai (support cross-midnight, mis 21:00-05:00)
  • AgentService.hitungPeriode() β€” ping 1 menit saat < 10 menit sebelum curfew, 5 menit normal
  • Handler.postDelayed menggantikan sleep loop β€” lebih responsif

Hasil

  • Normal: ping 5 menit (tidak boros baterai)
  • Mendekati curfew (<10 min): ping 1 menit β†’ lag maks 1 menit
  • Cross-midnight curfew (21:00-05:00) didukung
  • Tidak perlu permission baru

Security: Keystore di Repo Git

Masalah

keystore/rilis.keystore + keystore/rilis.keystore.pw committed langsung ke git repo.

Risiko

Siapa pun akses repo (public GitHub) = bisa:

1. Download keystore + password

2. Sign APK malicious dengan signature sama

3. Replace app di HP anak (auto-update dari GitHub releases!)

4. Akses semua data anak (agent service, lokasi, usage stats)

Attack vector nyata: auto-update (PemeriksaPembaruan.kt) download APK dari GitHub releases. Attacker sign APK malicious dengan keystore curian β†’ upload ke GitHub releases dengan tag versi lebih tinggi β†’ app anak auto-update β†’ install APK malicious. Signature cocok β†’ Android terima.

βœ… FIX DONE (commit `3fcb0647`, Okt 2026)

1. Generate keystore BARU β€” RSA 2048, alias zxagent, validitas 10000 hari

2. Simpan di GitHub Secrets: KEYSTORE_B64 (base64), KEYSTORE_PASSWORD, KEYSTORE_ALIAS, KEYSTORE_KEY_PASSWORD β€” semua via GitHub API

3. rilis.yml Secrets-first logic: decode dari Secret, fallback ke repo file (zero-downtime transition)

4. Hapus keystore dari repo β€” git rm --cached + .gitignore patterns

5. README update β€” dokumentasi rotasi

Kenapa tidak rewrite git history

  • Force-push breaks public repo clones
  • Keystore lama (commit 24c114e) tetap di history β€” anggap compromised permanently
  • Mitigasi: rotasi signature effective v2.4.0 (user reinstall)

Signature rotation impact

  • App anak belum di Play Store β†’ bebas ganti signature (user uninstall + reinstall)
  • App ortu sudah di Play Store β†’ signature locked, tidak bisa rotate tanpa Google "Upload key reset"
  • Effective dari release v2.4.0

Konflik: Auto-Update APK (GitHub) vs Play Store In-App Update

Masalah (ORIGINAL)

App anak punya 2 mekanisme update yang akan konflik:

1. PemeriksaPembaruan.kt β€” GitHub releases (bypass Play Store)

2. Play Store in-app update β€” AppUpdateManager FLEXIBLE type

βœ… FIX DONE

Play Store installer guard

PemeriksaPembaruan.kt sekarang cek installer:

private fun apasangDariPlayStore(c: Context): Boolean {
    val installer = c.packageManager.getInstallerPackageName(c.packageName)
    return installer == "com.android.vending"
}

private fun apasangDariPlayStore(c: Context): Boolean {

val installer = c.packageManager.getInstallerPackageName(c.packageName)

return installer == "com.android.vending"

}

- Jika install dari Play Store β†’ skip GitHub update check, return null
- Jika install dari GitHub (sideload) β†’ tetap pakai GitHub auto-update

### Play Store in-app update ready
- `AppUpdateManagerFactory.create(context)` + `AppUpdateType.FLEXIBLE`
- Dialog "Update tersedia" dari Play Store
- Update via Play Store, signature verified Google

## Status roadmap
- **Fase 1 (sekarang):** GitHub auto-update aktif untuk sideload install βœ…
- **Fase 5 (Play Store live):** installer guard + in-app update aktif untuk Play Store install βœ… ready
- **Permission:** `REQUEST_INSTALL_PACKAGES` tetap untuk pre-Play Store, hapus setelah live
  • Jika install dari Play Store β†’ skip GitHub update check, return null
  • Jika install dari GitHub (sideload) β†’ tetap pakai GitHub auto-update

Play Store in-app update ready

  • AppUpdateManagerFactory.create(context) + AppUpdateType.FLEXIBLE
  • Dialog "Update tersedia" dari Play Store
  • Update via Play Store, signature verified Google

Status roadmap

  • Fase 1 (sekarang): GitHub auto-update aktif untuk sideload install βœ…
  • Fase 5 (Play Store live): installer guard + in-app update aktif untuk Play Store install βœ… ready
  • Permission: REQUEST_INSTALL_PACKAGES tetap untuk pre-Play Store, hapus setelah live

Play Console: App Access untuk Reviewer

Masalah (ORIGINAL)

Google Play reviewer harus bisa test app tanpa device ortu untuk pairing.

βœ… FIX DONE β€” Demo Mode

Implementasi

  • LayarMasuk.kt (Compose) β€” tombol "Mode Demo" di login screen
  • Simpanan.setDemo(ctx) β€” flag demo mode di SharedPreferences
  • MainActivity.onCreate β€” cek Simpanan.isDemo(ctx) β†’ skip AgentService, tampilkan UI dengan sample data
  • DataDemo.kt β€” sample data: kuis, tugas, challenge, katalog hadiah, quest, ledger, pinjam

Reviewer flow

1. Install APK β†’ buka app

2. LayarMasuk muncul β†’ klik "Mode Demo"

3. Full UI tampil dengan sample data (5 tab: Beranda, Kuis, Tugas, Hadiah, Saya)

4. Tidak perlu pairing device ortu, tidak perlu login anak

5. Tombol "Hubungkan ke Ortu" di Saya β†’ kembali ke pairing flow

Play Console App Access form

App butuh pairing device ortu untuk full functionality.
Demo mode tersedia: buka app β†’ tombol "Mode Demo" di layar login.
Demo menampilkan sample kuis, tugas, hadiah, dan UI tanpa device ortu.

App butuh pairing device ortu untuk full functionality.

Demo mode tersedia: buka app β†’ tombol "Mode Demo" di layar login.

Demo menampilkan sample kuis, tugas, hadiah, dan UI tanpa device ortu.


### String resources
- EN: `demo_mode` = "Demo Mode", `demo_mode_desc` = "Browse the app with sample data..."
- ID: `demo_mode` = "Mode Demo", `demo_mode_desc` = "Jelajahi app dengan data contoh..."

### Alternatif (tidak dipakai)
Test account di backend β€” lebih realistic tapi butuh data dummy di DB + cleanup. Demo mode cukup untuk reviewer.

String resources

  • EN: demo_mode = "Demo Mode", demo_mode_desc = "Browse the app with sample data..."
  • ID: demo_mode = "Mode Demo", demo_mode_desc = "Jelajahi app dengan data contoh..."

Alternatif (tidak dipakai)

Test account di backend β€” lebih realistic tapi butuh data dummy di DB + cleanup. Demo mode cukup untuk reviewer.

Risiko: Accessibility Service Bisa Di-kill / Disable

Masalah (ORIGINAL)

AksesService (accessibility service untuk blokir per-app) bisa:

1. Di-kill Android (OOM, battery optimization)

  • Android kill service under memory pressure
  • AksesService.instance jadi null
  • Blokir.daftarApp() tetap di SharedPreferences (data bertahan)
  • Tapi enforcement hilang β€” app diblokir tidak terdeteksi lagi

2. Disable manual oleh anak

  • Settings β†’ Accessibility β†’ ZX Parenting Agent β†’ OFF
  • Anak bisa lakukan ini tanpa ortu tahu
  • App blocking total tidak berfungsi

3. Force stop app dari Settings

  • Semua service mati (AgentService + AksesService)
  • START_STICKY = Android restart eventually, tapi tidak immediately

Dampak

appBlokir (daftar app diblokir ortu) tidak enforced. Anak bisa buka app yang seharangnya diblokir. Ortu tidak tahu enforcement sedang tidak aktif.

βœ… FIX DONE (commit `edc2261c`, Okt 2026)

Solusi: deteksi + alert + log, bukan prevent (Android tidak allow prevent).

Implementasi

1. AgentService ping loop β€” cek AksesService.instance == null tiap ping

2. Notifikasi HIGH priority β€” channel alert (IMPORTANCE_HIGH), "ZX Parenting perlu perhatian"

3. Log ke backend β€” Api.kirimAktivitas("accessibility_off") β†’ ortu lihat di laporan

4. LayarSaya status card β€” tampilkan status accessibility (Aktif/Nonaktif), tombol "Buka Settings" langsung ke ACTION_ACCESSIBILITY_SETTINGS

Tidak bisa dicegah 100%

  • Android tidak allow app mencegah user disable accessibility (by design)
  • Device Admin tidak cover accessibility (hanya cover uninstall app)
  • Solusi = deteksi + alert + log, ortu dapat notifikasi

Mockup Desain UI

Mockup lengkap β€” 4 layar sesuai BP 38 bagian

URL: https://zxparenting.zomet.my.id/design/beranda-mockup.html

Repo: zx_parenting/public/design/beranda-mockup.html

4 Layar

1. Beranda

  • Greeting card: nama anak, streak, token, level
  • Challenge harian (BP #20, #31) β€” shape: {id, judul, topik, tokenReward, selesai}. Progress bar. Reward +5
  • Accessibility warning (BP #37) β€” banner kuning saat AksesService nonaktif
  • Feed kuis/artikel/video/tugas (sama dengan sebelumnya)
  • Bottom nav 5 tab

2. Hadiah (tab baru)

  • Saldo token header (BP #31 β€” dari response sesi-main atau endpoint baru)
  • Tukar token (BP #17) β€” dropdown 5/15/30/60/120 menit, min 5 max 120, tombol hijau
  • Quest keluarga (BP #19) β€” card per quest, status ⏳/βœ…, border teal/green
  • Katalog hadiah (BP #18) β€” card per hadiah, harga coin, status Minta/Sudah diminta/Habis
  • Riwayat token (BP #22) β€” ledger list, +/- hijau/merah, badge tipe (KUIS, TUGAS, CHALLENGE, dll)
  • Pinjam waktu (BP #21) β€” card oranye, hutang aktif, tombol pinjam

3. Blokir / Lock Screen

  • lockNow() + overlay double layer (BP #28, #29)
  • Fullscreen biru gelap, icon lock
  • Pesan: "HP terkunci sampai jam tidur berakhir"
  • Info: blokir aktif JAM_TIDUR (21:00-05:00), sistem lock Device Admin lockNow()
  • Tombol "Minta Izin Ortu"
  • Info anti-bypass: notification bar, recent apps, force stop

4. Saya

  • Status perangkat (BP #37, #12, #28):

- Aksesibilitas: Aktif/Nonaktif + tombol Settings

- Device Admin: Aktif (anti-uninstall)

- Laporan Pemakaian: Aktif

- GPS Lokasi: Aktif

- Kunci Layar (lockNow): Siap

  • Pengaturan: bahasa, cek pembaruan (BP #35 β€” GitHub vs Play Store), versi app
  • Lepas perangkat: disconnect dari ortu + hapus token

Bottom Nav: 5 tab

1. Beranda (house-door)

2. Kuis AI (stars)

3. Tugas (list-task)

4. Hadiah (card-giftcard) β€” BARU

5. Saya (person)

Tech

  • HTML/CSS murni, no framework
  • Bootstrap Icons 1.11.3
  • Inter font (Google Fonts)
  • Phone frame 390Γ—844px (iPhone 14)
  • Scrollable feed, fixed bottom nav
  • Dark background (#0f172a) untuk showcase

Desain i18n + Theme Switching

Tujuan

Default bahasa Inggris. Sementara hanya opsi Indonesia. Arsitektur harus mudah tambah bahasa + ganti tema.

βœ… Status: DONE (Okt 2026)

i18n

| Aspek | Status |

|-------|--------|

| strings.xml EN (values/) | βœ… 92+ strings |

| strings.xml ID (values-in/) | βœ… 92+ strings |

| ManajerBahasa.kt | βœ… AppCompatDelegate.setApplicationLocales, persist SharedPreferences |

| Language picker (LayarSaya) | βœ… EN/ID/system |

| Tambah bahasa | values-xx/strings.xml + 1 baris di picker |

Theme System β€” port dari ortu 1:1

| File | Status | Desc |

|------|--------|------|

| tema/Palet.kt | βœ… | data class 20 warna, dariJson() parser, DEFAULT hardcoded |

| tema/WarnaAktif.kt | βœ… | singleton mutableStateOf, gantiByKey(), inisialisasi(), TemaZX wrapper |

| assets/tema/default.json | βœ… | tema Default (biru) |

| assets/tema/gelap.json | βœ… | tema Malam (dark) |

| assets/tema/laut.json | βœ… | tema Laut (cyan) |

| Theme picker (LayarSaya) | βœ… | 3 pilihan, instant recompose |

View-based activities theme

| File | Status |

|------|--------|

| PasangActivity.kt | βœ… WarnaAktif.palet.*.toArgb() |

| StatusActivity.kt | βœ… WarnaAktif.palet.*.toArgb() |

| BlokirActivity.kt | βœ… WarnaAktif.palet.*.toArgb() |

Refactor hardcoded colors

  • Original: 106 hardcoded Color(0x...) di 8 file Compose
  • Compose files: βœ… zero hardcoded (semua pakai Bg, Biru, Teks, dst computed property)
  • View-based files: βœ… zero hardcoded (semua pakai WarnaAktif.palet.*.toArgb())
  • Remaining 23 Color(0x...) di Palet.kt (DEFAULT definition β€” correct, must be literal) + WarnaAktif.kt (dark/light scheme constants)

Tambah tema baru

Drop file JSON di assets/tema/nama.json β†’ otomatis muncul di picker. No recompile.

Tambah bahasa baru

Buat values-xx/strings.xml + 1 baris di language picker. Perlu recompile (APK split).