Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Restore private portal sources
env:
SAP_SENSITIVE_FILES: ${{ secrets.SAP_SENSITIVE_FILES }}
KAYA_SENSITIVE_FILES: ${{ secrets.KAYA_SENSITIVE_FILES }}
run: |
restore() {
[ -z "$1" ] && return
mkdir -p "$2"
printf '%s' "$1" | base64 -d | tar xz --strip-components=1 -C "$2"
}
restore "$SAP_SENSITIVE_FILES" "composeApp/src/commonMain/kotlin/com/kito/sap/sensitive"
restore "$KAYA_SENSITIVE_FILES" "composeApp/src/commonMain/kotlin/com/kito/kaya/sensitive"

- name: Make gradlew executable
run: chmod +x gradlew

Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/release-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore private portal sources
env:
SAP_SENSITIVE_FILES: ${{ secrets.SAP_SENSITIVE_FILES }}
KAYA_SENSITIVE_FILES: ${{ secrets.KAYA_SENSITIVE_FILES }}
run: |
restore() {
[ -z "$1" ] && return
mkdir -p "$2"
printf '%s' "$1" | base64 -d | tar xz --strip-components=1 -C "$2"
}
restore "$SAP_SENSITIVE_FILES" "composeApp/src/commonMain/kotlin/com/kito/sap/sensitive"
restore "$KAYA_SENSITIVE_FILES" "composeApp/src/commonMain/kotlin/com/kito/kaya/sensitive"
- uses: actions/setup-java@v4
with:
java-version: '17'
Expand All @@ -33,6 +45,18 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Restore private portal sources
env:
SAP_SENSITIVE_FILES: ${{ secrets.SAP_SENSITIVE_FILES }}
KAYA_SENSITIVE_FILES: ${{ secrets.KAYA_SENSITIVE_FILES }}
run: |
restore() {
[ -z "$1" ] && return
mkdir -p "$2"
printf '%s' "$1" | base64 -d | tar xz --strip-components=1 -C "$2"
}
restore "$SAP_SENSITIVE_FILES" "composeApp/src/commonMain/kotlin/com/kito/sap/sensitive"
restore "$KAYA_SENSITIVE_FILES" "composeApp/src/commonMain/kotlin/com/kito/kaya/sensitive"
- uses: actions/setup-java@v4
with:
java-version: '17'
Expand All @@ -48,6 +72,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore private portal sources
env:
SAP_SENSITIVE_FILES: ${{ secrets.SAP_SENSITIVE_FILES }}
KAYA_SENSITIVE_FILES: ${{ secrets.KAYA_SENSITIVE_FILES }}
run: |
restore() {
[ -z "$1" ] && return
mkdir -p "$2"
printf '%s' "$1" | base64 -d | tar xz --strip-components=1 -C "$2"
}
restore "$SAP_SENSITIVE_FILES" "composeApp/src/commonMain/kotlin/com/kito/sap/sensitive"
restore "$KAYA_SENSITIVE_FILES" "composeApp/src/commonMain/kotlin/com/kito/kaya/sensitive"
- uses: actions/setup-java@v4
with:
java-version: '17'
Expand Down
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ docs/google-auth-setup.md
docs/kmp-modernization-notes.md
docs/architecture-migration

# SAP Portal sensitive files
# Portal sensitive files
composeApp/src/androidMain/kotlin/kito/sap/
composeApp/src/commonMain/kotlin/com/kito/sap/sensitive/
composeApp/src/commonMain/kotlin/com/kito/kaya/sensitive/
kiit_login.main.kts
kiit_login_test.py
composeApp/src/androidMain/res/xml/network_security_config.xml
composeApp/src/androidUnitTest/kotlin/kito/sap
composeApp/src/iosSimulatorArm64Test/kotlin/com/kito/SapLoginIntegrationTest.kt
Expand Down Expand Up @@ -71,4 +74,4 @@ captures
node_modules/

# iOS secrets (mirrors Android's local.properties pattern)
iosApp/Configuration/Secrets.xcconfig
iosApp/Configuration/Secrets.xcconfig
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Kiito differs significantly from standard academic utilities by prioritizing **privacy, local-first data ownership, and seamless user experience**. It serves as a sophisticated frontend for your academic life, bridging the gap between legacy university portals (SAP) and modern mobile standards.

> [!NOTE]
> **Privacy Architecture**: Kito operates on a zero-knowledge principle regarding your credentials. All sensitive data is processed locally on your device or transmitted directly to the university portal via a secure, transient link. No intermediary servers store your login information.
> **Privacy Architecture**: Kiito operates on a zero-knowledge principle regarding your credentials. All sensitive data is processed locally on your device or transmitted directly to the university portal via a secure, transient link. No intermediary servers store your login information.

