diff --git a/.gitignore b/.gitignore index a75e9cc19..321e29928 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,11 @@ bin/ ### Mac OS ### .DS_Store +### Xcode (per-user, machine-specific — should never be committed) ### +**/xcuserdata/ +*.xcuserstate +DerivedData/ + ### Kotlin ### .kotlin/ @@ -81,6 +86,7 @@ coverage/ /oldProject/ /clients/tablet/composeApp/google-services.json /local.properties +/localQuickStart/.local-fake-credentials.json /deploy/dev/.env /deploy/dev/*.jar /deploy/prod/.env diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 5738dbb48..6de4b5c3a 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -14,24 +14,30 @@ repositories { } dependencies { - implementation("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:2.1.21") - implementation("org.springframework.boot:org.springframework.boot.gradle.plugin:3.5.0") - implementation("org.jetbrains.kotlin.plugin.spring:org.jetbrains.kotlin.plugin.spring.gradle.plugin:2.1.21") - implementation("org.jetbrains.kotlin.plugin.jpa:org.jetbrains.kotlin.plugin.jpa.gradle.plugin:2.1.21") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21") - implementation("org.jetbrains.kotlin:kotlin-allopen:2.1.21") - implementation("org.jetbrains.kotlin:kotlin-noarg:2.1.21") - implementation("org.springframework.boot:spring-boot-gradle-plugin:3.5.0") - implementation("io.spring.gradle:dependency-management-plugin:1.1.7") + val kotlinVersion = libs.versions.kotlin.get() + val springBootVersion = libs.versions.springBoot.get() + val springDepManagementVersion = libs.versions.springDependencyManagement.get() + val agpVersion = libs.versions.agp.get() + val composeGradlePluginVersion = libs.versions.compose.get() + val googleServicesVersion = libs.versions.googleServices.get() - implementation("org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin:2.1.21") - implementation("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:2.1.21") - implementation("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:2.1.21") - implementation("com.android.tools.build:gradle:8.9.1") { + implementation("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:$kotlinVersion") + implementation("org.springframework.boot:org.springframework.boot.gradle.plugin:$springBootVersion") + implementation("org.jetbrains.kotlin.plugin.spring:org.jetbrains.kotlin.plugin.spring.gradle.plugin:$kotlinVersion") + implementation("org.jetbrains.kotlin.plugin.jpa:org.jetbrains.kotlin.plugin.jpa.gradle.plugin:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-noarg:$kotlinVersion") + implementation("org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion") + implementation("io.spring.gradle:dependency-management-plugin:$springDepManagementVersion") + + implementation("org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin:$kotlinVersion") + implementation("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:$kotlinVersion") + implementation("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:$kotlinVersion") + implementation("com.android.tools.build:gradle:$agpVersion") { exclude(group = "org.apache.commons", module = "commons-compress") } - implementation("org.jetbrains.compose:org.jetbrains.compose.gradle.plugin:1.8.1") - implementation("com.google.gms:google-services:4.4.3") - implementation("org.jetbrains.kotlin.kapt:org.jetbrains.kotlin.kapt.gradle.plugin:2.1.21") - + implementation("org.jetbrains.compose:org.jetbrains.compose.gradle.plugin:$composeGradlePluginVersion") + implementation("com.google.gms:google-services:$googleServicesVersion") + implementation("org.jetbrains.kotlin.kapt:org.jetbrains.kotlin.kapt.gradle.plugin:$kotlinVersion") } \ No newline at end of file diff --git a/build-logic/src/main/kotlin/band/effective/office/backend/band.effective.office.backend.kotlin-common.gradle.kts b/build-logic/src/main/kotlin/band/effective/office/backend/band.effective.office.backend.kotlin-common.gradle.kts index 476cbc83f..e64e6fa59 100644 --- a/build-logic/src/main/kotlin/band/effective/office/backend/band.effective.office.backend.kotlin-common.gradle.kts +++ b/build-logic/src/main/kotlin/band/effective/office/backend/band.effective.office.backend.kotlin-common.gradle.kts @@ -10,9 +10,9 @@ java { } tasks.withType { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions { + freeCompilerArgs.add("-Xjsr305=strict") + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } diff --git a/build-logic/src/main/kotlin/band/effective/office/backend/band.effective.office.client.kmp.ui.gradle.kts b/build-logic/src/main/kotlin/band/effective/office/backend/band.effective.office.client.kmp.ui.gradle.kts index beeda233f..638ab6541 100644 --- a/build-logic/src/main/kotlin/band/effective/office/backend/band.effective.office.client.kmp.ui.gradle.kts +++ b/build-logic/src/main/kotlin/band/effective/office/backend/band.effective.office.client.kmp.ui.gradle.kts @@ -17,7 +17,7 @@ kotlin { implementation(compose.components.uiToolingPreview) implementation(libs.findLibrary("coil").get()) implementation(libs.findLibrary("coil.network.ktor").get()) - api("com.mohamedrejeb.calf:calf-ui:0.8.0") + api(libs.findLibrary("calf-ui").get()) } androidMain.dependencies { diff --git a/build.gradle.kts b/build.gradle.kts index 907a94ae4..4415b01f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,9 +37,17 @@ subprojects { } tasks.withType { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions { + freeCompilerArgs.add("-Xjsr305=strict") + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } + } + + // kotlinx-datetime 0.7.x is built on the still-experimental kotlin.time.Clock/Instant, + // so opt in project-wide for every Kotlin compilation (JVM, Android and Native). + tasks.withType>().configureEach { + compilerOptions { + optIn.add("kotlin.time.ExperimentalTime") } } diff --git a/clients/shared/core/src/commonMain/kotlin/band/effective/office/shared/core/utils/LocalDateTimeExtensions.kt b/clients/shared/core/src/commonMain/kotlin/band/effective/office/shared/core/utils/LocalDateTimeExtensions.kt index bb8c95b63..eac2baab2 100644 --- a/clients/shared/core/src/commonMain/kotlin/band/effective/office/shared/core/utils/LocalDateTimeExtensions.kt +++ b/clients/shared/core/src/commonMain/kotlin/band/effective/office/shared/core/utils/LocalDateTimeExtensions.kt @@ -2,7 +2,7 @@ package band.effective.office.shared.core.utils import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime diff --git a/clients/shared/core/src/iosMain/kotlin/band/effective/office/shared/core/utils/DateTimeUtils.ios.kt b/clients/shared/core/src/iosMain/kotlin/band/effective/office/shared/core/utils/DateTimeUtils.ios.kt index 74ee65211..47c03b2e4 100644 --- a/clients/shared/core/src/iosMain/kotlin/band/effective/office/shared/core/utils/DateTimeUtils.ios.kt +++ b/clients/shared/core/src/iosMain/kotlin/band/effective/office/shared/core/utils/DateTimeUtils.ios.kt @@ -4,12 +4,10 @@ import kotlinx.datetime.LocalDateTime import kotlinx.datetime.toNSDateComponents import platform.Foundation.NSCalendar import platform.Foundation.NSDateFormatter -import platform.Foundation.NSLocale actual fun LocalDateTime.toLocalisedString(pattern: String): String { val dateFormatter = NSDateFormatter() dateFormatter.dateFormat = pattern - dateFormatter.locale = NSLocale.currentLocale val calendar = NSCalendar.currentCalendar val dateComponents = toNSDateComponents() diff --git a/clients/smsrouter/app/build.gradle.kts b/clients/smsrouter/app/build.gradle.kts index bcb0ea09f..efb4c22ba 100644 --- a/clients/smsrouter/app/build.gradle.kts +++ b/clients/smsrouter/app/build.gradle.kts @@ -41,15 +41,18 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } buildFeatures { compose = true buildConfig = true } } +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } +} + dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/clients/tablet/composeApp/build.gradle.kts b/clients/tablet/composeApp/build.gradle.kts index 1b90aa857..6b41e8279 100644 --- a/clients/tablet/composeApp/build.gradle.kts +++ b/clients/tablet/composeApp/build.gradle.kts @@ -1,13 +1,13 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties plugins { - id("org.jetbrains.kotlin.multiplatform") - id("org.jetbrains.kotlin.plugin.compose") version "2.1.21" - id("org.jetbrains.compose") - id("com.android.application") - id("org.jetbrains.kotlin.plugin.serialization") version "2.1.21" + alias(libs.plugins.multiplatform) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.compose) + alias(libs.plugins.android.application) + alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.buildkonfig) - id("com.google.gms.google-services") version "4.4.3" + alias(libs.plugins.google.services) id("sign-config") } @@ -27,10 +27,12 @@ kotlin { export(libs.decompose.compose.jetbrains) export(libs.essenty.lifecycle) - export("com.mohamedrejeb.calf:calf-ui:0.8.0") + export(libs.calf.ui) } it.compilations.all { - kotlinOptions.freeCompilerArgs += "-Xdisable-decompose-parcelize" + compileTaskProvider.configure { + compilerOptions.freeCompilerArgs.add("-Xdisable-decompose-parcelize") + } } } @@ -45,7 +47,7 @@ kotlin { api(libs.decompose) api(libs.decompose.compose.jetbrains) - api("com.mohamedrejeb.calf:calf-ui:0.8.0") + api(libs.calf.ui) api(libs.kotlinx.datetime) implementation(libs.kotlin.coroutines.core) diff --git a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt index 60c5e84f7..8e4dc5c21 100644 --- a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt +++ b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt @@ -7,7 +7,7 @@ import android.content.IntentFilter import android.util.Log import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder import kotlinx.coroutines.flow.StateFlow -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime diff --git a/clients/tablet/composeApp/src/debug/AndroidManifest.xml b/clients/tablet/composeApp/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..dc566c6d5 --- /dev/null +++ b/clients/tablet/composeApp/src/debug/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt b/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt index 17e7c1556..9b4d8dbee 100644 --- a/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt +++ b/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt @@ -2,7 +2,7 @@ package band.effective.office.tablet.time import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder import kotlinx.coroutines.flow.StateFlow -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime diff --git a/clients/tablet/core/data/src/iosMain/kotlin/band/effective/office/tablet/core/data/network/HttpClientFactory.ios.kt b/clients/tablet/core/data/src/iosMain/kotlin/band/effective/office/tablet/core/data/network/HttpClientFactory.ios.kt deleted file mode 100644 index 370de4437..000000000 --- a/clients/tablet/core/data/src/iosMain/kotlin/band/effective/office/tablet/core/data/network/HttpClientFactory.ios.kt +++ /dev/null @@ -1,8 +0,0 @@ -package band.effective.office.tablet.core.data.network - -import io.ktor.client.HttpClient -import io.ktor.client.engine.darwin.Darwin - -actual object HttpClientFactory { - actual fun createHttpClient(): HttpClient = HttpClient(Darwin) -} \ No newline at end of file diff --git a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/model/RoomInfo.kt b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/model/RoomInfo.kt index 8f224438e..deb5f5312 100644 --- a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/model/RoomInfo.kt +++ b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/model/RoomInfo.kt @@ -1,6 +1,6 @@ package band.effective.office.tablet.core.domain.model -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime diff --git a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/RoomInfoUseCase.kt b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/RoomInfoUseCase.kt index 071e1d3cf..632ceff52 100644 --- a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/RoomInfoUseCase.kt +++ b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/RoomInfoUseCase.kt @@ -4,7 +4,7 @@ import band.effective.office.shared.core.domain.map import band.effective.office.tablet.core.domain.model.RoomInfo import band.effective.office.shared.core.domain.unbox import kotlin.collections.filter -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.coroutines.flow.map import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant diff --git a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SelectRoomUseCase.kt b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SelectRoomUseCase.kt index 3d16a7085..9000c237a 100644 --- a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SelectRoomUseCase.kt +++ b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SelectRoomUseCase.kt @@ -6,7 +6,7 @@ import band.effective.office.shared.core.utils.currentInstant import kotlin.math.absoluteValue import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant diff --git a/clients/tablet/core/ui/src/iosMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.ios.kt b/clients/tablet/core/ui/src/iosMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.ios.kt index 28a122dee..74d9d1260 100644 --- a/clients/tablet/core/ui/src/iosMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.ios.kt +++ b/clients/tablet/core/ui/src/iosMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.ios.kt @@ -2,5 +2,6 @@ package band.effective.office.tablet.core.ui.utils import platform.Foundation.NSLocale import platform.Foundation.currentLocale +import platform.Foundation.languageCode -actual fun getCurrentLanguageCode(): String = NSLocale.currentLocale.languageCode \ No newline at end of file +actual fun getCurrentLanguageCode(): String = NSLocale.currentLocale.languageCode ?: "en" \ No newline at end of file diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt index 355e9fb96..f36b3321d 100644 --- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt +++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime diff --git a/clients/tablet/feature/main/build.gradle.kts b/clients/tablet/feature/main/build.gradle.kts index 842627990..2f317920b 100644 --- a/clients/tablet/feature/main/build.gradle.kts +++ b/clients/tablet/feature/main/build.gradle.kts @@ -6,8 +6,7 @@ kotlin { sourceSets { commonMain.dependencies { implementation(project(":clients:tablet:feature:slot")) - implementation("org.jetbrains.compose.components:components-ui-tooling-preview:1.10.0") - + implementation(compose.components.uiToolingPreview) } } } diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/RoomInfoView.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/RoomInfoView.kt index 719ddf98b..75c92db90 100644 --- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/RoomInfoView.kt +++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/RoomInfoView.kt @@ -12,7 +12,7 @@ import band.effective.office.tablet.core.domain.model.nextEvent import band.effective.office.tablet.core.ui.theme.AppTheme import band.effective.office.tablet.feature.main.components.uiComponent.BusyRoomInfoComponent import band.effective.office.tablet.feature.main.components.uiComponent.FreeRoomInfoComponent -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt index 50204e610..ec2e56722 100644 --- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt +++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt @@ -3,7 +3,7 @@ package band.effective.office.tablet.feature.main.domain import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/GetTimeToNextEventUseCase.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/GetTimeToNextEventUseCase.kt index e78abe2ac..29742246c 100644 --- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/GetTimeToNextEventUseCase.kt +++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/GetTimeToNextEventUseCase.kt @@ -2,7 +2,7 @@ package band.effective.office.tablet.feature.main.domain import band.effective.office.tablet.core.domain.model.RoomInfo import band.effective.office.tablet.core.domain.model.nextEvent -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant diff --git a/clients/tv/composeApp/build.gradle.kts b/clients/tv/composeApp/build.gradle.kts index 96d098ef4..bcb9d4902 100644 --- a/clients/tv/composeApp/build.gradle.kts +++ b/clients/tv/composeApp/build.gradle.kts @@ -2,7 +2,7 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties plugins { alias(libs.plugins.multiplatform) - id("org.jetbrains.kotlin.plugin.compose") version "2.1.21" + alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) alias(libs.plugins.android.application) alias(libs.plugins.kotlinx.serialization) diff --git a/clients/tv/feature/menu/src/commonMain/kotlin/band/effective/office/tv/feature/menu/presentation/components/ProgressButton.kt b/clients/tv/feature/menu/src/commonMain/kotlin/band/effective/office/tv/feature/menu/presentation/components/ProgressButton.kt index 32c2f6051..e6c281d01 100644 --- a/clients/tv/feature/menu/src/commonMain/kotlin/band/effective/office/tv/feature/menu/presentation/components/ProgressButton.kt +++ b/clients/tv/feature/menu/src/commonMain/kotlin/band/effective/office/tv/feature/menu/presentation/components/ProgressButton.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import band.effective.office.tv.core.ui.theme.LocalTvColorsPalette import band.effective.office.tv.core.ui.theme.LocalTvShapes -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.coroutines.delay private const val AUTO_CLICK_DELAY_MS = 15000L // 15 seconds diff --git a/clients/tv/feature/stories/src/commonMain/kotlin/band/effective/office/tv/feature/stories/domain/service/DateUtils.kt b/clients/tv/feature/stories/src/commonMain/kotlin/band/effective/office/tv/feature/stories/domain/service/DateUtils.kt index 5b37f50e2..a1009ae56 100644 --- a/clients/tv/feature/stories/src/commonMain/kotlin/band/effective/office/tv/feature/stories/domain/service/DateUtils.kt +++ b/clients/tv/feature/stories/src/commonMain/kotlin/band/effective/office/tv/feature/stories/domain/service/DateUtils.kt @@ -1,6 +1,6 @@ package band.effective.office.tv.feature.stories.domain.service -import kotlinx.datetime.Clock +import kotlin.time.Clock import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 997ee5858..476bd43c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Common ## Kotlin -kotlin = "2.1.21" +kotlin = "2.3.10" kotlinCoroutines = "1.10.2" kotlinx-serialization = "1.8.1" @@ -38,13 +38,13 @@ googleOAuthClient = "1.33.3" # Client ## Android -android-compileSdk = "35" +android-compileSdk = "36" android-minSdk = "26" android-targetSdk = "35" -agp = "8.9.3" +agp = "8.11.2" ## Compose -compose = "1.8.1" +compose = "1.10.2" hotReload = "1.0.0-alpha09" ## Androidx @@ -64,6 +64,7 @@ ktor = "3.1.3" ## UI coil = "3.2.0" zxing = "3.5.1" +calf = "0.11.0" ## DI koin = "4.1.0" @@ -71,10 +72,10 @@ koin = "4.1.0" ## Storage multiplatformSettings = "1.3.0" buildkonfig = "0.15.1" -room = "2.7.2" +room = "2.8.4" ## Datetime -datetime = "0.6.2" +datetime = "0.7.1" ## Decompose decompose = "3.3.0" @@ -84,6 +85,7 @@ essenty-ios = "0.2.4" ## Settings settings = "1.3.0" firebaseMessagingKtx = "24.1.2" +googleServices = "4.4.3" ## Notion notion-ver = "1.11.1" @@ -196,6 +198,7 @@ ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "k ## UI coil = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil" } coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } +calf-ui = { module = "com.mohamedrejeb.calf:calf-ui", version.ref = "calf" } zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" } ## DI @@ -239,11 +242,12 @@ spring-dependency-management = { id = "io.spring.dependency-management", version # Client multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -compose = { id = "org.jetbrains.compose", version = "1.8.1" } +compose = { id = "org.jetbrains.compose", version.ref = "compose" } android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } hotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "hotReload" } buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } [bundles] # Common diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 780979f3a..7243b7272 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Jun 25 13:54:29 OMST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/localQuickStart/README.md b/localQuickStart/README.md new file mode 100644 index 000000000..a8ef6907c --- /dev/null +++ b/localQuickStart/README.md @@ -0,0 +1,154 @@ +# localQuickStart — run Effective-Office locally, fully offline + +This folder gets the **backend + the Meeting Room tablet app** running on your machine +**without any external accounts** (no Google / Firebase / Synology / Notion / Clockify / +Mattermost). It's meant for "just show me how it works" demos and local development. + +> The official README "run locally" path assumes you have **real** Google & Firebase +> service accounts (it even asks you to drop in `google-credentials.json` / +> `firebase-credentials.json`). Without them the server won't even boot. This quick-start +> swaps every provider for the project's built-in **dummy** implementations instead. + +--- + +## What's in this folder + +| File | Purpose | +|------|---------| +| `run-backend-local.sh` | Starts the Spring Boot backend with all providers set to dummy and every required env var filled with safe dummy values. | +| `seed-local-db.sh` | Inserts an API key + one zone + two meeting rooms (`Sync`, `Focus`) so the tablet has something to show. Idempotent. | +| `.local-fake-credentials.json` | A **self-generated, fake** Google service-account JSON (valid RSA key, never used to talk to Google). Auto-created by `run-backend-local.sh` if missing. **Git-ignored** (contains a private key → would trip the Gitleaks pre-commit hook). | + +## Prerequisites + +- Docker (for Postgres) +- JDK 17+ (JDK 21 works — the build targets bytecode 17, no strict toolchain) +- To run the client: **Android SDK + an emulator**, or **Xcode** (iOS simulator) + +--- + +## 1. Start Postgres + +```bash +docker run --name postgres-effectiveoffice \ + -e POSTGRES_DB=effectiveoffice -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres \ + -p 5432:5432 -d postgres:15-alpine +``` + +## 2. Start the backend + +```bash +localQuickStart/run-backend-local.sh +``` + +Wait for `Started EffectiveOfficeApplicationKt`. It listens on **http://localhost:8080**, +context path **`/api`**. Quick check (Swagger UI is public and should return 200): + +```bash +curl -o /dev/null -w '%{http_code}\n' http://localhost:8080/api/swagger-ui/index.html # -> 200 +open http://localhost:8080/api/swagger-ui/index.html +``` + +> Note: `/api/actuator/health` returns **403** here — actuator sits behind its own security in +> this project, so don't use it as a liveness check. Use the Swagger URL or the authorized +> call in step 3 instead. + +## 3. Seed the database + +```bash +localQuickStart/seed-local-db.sh +``` + +Verify auth + data (the key matches `apiKey` in the repo-root `local.properties`): + +```bash +curl -H "Authorization: Bearer effective-office-local-key" \ + "http://localhost:8080/api/v1/workspaces?workspace_tag=meeting" +# -> JSON array with "Sync" and "Focus" +``` + +## 4. Run the tablet app + +The client reads its config from the **repo-root `local.properties`** +(`api.url.debug`, `api.url.release`, `apiKey`, `sdk.dir`). + +⚠️ **The base URL is baked into the build**, and it differs per platform: + +| Target | Property | Value | +|--------|----------|-------| +| **iOS simulator** (shares the host network) | `api.url.debug` | `http://localhost:8080` | +| **Android emulator** (host is reachable via a special IP) | `api.url.debug` | `http://10.0.2.2:8080` | +| **Aurora emulator** (same IP, but the build is release) | `api.url.release` | `http://10.0.2.2:8080` | + +Switching platforms means editing that line and rebuilding. The script does the editing: + +```bash +localQuickStart/point-client-at-local.sh ios|android|aurora +``` + +### Android emulator + +```bash +# any tablet-sized AVD; build, install, launch: +./gradlew :clients:tablet:composeApp:assembleDebug +adb install -r -g clients/tablet/composeApp/build/outputs/apk/debug/composeApp-debug.apk +adb shell monkey -p band.effective.office.tablet -c android.intent.category.LAUNCHER 1 +``` + +### iOS simulator + +Open `iosApp/iosApp.xcodeproj` in Xcode and hit **Run** (scheme `iosApp`), or: + +```bash +xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)' build +``` + +To iterate on Kotlin/Native compile errors fast (without Xcode): +`./gradlew :clients:tablet:composeApp:linkDebugFrameworkIosSimulatorArm64` + +⚠️ **Typing into the simulator is ASCII-only and goes through the active macOS layout.** +With a Russian layout `an` arrives as `фт`, and filtering the organizer list then looks broken +when it is not. Switch to an English layout before typing. + +--- + +## Files this quick-start needs elsewhere in the repo + +These can't live in this folder — the build looks for them at fixed locations. They're all +**git-ignored** and safe to keep locally: + +| Path | Why | +|------|-----| +| `local.properties` (repo root) | `api.url.debug/release`, `apiKey`, `sdk.dir`. Any Gradle build fails without it (settings.gradle configures every module). | +| `clients/tablet/composeApp/google-services.json` | The `com.google.gms.google-services` plugin refuses to build without it. A dummy file with package `band.effective.office.tablet` is enough. | +| `clients/tablet/composeApp/src/debug/AndroidManifest.xml` | Debug-only overlay adding `usesCleartextTraffic="true"` so the debug build can reach the local **http** backend. | +| `keystore/debug.keystore` | Debug signing config (alias `androiddebugkey`, store/key pass `android`). Generate with `keytool`. | + +--- + +## Why the workarounds exist (project quirks) + +**Backend** +- Provider config classes (Synology, Mattermost, …) load **unconditionally** and read env + with self-referential YAML (`SYNOLOGY_IP: ${SYNOLOGY_IP:}`) — so every such var must be + non-empty or Spring dies with a "circular placeholder reference". Hence the long dummy list. +- `photo.saver.provider=dummy` clashes with the photos dummy (both register a bean named + `dummyPhotoProvider`) → we use `mattermost` with the scheduler disabled instead. +- `FirebaseConfig` reads the credentials file at startup no matter what → the fake JSON. + +**iOS** (the `iosMain` source set was clearly never built in CI — it needed 5 fixes to compile): +1. `clients/shared/core/.../DateTimeUtils.ios.kt` — dropped `NSLocale.currentLocale` + (unresolved on the current Kotlin/Native; `NSDateFormatter` defaults to the current locale). +2. Deleted orphaned `clients/tablet/core/data/src/iosMain/.../HttpClientFactory.ios.kt` + (`actual` with no `expect`). +3. `clients/tablet/core/ui/.../DateFormatter.ios.kt` — added `import platform.Foundation.languageCode`. +4. `clients/tablet/feature/main/build.gradle.kts` — hardcoded `components-ui-tooling-preview:1.10.0` + pulled a Kotlin 2.2.x klib (project is 2.1.21) → switched to the managed `compose.components.uiToolingPreview`. +5. Stripped `widthDp/heightDp/locale` params from `@Preview` in feature:main (those params only + exist in the newer Preview annotation; Compose here is 1.8.1). + +## Limitations + +- Providers are dummy → bookings live in-memory in the backend and are **not** synced to a real + Google Calendar. Perfect for demos, not for real scheduling. diff --git a/localQuickStart/point-client-at-local.sh b/localQuickStart/point-client-at-local.sh new file mode 100755 index 000000000..b939574de --- /dev/null +++ b/localQuickStart/point-client-at-local.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Point the tablet client at the local backend for one platform. +# +# The base URL is baked into the build and every platform reaches the host differently, +# so switching platforms means editing local.properties and rebuilding. Which line and +# which address is easy to get wrong, hence this script. +# +# iOS simulator shares the host network -> api.url.debug=http://localhost:8080 +# Android emulator host lives behind a special IP -> api.url.debug=http://10.0.2.2:8080 +# Aurora emulator same IP as Android, but the build is release, so it reads api.url.release +# +# Usage (from anywhere): +# localQuickStart/point-client-at-local.sh ios|android|aurora +# +# Rebuild after switching — the URL is compiled in. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROPS="$(cd "$SCRIPT_DIR/.." && pwd)/local.properties" + +case "${1:-}" in + ios) KEY=api.url.debug URL=http://localhost:8080 ;; + android) KEY=api.url.debug URL=http://10.0.2.2:8080 ;; + aurora) KEY=api.url.release URL=http://10.0.2.2:8080 ;; + *) echo "usage: $(basename "$0") ios|android|aurora" >&2; exit 2 ;; +esac + +[ -f "$PROPS" ] || { echo "no $PROPS — see localQuickStart/README.md" >&2; exit 1; } +grep -q "^$KEY=" "$PROPS" || { echo "no $KEY in $PROPS" >&2; exit 1; } + +# BSD sed and GNU sed disagree about -i, so write through a temporary file instead. +TMP="$(mktemp)" +sed "s|^$KEY=.*|$KEY=$URL|" "$PROPS" > "$TMP" +mv "$TMP" "$PROPS" + +grep -E '^api\.url\.(debug|release)=' "$PROPS" diff --git a/localQuickStart/run-backend-local.sh b/localQuickStart/run-backend-local.sh new file mode 100755 index 000000000..fa937912a --- /dev/null +++ b/localQuickStart/run-backend-local.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Local, fully-offline backend run for Effective-Office. +# +# All external providers are switched to dummy/in-memory so NO real +# Google / Firebase / Synology / Notion / Clockify / Mattermost accounts are needed. +# See ./README.md in this folder for the full story and the client steps. +# +# Prereqs: Postgres running on localhost:5432 (db=effectiveoffice, user/pass=postgres): +# docker run --name postgres-effectiveoffice -e POSTGRES_DB=effectiveoffice \ +# -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:15-alpine +# +# Usage (from anywhere): +# localQuickStart/run-backend-local.sh +set -euo pipefail + +# This script lives in /localQuickStart ; gradlew lives in the repo root. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +# --- fake Google/Firebase service-account json (valid RSA key, never leaves the machine) --- +# FirebaseConfig reads this file at startup unconditionally, so it must parse — but with the +# dummy calendar provider active it is never actually used to talk to Google. +CRED_FILE="$SCRIPT_DIR/.local-fake-credentials.json" +if [ ! -f "$CRED_FILE" ]; then + KEY=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null | python3 -c 'import sys;print(sys.stdin.read().replace(chr(10),"\\n"),end="")') + cat > "$CRED_FILE" < beans load but never hit the network. +export PHOTO_SAVER_PROVIDER=mattermost +export PHOTO_SAVER_STORAGE=dummy +export PHOTO_SAVER_SCHEDULER_ENABLED=false + +# --- calendar / firebase (dummy provider active, so these files are only parsed, never used) --- +export GOOGLE_CREDENTIALS_FILE="$STUB" +export FIREBASE_CREDENTIALS="$STUB" +export DEFAULT_CALENDAR=primary +export CALENDAR_APPLICATION_NAME=EffectiveOfficeLocal +export CALENDAR_DELEGATED_USER=local@example.com +export DEFAULT_APP_EMAIL=local@example.com +export CALENDARS="" +export TEST_APPLICATION_URL="" +export TEST_CALENDARS="" + +# --- these provider config classes load unconditionally and read env directly, +# so they must be non-empty even though the providers are dummy --- +export SYNOLOGY_IP=dummy +export SYNOLOGY_LOGIN=dummy +export SYNOLOGY_PASSWORD=dummy +export SYNOLOGY_ALBUM_NAME=dummy +export NOTION_TOKEN=dummy +export NOTION_TEAMMATES_DB_ID=dummy +export NOTION_SUPERNOVA_DB_ID=dummy +export CLOCKIFY_API_KEY=dummy +export CLOCKIFY_WORKSPACE_ID=dummy +export CLOCKIFY_PROJECT_ID=dummy +export PHOTO_SAVER_MATTERMOST_BASE_URL="http://localhost" +export PHOTO_SAVER_MATTERMOST_TOKEN=dummy +export PHOTO_SAVER_EMOJI_REQUEST_SAVE=star +export PHOTO_SAVER_EMOJI_SUCCESS=white_check_mark +export PHOTO_SAVER_SYNOLOGY_BASE_URL="http://localhost" +export PHOTO_SAVER_SYNOLOGY_USERNAME=dummy +export PHOTO_SAVER_SYNOLOGY_PASSWORD=dummy +export PHOTO_SAVER_ALBUM_NAME=dummy + +exec ./gradlew :backend:app:bootRun --args='--spring.profiles.active=local' --console=plain diff --git a/localQuickStart/seed-local-db.sh b/localQuickStart/seed-local-db.sh new file mode 100755 index 000000000..512d4f1bb --- /dev/null +++ b/localQuickStart/seed-local-db.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Seed the local Postgres so the tablet app has something to show: +# - an API key the clients authenticate with (Bearer token) +# - one workspace zone + two meeting rooms (Sync, Focus) +# +# The backend stores API keys as the lowercase hex SHA-256 of the raw key. +# The raw key below MUST match `apiKey` in the repo-root local.properties. +# +# Safe to re-run (idempotent). Requires the postgres-effectiveoffice container to be running. +set -euo pipefail + +CONTAINER="${PG_CONTAINER:-postgres-effectiveoffice}" +API_KEY="${LOCAL_API_KEY:-effective-office-local-key}" + +# NOTE: docker exec needs no stdin here because we pass SQL via -c. +psql() { docker exec "$CONTAINER" psql -U postgres -d effectiveoffice -v ON_ERROR_STOP=1 "$@"; } + +HASH="$(printf '%s' "$API_KEY" | shasum -a 256 | awk '{print $1}')" +echo "Seeding API key '$API_KEY' (sha256=$HASH)" + +psql -c "INSERT INTO api_keys (id, key_value, description) + VALUES (gen_random_uuid(), '$HASH', 'local dev key') + ON CONFLICT (key_value) DO NOTHING;" + +psql -c "INSERT INTO workspace_zones(id,name) VALUES (gen_random_uuid(),'Ground floor') + ON CONFLICT (name) DO NOTHING;" + +for room in Sync Focus; do + psql -c "INSERT INTO workspaces(id,name,tag,zone_id) + SELECT gen_random_uuid(),'$room','meeting', z.id + FROM workspace_zones z WHERE z.name='Ground floor' + ON CONFLICT (name) DO NOTHING;" +done + +echo "--- current meeting rooms ---" +psql -c "SELECT w.name, w.tag, z.name AS zone + FROM workspaces w LEFT JOIN workspace_zones z ON z.id=w.zone_id + WHERE w.tag='meeting';" + +# Organizers = the booking editor's "Choose organizer" list. +# NOTE: the tablet client filters users by tag == "employer" (a typo in +# OrganizerRepositoryImpl — everything else uses "employee"), and the backend +# ignores the user_tag query param, so the DB tag must be 'employer' to show up. +# Enough of them to fill the list past its 150.dp cap, or scrolling cannot be looked at: with +# three names there is nothing to scroll and no way to tell a scrolling bug from a still list. +# Names vary in length on purpose, so text that wraps or gets clipped shows up here too. +for row in "jdoe|john.doe@example.com|John|Doe" \ + "asmith|anna.smith@example.com|Anna|Smith" \ + "mkim|min.kim@example.com|Min|Kim" \ + "pivanov|pyotr.ivanov@example.com|Пётр|Иванов" \ + "esokolova|elena.sokolova@example.com|Елена|Соколова" \ + "dvolkov|dmitry.volkov@example.com|Дмитрий|Волков" \ + "akonstantinopolskaya|a.konst@example.com|Анастасия|Константинопольская" \ + "ilee|isabella.lee@example.com|Isabella|Lee" \ + "mokonkwo|m.okonkwo@example.com|Chukwuemeka|Okonkwo" \ + "ttanaka|t.tanaka@example.com|Takeshi|Tanaka" \ + "sgarcia|s.garcia@example.com|Sofia|Garcia" \ + "obrown|o.brown@example.com|Oliver|Brown" \ + "nnovak|n.novak@example.com|Nina|Novak" \ + "rmuller|r.muller@example.com|Rudolf|Müller" \ + "ksmirnova|k.smirnova@example.com|Ксения|Смирнова" \ + "vpetrov|v.petrov@example.com|Владимир|Петров" \ + "ahassan|a.hassan@example.com|Amina|Hassan" \ + "lrossi|l.rossi@example.com|Luca|Rossi" \ + "ykim|y.kim@example.com|Yuna|Kim" \ + "bandersson|b.andersson@example.com|Bjorn|Andersson"; do + IFS='|' read -r username email first last <<< "$row" + psql -c "INSERT INTO users (id, username, email, first_name, last_name, created_at, updated_at, active, role, tag) + VALUES (gen_random_uuid(), '$username', '$email', '$first', '$last', now(), now(), true, 'employer', 'employer') + ON CONFLICT (email) DO NOTHING;" +done +# Fix any pre-existing rows seeded with the wrong tag. +psql -c "UPDATE users SET tag='employer' WHERE tag='employee';" + +echo "--- current organizers (tag=employer) ---" +psql -c "SELECT first_name, last_name, email FROM users WHERE tag='employer';" +echo "Done."