📲 **Available on Google Play**
👉 [https://play.google.com/store/apps/details?id=com.kito](https://play.google.com/store/apps/details?id=com.kito)
Expand Down
4 changes: 2 additions & 2 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ android {
applicationId = "com.kito"
minSdk = 26
targetSdk = 37
versionCode = 47
versionName = "5.4.1"
versionCode = 49
versionName = "5.5.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
23 changes: 13 additions & 10 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import java.time.Duration
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

// Automatically bootstrap compilable stubs if sensitive folder is empty/missing
val sensitiveDir = file("src/commonMain/kotlin/com/kito/sap/sensitive")
val stubsDir = file("sensitive_stubs")
if (stubsDir.exists() && (!sensitiveDir.exists() || sensitiveDir.list()?.isEmpty() == true)) {
println("Sensitive SAP folder is missing or empty. Bootstrapping compilable stubs...")
sensitiveDir.mkdirs()
copy {
from(stubsDir)
into(sensitiveDir)
// Bootstrap compilable stubs when private portal sources are unavailable.
listOf(
file("src/commonMain/kotlin/com/kito/sap/sensitive") to file("sensitive_stubs"),
file("src/commonMain/kotlin/com/kito/kaya/sensitive") to file("kaya_sensitive_stubs"),
).forEach { (sensitiveDir, stubsDir) ->
if (stubsDir.exists() && (!sensitiveDir.exists() || sensitiveDir.list()?.isEmpty() == true)) {
println("${sensitiveDir.name} is missing or empty. Bootstrapping compilable stubs...")
sensitiveDir.mkdirs()
copy {
from(stubsDir)
into(sensitiveDir)
}
}
}

Expand Down Expand Up @@ -245,4 +248,4 @@ configurations.all {
useVersion("2.3.20")
}
}
}
}
8 changes: 8 additions & 0 deletions composeApp/kaya_sensitive_stubs/KayaPortalClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.kito.kaya.sensitive

import com.kito.kaya.KayaResult

class KayaPortalClient {
suspend fun fetchTimetable(username: String, password: String): KayaResult =
KayaResult.Error("KAYA private sources are unavailable")
}
1 change: 1 addition & 0 deletions composeApp/src/androidMain/assets/khaoo_gully_banner.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ actual fun openUrl(url: String) {
}
}

actual fun openDeepLinkOrFallback(deepLink: String, fallbackUrl: String) {
val context = PlatformContext.applicationContext
?: throw IllegalStateException("PlatformContext not initialized")
val intent = Intent(Intent.ACTION_VIEW, deepLink.toUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
// No app registered for this scheme — fall back to the store listing.
openUrl(fallbackUrl)
}
}

actual fun createHttpEngine(): HttpClientEngine = OkHttp.create()

actual fun toast(message: String) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class PrefsRepositoryImpl(
private val KEY_USER_ROLLNUMBER = stringPreferencesKey("User_Password")
private val KEY_REQUIRED_ATTENDANCE = intPreferencesKey("required_attendance")
private val KEY_RESET_FIX_V3 = booleanPreferencesKey("reset_fix_V3")
private val KEY_KAYA_INTRO_SEEN = booleanPreferencesKey("kaya_intro_seen")
private val KEY_KAYA_CONNECTED = booleanPreferencesKey("kaya_connected")
private val KEY_NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
private val KEY_FRIEND_ROLLS = stringPreferencesKey("friend_rolls")
private val KEY_SELECTED_FRIEND_ROLL = stringPreferencesKey("selected_friend_roll")
Expand All @@ -34,6 +36,12 @@ class PrefsRepositoryImpl(
override val resetFixFlow: Flow<Boolean> = dataStore.data
.map { it[KEY_RESET_FIX_V3] ?: false }

override val kayaIntroSeenFlow: Flow<Boolean> = dataStore.data
.map { it[KEY_KAYA_INTRO_SEEN] ?: false }

override val kayaConnectedFlow: Flow<Boolean> = dataStore.data
.map { it[KEY_KAYA_CONNECTED] ?: false }

override val requiredAttendanceFlow: Flow<Int> = dataStore.data
.map { it[KEY_REQUIRED_ATTENDANCE] ?: 75 }

Expand Down Expand Up @@ -86,6 +94,14 @@ class PrefsRepositoryImpl(
dataStore.edit { it[KEY_ONBOARDING_DONE] = true }
}

override suspend fun setKayaIntroSeen() {
dataStore.edit { it[KEY_KAYA_INTRO_SEEN] = true }
}

override suspend fun setKayaConnected(connected: Boolean) {
dataStore.edit { it[KEY_KAYA_CONNECTED] = connected }
}

override suspend fun setAcademicYear(year: String) {
dataStore.edit { it[KEY_ACADEMIC_YEAR] = year }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ interface PrefsRepository {
val userSetupDoneFlow: Flow<Boolean>
val friendRollsFlow: Flow<List<String>>
val selectedFriendRollFlow: Flow<String>
/** False until the user has seen the "connect to KAYA" intro on the Schedule screen. */
val kayaIntroSeenFlow: Flow<Boolean>
/** True once the user has connected their KAYA account (credentials verified). */
val kayaConnectedFlow: Flow<Boolean>

suspend fun setUserName(username: String)
suspend fun setUserRollNumber(rollNumber: String)
Expand All @@ -32,4 +36,6 @@ interface PrefsRepository {
suspend fun removeFriendRoll(roll: String)
suspend fun setSelectedFriendRoll(roll: String)
suspend fun clearSelectedFriend()
suspend fun setKayaIntroSeen()
suspend fun setKayaConnected(connected: Boolean)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import com.kito.core.sync.domain.AppSyncUseCase
import com.kito.core.sync.domain.SyncUseCase
import com.kito.core.sync.domain.usecase.ChangeYearTermUseCase
import com.kito.feature.app.presentation.AppViewModel
import com.kito.kaya.KayaRepository
import com.kito.kaya.sensitive.KayaPortalClient
import com.kito.sap.SapPortalClient
import com.kito.sap.SapRepository
import kotlinx.coroutines.CoroutineScope
Expand All @@ -35,6 +37,8 @@ val commonModule = module {

single<SapPortalClient>()
single<SapRepository>()
single<KayaPortalClient>()
single<KayaRepository>()
single<SyncRemoteDataSource>()
single<SectionRepository>()
single<StudentRepository>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import io.ktor.client.engine.HttpClientEngine

expect fun openUrl(url: String)

expect fun openDeepLinkOrFallback(deepLink: String, fallbackUrl: String)

expect fun createHttpEngine(): HttpClientEngine

expect fun toast(message: String)
Expand Down
Loading