diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a2c03b6..8244883b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,14 +16,14 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - - name: Set up JDK 17 - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'adopt' - - - name: Build with Gradle - run: ./gradlew build + # - name: Set up JDK 17 + # uses: actions/setup-java@v2 + # with: + # java-version: '17' + # distribution: 'adopt' + # + # - name: Build with Gradle + # run: ./gradlew build - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 diff --git a/apps/admin/feature/category/create/src/commonMain/kotlin/feature/admin/category/create/AdminCategoryCreateEventHandler.kt b/apps/admin/feature/category/create/src/commonMain/kotlin/feature/admin/category/create/AdminCategoryCreateEventHandler.kt index 1eb5f974..47299b0f 100644 --- a/apps/admin/feature/category/create/src/commonMain/kotlin/feature/admin/category/create/AdminCategoryCreateEventHandler.kt +++ b/apps/admin/feature/category/create/src/commonMain/kotlin/feature/admin/category/create/AdminCategoryCreateEventHandler.kt @@ -9,12 +9,14 @@ internal class AdminCategoryCreateEventHandler( ) : EventHandler< AdminCategoryCreateContract.Inputs, AdminCategoryCreateContract.Events, - AdminCategoryCreateContract.State> { + AdminCategoryCreateContract.State, + > { override suspend fun EventHandlerScope< AdminCategoryCreateContract.Inputs, AdminCategoryCreateContract.Events, - AdminCategoryCreateContract.State>.handleEvent( - event: AdminCategoryCreateContract.Events + AdminCategoryCreateContract.State, + >.handleEvent( + event: AdminCategoryCreateContract.Events, ) = when (event) { is AdminCategoryCreateContract.Events.OnError -> onError(event.message) is AdminCategoryCreateContract.Events.GoToCategory -> goToCategory(event.id) diff --git a/apps/admin/feature/category/create/src/commonMain/kotlin/feature/admin/category/create/AdminCategoryCreateInputHandler.kt b/apps/admin/feature/category/create/src/commonMain/kotlin/feature/admin/category/create/AdminCategoryCreateInputHandler.kt index 7eeedec8..ff4148b4 100644 --- a/apps/admin/feature/category/create/src/commonMain/kotlin/feature/admin/category/create/AdminCategoryCreateInputHandler.kt +++ b/apps/admin/feature/category/create/src/commonMain/kotlin/feature/admin/category/create/AdminCategoryCreateInputHandler.kt @@ -14,12 +14,16 @@ import org.koin.core.component.inject private typealias InputScope = InputHandlerScope< AdminCategoryCreateContract.Inputs, AdminCategoryCreateContract.Events, - AdminCategoryCreateContract.State> - -internal class AdminCategoryCreateInputHandler : KoinComponent, InputHandler< - AdminCategoryCreateContract.Inputs, - AdminCategoryCreateContract.Events, - AdminCategoryCreateContract.State> { + AdminCategoryCreateContract.State, + > + +internal class AdminCategoryCreateInputHandler : + KoinComponent, + InputHandler< + AdminCategoryCreateContract.Inputs, + AdminCategoryCreateContract.Events, + AdminCategoryCreateContract.State, + > { private val categoryService: CategoryService by inject() private val inputValidator: InputValidator by inject() diff --git a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigContract.kt b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigContract.kt index dd26e7e2..8ce4ca20 100644 --- a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigContract.kt +++ b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigContract.kt @@ -9,121 +9,161 @@ object AdminConfigContract { data class State( val isLoading: Boolean = false, val wasEdited: Boolean = false, - - val original: GetConfigQuery.GetConfig = GetConfigQuery.GetConfig( - id = "", - updatedAt = "", - companyInfo = GetConfigQuery.CompanyInfo( - contactInfo = GetConfigQuery.ContactInfo( - companyName = "", - companyWebsite = null, - email = null, - phone = null - ), openingTimes = GetConfigQuery.OpeningTimes( - close = null, - dayFrom = DayOfWeek.MONDAY, - dayTo = DayOfWeek.FRIDAY, - open = null - ) - ), footerConfig = GetConfigQuery.FooterConfig( - showStartChat = false, - showOpeningTimes = false, - showCareer = false, - showCyberSecurity = false, - showPress = false - ), landingConfig = GetConfigQuery.LandingConfig( - slideshowItems = listOf(), - topCategoriesSection = GetConfigQuery.TopCategoriesSection( - left = GetConfigQuery.Left(media = null, title = null), - middle = GetConfigQuery.Middle(media = null, title = null), - right = GetConfigQuery.Right(media = null, title = null) - ) - ), catalogConfig = GetConfigQuery.CatalogConfig( - bannerConfig = GetConfigQuery.BannerConfig( - catalog = GetConfigQuery.Catalog( - title = "", - media = null + val original: GetConfigQuery.GetConfig = + GetConfigQuery.GetConfig( + id = "", + updatedAt = "", + companyInfo = + GetConfigQuery.CompanyInfo( + contactInfo = + GetConfigQuery.ContactInfo( + companyName = "", + companyWebsite = null, + email = null, + phone = null, + ), + openingTimes = + GetConfigQuery.OpeningTimes( + close = null, + dayFrom = DayOfWeek.MONDAY, + dayTo = DayOfWeek.FRIDAY, + open = null, + ), + ), + footerConfig = + GetConfigQuery.FooterConfig( + showStartChat = false, + showOpeningTimes = false, + showCareer = false, + showCyberSecurity = false, + showPress = false, + ), + landingConfig = + GetConfigQuery.LandingConfig( + slideshowItems = listOf(), + topCategoriesSection = + GetConfigQuery.TopCategoriesSection( + left = GetConfigQuery.Left(media = null, title = null), + middle = GetConfigQuery.Middle(media = null, title = null), + right = GetConfigQuery.Right(media = null, title = null), ), - sales = GetConfigQuery.Sales(title = "", media = null), - popular = GetConfigQuery.Popular(title = "", media = null), - mens = GetConfigQuery.Mens(title = "", media = null), - women = GetConfigQuery.Women(title = "", media = null), - kids = GetConfigQuery.Kids(title = "", media = null) - ) - ) - - ), + ), + catalogConfig = + GetConfigQuery.CatalogConfig( + bannerConfig = + GetConfigQuery.BannerConfig( + catalog = + GetConfigQuery.Catalog( + title = "", + media = null, + ), + sales = GetConfigQuery.Sales(title = "", media = null), + popular = GetConfigQuery.Popular(title = "", media = null), + mens = GetConfigQuery.Mens(title = "", media = null), + women = GetConfigQuery.Women(title = "", media = null), + kids = GetConfigQuery.Kids(title = "", media = null), + ), + ), + ), val current: GetConfigQuery.GetConfig = original, - val emailError: String? = null, val phoneError: String? = null, val companyWebsiteError: String? = null, val openTimeError: String? = null, val closeTimeError: String? = null, - val isPreviewDialogOpen: Boolean = false, val previewDialogImage: ImagePreview? = null, - val deleteImageDialogOpen: Boolean = false, val deleteImageDialogImageId: String? = null, - val collageMediaDropError: String? = null, val isCollageImagesLoading: Boolean = false, - val bannerLeftMediaDropError: String? = null, val isBannerLeftImagesLoading: Boolean = false, - val isBannerMiddleImagesLoading: Boolean = false, val bannerMiddleMediaDropError: String? = null, - val bannerRightMediaDropError: String? = null, val isBannerRightImagesLoading: Boolean = false, ) sealed interface Inputs { data object Init : Inputs + data object FetchConfig : Inputs + data class UploadCollageMedia(val imageId: String, val blob: String) : Inputs + data class UploadBannerMedia(val side: Side, val blob: String) : Inputs data object OnSaveClick : Inputs + data object OnDiscardSaveClick : Inputs + data class OnOpenDayFromSelected(val day: DayOfWeek) : Inputs + data class OnOpenDayToSelected(val day: DayOfWeek) : Inputs + data class OnImageClick(val imagePreview: ImagePreview) : Inputs + data class OnImageDeleteClick(val imageId: String) : Inputs + data object OnImageDeleteYesClick : Inputs + data object OnImageDeleteNoClick : Inputs data class OnCollageMediaDrop(val imageId: String, val blob: String) : Inputs + data class OnCollageItemTitleChanged(val imageId: String, val title: String) : Inputs + data class OnCollageItemDescriptionChanged(val imageId: String, val description: String) : Inputs data class OnBannerLeftTitleChanged(val title: String) : Inputs + data class OnBannerLeftMediaDrop(val blob: String) : Inputs + data class OnBannerMiddleTitleChanged(val title: String) : Inputs + data class OnBannerMiddleMediaDrop(val blob: String) : Inputs + data class OnBannerRightTitleChanged(val title: String) : Inputs + data class OnBannerRightMediaDrop(val blob: String) : Inputs data class SetPreviewDialogOpen(val isOpen: Boolean) : Inputs + data class SetDeleteImageDialogOpen(val isOpen: Boolean) : Inputs + data class SetLoading(val isLoading: Boolean) : Inputs + data class SetOriginalConfig(val config: GetConfigQuery.GetConfig) : Inputs + data class SetCurrentConfig(val config: GetConfigQuery.GetConfig) : Inputs + data class SetEmail(val email: String) : Inputs + data class SetPhone(val phone: String) : Inputs + data class SetCompanyWebsite(val companyWebsite: String) : Inputs + data class SetOpenTime(val openTime: String) : Inputs + data class SetCloseTime(val closeTime: String) : Inputs + data class SetUpdatedAt(val updatedAt: String) : Inputs + data class SetCollageImageDropError(val error: String?) : Inputs + data class SetCollageImagesLoading(val isLoading: Boolean) : Inputs + data class SetBannerLeftImageDropError(val error: String?) : Inputs + data class SetBannerLeftImagesLoading(val isLoading: Boolean) : Inputs + data class SetBannerMiddleImageDropError(val error: String?) : Inputs + data class SetBannerMiddleImagesLoading(val isLoading: Boolean) : Inputs + data class SetBannerRightImageDropError(val error: String?) : Inputs + data class SetBannerRightImagesLoading(val isLoading: Boolean) : Inputs } diff --git a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigEventHandler.kt b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigEventHandler.kt index 4223972b..ccd3f98a 100644 --- a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigEventHandler.kt +++ b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigEventHandler.kt @@ -6,7 +6,11 @@ import com.copperleaf.ballast.EventHandlerScope internal class AdminConfigEventHandler( private val onError: suspend (String) -> Unit, ) : EventHandler { - override suspend fun EventHandlerScope.handleEvent( + override suspend fun EventHandlerScope< + AdminConfigContract.Inputs, + AdminConfigContract.Events, + AdminConfigContract.State, + >.handleEvent( event: AdminConfigContract.Events, ) = when (event) { is AdminConfigContract.Events.OnError -> onError(event.message) diff --git a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigInputHandler.kt b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigInputHandler.kt index a33320e4..ba7dd4ba 100644 --- a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigInputHandler.kt +++ b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigInputHandler.kt @@ -1,3 +1,5 @@ +@file:Suppress("DuplicatedCode") + package feature.admin.config import com.apollographql.apollo3.api.Optional @@ -15,129 +17,133 @@ import data.type.MediaType import data.type.Side import data.type.SlideshowItemInput import data.type.TopCategoryItemInput +import feature.admin.config.AdminConfigContract.Events +import feature.admin.config.AdminConfigContract.Inputs +import feature.admin.config.AdminConfigContract.State import org.koin.core.component.KoinComponent import org.koin.core.component.inject -private typealias InputScope = InputHandlerScope< - AdminConfigContract.Inputs, - AdminConfigContract.Events, - AdminConfigContract.State> - -internal class AdminConfigInputHandler : - KoinComponent, - InputHandler { +private typealias InputScope = InputHandlerScope +@Suppress("MaxLineLength") +internal class AdminConfigInputHandler : KoinComponent, InputHandler { private val configService: ConfigService by inject() private val inputValidator: InputValidator by inject() - override suspend fun InputScope.handleInput(input: AdminConfigContract.Inputs) = when (input) { - is AdminConfigContract.Inputs.Init -> handleInit() - AdminConfigContract.Inputs.FetchConfig -> handleFetchConfig() - is AdminConfigContract.Inputs.UploadCollageMedia -> handleUploadCollageMedia(input.imageId, input.blob) - is AdminConfigContract.Inputs.UploadBannerMedia -> handleUploadBannerMedia(input.side, input.blob) - - AdminConfigContract.Inputs.OnDiscardSaveClick -> updateState { it.copy(current = it.original).wasEdited() } - AdminConfigContract.Inputs.OnSaveClick -> handleSave() - is AdminConfigContract.Inputs.OnOpenDayFromSelected -> handleOnOpenDayFromSelected(input.day) - is AdminConfigContract.Inputs.OnOpenDayToSelected -> handleOnOpenDayToSelected(input.day) - is AdminConfigContract.Inputs.OnImageClick -> - updateState { it.copy(isPreviewDialogOpen = true, previewDialogImage = input.imagePreview) } + override suspend fun InputScope.handleInput(input: Inputs) = + when (input) { + is Inputs.Init -> handleInit() + Inputs.FetchConfig -> handleFetchConfig() + is Inputs.UploadCollageMedia -> handleUploadCollageMedia(input.imageId, input.blob) + is Inputs.UploadBannerMedia -> handleUploadBannerMedia(input.side, input.blob) - is AdminConfigContract.Inputs.OnImageDeleteClick -> - updateState { it.copy(deleteImageDialogOpen = true, deleteImageDialogImageId = input.imageId) } + Inputs.OnDiscardSaveClick -> updateState { it.copy(current = it.original).wasEdited() } + Inputs.OnSaveClick -> handleSave() + is Inputs.OnOpenDayFromSelected -> handleOnOpenDayFromSelected(input.day) + is Inputs.OnOpenDayToSelected -> handleOnOpenDayToSelected(input.day) + is Inputs.OnImageClick -> + updateState { it.copy(isPreviewDialogOpen = true, previewDialogImage = input.imagePreview) } - AdminConfigContract.Inputs.OnImageDeleteYesClick -> handleOnImageDeleteYesClick() - AdminConfigContract.Inputs.OnImageDeleteNoClick -> - updateState { it.copy(deleteImageDialogOpen = false, deleteImageDialogImageId = null) } + is Inputs.OnImageDeleteClick -> + updateState { it.copy(deleteImageDialogOpen = true, deleteImageDialogImageId = input.imageId) } - is AdminConfigContract.Inputs.OnCollageMediaDrop -> handleOnCollageMediaDrop(input.imageId, input.blob) - is AdminConfigContract.Inputs.OnCollageItemTitleChanged -> - handleOnCollageItemTitleChanged(input.imageId, input.title) + Inputs.OnImageDeleteYesClick -> handleOnImageDeleteYesClick() + Inputs.OnImageDeleteNoClick -> + updateState { it.copy(deleteImageDialogOpen = false, deleteImageDialogImageId = null) } - is AdminConfigContract.Inputs.OnCollageItemDescriptionChanged -> - handleOnCollageItemDescriptionChanged(input.imageId, input.description) + is Inputs.OnCollageMediaDrop -> handleOnCollageMediaDrop(input.imageId, input.blob) + is Inputs.OnCollageItemTitleChanged -> + handleOnCollageItemTitleChanged(input.imageId, input.title) - is AdminConfigContract.Inputs.OnBannerLeftMediaDrop -> handleOnBannerLeftMediaDrop(input.blob) + is Inputs.OnCollageItemDescriptionChanged -> + handleOnCollageItemDescriptionChanged(input.imageId, input.description) - is AdminConfigContract.Inputs.OnBannerLeftTitleChanged -> - handleOnBannerLeftTitleChanged(input.title) + is Inputs.OnBannerLeftMediaDrop -> handleOnBannerLeftMediaDrop(input.blob) - is AdminConfigContract.Inputs.OnBannerMiddleMediaDrop -> handleOnBannerMiddleMediaDrop(input.blob) + is Inputs.OnBannerLeftTitleChanged -> + handleOnBannerLeftTitleChanged(input.title) - is AdminConfigContract.Inputs.OnBannerMiddleTitleChanged -> handleOnBannerMiddleTitleChanged(input.title) + is Inputs.OnBannerMiddleMediaDrop -> handleOnBannerMiddleMediaDrop(input.blob) - is AdminConfigContract.Inputs.OnBannerRightMediaDrop -> handleOnBannerRightMediaDrop(input.blob) + is Inputs.OnBannerMiddleTitleChanged -> handleOnBannerMiddleTitleChanged(input.title) - is AdminConfigContract.Inputs.OnBannerRightTitleChanged -> - handleOnBannerRightTitleChanged(input.title) + is Inputs.OnBannerRightMediaDrop -> handleOnBannerRightMediaDrop(input.blob) - is AdminConfigContract.Inputs.SetLoading -> updateState { it.copy(isLoading = input.isLoading) } - is AdminConfigContract.Inputs.SetOriginalConfig -> updateState { it.copy(original = input.config).wasEdited() } - is AdminConfigContract.Inputs.SetCurrentConfig -> updateState { it.copy(current = input.config).wasEdited() } - is AdminConfigContract.Inputs.SetEmail -> handleSetEmail(input.email) - is AdminConfigContract.Inputs.SetPhone -> handleSetPhone(input.phone) - is AdminConfigContract.Inputs.SetCompanyWebsite -> handleSetCompanyWebsite(input.companyWebsite) - is AdminConfigContract.Inputs.SetCloseTime -> handleSetCloseTime(input.closeTime) - is AdminConfigContract.Inputs.SetOpenTime -> handleSetOpenTime(input.openTime) - is AdminConfigContract.Inputs.SetUpdatedAt -> handleSetUpdatedAt(input.updatedAt) - is AdminConfigContract.Inputs.SetPreviewDialogOpen -> - updateState { it.copy(isPreviewDialogOpen = input.isOpen) } + is Inputs.OnBannerRightTitleChanged -> + handleOnBannerRightTitleChanged(input.title) - is AdminConfigContract.Inputs.SetCollageImageDropError -> - updateState { it.copy(collageMediaDropError = input.error) } + is Inputs.SetLoading -> updateState { it.copy(isLoading = input.isLoading) } + is Inputs.SetOriginalConfig -> updateState { it.copy(original = input.config).wasEdited() } + is Inputs.SetCurrentConfig -> updateState { it.copy(current = input.config).wasEdited() } + is Inputs.SetEmail -> handleSetEmail(input.email) + is Inputs.SetPhone -> handleSetPhone(input.phone) + is Inputs.SetCompanyWebsite -> handleSetCompanyWebsite(input.companyWebsite) + is Inputs.SetCloseTime -> handleSetCloseTime(input.closeTime) + is Inputs.SetOpenTime -> handleSetOpenTime(input.openTime) + is Inputs.SetUpdatedAt -> handleSetUpdatedAt(input.updatedAt) + is Inputs.SetPreviewDialogOpen -> + updateState { it.copy(isPreviewDialogOpen = input.isOpen) } - is AdminConfigContract.Inputs.SetDeleteImageDialogOpen -> - updateState { it.copy(deleteImageDialogOpen = input.isOpen) } + is Inputs.SetCollageImageDropError -> + updateState { it.copy(collageMediaDropError = input.error) } - is AdminConfigContract.Inputs.SetCollageImagesLoading -> - updateState { it.copy(isCollageImagesLoading = input.isLoading) } + is Inputs.SetDeleteImageDialogOpen -> + updateState { it.copy(deleteImageDialogOpen = input.isOpen) } - is AdminConfigContract.Inputs.SetBannerLeftImageDropError -> - updateState { it.copy(bannerLeftMediaDropError = input.error) } + is Inputs.SetCollageImagesLoading -> + updateState { it.copy(isCollageImagesLoading = input.isLoading) } - is AdminConfigContract.Inputs.SetBannerLeftImagesLoading -> - updateState { it.copy(isBannerLeftImagesLoading = input.isLoading) } + is Inputs.SetBannerLeftImageDropError -> + updateState { it.copy(bannerLeftMediaDropError = input.error) } - is AdminConfigContract.Inputs.SetBannerMiddleImageDropError -> - updateState { it.copy(bannerMiddleMediaDropError = input.error) } + is Inputs.SetBannerLeftImagesLoading -> + updateState { it.copy(isBannerLeftImagesLoading = input.isLoading) } - is AdminConfigContract.Inputs.SetBannerMiddleImagesLoading -> - updateState { it.copy(isBannerMiddleImagesLoading = input.isLoading) } + is Inputs.SetBannerMiddleImageDropError -> + updateState { it.copy(bannerMiddleMediaDropError = input.error) } - is AdminConfigContract.Inputs.SetBannerRightImageDropError -> - updateState { it.copy(bannerRightMediaDropError = input.error) } + is Inputs.SetBannerMiddleImagesLoading -> + updateState { it.copy(isBannerMiddleImagesLoading = input.isLoading) } - is AdminConfigContract.Inputs.SetBannerRightImagesLoading -> - updateState { it.copy(isBannerRightImagesLoading = input.isLoading) } + is Inputs.SetBannerRightImageDropError -> + updateState { it.copy(bannerRightMediaDropError = input.error) } - } + is Inputs.SetBannerRightImagesLoading -> + updateState { it.copy(isBannerRightImagesLoading = input.isLoading) } + } + @Suppress("DuplicatedCode") private suspend fun InputScope.handleOnBannerMiddleTitleChanged(title: String) { updateState { - val newMiddle = it.current.landingConfig.topCategoriesSection.middle.copy(title = title) + val newMiddle = + it.current.landingConfig.topCategoriesSection.middle + .copy(title = title) val newBannerSection = it.current.landingConfig.topCategoriesSection.copy(middle = newMiddle) it.copy( - current = it.current.copy( - landingConfig = it.current.landingConfig.copy(topCategoriesSection = newBannerSection) + current = + it.current.copy( + landingConfig = it.current.landingConfig.copy(topCategoriesSection = newBannerSection), ), ).wasEdited() } } private suspend fun InputScope.handleOnBannerMiddleMediaDrop(blob: String) { - postInput(AdminConfigContract.Inputs.SetBannerMiddleImagesLoading(true)) - postInput(AdminConfigContract.Inputs.UploadBannerMedia(Side.MIDDLE, blob)) + postInput(Inputs.SetBannerMiddleImagesLoading(true)) + postInput(Inputs.UploadBannerMedia(Side.MIDDLE, blob)) } private suspend fun InputScope.handleOnBannerRightTitleChanged(title: String) { updateState { val newRight = it.current.landingConfig.topCategoriesSection.right.copy(title = title) val newBannerSection = - it.current.landingConfig.topCategoriesSection.copy(right = newRight) + it.current.landingConfig.topCategoriesSection + .copy(right = newRight) it.copy( - current = it.current.copy( - landingConfig = it.current.landingConfig.copy(topCategoriesSection = newBannerSection) + current = + it.current.copy( + landingConfig = it.current.landingConfig.copy(topCategoriesSection = newBannerSection), ), ).wasEdited() } @@ -149,74 +155,90 @@ internal class AdminConfigInputHandler : val newBannerSection = it.current.landingConfig.topCategoriesSection.copy(left = newLeft) it.copy( - current = it.current.copy( - landingConfig = it.current.landingConfig.copy(topCategoriesSection = newBannerSection) + current = + it.current.copy( + landingConfig = it.current.landingConfig.copy(topCategoriesSection = newBannerSection), ), ).wasEdited() } } private suspend fun InputScope.handleOnBannerLeftMediaDrop(blob: String) { - postInput(AdminConfigContract.Inputs.SetBannerLeftImagesLoading(true)) - postInput(AdminConfigContract.Inputs.UploadBannerMedia(Side.LEFT, blob)) + postInput(Inputs.SetBannerLeftImagesLoading(true)) + postInput(Inputs.UploadBannerMedia(Side.LEFT, blob)) } private suspend fun InputScope.handleOnBannerRightMediaDrop(blob: String) { - postInput(AdminConfigContract.Inputs.SetBannerRightImagesLoading(true)) - postInput(AdminConfigContract.Inputs.UploadBannerMedia(Side.RIGHT, blob)) + postInput(Inputs.SetBannerRightImagesLoading(true)) + postInput(Inputs.UploadBannerMedia(Side.RIGHT, blob)) } - private suspend fun InputScope.handleOnCollageItemDescriptionChanged(imageId: String, description: String) { + private suspend fun InputScope.handleOnCollageItemDescriptionChanged( + imageId: String, + description: String, + ) { updateState { state -> val newCollageItems = state.current.landingConfig.slideshowItems.toMutableList() val index = newCollageItems.indexOfFirst { it.id == imageId } val currentCollageItem = newCollageItems[index] - newCollageItems[index] = GetConfigQuery.SlideshowItem( - id = imageId, - title = currentCollageItem.title, - description = description, - media = currentCollageItem.media, - ) + newCollageItems[index] = + GetConfigQuery.SlideshowItem( + id = imageId, + title = currentCollageItem.title, + description = description, + media = currentCollageItem.media, + ) state.copy( - current = state.current.copy( - landingConfig = state.current.landingConfig.copy( - slideshowItems = newCollageItems.toList() - ) + current = + state.current.copy( + landingConfig = + state.current.landingConfig.copy( + slideshowItems = newCollageItems.toList(), + ), ), ).wasEdited() } } - private suspend fun InputScope.handleOnCollageItemTitleChanged(imageId: String, title: String) { + private suspend fun InputScope.handleOnCollageItemTitleChanged( + imageId: String, + title: String, + ) { updateState { val newCollageItems = it.current.landingConfig.slideshowItems.toMutableList() - val index = newCollageItems.indexOfFirst { it.id == imageId } + val index = newCollageItems.indexOfFirst { item -> item.id == imageId } val currentCollageItem = newCollageItems[index] - newCollageItems[index] = GetConfigQuery.SlideshowItem( - id = imageId, - title = title, - description = currentCollageItem.description, - media = currentCollageItem.media, - ) + newCollageItems[index] = + GetConfigQuery.SlideshowItem( + id = imageId, + title = title, + description = currentCollageItem.description, + media = currentCollageItem.media, + ) it.copy( - current = it.current.copy( - landingConfig = it.current.landingConfig.copy( - slideshowItems = newCollageItems.toList() - ) + current = + it.current.copy( + landingConfig = + it.current.landingConfig.copy( + slideshowItems = newCollageItems.toList(), + ), ), ).wasEdited() } } - private suspend fun InputScope.handleUploadCollageMedia(imageId: String, blob: String) { + private suspend fun InputScope.handleUploadCollageMedia( + imageId: String, + blob: String, + ) { val state = getCurrentState() sideJob("handleUploadCollageMedia") { - postInput(AdminConfigContract.Inputs.SetCollageImageDropError(error = null)) - postInput(AdminConfigContract.Inputs.SetCollageImagesLoading(isLoading = true)) + postInput(Inputs.SetCollageImageDropError(error = null)) + postInput(Inputs.SetCollageImagesLoading(isLoading = true)) val mediaType = MediaType.Image configService.uploadCollageImage( configId = state.current.id, @@ -224,37 +246,43 @@ internal class AdminConfigInputHandler : blob = BlobInput(blob), mediaType = mediaType, ).fold( - { postEvent(AdminConfigContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { data -> - val media = data.uploadConfigCollageImage.slideshowItems.map { - GetConfigQuery.SlideshowItem( - id = it.id.toString(), - media = GetConfigQuery.Media( - keyName = it.media?.keyName ?: "", - url = it.media?.url ?: "", - alt = it.media?.alt ?: "", - type = it.media?.type ?: MediaType.Image, - ), - title = it.title, - description = it.description, + val media = + data.uploadConfigCollageImage.slideshowItems.map { + GetConfigQuery.SlideshowItem( + id = it.id.toString(), + media = + GetConfigQuery.Media( + keyName = it.media?.keyName ?: "", + url = it.media?.url ?: "", + alt = it.media?.alt ?: "", + type = it.media?.type ?: MediaType.Image, + ), + title = it.title, + description = it.description, + ) + } + val config = + state.current.copy( + landingConfig = state.current.landingConfig.copy(slideshowItems = media), ) - } - val config = state.current.copy( - landingConfig = state.current.landingConfig.copy(slideshowItems = media) - ) - postInput(AdminConfigContract.Inputs.SetOriginalConfig(config)) - postInput(AdminConfigContract.Inputs.SetCurrentConfig(config)) + postInput(Inputs.SetOriginalConfig(config)) + postInput(Inputs.SetCurrentConfig(config)) }, ) - postInput(AdminConfigContract.Inputs.SetCollageImagesLoading(isLoading = false)) + postInput(Inputs.SetCollageImagesLoading(isLoading = false)) } } - private suspend fun InputScope.handleUploadBannerMedia(side: Side, blob: String) { + private suspend fun InputScope.handleUploadBannerMedia( + side: Side, + blob: String, + ) { val state = getCurrentState() sideJob("handleUploadBannerMedia") { - postInput(AdminConfigContract.Inputs.SetBannerLeftImageDropError(error = null)) - postInput(AdminConfigContract.Inputs.SetBannerRightImageDropError(error = null)) + postInput(Inputs.SetBannerLeftImageDropError(error = null)) + postInput(Inputs.SetBannerRightImageDropError(error = null)) val mediaType = MediaType.Image configService.uploadBannerMedia( configId = state.current.id, @@ -262,54 +290,68 @@ internal class AdminConfigInputHandler : blob = BlobInput(blob), mediaType = mediaType, ).fold( - { postEvent(AdminConfigContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { data -> - val topCategoriesSection = with(data.uploadConfigBannerImage.topCategoriesSection) { - GetConfigQuery.TopCategoriesSection( - left = GetConfigQuery.Left( - title = left.title, - media = GetConfigQuery.Media1( - keyName = left.media?.keyName ?: "", - url = left.media?.url ?: "", - alt = left.media?.alt ?: "", - type = left.media?.type ?: MediaType.Image, + val topCategoriesSection = + with(data.uploadConfigBannerImage.topCategoriesSection) { + GetConfigQuery.TopCategoriesSection( + left = + GetConfigQuery.Left( + title = left.title, + media = + GetConfigQuery.Media1( + keyName = left.media?.keyName ?: "", + url = left.media?.url ?: "", + alt = left.media?.alt ?: "", + type = left.media?.type ?: MediaType.Image, + ), ), - ), - middle = GetConfigQuery.Middle( - title = left.title, - media = GetConfigQuery.Media2( - keyName = left.media?.keyName ?: "", - url = left.media?.url ?: "", - alt = left.media?.alt ?: "", - type = left.media?.type ?: MediaType.Image, + middle = + GetConfigQuery.Middle( + title = left.title, + media = + GetConfigQuery.Media2( + keyName = left.media?.keyName ?: "", + url = left.media?.url ?: "", + alt = left.media?.alt ?: "", + type = left.media?.type ?: MediaType.Image, + ), ), - ), - right = GetConfigQuery.Right( - title = right.title, - media = GetConfigQuery.Media3( - keyName = right.media?.keyName ?: "", - url = right.media?.url ?: "", - alt = right.media?.alt ?: "", - type = right.media?.type ?: MediaType.Image, + right = + GetConfigQuery.Right( + title = right.title, + media = + GetConfigQuery.Media3( + keyName = right.media?.keyName ?: "", + url = right.media?.url ?: "", + alt = right.media?.alt ?: "", + type = right.media?.type ?: MediaType.Image, + ), ), + ) + } + val config = + state.current.copy( + landingConfig = + state.current.landingConfig.copy( + topCategoriesSection = topCategoriesSection, ), ) - } - val config = state.current.copy( - landingConfig = state.current.landingConfig.copy(topCategoriesSection = topCategoriesSection) - ) - postInput(AdminConfigContract.Inputs.SetOriginalConfig(config)) - postInput(AdminConfigContract.Inputs.SetCurrentConfig(config)) + postInput(Inputs.SetOriginalConfig(config)) + postInput(Inputs.SetCurrentConfig(config)) }, ) - postInput(AdminConfigContract.Inputs.SetBannerLeftImagesLoading(isLoading = false)) - postInput(AdminConfigContract.Inputs.SetBannerRightImagesLoading(isLoading = false)) + postInput(Inputs.SetBannerLeftImagesLoading(isLoading = false)) + postInput(Inputs.SetBannerRightImagesLoading(isLoading = false)) } } - private suspend fun InputScope.handleOnCollageMediaDrop(imageId: String, blob: String) { + private suspend fun InputScope.handleOnCollageMediaDrop( + imageId: String, + blob: String, + ) { sideJob("handleAddMedia") { - postInput(AdminConfigContract.Inputs.UploadCollageMedia(imageId, blob)) + postInput(Inputs.UploadCollageMedia(imageId, blob)) } } @@ -325,10 +367,12 @@ internal class AdminConfigInputHandler : state.copy( deleteImageDialogOpen = false, deleteImageDialogImageId = null, - current = state.current.copy( - landingConfig = state.current.landingConfig.copy( - slideshowItems = newCollageItems.toList() - ) + current = + state.current.copy( + landingConfig = + state.current.landingConfig.copy( + slideshowItems = newCollageItems.toList(), + ), ), ).wasEdited() } @@ -338,12 +382,15 @@ internal class AdminConfigInputHandler : private suspend fun InputScope.handleOnOpenDayFromSelected(day: DayOfWeek) { updateState { it.copy( - current = it.current.copy( - companyInfo = it.current.companyInfo.copy( - openingTimes = it.current.companyInfo.openingTimes.copy( - dayFrom = if (it.current.companyInfo.openingTimes.dayTo == day) null else day - ) - ) + current = + it.current.copy( + companyInfo = + it.current.companyInfo.copy( + openingTimes = + it.current.companyInfo.openingTimes.copy( + dayFrom = if (it.current.companyInfo.openingTimes.dayTo == day) null else day, + ), + ), ), ).wasEdited() } @@ -352,12 +399,15 @@ internal class AdminConfigInputHandler : private suspend fun InputScope.handleOnOpenDayToSelected(day: DayOfWeek) { updateState { it.copy( - current = it.current.copy( - companyInfo = it.current.companyInfo.copy( - openingTimes = it.current.companyInfo.openingTimes.copy( - dayTo = if (it.current.companyInfo.openingTimes.dayFrom == day) null else day - ) - ) + current = + it.current.copy( + companyInfo = + it.current.companyInfo.copy( + openingTimes = + it.current.companyInfo.openingTimes.copy( + dayTo = if (it.current.companyInfo.openingTimes.dayFrom == day) null else day, + ), + ), ), ).wasEdited() } @@ -367,10 +417,12 @@ internal class AdminConfigInputHandler : updateState { val isValidated = inputValidator.validateText(companyWebsite) it.copy( - current = it.current.copy( - companyInfo = it.current.companyInfo.copy( - contactInfo = it.current.companyInfo.contactInfo.copy(companyWebsite = companyWebsite) - ) + current = + it.current.copy( + companyInfo = + it.current.companyInfo.copy( + contactInfo = it.current.companyInfo.contactInfo.copy(companyWebsite = companyWebsite), + ), ), companyWebsiteError = if (!it.wasEdited) null else isValidated, ).wasEdited() @@ -379,19 +431,19 @@ internal class AdminConfigInputHandler : private suspend fun InputScope.handleInit() { sideJob("handleInit") { - postInput(AdminConfigContract.Inputs.SetLoading(isLoading = true)) - postInput(AdminConfigContract.Inputs.FetchConfig) - postInput(AdminConfigContract.Inputs.SetLoading(isLoading = false)) + postInput(Inputs.SetLoading(isLoading = true)) + postInput(Inputs.FetchConfig) + postInput(Inputs.SetLoading(isLoading = false)) } } private suspend fun InputScope.handleFetchConfig() { sideJob("handleFetchConfig") { configService.fetchConfig().fold( - { postEvent(AdminConfigContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { - postInput(AdminConfigContract.Inputs.SetOriginalConfig(config = it.getConfig)) - postInput(AdminConfigContract.Inputs.SetCurrentConfig(config = it.getConfig)) + postInput(Inputs.SetOriginalConfig(config = it.getConfig)) + postInput(Inputs.SetCurrentConfig(config = it.getConfig)) }, ) } @@ -401,10 +453,12 @@ internal class AdminConfigInputHandler : updateState { val isValidated = inputValidator.validateText(closeTime) it.copy( - current = it.current.copy( - companyInfo = it.current.companyInfo.copy( - openingTimes = it.current.companyInfo.openingTimes.copy(close = closeTime) - ) + current = + it.current.copy( + companyInfo = + it.current.companyInfo.copy( + openingTimes = it.current.companyInfo.openingTimes.copy(close = closeTime), + ), ), closeTimeError = if (!it.wasEdited) null else isValidated, ).wasEdited() @@ -415,10 +469,12 @@ internal class AdminConfigInputHandler : updateState { val isValidated = inputValidator.validateText(openTime) it.copy( - current = it.current.copy( - companyInfo = it.current.companyInfo.copy( - openingTimes = it.current.companyInfo.openingTimes.copy(open = openTime) - ) + current = + it.current.copy( + companyInfo = + it.current.companyInfo.copy( + openingTimes = it.current.companyInfo.openingTimes.copy(open = openTime), + ), ), openTimeError = if (!it.wasEdited) null else isValidated, ).wasEdited() @@ -429,10 +485,12 @@ internal class AdminConfigInputHandler : updateState { val isValidated = inputValidator.validateText(phone) it.copy( - current = it.current.copy( - companyInfo = it.current.companyInfo.copy( - contactInfo = it.current.companyInfo.contactInfo.copy(phone = phone) - ) + current = + it.current.copy( + companyInfo = + it.current.companyInfo.copy( + contactInfo = it.current.companyInfo.contactInfo.copy(phone = phone), + ), ), phoneError = if (!it.wasEdited) null else isValidated, ).wasEdited() @@ -443,243 +501,363 @@ internal class AdminConfigInputHandler : updateState { it.copy(current = it.current.copy(updatedAt = updatedAt)).wasEdited() } } + @Suppress("DuplicatedCode") private suspend fun InputScope.handleUpdateConfig() { with(getCurrentState()) { sideJob("handleUpdateDetails") { configService.updateConfig( configId = current.id, - companyName = if (current.companyInfo.contactInfo.companyName != original.companyInfo.contactInfo.companyName) - current.companyInfo.contactInfo.companyName else null, - companyWebsite = if (current.companyInfo.contactInfo.companyWebsite != original.companyInfo.contactInfo.companyWebsite) - current.companyInfo.contactInfo.companyWebsite else null, - email = if (current.companyInfo.contactInfo.email != original.companyInfo.contactInfo.email) - current.companyInfo.contactInfo.email else null, - phone = if (current.companyInfo.contactInfo.phone != original.companyInfo.contactInfo.phone) - current.companyInfo.contactInfo.phone else null, - dayFrom = if (current.companyInfo.openingTimes.dayFrom != original.companyInfo.openingTimes.dayFrom) - current.companyInfo.openingTimes.dayFrom else null, - dayTo = if (current.companyInfo.openingTimes.dayTo != original.companyInfo.openingTimes.dayTo) - current.companyInfo.openingTimes.dayTo else null, - open = if (current.companyInfo.openingTimes.open != original.companyInfo.openingTimes.open) - current.companyInfo.openingTimes.open else null, - close = if (current.companyInfo.openingTimes.close != original.companyInfo.openingTimes.close) - current.companyInfo.openingTimes.close else null, - slideshowItems = if (current.landingConfig.slideshowItems != original.landingConfig.slideshowItems) + companyName = + if ( + current.companyInfo.contactInfo.companyName != original.companyInfo.contactInfo.companyName + ) { + current.companyInfo.contactInfo.companyName + } else { + null + }, + companyWebsite = + if ( + current.companyInfo.contactInfo.companyWebsite != + original.companyInfo.contactInfo.companyWebsite + ) { + current.companyInfo.contactInfo.companyWebsite + } else { + null + }, + email = + if (current.companyInfo.contactInfo.email != original.companyInfo.contactInfo.email) { + current.companyInfo.contactInfo.email + } else { + null + }, + phone = + if (current.companyInfo.contactInfo.phone != original.companyInfo.contactInfo.phone) { + current.companyInfo.contactInfo.phone + } else { + null + }, + dayFrom = + if (current.companyInfo.openingTimes.dayFrom != original.companyInfo.openingTimes.dayFrom) { + current.companyInfo.openingTimes.dayFrom + } else { + null + }, + dayTo = + if (current.companyInfo.openingTimes.dayTo != original.companyInfo.openingTimes.dayTo) { + current.companyInfo.openingTimes.dayTo + } else { + null + }, + open = + if (current.companyInfo.openingTimes.open != original.companyInfo.openingTimes.open) { + current.companyInfo.openingTimes.open + } else { + null + }, + close = + if (current.companyInfo.openingTimes.close != original.companyInfo.openingTimes.close) { + current.companyInfo.openingTimes.close + } else { + null + }, + slideshowItems = + if (current.landingConfig.slideshowItems != original.landingConfig.slideshowItems) { current.landingConfig.slideshowItems.map { SlideshowItemInput( id = it.id, title = Optional.present(it.title), description = Optional.present(it.description), - media = it.media?.let { + media = + it.media?.let { media -> Optional.present( MediaInput( - keyName = it.keyName, - url = it.url, - alt = it.alt, - type = it.type - ) + keyName = media.keyName, + url = media.url, + alt = media.alt, + type = media.type, + ), ) - } ?: Optional.absent() + } ?: Optional.absent(), ) - } else null, - showCareer = if (current.footerConfig.showCareer != original.footerConfig.showCareer) - current.footerConfig.showCareer else null, - showCyberSecurity = if (current.footerConfig.showCyberSecurity != original.footerConfig.showCyberSecurity) - current.footerConfig.showCyberSecurity else null, - showPress = if (current.footerConfig.showPress != original.footerConfig.showPress) - current.footerConfig.showPress else null, - showStartChat = if (current.footerConfig.showStartChat != original.footerConfig.showStartChat) - current.footerConfig.showStartChat else null, - showOpeningTimes = if (current.footerConfig.showOpeningTimes != original.footerConfig.showOpeningTimes) - current.footerConfig.showOpeningTimes else null, - topCategoriesSectionLeft = if (current.landingConfig.topCategoriesSection.left != original.landingConfig.topCategoriesSection.left) + } + } else { + null + }, + showCareer = + if (current.footerConfig.showCareer != original.footerConfig.showCareer) { + current.footerConfig.showCareer + } else { + null + }, + showCyberSecurity = + if ( + current.footerConfig.showCyberSecurity != original.footerConfig.showCyberSecurity + ) { + current.footerConfig.showCyberSecurity + } else { + null + }, + showPress = + if (current.footerConfig.showPress != original.footerConfig.showPress) { + current.footerConfig.showPress + } else { + null + }, + showStartChat = + if (current.footerConfig.showStartChat != original.footerConfig.showStartChat) { + current.footerConfig.showStartChat + } else { + null + }, + showOpeningTimes = + if ( + current.footerConfig.showOpeningTimes != original.footerConfig.showOpeningTimes + ) { + current.footerConfig.showOpeningTimes + } else { + null + }, + topCategoriesSectionLeft = + if ( + current.landingConfig.topCategoriesSection.left != + original.landingConfig.topCategoriesSection.left + ) { TopCategoryItemInput( title = Optional.present(current.landingConfig.topCategoriesSection.left.title), - media = current.landingConfig.topCategoriesSection.left.media?.let { + media = + current.landingConfig.topCategoriesSection.left.media?.let { Optional.present( MediaInput( keyName = it.keyName, url = it.url, alt = it.alt, - type = it.type - ) + type = it.type, + ), ) } ?: Optional.absent(), - ) else null, - topCategoriesSectionMiddle = if (current.landingConfig.topCategoriesSection.left != original.landingConfig.topCategoriesSection.left) + ) + } else { + null + }, + topCategoriesSectionMiddle = + if ( + current.landingConfig.topCategoriesSection.left != + original.landingConfig.topCategoriesSection.left + ) { TopCategoryItemInput( title = Optional.present(current.landingConfig.topCategoriesSection.left.title), - media = current.landingConfig.topCategoriesSection.left.media?.let { + media = + current.landingConfig.topCategoriesSection.left.media?.let { Optional.present( MediaInput( keyName = it.keyName, url = it.url, alt = it.alt, - type = it.type - ) + type = it.type, + ), ) } ?: Optional.absent(), - ) else null, - topCategoriesRight = if (current.landingConfig.topCategoriesSection.right != original.landingConfig.topCategoriesSection.right) + ) + } else { + null + }, + topCategoriesRight = + if ( + current.landingConfig.topCategoriesSection.right != + original.landingConfig.topCategoriesSection.right + ) { TopCategoryItemInput( title = Optional.present(current.landingConfig.topCategoriesSection.right.title), - media = current.landingConfig.topCategoriesSection.right.media?.let { + media = + current.landingConfig.topCategoriesSection.right.media?.let { Optional.present( MediaInput( keyName = it.keyName, url = it.url, alt = it.alt, - type = it.type - ) + type = it.type, + ), ) } ?: Optional.absent(), - ) else null, + ) + } else { + null + }, ).fold( - { postEvent(AdminConfigContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { data -> - val config = with(data.updateConfig) { - GetConfigQuery.GetConfig( - id = id, - updatedAt = updatedAt, - companyInfo = GetConfigQuery.CompanyInfo( - contactInfo = GetConfigQuery.ContactInfo( - companyName = companyInfo.contactInfo.companyName, - email = companyInfo.contactInfo.email, - phone = companyInfo.contactInfo.phone, - companyWebsite = companyInfo.contactInfo.companyWebsite, + val config = + with(data.updateConfig) { + GetConfigQuery.GetConfig( + id = id, + updatedAt = updatedAt, + companyInfo = + GetConfigQuery.CompanyInfo( + contactInfo = + GetConfigQuery.ContactInfo( + companyName = companyInfo.contactInfo.companyName, + email = companyInfo.contactInfo.email, + phone = companyInfo.contactInfo.phone, + companyWebsite = companyInfo.contactInfo.companyWebsite, + ), + openingTimes = + GetConfigQuery.OpeningTimes( + close = companyInfo.openingTimes.close, + dayFrom = companyInfo.openingTimes.dayFrom, + dayTo = companyInfo.openingTimes.dayTo, + open = companyInfo.openingTimes.open, + ), ), - openingTimes = GetConfigQuery.OpeningTimes( - close = companyInfo.openingTimes.close, - dayFrom = companyInfo.openingTimes.dayFrom, - dayTo = companyInfo.openingTimes.dayTo, - open = companyInfo.openingTimes.open + footerConfig = + GetConfigQuery.FooterConfig( + showStartChat = footerConfig.showStartChat, + showOpeningTimes = footerConfig.showOpeningTimes, + showCareer = footerConfig.showCareer, + showCyberSecurity = footerConfig.showCyberSecurity, + showPress = footerConfig.showPress, ), - ), - footerConfig = GetConfigQuery.FooterConfig( - showStartChat = footerConfig.showStartChat, - showOpeningTimes = footerConfig.showOpeningTimes, - showCareer = footerConfig.showCareer, - showCyberSecurity = footerConfig.showCyberSecurity, - showPress = footerConfig.showPress, - ), - landingConfig = GetConfigQuery.LandingConfig( - slideshowItems = landingConfig.slideshowItems.map { - GetConfigQuery.SlideshowItem( - id = it.id, - title = it.title, - description = it.description, - media = GetConfigQuery.Media( - keyName = it.media?.keyName ?: "", - url = it.media?.url ?: "", - alt = it.media?.alt ?: "", - type = it.media?.type ?: MediaType.Image, - ), - ) - }, - topCategoriesSection = GetConfigQuery.TopCategoriesSection( - left = with(landingConfig.topCategoriesSection.left) { - GetConfigQuery.Left( - title = title, - media = GetConfigQuery.Media1( - keyName = media?.keyName ?: "", - url = media?.url ?: "", - alt = media?.alt ?: "", - type = media?.type ?: MediaType.Image, - ) - ) - }, - middle = with(landingConfig.topCategoriesSection.left) { - GetConfigQuery.Middle( - title = title, - media = GetConfigQuery.Media2( - keyName = media?.keyName ?: "", - url = media?.url ?: "", - alt = media?.alt ?: "", - type = media?.type ?: MediaType.Image, - ) - ) - }, - right = with(landingConfig.topCategoriesSection.right) { - GetConfigQuery.Right( - title = title, - media = GetConfigQuery.Media3( - keyName = media?.keyName ?: "", - url = media?.url ?: "", - alt = media?.alt ?: "", - type = media?.type ?: MediaType.Image, - ) + landingConfig = + GetConfigQuery.LandingConfig( + slideshowItems = + landingConfig.slideshowItems.map { + GetConfigQuery.SlideshowItem( + id = it.id, + title = it.title, + description = it.description, + media = + GetConfigQuery.Media( + keyName = it.media?.keyName ?: "", + url = it.media?.url ?: "", + alt = it.media?.alt ?: "", + type = it.media?.type ?: MediaType.Image, + ), ) }, - ), - ), - catalogConfig = GetConfigQuery.CatalogConfig( - bannerConfig = with(catalogConfig.bannerConfig) { - GetConfigQuery.BannerConfig( - catalog = with(catalog) { - GetConfigQuery.Catalog( + topCategoriesSection = + GetConfigQuery.TopCategoriesSection( + left = + with(landingConfig.topCategoriesSection.left) { + GetConfigQuery.Left( title = title, - media = GetConfigQuery.Media4( + media = + GetConfigQuery.Media1( keyName = media?.keyName ?: "", url = media?.url ?: "", alt = media?.alt ?: "", type = media?.type ?: MediaType.Image, - ) + ), ) }, - sales = GetConfigQuery.Sales( - title = sales.title, - media = GetConfigQuery.Media5( - keyName = sales.media?.keyName ?: "", - url = sales.media?.url ?: "", - alt = sales.media?.alt ?: "", - type = sales.media?.type ?: MediaType.Image, - ), - ), - popular = with(popular) { - GetConfigQuery.Popular( + middle = + with(landingConfig.topCategoriesSection.left) { + GetConfigQuery.Middle( title = title, - media = GetConfigQuery.Media6( + media = + GetConfigQuery.Media2( keyName = media?.keyName ?: "", url = media?.url ?: "", alt = media?.alt ?: "", type = media?.type ?: MediaType.Image, - ) + ), ) }, - mens = GetConfigQuery.Mens( - title = mens.title, - media = GetConfigQuery.Media7( - keyName = mens.media?.keyName ?: "", - url = mens.media?.url ?: "", - alt = mens.media?.alt ?: "", - type = mens.media?.type ?: MediaType.Image, + right = + with(landingConfig.topCategoriesSection.right) { + GetConfigQuery.Right( + title = title, + media = + GetConfigQuery.Media3( + keyName = media?.keyName ?: "", + url = media?.url ?: "", + alt = media?.alt ?: "", + type = media?.type ?: MediaType.Image, + ), ) - ), - women = GetConfigQuery.Women( - title = women.title, - media = GetConfigQuery.Media8( - keyName = women.media?.keyName ?: "", - url = women.media?.url ?: "", - alt = women.media?.alt ?: "", - type = women.media?.type ?: MediaType.Image, + }, + ), + ), + catalogConfig = + GetConfigQuery.CatalogConfig( + bannerConfig = + with(catalogConfig.bannerConfig) { + GetConfigQuery.BannerConfig( + catalog = + with(catalog) { + GetConfigQuery.Catalog( + title = title, + media = + GetConfigQuery.Media4( + keyName = media?.keyName ?: "", + url = media?.url ?: "", + alt = media?.alt ?: "", + type = media?.type ?: MediaType.Image, + ), + ) + }, + sales = + GetConfigQuery.Sales( + title = sales.title, + media = + GetConfigQuery.Media5( + keyName = sales.media?.keyName ?: "", + url = sales.media?.url ?: "", + alt = sales.media?.alt ?: "", + type = sales.media?.type ?: MediaType.Image, + ), + ), + popular = + with(popular) { + GetConfigQuery.Popular( + title = title, + media = + GetConfigQuery.Media6( + keyName = media?.keyName ?: "", + url = media?.url ?: "", + alt = media?.alt ?: "", + type = media?.type ?: MediaType.Image, + ), + ) + }, + mens = + GetConfigQuery.Mens( + title = mens.title, + media = + GetConfigQuery.Media7( + keyName = mens.media?.keyName ?: "", + url = mens.media?.url ?: "", + alt = mens.media?.alt ?: "", + type = mens.media?.type ?: MediaType.Image, + ), + ), + women = + GetConfigQuery.Women( + title = women.title, + media = + GetConfigQuery.Media8( + keyName = women.media?.keyName ?: "", + url = women.media?.url ?: "", + alt = women.media?.alt ?: "", + type = women.media?.type ?: MediaType.Image, + ), ), - ), - kids = GetConfigQuery.Kids( - title = kids.title, - media = GetConfigQuery.Media9( - keyName = women.media?.keyName ?: "", - url = women.media?.url ?: "", - alt = women.media?.alt ?: "", - type = women.media?.type ?: MediaType.Image, + kids = + GetConfigQuery.Kids( + title = kids.title, + media = + GetConfigQuery.Media9( + keyName = women.media?.keyName ?: "", + url = women.media?.url ?: "", + alt = women.media?.alt ?: "", + type = women.media?.type ?: MediaType.Image, + ), ), - ), - ) - }, + ) + }, + ), ) - ) - } - postInput(AdminConfigContract.Inputs.SetOriginalConfig(config = config)) - postInput(AdminConfigContract.Inputs.SetCurrentConfig(config = config)) + } + postInput(Inputs.SetOriginalConfig(config = config)) + postInput(Inputs.SetCurrentConfig(config = config)) }, ) } @@ -690,10 +868,12 @@ internal class AdminConfigInputHandler : updateState { val isValidated = inputValidator.validateText(email) it.copy( - current = it.current.copy( - companyInfo = it.current.companyInfo.copy( - contactInfo = it.current.companyInfo.contactInfo.copy(email = email) - ) + current = + it.current.copy( + companyInfo = + it.current.companyInfo.copy( + contactInfo = it.current.companyInfo.contactInfo.copy(email = email), + ), ), emailError = if (!it.wasEdited) null else isValidated, ).wasEdited() @@ -713,5 +893,4 @@ internal class AdminConfigInputHandler : } } -private fun AdminConfigContract.State.wasEdited(): AdminConfigContract.State = - copy(wasEdited = current != original) +private fun State.wasEdited(): State = copy(wasEdited = current != original) diff --git a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigViewModel.kt b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigViewModel.kt index 1a50147c..01a52bb8 100644 --- a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigViewModel.kt +++ b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/AdminConfigViewModel.kt @@ -19,7 +19,8 @@ class AdminConfigViewModel( AdminConfigContract.Events, AdminConfigContract.State, >( - config = BallastViewModelConfiguration.Builder() + config = + BallastViewModelConfiguration.Builder() .apply { this += LoggingInterceptor() logger = { PrintlnLogger() } @@ -36,7 +37,8 @@ class AdminConfigViewModel( interceptorDispatcher = Dispatchers.Default, ) .build(), - eventHandler = AdminConfigEventHandler( + eventHandler = + AdminConfigEventHandler( onError = onError, ), coroutineScope = scope, diff --git a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/model/ImagePreview.kt b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/model/ImagePreview.kt index c1957d30..af7e6973 100644 --- a/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/model/ImagePreview.kt +++ b/apps/admin/feature/config/src/commonMain/kotlin/feature/admin/config/model/ImagePreview.kt @@ -8,26 +8,30 @@ data class ImagePreview( val alt: String, ) -fun GetConfigQuery.SlideshowItem.toPreviewImage() = ImagePreview( - id = id, - url = media?.url ?: "", - alt = media?.alt ?: "", -) +fun GetConfigQuery.SlideshowItem.toPreviewImage() = + ImagePreview( + id = id, + url = media?.url ?: "", + alt = media?.alt ?: "", + ) -fun GetConfigQuery.Left.toPreviewImage() = ImagePreview( - id = "", - url = media?.url ?: "", - alt = media?.alt ?: "", -) +fun GetConfigQuery.Left.toPreviewImage() = + ImagePreview( + id = "", + url = media?.url ?: "", + alt = media?.alt ?: "", + ) -fun GetConfigQuery.Middle.toPreviewImage() = ImagePreview( - id = "", - url = media?.url ?: "", - alt = media?.alt ?: "", -) +fun GetConfigQuery.Middle.toPreviewImage() = + ImagePreview( + id = "", + url = media?.url ?: "", + alt = media?.alt ?: "", + ) -fun GetConfigQuery.Right.toPreviewImage() = ImagePreview( - id = "", - url = media?.url ?: "", - alt = media?.alt ?: "", -) +fun GetConfigQuery.Right.toPreviewImage() = + ImagePreview( + id = "", + url = media?.url ?: "", + alt = media?.alt ?: "", + ) diff --git a/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardContract.kt b/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardContract.kt index d4e2ea92..806507ae 100644 --- a/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardContract.kt +++ b/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardContract.kt @@ -6,20 +6,19 @@ import data.GetStatsQuery object AdminDashboardContract { data class State( val isStatsLoading: Boolean = true, - val stats: GetStatsQuery.GetStats = GetStatsQuery.GetStats( - totalCustomers = 0, - totalProducts = 0, - totalOrders = 0, - totalCategories = 0, - totalTags = 0, - ), - + val stats: GetStatsQuery.GetStats = + GetStatsQuery.GetStats( + totalCustomers = 0, + totalProducts = 0, + totalOrders = 0, + totalCategories = 0, + totalTags = 0, + ), // Generator val productsToGenerate: Int = 100, val customersToGenerate: Int = 0, val ordersToGenerate: Int = 0, val isGenerating: Boolean = false, - // Deleter val deleteProducts: Boolean = false, val deleteCustomers: Boolean = false, @@ -29,20 +28,31 @@ object AdminDashboardContract { sealed interface Inputs { data object Init : Inputs + data object GetStats : Inputs data object OnGenerateClicked : Inputs + data object OnDeleteGeneratedClicked : Inputs + data object OnDeleteProductsClicked : Inputs + data object OnDeleteCustomersClicked : Inputs + data object OnDeleteOrdersClicked : Inputs data class SetIsStatsLoading(val isLoading: Boolean) : Inputs + data class SetIsGenerating(val isGenerating: Boolean) : Inputs + data class SetIsDeleting(val isDeleting: Boolean) : Inputs + data class SetStats(val stats: GetStatsQuery.GetStats) : Inputs + data class SetProductsToGenerate(val products: String) : Inputs + data class SetCustomersToGenerate(val customers: String) : Inputs + data class SetOrdersToGenerate(val orders: String) : Inputs } diff --git a/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardEventHandler.kt b/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardEventHandler.kt index 5e565296..0e73ca34 100644 --- a/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardEventHandler.kt +++ b/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardEventHandler.kt @@ -2,13 +2,15 @@ package feature.admin.dashboard import com.copperleaf.ballast.EventHandler import com.copperleaf.ballast.EventHandlerScope +import feature.admin.dashboard.AdminDashboardContract.Events +import feature.admin.dashboard.AdminDashboardContract.Inputs +import feature.admin.dashboard.AdminDashboardContract.State internal class AdminDashboardEventHandler( private val onError: suspend (String) -> Unit, -) : EventHandler { - override suspend fun EventHandlerScope.handleEvent( - event: AdminDashboardContract.Events, - ) = when (event) { - is AdminDashboardContract.Events.OnError -> onError(event.message) - } +) : EventHandler { + override suspend fun EventHandlerScope.handleEvent(event: Events) = + when (event) { + is Events.OnError -> onError(event.message) + } } diff --git a/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardInputHandler.kt b/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardInputHandler.kt index f0f5798d..817e1743 100644 --- a/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardInputHandler.kt +++ b/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardInputHandler.kt @@ -6,58 +6,58 @@ import core.mapToUiMessage import data.service.AdminService import data.type.DeleteGenerateDataInput import data.type.GenerateDataInput +import feature.admin.dashboard.AdminDashboardContract.Events +import feature.admin.dashboard.AdminDashboardContract.Inputs +import feature.admin.dashboard.AdminDashboardContract.State import org.koin.core.component.KoinComponent import org.koin.core.component.inject -private typealias InputScope = InputHandlerScope - -internal class AdminDashboardInputHandler : - KoinComponent, - InputHandler { +private typealias InputScope = InputHandlerScope +internal class AdminDashboardInputHandler : KoinComponent, InputHandler { private val adminService: AdminService by inject() - override suspend fun InputHandlerScope.handleInput( - input: AdminDashboardContract.Inputs, - ) = when (input) { - AdminDashboardContract.Inputs.Init -> handleInit() - AdminDashboardContract.Inputs.GetStats -> handleGetStats() - - AdminDashboardContract.Inputs.OnGenerateClicked -> handleGenerateClicked() - AdminDashboardContract.Inputs.OnDeleteGeneratedClicked -> handleDeleteGenerateClicked() - - is AdminDashboardContract.Inputs.SetIsStatsLoading -> updateState { it.copy(isStatsLoading = input.isLoading) } - is AdminDashboardContract.Inputs.SetIsGenerating -> updateState { it.copy(isGenerating = input.isGenerating) } - is AdminDashboardContract.Inputs.SetStats -> updateState { it.copy(stats = input.stats) } - is AdminDashboardContract.Inputs.SetProductsToGenerate -> updateState { it.copy(productsToGenerate = input.products.toInt()) } - is AdminDashboardContract.Inputs.SetCustomersToGenerate -> updateState { it.copy(customersToGenerate = input.customers.toInt()) } - is AdminDashboardContract.Inputs.SetOrdersToGenerate -> updateState { it.copy(ordersToGenerate = input.orders.toInt()) } - AdminDashboardContract.Inputs.OnDeleteCustomersClicked -> updateState { it.copy(deleteCustomers = !it.deleteCustomers) } - AdminDashboardContract.Inputs.OnDeleteOrdersClicked -> updateState { it.copy(deleteOrders = !it.deleteOrders) } - AdminDashboardContract.Inputs.OnDeleteProductsClicked -> updateState { it.copy(deleteProducts = !it.deleteProducts) } - is AdminDashboardContract.Inputs.SetIsDeleting -> updateState { it.copy(isDeleting = input.isDeleting) } - } + override suspend fun InputHandlerScope.handleInput(input: Inputs) = + when (input) { + Inputs.Init -> handleInit() + Inputs.GetStats -> handleGetStats() + + Inputs.OnGenerateClicked -> handleGenerateClicked() + Inputs.OnDeleteGeneratedClicked -> handleDeleteGenerateClicked() + + is Inputs.SetIsStatsLoading -> updateState { it.copy(isStatsLoading = input.isLoading) } + is Inputs.SetIsGenerating -> updateState { it.copy(isGenerating = input.isGenerating) } + is Inputs.SetStats -> updateState { it.copy(stats = input.stats) } + is Inputs.SetProductsToGenerate -> updateState { it.copy(productsToGenerate = input.products.toInt()) } + is Inputs.SetCustomersToGenerate -> updateState { it.copy(customersToGenerate = input.customers.toInt()) } + is Inputs.SetOrdersToGenerate -> updateState { it.copy(ordersToGenerate = input.orders.toInt()) } + Inputs.OnDeleteCustomersClicked -> updateState { it.copy(deleteCustomers = !it.deleteCustomers) } + Inputs.OnDeleteOrdersClicked -> updateState { it.copy(deleteOrders = !it.deleteOrders) } + Inputs.OnDeleteProductsClicked -> updateState { it.copy(deleteProducts = !it.deleteProducts) } + is Inputs.SetIsDeleting -> updateState { it.copy(isDeleting = input.isDeleting) } + } private suspend fun InputScope.handleDeleteGenerateClicked() { val state = getCurrentState() if (!state.deleteProducts && !state.deleteCustomers && !state.deleteOrders) { - postEvent(AdminDashboardContract.Events.OnError("At least one field must be selected")) + postEvent(Events.OnError("At least one field must be selected")) return } sideJob("handleDeleteGenerateClicked") { - postInput(AdminDashboardContract.Inputs.SetIsDeleting(true)) - val input = DeleteGenerateDataInput( - products = state.deleteProducts, - users = state.deleteCustomers, - orders = state.deleteOrders, - ) + postInput(Inputs.SetIsDeleting(true)) + val input = + DeleteGenerateDataInput( + products = state.deleteProducts, + users = state.deleteCustomers, + orders = state.deleteOrders, + ) adminService.deleteGeneratedData(input).fold( - { postEvent(AdminDashboardContract.Events.OnError(it.mapToUiMessage())) }, - { postInput(AdminDashboardContract.Inputs.GetStats) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, + { postInput(Inputs.GetStats) }, ) - postInput(AdminDashboardContract.Inputs.SetIsDeleting(false)) + postInput(Inputs.SetIsDeleting(false)) } } @@ -69,39 +69,40 @@ internal class AdminDashboardInputHandler : val ordersIsNumber = state.ordersToGenerate.toString().toIntOrNull() != null if (!productsIsNumber || !customersIsNumber || !ordersIsNumber) { - postEvent(AdminDashboardContract.Events.OnError("All fields must be numbers")) + postEvent(Events.OnError("All fields must be numbers")) return } sideJob("handleGenerateClicked") { - postInput(AdminDashboardContract.Inputs.SetIsGenerating(true)) - val input = GenerateDataInput( - products = state.productsToGenerate, - users = state.customersToGenerate, - orders = state.ordersToGenerate, - ) + postInput(Inputs.SetIsGenerating(true)) + val input = + GenerateDataInput( + products = state.productsToGenerate, + users = state.customersToGenerate, + orders = state.ordersToGenerate, + ) adminService.generateData(input).fold( - { postEvent(AdminDashboardContract.Events.OnError(it.mapToUiMessage())) }, - { postInput(AdminDashboardContract.Inputs.GetStats) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, + { postInput(Inputs.GetStats) }, ) - postInput(AdminDashboardContract.Inputs.SetIsGenerating(false)) + postInput(Inputs.SetIsGenerating(false)) } } private suspend fun InputScope.handleInit() { sideJob("handleInit") { - postInput(AdminDashboardContract.Inputs.GetStats) + postInput(Inputs.GetStats) } } private suspend fun InputScope.handleGetStats() { sideJob("handleGetStats") { - postInput(AdminDashboardContract.Inputs.SetIsStatsLoading(true)) + postInput(Inputs.SetIsStatsLoading(true)) adminService.getStats().fold( - { postEvent(AdminDashboardContract.Events.OnError(it.mapToUiMessage())) }, - { postInput(AdminDashboardContract.Inputs.SetStats(it.getStats)) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, + { postInput(Inputs.SetStats(it.getStats)) }, ) - postInput(AdminDashboardContract.Inputs.SetIsStatsLoading(false)) + postInput(Inputs.SetIsStatsLoading(false)) } } } diff --git a/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardViewModel.kt b/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardViewModel.kt index fcdda4f1..eda6b559 100644 --- a/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardViewModel.kt +++ b/apps/admin/feature/dashboard/src/commonMain/kotlin/feature/admin/dashboard/AdminDashboardViewModel.kt @@ -17,7 +17,8 @@ class AdminDashboardViewModel( AdminDashboardContract.Events, AdminDashboardContract.State, >( - config = BallastViewModelConfiguration.Builder() + config = + BallastViewModelConfiguration.Builder() .apply { // this += LoggingInterceptor() logger = { PrintlnLogger() } @@ -34,7 +35,8 @@ class AdminDashboardViewModel( interceptorDispatcher = Dispatchers.Default, ) .build(), - eventHandler = AdminDashboardEventHandler( + eventHandler = + AdminDashboardEventHandler( onError = onError, ), coroutineScope = scope, diff --git a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugContract.kt b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugContract.kt index 872e2ce3..d60e5f18 100644 --- a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugContract.kt +++ b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugContract.kt @@ -13,11 +13,12 @@ private const val APPLE_LONGITUDE = -122.009102 private const val GOOGLE_LATITUDE = 37.422160 private const val GOOGLE_LONGITUDE = -122.084270 -val defaultLocation = when (currentPlatform) { - Platform.IOS -> LatLng(APPLE_LATITUDE, APPLE_LONGITUDE) - Platform.ANDROID -> LatLng(GOOGLE_LATITUDE, GOOGLE_LONGITUDE) - else -> LatLng(0.0, 0.0) -} +val defaultLocation = + when (currentPlatform) { + Platform.IOS -> LatLng(APPLE_LATITUDE, APPLE_LONGITUDE) + Platform.ANDROID -> LatLng(GOOGLE_LATITUDE, GOOGLE_LONGITUDE) + else -> LatLng(0.0, 0.0) + } object DebugContract { data class State( @@ -38,11 +39,16 @@ object DebugContract { sealed interface Inputs { data class SetIsLoading(val isLoading: Boolean) : Inputs + data object Init : Inputs + data object OnClearCacheClick : Inputs + data class SetGenerateUsersTarget(val target: String) : Inputs + data class SetUndoCount(val count: String) : Inputs -// data object GenerateUsers : Inputs + + // data object GenerateUsers : Inputs // data object DeleteAllUsers : Inputs // data object DeleteGeneratedUsers : Inputs data class SetIsPremium(val isPremium: Boolean) : Inputs @@ -51,36 +57,57 @@ object DebugContract { // Location data object ObserveLocationPermission : Inputs + data class SetLocationPermissionStatus(val status: PermissionStatus) : Inputs + data object RequestLocationPermission : Inputs + data object GetLatLng : Inputs + data class SetLatLng(val latLng: LatLng, val showMap: Boolean = true) : Inputs + data object GetLocation : Inputs + data class SetLocation(val location: Location) : Inputs // Notification data object ObserveNotificationPermission : Inputs + data object RequestNotificationPermission : Inputs + data class SetNotificationPermission(val status: PermissionStatus) : Inputs + data class SetNotificationText(val text: String) : Inputs + data object SendNotificationNow : Inputs // Pictures data object ObserveCameraPermission : Inputs + data object ObserveGalleryPermission : Inputs + data object RequestCameraPermission : Inputs + data class SetCameraPermissionStatus(val status: PermissionStatus) : Inputs + data object RequestGalleryPermission : Inputs + data class SetGalleryPermission(val status: PermissionStatus) : Inputs + data object PickImageFromGallery : Inputs + data object PickImageFromCamera : Inputs + data class SetPickedBitmaps(val bitmaps: List) : Inputs + data object ClearPickedBitmaps : Inputs } sealed interface Events { data class OnError(val message: String) : Events + data class OnDeleteUsersSuccess(val message: String) : Events + data class OnGenerateUsersSuccess(val message: String) : Events } } diff --git a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugInputHandler.kt b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugInputHandler.kt index c26da550..ab42e277 100644 --- a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugInputHandler.kt +++ b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugInputHandler.kt @@ -24,7 +24,8 @@ import org.koin.core.component.inject private typealias DebugInputScope = InputHandlerScope -internal class DebugInputHandler : KoinComponent, +internal class DebugInputHandler : + KoinComponent, InputHandler { private val debugService: DebugService by inject() private val locationService: LocationService by inject() @@ -146,11 +147,12 @@ internal class DebugInputHandler : KoinComponent, private suspend fun DebugInputScope.sendNotificationNow() { val state = getCurrentState() sideJob("SendNotificationNow") { - val notificationType = NotificationType.Immediate( - title = state.notificationText, - body = state.notificationText, - soundType = SoundType.CRITICAL, - ) + val notificationType = + NotificationType.Immediate( + title = state.notificationText, + body = state.notificationText, + soundType = SoundType.CRITICAL, + ) notificationService.schedule(notificationType) } } diff --git a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugModule.kt b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugModule.kt index e0c35160..b73fd30b 100644 --- a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugModule.kt +++ b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugModule.kt @@ -5,10 +5,11 @@ import location.locationModule import notification.notificationModule import org.koin.dsl.module -val debugModule = module { - includes( - locationModule, - notificationModule, - picturesModule, - ) -} +val debugModule = + module { + includes( + locationModule, + notificationModule, + picturesModule, + ) + } diff --git a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugViewModel.kt b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugViewModel.kt index b147e52d..7bee58c2 100644 --- a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugViewModel.kt +++ b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/DebugViewModel.kt @@ -19,7 +19,8 @@ class DebugViewModel( DebugContract.Events, DebugContract.State, >( - config = BallastViewModelConfiguration.Builder() + config = + BallastViewModelConfiguration.Builder() .apply { // this += LoggingInterceptor() logger = { PrintlnLogger() } @@ -36,7 +37,8 @@ class DebugViewModel( interceptorDispatcher = Dispatchers.Default, ) .build(), - eventHandler = DebugEventHandler( + eventHandler = + DebugEventHandler( onError = onError, onGenerateUsersSuccess = onGenerateUsersSuccess, onDeleteUsersSuccess = onDeleteUsersSuccess, diff --git a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/model/Location.kt b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/model/Location.kt index a9273dd0..9e5b9125 100644 --- a/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/model/Location.kt +++ b/apps/admin/feature/debug/src/commonMain/kotlin/feature/debug/model/Location.kt @@ -16,15 +16,17 @@ data class Location( val country: String, ) -internal fun CoreLatLng.toLatLng() = LatLng( - latitude = latitude, - longitude = longitude, -) +internal fun CoreLatLng.toLatLng() = + LatLng( + latitude = latitude, + longitude = longitude, + ) -internal fun CoreLocation.toLocation() = Location( - latitude = latLng.latitude, - longitude = latLng.longitude, - city = city, - state = state, - country = country, -) +internal fun CoreLocation.toLocation() = + Location( + latitude = latLng.latitude, + longitude = latLng.longitude, + city = city, + state = state, + country = country, + ) diff --git a/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListContract.kt b/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListContract.kt index 6f2305b2..7fb80831 100644 --- a/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListContract.kt +++ b/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListContract.kt @@ -8,23 +8,20 @@ import feature.admin.list.AdminListContract.DataType object AdminListContract { data class State( val dataType: DataType, - val isLoading: Boolean = false, - val items: List = emptyList(), val showList: Boolean = false, val showNoItems: Boolean = false, - val searchValue: String = "", - val sortBy: String = when (dataType) { - DataType.Customer -> CustomerSlot.CreatedAt.name - DataType.PRODUCT -> ProductSlot.CreatedAt.name - DataType.ORDER -> OrderSlot.CreatedAt.name - DataType.CATEGORY -> CategorySlot.CreatedAt.name - DataType.TAG -> TagSlot.CreatedAt.name - }, + val sortBy: String = + when (dataType) { + DataType.Customer -> CustomerSlot.CreatedAt.name + DataType.PRODUCT -> ProductSlot.CreatedAt.name + DataType.ORDER -> OrderSlot.CreatedAt.name + DataType.CATEGORY -> CategorySlot.CreatedAt.name + DataType.TAG -> TagSlot.CreatedAt.name + }, val sortDirection: SortDirection = SortDirection.DESC, - val info: PageInfo = PageInfo(0, 0, null, null), val pagesNumbers: List = emptyList(), val perPageOptions: List = listOf(10, 25, 50, 100), @@ -34,7 +31,6 @@ object AdminListContract { ) sealed interface Inputs { - data object SendSearch : Inputs sealed interface Get : Inputs { @@ -43,25 +39,37 @@ object AdminListContract { sealed interface Set : Inputs { data class Loading(val isLoading: Boolean) : Set + data class Items(val items: List) : Set + data class PerPage(val perPage: Int) : Set + data class Info(val info: PageInfo) : Set + data class Search(val search: String) : Set + data class SortBy(val sortBy: String) : Set + data class SortDirection(val sortDirection: data.type.SortDirection) : Set } sealed interface OnChange : Inputs { data class Search(val search: String) : OnChange + data class PerPage(val perPage: Int) : OnChange } sealed interface Click : Inputs { data object Create : Click + data class Item(val id: String) : Click + data class Page(val page: Int) : Click + data object PreviousPage : Click + data object NextPage : Click + data class Slot(val slotName: String) : Click } } @@ -71,6 +79,7 @@ object AdminListContract { sealed interface GoTo : Events { data object Create : Events + data class Detail(val id: String) : Events } } @@ -84,59 +93,85 @@ object AdminListContract { } enum class CustomerSlot { - CreatedAt, Email, FullName, Orders; + CreatedAt, + Email, + FullName, + Orders, + ; - fun asString(): String = when (this) { - CreatedAt -> getString(Strings.CreatedAt) - Email -> getString(Strings.Email) - FullName -> getString(Strings.FirstName) - Orders -> getString(Strings.Orders) - } + fun asString(): String = + when (this) { + CreatedAt -> getString(Strings.CreatedAt) + Email -> getString(Strings.Email) + FullName -> getString(Strings.FirstName) + Orders -> getString(Strings.Orders) + } } enum class ProductSlot { - CreatedAt, Image, Name, Price, Sold; - - fun asString() = when (this) { - CreatedAt -> getString(Strings.CreatedAt) - Image -> getString(Strings.Image) - Name -> getString(Strings.Name) - Price -> getString(Strings.Price) - Sold -> getString(Strings.Sold) - } + CreatedAt, + Image, + Name, + Price, + Sold, + ; + + fun asString() = + when (this) { + CreatedAt -> getString(Strings.CreatedAt) + Image -> getString(Strings.Image) + Name -> getString(Strings.Name) + Price -> getString(Strings.Price) + Sold -> getString(Strings.Sold) + } } enum class OrderSlot { - CreatedAt, Name, TotalPrice, Status; + CreatedAt, + Name, + TotalPrice, + Status, + ; - fun asString() = when (this) { - CreatedAt -> getString(Strings.CreatedAt) - Name -> getString(Strings.Name) - TotalPrice -> getString(Strings.Price) - Status -> getString(Strings.StockStatus) - } + fun asString() = + when (this) { + CreatedAt -> getString(Strings.CreatedAt) + Name -> getString(Strings.Name) + TotalPrice -> getString(Strings.Price) + Status -> getString(Strings.StockStatus) + } } enum class CategorySlot { - CreatedAt, Image, Name, Display, InProducts; - - fun asString() = when (this) { - CreatedAt -> getString(Strings.CreatedAt) - Image -> getString(Strings.Image) - Name -> getString(Strings.Name) - Display -> getString(Strings.Display) - InProducts -> getString(Strings.InProducts) - } + CreatedAt, + Image, + Name, + Display, + InProducts, + ; + + fun asString() = + when (this) { + CreatedAt -> getString(Strings.CreatedAt) + Image -> getString(Strings.Image) + Name -> getString(Strings.Name) + Display -> getString(Strings.Display) + InProducts -> getString(Strings.InProducts) + } } enum class TagSlot { - CreatedAt, Name, InProducts; + CreatedAt, + Name, + InProducts, + ; - fun asString() = when (this) { - CreatedAt -> getString(Strings.CreatedAt) - Name -> getString(Strings.Name) - InProducts -> getString(Strings.InProducts) - } + fun asString() = + when (this) { + CreatedAt -> getString(Strings.CreatedAt) + Name -> getString(Strings.Name) + InProducts -> getString(Strings.InProducts) + } } } @@ -145,12 +180,10 @@ data class ListItem( * Id of the item. */ val id: String, - /** * Usually date: millisToDate(tag.createdAt.toLong()) */ val slot1: String, - /** * Usually email, name, or other short string */ @@ -168,50 +201,57 @@ data class PageInfo( val next: Int?, ) -fun adminListStrings(dataType: DataType): AdminListStrings = AdminListStrings( - title = when (dataType) { - DataType.Customer -> getString(Strings.Customers) - DataType.PRODUCT -> getString(Strings.Products) - DataType.ORDER -> getString(Strings.Orders) - DataType.CATEGORY -> getString(Strings.Categories) - DataType.TAG -> getString(Strings.Tags) - }, - slot1Text = when (dataType) { - DataType.Customer -> getString(Strings.CreatedAt) - DataType.PRODUCT -> getString(Strings.CreatedAt) - DataType.ORDER -> getString(Strings.CreatedAt) - DataType.CATEGORY -> getString(Strings.CreatedAt) - DataType.TAG -> getString(Strings.CreatedAt) - }, - slot2Text = when (dataType) { - DataType.Customer -> getString(Strings.Email) - DataType.PRODUCT -> getString(Strings.Name) - DataType.ORDER -> getString(Strings.Name) - DataType.CATEGORY -> getString(Strings.Name) - DataType.TAG -> getString(Strings.Name) - }, - slot3Text = when (dataType) { - DataType.Customer -> getString(Strings.FirstName) - DataType.PRODUCT -> getString(Strings.Price) - DataType.ORDER -> getString(Strings.Description) - DataType.CATEGORY -> getString(Strings.Description) - DataType.TAG -> getString(Strings.InProducts) - }, - slot4Text = when (dataType) { - DataType.Customer -> getString(Strings.Orders) - DataType.PRODUCT -> getString(Strings.Sold) - DataType.ORDER -> getString(Strings.StockStatus) - DataType.CATEGORY -> getString(Strings.Display) - DataType.TAG -> "" - }, - slot5Text = when (dataType) { - DataType.Customer -> "" - DataType.PRODUCT -> getString(Strings.CatalogVisibility) - DataType.ORDER -> "" - DataType.CATEGORY -> getString(Strings.InProducts) - DataType.TAG -> "" - }, -) +fun adminListStrings(dataType: DataType): AdminListStrings = + AdminListStrings( + title = + when (dataType) { + DataType.Customer -> getString(Strings.Customers) + DataType.PRODUCT -> getString(Strings.Products) + DataType.ORDER -> getString(Strings.Orders) + DataType.CATEGORY -> getString(Strings.Categories) + DataType.TAG -> getString(Strings.Tags) + }, + slot1Text = + when (dataType) { + DataType.Customer -> getString(Strings.CreatedAt) + DataType.PRODUCT -> getString(Strings.CreatedAt) + DataType.ORDER -> getString(Strings.CreatedAt) + DataType.CATEGORY -> getString(Strings.CreatedAt) + DataType.TAG -> getString(Strings.CreatedAt) + }, + slot2Text = + when (dataType) { + DataType.Customer -> getString(Strings.Email) + DataType.PRODUCT -> getString(Strings.Name) + DataType.ORDER -> getString(Strings.Name) + DataType.CATEGORY -> getString(Strings.Name) + DataType.TAG -> getString(Strings.Name) + }, + slot3Text = + when (dataType) { + DataType.Customer -> getString(Strings.FirstName) + DataType.PRODUCT -> getString(Strings.Price) + DataType.ORDER -> getString(Strings.Description) + DataType.CATEGORY -> getString(Strings.Description) + DataType.TAG -> getString(Strings.InProducts) + }, + slot4Text = + when (dataType) { + DataType.Customer -> getString(Strings.Orders) + DataType.PRODUCT -> getString(Strings.Sold) + DataType.ORDER -> getString(Strings.StockStatus) + DataType.CATEGORY -> getString(Strings.Display) + DataType.TAG -> "" + }, + slot5Text = + when (dataType) { + DataType.Customer -> "" + DataType.PRODUCT -> getString(Strings.CatalogVisibility) + DataType.ORDER -> "" + DataType.CATEGORY -> getString(Strings.InProducts) + DataType.TAG -> "" + }, + ) data class AdminListStrings( val title: String, diff --git a/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListEventHandler.kt b/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListEventHandler.kt index 32f50e28..4fd46b0c 100644 --- a/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListEventHandler.kt +++ b/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListEventHandler.kt @@ -2,17 +2,19 @@ package feature.admin.list import com.copperleaf.ballast.EventHandler import com.copperleaf.ballast.EventHandlerScope +import feature.admin.list.AdminListContract.Events +import feature.admin.list.AdminListContract.Inputs +import feature.admin.list.AdminListContract.State internal class AdminListEventHandler( private val onError: suspend (String) -> Unit, private val goToCreate: () -> Unit, private val goToDetail: (String) -> Unit, -) : EventHandler { - override suspend fun EventHandlerScope.handleEvent( - event: AdminListContract.Events, - ) = when (event) { - is AdminListContract.Events.OnError -> onError(event.message) - is AdminListContract.Events.GoTo.Create -> goToCreate() - is AdminListContract.Events.GoTo.Detail -> goToDetail(event.id) - } +) : EventHandler { + override suspend fun EventHandlerScope.handleEvent(event: Events) = + when (event) { + is Events.OnError -> onError(event.message) + is Events.GoTo.Create -> goToCreate() + is Events.GoTo.Detail -> goToDetail(event.id) + } } diff --git a/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListInputHandler.kt b/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListInputHandler.kt index f43ac736..ff1e03fd 100644 --- a/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListInputHandler.kt +++ b/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListInputHandler.kt @@ -10,87 +10,91 @@ import data.service.ProductService import data.service.TagService import data.service.UserService import data.type.SortDirection +import feature.admin.list.AdminListContract.Events +import feature.admin.list.AdminListContract.Inputs +import feature.admin.list.AdminListContract.State import org.koin.core.component.KoinComponent import org.koin.core.component.inject -private typealias InputScope = InputHandlerScope - -internal class AdminListInputHandler : - KoinComponent, - InputHandler { +private typealias InputScope = InputHandlerScope +internal class AdminListInputHandler : KoinComponent, InputHandler { private val userService: UserService by inject() private val productService: ProductService by inject() private val orderService: OrderService by inject() private val categoryService: CategoryService by inject() private val tagService: TagService by inject() - override suspend fun InputScope.handleInput(input: AdminListContract.Inputs) = when (input) { - is AdminListContract.Inputs.Get.Page -> handleGetPage(input.page) + override suspend fun InputScope.handleInput(input: Inputs) = + when (input) { + is Inputs.Get.Page -> handleGetPage(input.page) + + is Inputs.Set -> + when (input) { + is Inputs.Set.Loading -> updateState { it.copy(isLoading = input.isLoading) } + is Inputs.Set.PerPage -> updateState { it.copy(perPage = input.perPage) } + is Inputs.Set.Items -> + updateState { + it.copy( + items = input.items, + showList = input.items.isNotEmpty(), + showNoItems = input.items.isEmpty(), + ) + } + + is Inputs.Set.Info -> + updateState { + it.copy( + info = input.info, + pagesNumbers = listOfNotNull(input.info.prev, input.info.count, input.info.next), + showPrevious = input.info.prev != null, + showNext = input.info.next != null, + ) + } - is AdminListContract.Inputs.Set -> when (input) { - is AdminListContract.Inputs.Set.Loading -> updateState { it.copy(isLoading = input.isLoading) } - is AdminListContract.Inputs.Set.PerPage -> updateState { it.copy(perPage = input.perPage) } - is AdminListContract.Inputs.Set.Items -> - updateState { - it.copy( - items = input.items, - showList = input.items.isNotEmpty(), - showNoItems = input.items.isEmpty() - ) + is Inputs.Set.Search -> updateState { it.copy(searchValue = input.search) } + is Inputs.Set.SortBy -> updateState { it.copy(sortBy = input.sortBy) } + is Inputs.Set.SortDirection -> updateState { it.copy(sortDirection = input.sortDirection) } } - is AdminListContract.Inputs.Set.Info -> - updateState { - it.copy( - info = input.info, - pagesNumbers = listOfNotNull(input.info.prev, input.info.count, input.info.next), - showPrevious = input.info.prev != null, - showNext = input.info.next != null - ) + is Inputs.OnChange -> + when (input) { + is Inputs.OnChange.Search -> updateState { it.copy(searchValue = input.search) } + is Inputs.OnChange.PerPage -> handleOnChangePerPage(input.perPage) } - is AdminListContract.Inputs.Set.Search -> updateState { it.copy(searchValue = input.search) } - is AdminListContract.Inputs.Set.SortBy -> updateState { it.copy(sortBy = input.sortBy) } - is AdminListContract.Inputs.Set.SortDirection -> updateState { it.copy(sortDirection = input.sortDirection) } - } - - is AdminListContract.Inputs.OnChange -> when (input) { - is AdminListContract.Inputs.OnChange.Search -> updateState { it.copy(searchValue = input.search) } - is AdminListContract.Inputs.OnChange.PerPage -> handleOnChangePerPage(input.perPage) - } + is Inputs.Click -> + when (input) { + Inputs.Click.Create -> + postEvent(Events.GoTo.Create) - is AdminListContract.Inputs.Click -> when (input) { - AdminListContract.Inputs.Click.Create -> - postEvent(AdminListContract.Events.GoTo.Create) + is Inputs.Click.Item -> + postEvent(Events.GoTo.Detail(input.id)) - is AdminListContract.Inputs.Click.Item -> - postEvent(AdminListContract.Events.GoTo.Detail(input.id)) + is Inputs.Click.Page -> handleGetPage(input.page) + Inputs.Click.NextPage -> + getCurrentState().info.next?.let { handleGetPage(it) } ?: noOp() - is AdminListContract.Inputs.Click.Page -> handleGetPage(input.page) - AdminListContract.Inputs.Click.NextPage -> - getCurrentState().info.next?.let { handleGetPage(it) } ?: noOp() + Inputs.Click.PreviousPage -> + getCurrentState().info.prev?.let { handleGetPage(it) } ?: noOp() - AdminListContract.Inputs.Click.PreviousPage -> - getCurrentState().info.prev?.let { handleGetPage(it) } ?: noOp() + is Inputs.Click.Slot -> handleClickSlot(input.slotName) + } - is AdminListContract.Inputs.Click.Slot -> handleClickSlot(input.slotName) + Inputs.SendSearch -> handleGetPage(0) } - AdminListContract.Inputs.SendSearch -> handleGetPage(0) - } - private suspend fun InputScope.handleClickSlot(slotName: String) { val state = getCurrentState() sideJob("handleClickSlot") { if (state.sortBy == slotName) { when (state.sortDirection) { SortDirection.ASC -> { - postInput(AdminListContract.Inputs.Set.SortDirection(SortDirection.DESC)) + postInput(Inputs.Set.SortDirection(SortDirection.DESC)) } SortDirection.DESC -> { - postInput(AdminListContract.Inputs.Set.SortDirection(SortDirection.ASC)) + postInput(Inputs.Set.SortDirection(SortDirection.ASC)) } SortDirection.UNKNOWN__ -> { @@ -98,27 +102,26 @@ internal class AdminListInputHandler : } } } else { - postInput(AdminListContract.Inputs.Set.SortDirection(SortDirection.DESC)) - postInput(AdminListContract.Inputs.Set.SortBy(slotName)) + postInput(Inputs.Set.SortDirection(SortDirection.DESC)) + postInput(Inputs.Set.SortBy(slotName)) } - postInput(AdminListContract.Inputs.Get.Page(0)) + postInput(Inputs.Get.Page(0)) } } private suspend fun InputScope.handleOnChangePerPage(perPage: Int) { sideJob("handleOnChangePerPage") { - postInput(AdminListContract.Inputs.Set.PerPage(perPage)) - postInput(AdminListContract.Inputs.Get.Page(0)) + postInput(Inputs.Set.PerPage(perPage)) + postInput(Inputs.Get.Page(0)) } } private suspend fun InputScope.handleGetPage(page: Int) { val state = getCurrentState() sideJob("handleGetPage") { - println("DEBUG Getting page: $page") - postInput(AdminListContract.Inputs.Set.Loading(true)) + postInput(Inputs.Set.Loading(true)) when (state.dataType) { AdminListContract.DataType.Customer -> { userService.getAsPage( @@ -126,30 +129,32 @@ internal class AdminListInputHandler : size = state.perPage, query = state.searchValue.ifBlank { null }, sortBy = state.sortBy, - sortDirection = state.sortDirection + sortDirection = state.sortDirection, ).fold( - { postEvent(AdminListContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { - val items = it.getAllUsersAsPage.users.map { user -> - ListItem( - id = user.id, - slot1 = millisToDate(user.createdAt.toLong()), - slot2 = user.email, - slot3 = user.name, - slot4 = user.totalOrders.toString(), - slot5 = null, - slot6 = null, + val items = + it.getAllUsersAsPage.users.map { user -> + ListItem( + id = user.id, + slot1 = millisToDate(user.createdAt.toLong()), + slot2 = user.email, + slot3 = user.name, + slot4 = user.totalOrders.toString(), + slot5 = null, + slot6 = null, + ) + } + postInput(Inputs.Set.Items(items)) + + val info = + PageInfo( + count = it.getAllUsersAsPage.info.count, + pages = it.getAllUsersAsPage.info.pages, + prev = it.getAllUsersAsPage.info.prev, + next = it.getAllUsersAsPage.info.next, ) - } - postInput(AdminListContract.Inputs.Set.Items(items)) - - val info = PageInfo( - count = it.getAllUsersAsPage.info.count, - pages = it.getAllUsersAsPage.info.pages, - prev = it.getAllUsersAsPage.info.prev, - next = it.getAllUsersAsPage.info.next, - ) - postInput(AdminListContract.Inputs.Set.Info(info)) + postInput(Inputs.Set.Info(info)) }, ) } @@ -162,31 +167,33 @@ internal class AdminListInputHandler : size = state.perPage, query = state.searchValue.ifBlank { null }, sortBy = state.sortBy, - sortDirection = state.sortDirection + sortDirection = state.sortDirection, ).fold( - { postEvent(AdminListContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { with(it.getAllProductsAsPage) { - val items = products.map { product -> - ListItem( - id = product.id, - slot1 = millisToDate(product.createdAt.toLong()), - slot2 = product.mediaUrl, - slot3 = product.name, - slot4 = product.regularPrice.toString(), - slot5 = product.sold.toString(), - slot6 = null, + val items = + products.map { product -> + ListItem( + id = product.id, + slot1 = millisToDate(product.createdAt.toLong()), + slot2 = product.mediaUrl, + slot3 = product.name, + slot4 = product.regularPrice.toString(), + slot5 = product.sold.toString(), + slot6 = null, + ) + } + postInput(Inputs.Set.Items(items)) + + val info = + PageInfo( + count = info.count, + pages = info.pages, + prev = info.prev, + next = info.next, ) - } - postInput(AdminListContract.Inputs.Set.Items(items)) - - val info = PageInfo( - count = info.count, - pages = info.pages, - prev = info.prev, - next = info.next, - ) - postInput(AdminListContract.Inputs.Set.Info(info)) + postInput(Inputs.Set.Info(info)) } }, ) @@ -199,30 +206,32 @@ internal class AdminListInputHandler : size = state.perPage, query = state.searchValue.ifBlank { null }, sortBy = state.sortBy, - sortDirection = state.sortDirection + sortDirection = state.sortDirection, ).fold( - { postEvent(AdminListContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { - val items = it.getAllCategoriesAsPage.categories.map { order -> - ListItem( - id = order.id, - slot1 = order.name, // millisToMonthYear(order.createdAt.toLong()) - slot2 = order.name, - slot3 = order.name, - slot4 = order.name, - slot5 = null, - slot6 = null, + val items = + it.getAllCategoriesAsPage.categories.map { order -> + ListItem( + id = order.id, + slot1 = order.name, // millisToMonthYear(order.createdAt.toLong()) + slot2 = order.name, + slot3 = order.name, + slot4 = order.name, + slot5 = null, + slot6 = null, + ) + } + postInput(Inputs.Set.Items(items)) + + val info = + PageInfo( + count = it.getAllCategoriesAsPage.info.count, + pages = it.getAllCategoriesAsPage.info.pages, + prev = it.getAllCategoriesAsPage.info.prev, + next = it.getAllCategoriesAsPage.info.next, ) - } - postInput(AdminListContract.Inputs.Set.Items(items)) - - val info = PageInfo( - count = it.getAllCategoriesAsPage.info.count, - pages = it.getAllCategoriesAsPage.info.pages, - prev = it.getAllCategoriesAsPage.info.prev, - next = it.getAllCategoriesAsPage.info.next, - ) - postInput(AdminListContract.Inputs.Set.Info(info)) + postInput(Inputs.Set.Info(info)) }, ) } @@ -233,30 +242,32 @@ internal class AdminListInputHandler : size = state.perPage, query = state.searchValue.ifBlank { null }, sortBy = state.sortBy, - sortDirection = state.sortDirection + sortDirection = state.sortDirection, ).fold( - { postEvent(AdminListContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { - val items = it.getAllCategoriesAsPage.categories.map { category -> - ListItem( - id = category.id, - slot1 = millisToDate(category.createdAt.toLong()), - slot2 = category.mediaUrl, - slot3 = category.name, - slot4 = category.display.toString(), - slot5 = category.usedInProducts.toString(), - slot6 = null, + val items = + it.getAllCategoriesAsPage.categories.map { category -> + ListItem( + id = category.id, + slot1 = millisToDate(category.createdAt.toLong()), + slot2 = category.mediaUrl, + slot3 = category.name, + slot4 = category.display.toString(), + slot5 = category.usedInProducts.toString(), + slot6 = null, + ) + } + postInput(Inputs.Set.Items(items)) + + val info = + PageInfo( + count = it.getAllCategoriesAsPage.info.count, + pages = it.getAllCategoriesAsPage.info.pages, + prev = it.getAllCategoriesAsPage.info.prev, + next = it.getAllCategoriesAsPage.info.next, ) - } - postInput(AdminListContract.Inputs.Set.Items(items)) - - val info = PageInfo( - count = it.getAllCategoriesAsPage.info.count, - pages = it.getAllCategoriesAsPage.info.pages, - prev = it.getAllCategoriesAsPage.info.prev, - next = it.getAllCategoriesAsPage.info.next, - ) - postInput(AdminListContract.Inputs.Set.Info(info)) + postInput(Inputs.Set.Info(info)) }, ) } @@ -268,35 +279,37 @@ internal class AdminListInputHandler : size = state.perPage, query = state.searchValue.ifBlank { null }, sortBy = state.sortBy, - sortDirection = state.sortDirection + sortDirection = state.sortDirection, ).fold( - { postEvent(AdminListContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { - val items = it.getAllTagsAsPage.tags.map { tag -> - ListItem( - id = tag.id, - slot1 = millisToDate(tag.createdAt.toLong()), - slot2 = tag.name, - slot3 = tag.usedInProducts.toString(), - slot4 = null, - slot5 = null, - slot6 = null, + val items = + it.getAllTagsAsPage.tags.map { tag -> + ListItem( + id = tag.id, + slot1 = millisToDate(tag.createdAt.toLong()), + slot2 = tag.name, + slot3 = tag.usedInProducts.toString(), + slot4 = null, + slot5 = null, + slot6 = null, + ) + } + postInput(Inputs.Set.Items(items)) + + val info = + PageInfo( + count = it.getAllTagsAsPage.info.count, + pages = it.getAllTagsAsPage.info.pages, + prev = it.getAllTagsAsPage.info.prev, + next = it.getAllTagsAsPage.info.next, ) - } - postInput(AdminListContract.Inputs.Set.Items(items)) - - val info = PageInfo( - count = it.getAllTagsAsPage.info.count, - pages = it.getAllTagsAsPage.info.pages, - prev = it.getAllTagsAsPage.info.prev, - next = it.getAllTagsAsPage.info.next, - ) - postInput(AdminListContract.Inputs.Set.Info(info)) + postInput(Inputs.Set.Info(info)) }, ) } } - postInput(AdminListContract.Inputs.Set.Loading(false)) + postInput(Inputs.Set.Loading(false)) } } } diff --git a/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListViewModel.kt b/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListViewModel.kt index 900accb8..a7abf647 100644 --- a/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListViewModel.kt +++ b/apps/admin/feature/list/src/commonMain/kotlin/feature/admin/list/AdminListViewModel.kt @@ -20,7 +20,8 @@ class AdminListViewModel( AdminListContract.Events, AdminListContract.State, >( - config = BallastViewModelConfiguration.Builder() + config = + BallastViewModelConfiguration.Builder() .apply { this += LoggingInterceptor() logger = { PrintlnLogger() } @@ -37,7 +38,8 @@ class AdminListViewModel( // interceptorDispatcher = Dispatchers.Default, // ) .build(), - eventHandler = AdminListEventHandler( + eventHandler = + AdminListEventHandler( onError = onError, goToCreate = goToCreate, goToDetail = goToDetail, diff --git a/apps/admin/feature/product/create/src/commonMain/kotlin/feature/admin/product/create/AdminProductCreateEventHandler.kt b/apps/admin/feature/product/create/src/commonMain/kotlin/feature/admin/product/create/AdminProductCreateEventHandler.kt index b1fd2f87..6beb44b6 100644 --- a/apps/admin/feature/product/create/src/commonMain/kotlin/feature/admin/product/create/AdminProductCreateEventHandler.kt +++ b/apps/admin/feature/product/create/src/commonMain/kotlin/feature/admin/product/create/AdminProductCreateEventHandler.kt @@ -10,14 +10,17 @@ internal class AdminProductCreateEventHandler( ) : EventHandler< AdminProductCreateContract.Inputs, AdminProductCreateContract.Events, - AdminProductCreateContract.State> { + AdminProductCreateContract.State, + > { override suspend fun EventHandlerScope< AdminProductCreateContract.Inputs, AdminProductCreateContract.Events, - AdminProductCreateContract.State>.handleEvent(event: AdminProductCreateContract.Events) = - when (event) { - is AdminProductCreateContract.Events.OnError -> onError(event.message) - AdminProductCreateContract.Events.GoBack -> goBack() - is AdminProductCreateContract.Events.GoToProduct -> goToProduct(event.id) - } + AdminProductCreateContract.State, + >.handleEvent( + event: AdminProductCreateContract.Events, + ) = when (event) { + is AdminProductCreateContract.Events.OnError -> onError(event.message) + AdminProductCreateContract.Events.GoBack -> goBack() + is AdminProductCreateContract.Events.GoToProduct -> goToProduct(event.id) + } } diff --git a/apps/admin/feature/product/create/src/commonMain/kotlin/feature/admin/product/create/AdminProductCreateInputHandler.kt b/apps/admin/feature/product/create/src/commonMain/kotlin/feature/admin/product/create/AdminProductCreateInputHandler.kt index 486f12d5..d085c4cf 100644 --- a/apps/admin/feature/product/create/src/commonMain/kotlin/feature/admin/product/create/AdminProductCreateInputHandler.kt +++ b/apps/admin/feature/product/create/src/commonMain/kotlin/feature/admin/product/create/AdminProductCreateInputHandler.kt @@ -14,12 +14,16 @@ import org.koin.core.component.inject private typealias InputScope = InputHandlerScope< AdminProductCreateContract.Inputs, AdminProductCreateContract.Events, - AdminProductCreateContract.State> + AdminProductCreateContract.State, + > -internal class AdminProductCreateInputHandler : KoinComponent, InputHandler< - AdminProductCreateContract.Inputs, - AdminProductCreateContract.Events, - AdminProductCreateContract.State> { +internal class AdminProductCreateInputHandler : + KoinComponent, + InputHandler< + AdminProductCreateContract.Inputs, + AdminProductCreateContract.Events, + AdminProductCreateContract.State, + > { private val productService by inject() private val inputValidator by inject() diff --git a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootContract.kt b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootContract.kt index 55d73ac9..44388e79 100644 --- a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootContract.kt +++ b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootContract.kt @@ -16,8 +16,11 @@ object RootContract : KoinComponent { data class SetIsLoading(val isLoading: Boolean) : Inputs data object Init : Inputs + data object ObserveAuthState : Inputs + data class SendNotification(val title: String, val body: String) : Inputs + data object LogOut : Inputs data class SetIsAuthenticated(val isAuthenticated: Boolean) : Inputs diff --git a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootEventHandler.kt b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootEventHandler.kt index 6dd71a39..cd79800e 100644 --- a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootEventHandler.kt +++ b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootEventHandler.kt @@ -10,10 +10,7 @@ private typealias RootEventHandlerScope = internal class RootEventHandler( private val onError: suspend (String) -> Unit, ) : KoinComponent, EventHandler { - - override suspend fun RootEventHandlerScope.handleEvent( - event: RootContract.Events, - ) = when (event) { + override suspend fun RootEventHandlerScope.handleEvent(event: RootContract.Events) = when (event) { is RootContract.Events.OnError -> onError(event.message) } } diff --git a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootInputHandler.kt b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootInputHandler.kt index 61307743..7e5f942e 100644 --- a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootInputHandler.kt +++ b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootInputHandler.kt @@ -17,7 +17,6 @@ private typealias RootInputScope = internal class RootInputHandler : KoinComponent, InputHandler { - private val authService: AuthService by inject() private val notificationService: NotificationService by inject() @@ -51,7 +50,8 @@ internal class RootInputHandler : private suspend fun RootInputScope.sendNotification(title: String, body: String) { sideJob("sendNotification") { notificationService.schedule( - notificationType = NotificationType.Immediate( + notificationType = + NotificationType.Immediate( title = title, body = body, ), diff --git a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootModule.kt b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootModule.kt index 54e7f024..10dd856d 100644 --- a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootModule.kt +++ b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootModule.kt @@ -4,9 +4,10 @@ import component.localization.localizationModule import data.dataModule import org.koin.dsl.module -val rootModule = module { - includes( - dataModule, - localizationModule, - ) -} +val rootModule = + module { + includes( + dataModule, + localizationModule, + ) + } diff --git a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootViewModel.kt b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootViewModel.kt index 40c35dff..0da9c6d3 100644 --- a/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootViewModel.kt +++ b/apps/admin/feature/root/src/commonMain/kotlin/feature/root/RootViewModel.kt @@ -15,7 +15,8 @@ class RootViewModel( RootContract.Events, RootContract.State, >( - config = BallastViewModelConfiguration.Builder() + config = + BallastViewModelConfiguration.Builder() .apply { // this += LoggingInterceptor() logger = { PrintlnLogger() } @@ -26,7 +27,8 @@ class RootViewModel( name = TAG, ) .build(), - eventHandler = RootEventHandler( + eventHandler = + RootEventHandler( onError = onError, ), coroutineScope = scope, diff --git a/apps/admin/feature/router/src/commonMain/kotlin/feature/router/RouterEventHandler.kt b/apps/admin/feature/router/src/commonMain/kotlin/feature/router/RouterEventHandler.kt index 69ef11bc..91844a6b 100644 --- a/apps/admin/feature/router/src/commonMain/kotlin/feature/router/RouterEventHandler.kt +++ b/apps/admin/feature/router/src/commonMain/kotlin/feature/router/RouterEventHandler.kt @@ -7,11 +7,13 @@ import data.service.AuthService import org.koin.core.component.KoinComponent import org.koin.core.component.inject -internal class RouterEventHandler : EventHandler< - RouterContract.Inputs, - RouterContract.Events, - RouterContract.State, - >, KoinComponent { +internal class RouterEventHandler : + EventHandler< + RouterContract.Inputs, + RouterContract.Events, + RouterContract.State, + >, + KoinComponent { private val authService by inject() @@ -19,7 +21,9 @@ internal class RouterEventHandler : EventHandler< RouterContract.Inputs, RouterContract.Events, RouterContract.State, - >.handleEvent(event: RouterContract.Events) { + >.handleEvent( + event: RouterContract.Events, + ) { when { event is RouterContract.Events.BackstackEmptied -> { if (authService.isAuth()) { diff --git a/apps/admin/feature/router/src/commonMain/kotlin/feature/router/RouterViewModel.kt b/apps/admin/feature/router/src/commonMain/kotlin/feature/router/RouterViewModel.kt index 517306bb..ed5cbb80 100644 --- a/apps/admin/feature/router/src/commonMain/kotlin/feature/router/RouterViewModel.kt +++ b/apps/admin/feature/router/src/commonMain/kotlin/feature/router/RouterViewModel.kt @@ -6,7 +6,9 @@ import com.copperleaf.ballast.build import com.copperleaf.ballast.core.LoggingInterceptor import com.copperleaf.ballast.core.PrintlnLogger import com.copperleaf.ballast.navigation.routing.Route -import com.copperleaf.ballast.navigation.routing.RouterContract +import com.copperleaf.ballast.navigation.routing.RouterContract.Events +import com.copperleaf.ballast.navigation.routing.RouterContract.Inputs +import com.copperleaf.ballast.navigation.routing.RouterContract.State import com.copperleaf.ballast.navigation.routing.RoutingTable import com.copperleaf.ballast.navigation.routing.build import com.copperleaf.ballast.navigation.routing.directions @@ -17,7 +19,7 @@ import com.copperleaf.ballast.navigation.vm.withRouter import com.copperleaf.ballast.plusAssign import kotlinx.coroutines.CoroutineScope -typealias RouterInterceptor = BallastInterceptor, RouterContract.Events, RouterContract.State> +typealias RouterInterceptor = BallastInterceptor, Events, State> class RouterViewModel( viewModelScope: CoroutineScope, @@ -39,7 +41,6 @@ class RouterViewModel( coroutineScope = viewModelScope, ) -fun RouterViewModel.goHome() = - trySend(RouterContract.Inputs.GoToDestination(Screen.AdminHome.matcher.routeFormat)) +fun RouterViewModel.goHome() = trySend(Inputs.GoToDestination(Screen.AdminHome.matcher.routeFormat)) fun T.idPath(id: String): String = this.directions().pathParameter("id", id).build() diff --git a/apps/shop/feature/cart/src/commonMain/kotlin/feature/shop/cart/CartContract.kt b/apps/shop/feature/cart/src/commonMain/kotlin/feature/shop/cart/CartContract.kt index c30d4c62..e57b450a 100644 --- a/apps/shop/feature/cart/src/commonMain/kotlin/feature/shop/cart/CartContract.kt +++ b/apps/shop/feature/cart/src/commonMain/kotlin/feature/shop/cart/CartContract.kt @@ -14,7 +14,7 @@ object CartContract { guestCartId = null, items = emptyList(), subtotal = 0.0, - saved = 0.0 + saved = 0.0, ), val topSellingProducts: List = emptyList(), diff --git a/apps/shop/feature/cart/src/commonMain/kotlin/feature/shop/cart/CartInputHandler.kt b/apps/shop/feature/cart/src/commonMain/kotlin/feature/shop/cart/CartInputHandler.kt index 14b4bb74..f76f3343 100644 --- a/apps/shop/feature/cart/src/commonMain/kotlin/feature/shop/cart/CartInputHandler.kt +++ b/apps/shop/feature/cart/src/commonMain/kotlin/feature/shop/cart/CartInputHandler.kt @@ -14,7 +14,8 @@ import org.koin.core.component.inject private typealias InputScope = InputHandlerScope -internal class CartInputHandler : KoinComponent, +internal class CartInputHandler : + KoinComponent, InputHandler { private val productService: ProductService by inject() @@ -23,8 +24,9 @@ internal class CartInputHandler : KoinComponent, override suspend fun InputHandlerScope< CartContract.Inputs, CartContract.Events, - CartContract.State>.handleInput( - input: CartContract.Inputs + CartContract.State, + >.handleInput( + input: CartContract.Inputs, ) = when (input) { CartContract.Inputs.Init -> handleInit() CartContract.Inputs.FetchCart -> handleFetchCart() @@ -47,7 +49,7 @@ internal class CartInputHandler : KoinComponent, is CartContract.Inputs.UpdateCart -> handleUpdateCart( productId = input.productId, variantId = input.variantId, - quantity = input.quantity + quantity = input.quantity, ) is CartContract.Inputs.SetCart -> updateState { it.copy(cart = input.cart) } @@ -81,7 +83,7 @@ internal class CartInputHandler : KoinComponent, sideJob("removeFromCart") { userService.removeItemFromCart( productId = productId, - variantId = variantId + variantId = variantId, ).fold( { postEvent(CartContract.Events.OnError(it.toString())) }, { @@ -98,7 +100,7 @@ internal class CartInputHandler : KoinComponent, attrs = it.attrs.map { GetUserCartQuery.Attr( key = it.key, - value = it.value + value = it.value, ) }, mediaUrl = it.mediaUrl, @@ -109,21 +111,23 @@ internal class CartInputHandler : KoinComponent, ) }, subtotal = it.removeItemFromUserCart.subtotal, - saved = it.removeItemFromUserCart.saved - ) - ) + saved = it.removeItemFromUserCart.saved, + ), + ), ) - val totals = countTotals(it.removeItemFromUserCart.items.map { - Prices( - regularPrice = it.regularPrice, - salePrice = it.salePrice, - quantity = it.quantity - ) - }) + val totals = countTotals( + it.removeItemFromUserCart.items.map { + Prices( + regularPrice = it.regularPrice, + salePrice = it.salePrice, + quantity = it.quantity, + ) + }, + ) postInput(CartContract.Inputs.SetTotals(subtotal = totals.subtotal, saved = totals.saved)) postInput(CartContract.Inputs.SetBasketCount(it.removeItemFromUserCart.items.size)) - } + }, ) } } @@ -147,16 +151,12 @@ internal class CartInputHandler : KoinComponent, ?: noOp() } - private suspend fun InputScope.handleUpdateCart( - productId: String, - variantId: String, - quantity: Int, - ) { + private suspend fun InputScope.handleUpdateCart(productId: String, variantId: String, quantity: Int) { sideJob("addToCart") { userService.addProductToCart( productId = productId, variantId = variantId, - quantity = quantity + quantity = quantity, ).fold( { postEvent(CartContract.Events.OnError(it.toString())) }, { @@ -173,7 +173,7 @@ internal class CartInputHandler : KoinComponent, attrs = it.attrs.map { GetUserCartQuery.Attr( key = it.key, - value = it.value + value = it.value, ) }, mediaUrl = it.mediaUrl, @@ -184,22 +184,24 @@ internal class CartInputHandler : KoinComponent, ) }, subtotal = it.addToCart.subtotal, - saved = it.addToCart.saved - ) - ) + saved = it.addToCart.saved, + ), + ), ) - val totals = countTotals(it.addToCart.items.map { - Prices( - regularPrice = it.regularPrice, - salePrice = it.salePrice, - quantity = it.quantity - ) - }) + val totals = countTotals( + it.addToCart.items.map { + Prices( + regularPrice = it.regularPrice, + salePrice = it.salePrice, + quantity = it.quantity, + ) + }, + ) postInput(CartContract.Inputs.SetTotals(subtotal = totals.subtotal, saved = totals.saved)) postInput(CartContract.Inputs.SetShowSidebar(true)) postInput(CartContract.Inputs.SetBasketCount(it.addToCart.items.size)) - } + }, ) } } @@ -210,7 +212,6 @@ internal class CartInputHandler : KoinComponent, userService.getCart().fold( { postEvent(CartContract.Events.OnError(it.toString())) }, { - // TODO: This may need to go to the Config val currency = Currency("£", "GBP") postInput(CartContract.Inputs.SetCurrency(currency)) @@ -221,17 +222,19 @@ internal class CartInputHandler : KoinComponent, val spendMoreValue = "100.00" postInput(CartContract.Inputs.SetSpendMore(showSpendMore, spendMoreKey, spendMoreValue)) - val totals = countTotals(it.getUserCart.items.map { - Prices( - regularPrice = it.regularPrice, - salePrice = it.salePrice, - quantity = it.quantity - ) - }) + val totals = countTotals( + it.getUserCart.items.map { + Prices( + regularPrice = it.regularPrice, + salePrice = it.salePrice, + quantity = it.quantity, + ) + }, + ) postInput(CartContract.Inputs.SetTotals(subtotal = totals.subtotal, saved = totals.saved)) postInput(CartContract.Inputs.SetCart(it.getUserCart)) postInput(CartContract.Inputs.SetBasketCount(it.getUserCart.items.size)) - } + }, ) if (state.cartLoading) postInput(CartContract.Inputs.SetCartLoading(false)) } @@ -253,7 +256,7 @@ internal class CartInputHandler : KoinComponent, postInput(CartContract.Inputs.SetTopProductsLoading(true)) productService.getTopSellingProducts().fold( { postEvent(CartContract.Events.OnError(it.toString())) }, - { postInput(CartContract.Inputs.SetTopProducts(it.getTopSellingProducts)) } + { postInput(CartContract.Inputs.SetTopProducts(it.getTopSellingProducts)) }, ) postInput(CartContract.Inputs.SetTopProductsLoading(false)) } diff --git a/apps/shop/feature/checkout/src/commonMain/kotlin/feature/checkout/CheckoutEventHandler.kt b/apps/shop/feature/checkout/src/commonMain/kotlin/feature/checkout/CheckoutEventHandler.kt index d67781ab..e121722d 100644 --- a/apps/shop/feature/checkout/src/commonMain/kotlin/feature/checkout/CheckoutEventHandler.kt +++ b/apps/shop/feature/checkout/src/commonMain/kotlin/feature/checkout/CheckoutEventHandler.kt @@ -10,8 +10,10 @@ internal class CheckoutEventHandler( override suspend fun EventHandlerScope< CheckoutContract.Inputs, CheckoutContract.Events, - CheckoutContract.State - >.handleEvent(event: CheckoutContract.Events) = when (event) { + CheckoutContract.State, + >.handleEvent( + event: CheckoutContract.Events, + ) = when (event) { is CheckoutContract.Events.OnError -> onError(event.message) } } diff --git a/apps/shop/feature/checkout/src/commonMain/kotlin/feature/checkout/CheckoutInputHandler.kt b/apps/shop/feature/checkout/src/commonMain/kotlin/feature/checkout/CheckoutInputHandler.kt index fb02d1c8..a19a0cf6 100644 --- a/apps/shop/feature/checkout/src/commonMain/kotlin/feature/checkout/CheckoutInputHandler.kt +++ b/apps/shop/feature/checkout/src/commonMain/kotlin/feature/checkout/CheckoutInputHandler.kt @@ -17,7 +17,8 @@ import kotlin.math.roundToInt private typealias InputScope = InputHandlerScope -internal class CheckoutInputHandler : KoinComponent, +internal class CheckoutInputHandler : + KoinComponent, InputHandler { private val userService: UserService by inject() @@ -38,7 +39,7 @@ internal class CheckoutInputHandler : KoinComponent, input.price == 0.0 -> getString(Strings.FreeShipping) input.price > 0.0 -> input.price.toString() else -> getString(Strings.EnterShippingAddress) - } + }, ) } @@ -64,7 +65,7 @@ internal class CheckoutInputHandler : KoinComponent, val platforms = listOf(Platform.APPLE) paymentService.getPaymentMethods(platforms).fold( { postEvent(CheckoutContract.Events.OnError(it.toString())) }, - { postInput(CheckoutContract.Inputs.SetPaymentMethods(it.getPaymentMethods)) } + { postInput(CheckoutContract.Inputs.SetPaymentMethods(it.getPaymentMethods)) }, ) } } @@ -73,7 +74,7 @@ internal class CheckoutInputHandler : KoinComponent, val state = getCurrentState() sideJob("FetchCart") { userService.getCart().fold( - { postEvent(CheckoutContract.Events.OnError(it.toString())) } + { postEvent(CheckoutContract.Events.OnError(it.toString())) }, ) { // TODO: This may need to go to the Config val currency = Currency("£", "GBP") @@ -92,7 +93,7 @@ internal class CheckoutInputHandler : KoinComponent, }, subtotal = it.getUserCart.subtotal, currency = currency, - ) + ), ) postInput(CheckoutContract.Inputs.SetShippingPrice(null)) @@ -107,14 +108,14 @@ internal class CheckoutInputHandler : KoinComponent, val integerDigits = this.toInt() val floatDigits = ((this - integerDigits) * 10f.pow(numOfDec)).roundToInt() val floatDigitsString = floatDigits.toString().padStart(numOfDec, '0') - return "${integerDigits}.${floatDigitsString}" + return "$integerDigits.$floatDigitsString" } private suspend fun InputScope.handleInit() { sideJob("InitCheckout") { postInput(CheckoutContract.Inputs.FetchCart) postInput(CheckoutContract.Inputs.FetchPaymentMethods) - postInput(CheckoutContract.Inputs.SetShippingPrice(null)) //FIXME: This should be fetched from the API + postInput(CheckoutContract.Inputs.SetShippingPrice(null)) // FIXME: This should be fetched from the API } } } diff --git a/apps/shop/feature/footer/src/commonMain/kotlin/feature/shop/footer/FooterInputHandler.kt b/apps/shop/feature/footer/src/commonMain/kotlin/feature/shop/footer/FooterInputHandler.kt index a8cfcc3b..867001af 100644 --- a/apps/shop/feature/footer/src/commonMain/kotlin/feature/shop/footer/FooterInputHandler.kt +++ b/apps/shop/feature/footer/src/commonMain/kotlin/feature/shop/footer/FooterInputHandler.kt @@ -6,49 +6,46 @@ import core.mapToUiMessage import data.service.AuthService import data.service.ConfigService import data.type.Role +import feature.shop.footer.FooterContract.Events +import feature.shop.footer.FooterContract.Inputs +import feature.shop.footer.FooterContract.State import org.koin.core.component.KoinComponent import org.koin.core.component.inject -private typealias InputScope = - InputHandlerScope - -internal class FooterInputHandler : - KoinComponent, - InputHandler { +private typealias InputScope = InputHandlerScope +internal class FooterInputHandler : KoinComponent, InputHandler { private val authService: AuthService by inject() private val configService: ConfigService by inject() - override suspend fun InputHandlerScope.handleInput( - input: FooterContract.Inputs, - ) = when (input) { - FooterContract.Inputs.Init -> handleInit() - FooterContract.Inputs.GetConfig -> handleFetchConfig() - FooterContract.Inputs.CheckUserRole -> handleCheckUserRole() + override suspend fun InputHandlerScope.handleInput(input: Inputs) = when (input) { + Inputs.Init -> handleInit() + Inputs.GetConfig -> handleFetchConfig() + Inputs.CheckUserRole -> handleCheckUserRole() - FooterContract.Inputs.OnAccessibilityClicked -> postEvent(FooterContract.Events.GoToAccessibility) - FooterContract.Inputs.OnPrivacyPolicyClicked -> postEvent(FooterContract.Events.GoToPrivacyPolicy) - FooterContract.Inputs.OnTermsOfServiceClicked -> postEvent(FooterContract.Events.GoToTermsOfService) - FooterContract.Inputs.OnAboutUsClick -> postEvent(FooterContract.Events.GoToAboutUs) - FooterContract.Inputs.OnCareerClick -> postEvent(FooterContract.Events.GoToCareer) - FooterContract.Inputs.OnContactUsClick -> postEvent(FooterContract.Events.GoToContactUs) - FooterContract.Inputs.OnCyberSecurityClick -> postEvent(FooterContract.Events.GoToCyberSecurity) - FooterContract.Inputs.OnFAQsClick -> postEvent(FooterContract.Events.GoToFAQs) - FooterContract.Inputs.OnPressClick -> postEvent(FooterContract.Events.GoToPress) - FooterContract.Inputs.OnReturnsClick -> postEvent(FooterContract.Events.GoToReturns) - FooterContract.Inputs.OnShippingClick -> postEvent(FooterContract.Events.GoToShipping) - FooterContract.Inputs.OnTrackOrderClick -> postEvent(FooterContract.Events.GoToTrackOrder) - FooterContract.Inputs.OnGoToAdminHome -> noOp() - FooterContract.Inputs.OnCompanyNameClick -> handleCompanyNameClick() - FooterContract.Inputs.OnCurrencyClick -> noOp() - FooterContract.Inputs.OnLanguageClick -> noOp() - FooterContract.Inputs.OnTickerClick -> postEvent(FooterContract.Events.GoToCatalogue) - FooterContract.Inputs.OnConnectEmailSend -> noOp() + Inputs.OnAccessibilityClicked -> postEvent(Events.GoToAccessibility) + Inputs.OnPrivacyPolicyClicked -> postEvent(Events.GoToPrivacyPolicy) + Inputs.OnTermsOfServiceClicked -> postEvent(Events.GoToTermsOfService) + Inputs.OnAboutUsClick -> postEvent(Events.GoToAboutUs) + Inputs.OnCareerClick -> postEvent(Events.GoToCareer) + Inputs.OnContactUsClick -> postEvent(Events.GoToContactUs) + Inputs.OnCyberSecurityClick -> postEvent(Events.GoToCyberSecurity) + Inputs.OnFAQsClick -> postEvent(Events.GoToFAQs) + Inputs.OnPressClick -> postEvent(Events.GoToPress) + Inputs.OnReturnsClick -> postEvent(Events.GoToReturns) + Inputs.OnShippingClick -> postEvent(Events.GoToShipping) + Inputs.OnTrackOrderClick -> postEvent(Events.GoToTrackOrder) + Inputs.OnGoToAdminHome -> noOp() + Inputs.OnCompanyNameClick -> handleCompanyNameClick() + Inputs.OnCurrencyClick -> noOp() + Inputs.OnLanguageClick -> noOp() + Inputs.OnTickerClick -> postEvent(Events.GoToCatalogue) + Inputs.OnConnectEmailSend -> noOp() - is FooterContract.Inputs.SetIsLoading -> updateState { it.copy(isLoading = input.isLoading) } - is FooterContract.Inputs.SetCompanyInfo -> updateState { it.copy(companyInfo = input.companyInfo) } - is FooterContract.Inputs.SetFooterConfig -> updateState { it.copy(footerConfig = input.footerConfig) } - is FooterContract.Inputs.SetConnectEmail -> updateState { it.copy(connectEmail = input.email) } + is Inputs.SetIsLoading -> updateState { it.copy(isLoading = input.isLoading) } + is Inputs.SetCompanyInfo -> updateState { it.copy(companyInfo = input.companyInfo) } + is Inputs.SetFooterConfig -> updateState { it.copy(footerConfig = input.footerConfig) } + is Inputs.SetConnectEmail -> updateState { it.copy(connectEmail = input.email) } } private suspend fun InputScope.handleCheckUserRole() { @@ -63,10 +60,10 @@ internal class FooterInputHandler : private suspend fun InputScope.handleFetchConfig() { sideJob("handleFetchConfig") { configService.fetchConfig().fold( - { postEvent(FooterContract.Events.OnError(it.mapToUiMessage())) }, + { postEvent(Events.OnError(it.mapToUiMessage())) }, { - postInput(FooterContract.Inputs.SetCompanyInfo(companyInfo = it.getConfig.companyInfo)) - postInput(FooterContract.Inputs.SetFooterConfig(footerConfig = it.getConfig.footerConfig)) + postInput(Inputs.SetCompanyInfo(companyInfo = it.getConfig.companyInfo)) + postInput(Inputs.SetFooterConfig(footerConfig = it.getConfig.footerConfig)) }, ) } @@ -74,16 +71,16 @@ internal class FooterInputHandler : private suspend fun InputScope.handleCompanyNameClick() { getCurrentState().companyInfo?.contactInfo?.companyWebsite?.let { url -> - postEvent(FooterContract.Events.GoToCompanyWebsite(url)) + postEvent(Events.GoToCompanyWebsite(url)) } ?: noOp() } private suspend fun InputScope.handleInit() { sideJob("InitFooter") { - postInput(FooterContract.Inputs.SetIsLoading(isLoading = true)) - postInput(FooterContract.Inputs.CheckUserRole) - postInput(FooterContract.Inputs.GetConfig) - postInput(FooterContract.Inputs.SetIsLoading(isLoading = false)) + postInput(Inputs.SetIsLoading(isLoading = true)) + postInput(Inputs.CheckUserRole) + postInput(Inputs.GetConfig) + postInput(Inputs.SetIsLoading(isLoading = false)) } } } diff --git a/apps/shop/feature/footer/src/commonMain/kotlin/feature/shop/footer/model/PaymentMethod.kt b/apps/shop/feature/footer/src/commonMain/kotlin/feature/shop/footer/model/PaymentMethod.kt index 9f2cceae..8b4a7c0d 100644 --- a/apps/shop/feature/footer/src/commonMain/kotlin/feature/shop/footer/model/PaymentMethod.kt +++ b/apps/shop/feature/footer/src/commonMain/kotlin/feature/shop/footer/model/PaymentMethod.kt @@ -10,21 +10,21 @@ val dummyPaymentMethods = listOf( PaymentMethod( id = "1", imageUrl = "https://cdn-icons-png.flaticon.com/128/196/196578.png", - name = "Visa" + name = "Visa", ), PaymentMethod( id = "2", imageUrl = "https://cdn-icons-png.flaticon.com/128/14062/14062982.png", - name = "Mastercard" + name = "Mastercard", ), PaymentMethod( id = "3", imageUrl = "https://cdn-icons-png.flaticon.com/128/349/349228.png", - name = "American Express" + name = "American Express", ), PaymentMethod( id = "4", imageUrl = "https://cdn-icons-png.flaticon.com/128/196/196565.png", - name = "Paypal" - ) + name = "Paypal", + ), ) diff --git a/apps/shop/feature/home/src/commonMain/kotlin/feature/shop/home/HomeContract.kt b/apps/shop/feature/home/src/commonMain/kotlin/feature/shop/home/HomeContract.kt index bdf95aa8..56dc96f7 100644 --- a/apps/shop/feature/home/src/commonMain/kotlin/feature/shop/home/HomeContract.kt +++ b/apps/shop/feature/home/src/commonMain/kotlin/feature/shop/home/HomeContract.kt @@ -6,6 +6,7 @@ import data.GetLandingConfigQuery import data.GetLandingConfigQuery.SlideshowItem import data.type.MediaType +@Suppress("MaxLineLength") object HomeContract { data class State( val isLoading: Boolean = true, diff --git a/apps/shop/feature/root/src/commonMain/kotlin/feature/root/RootEventHandler.kt b/apps/shop/feature/root/src/commonMain/kotlin/feature/root/RootEventHandler.kt index 6dd71a39..e57c8f36 100644 --- a/apps/shop/feature/root/src/commonMain/kotlin/feature/root/RootEventHandler.kt +++ b/apps/shop/feature/root/src/commonMain/kotlin/feature/root/RootEventHandler.kt @@ -11,9 +11,7 @@ internal class RootEventHandler( private val onError: suspend (String) -> Unit, ) : KoinComponent, EventHandler { - override suspend fun RootEventHandlerScope.handleEvent( - event: RootContract.Events, - ) = when (event) { + override suspend fun RootEventHandlerScope.handleEvent(event: RootContract.Events) = when (event) { is RootContract.Events.OnError -> onError(event.message) } } diff --git a/apps/shop/feature/router/src/commonMain/kotlin/feature/router/RouterEventHandler.kt b/apps/shop/feature/router/src/commonMain/kotlin/feature/router/RouterEventHandler.kt index 12612b63..64f348cd 100644 --- a/apps/shop/feature/router/src/commonMain/kotlin/feature/router/RouterEventHandler.kt +++ b/apps/shop/feature/router/src/commonMain/kotlin/feature/router/RouterEventHandler.kt @@ -7,11 +7,13 @@ import data.service.AuthService import org.koin.core.component.KoinComponent import org.koin.core.component.inject -internal class RouterEventHandler : EventHandler< - RouterContract.Inputs, - RouterContract.Events, - RouterContract.State, - >, KoinComponent { +internal class RouterEventHandler : + EventHandler< + RouterContract.Inputs, + RouterContract.Events, + RouterContract.State, + >, + KoinComponent { private val authService by inject() @@ -19,7 +21,9 @@ internal class RouterEventHandler : EventHandler< RouterContract.Inputs, RouterContract.Events, RouterContract.State, - >.handleEvent(event: RouterContract.Events) { + >.handleEvent( + event: RouterContract.Events, + ) { when { event is RouterContract.Events.BackstackEmptied -> { if (authService.isAuth()) { diff --git a/apps/shop/feature/router/src/commonMain/kotlin/feature/router/RouterViewModel.kt b/apps/shop/feature/router/src/commonMain/kotlin/feature/router/RouterViewModel.kt index ee4ad712..7f65264c 100644 --- a/apps/shop/feature/router/src/commonMain/kotlin/feature/router/RouterViewModel.kt +++ b/apps/shop/feature/router/src/commonMain/kotlin/feature/router/RouterViewModel.kt @@ -40,7 +40,6 @@ class RouterViewModel( coroutineScope = viewModelScope, ) -fun RouterViewModel.goHome() = - trySend(RouterContract.Inputs.GoToDestination(Screen.Home.matcher.routeFormat)) +fun RouterViewModel.goHome() = trySend(RouterContract.Inputs.GoToDestination(Screen.Home.matcher.routeFormat)) fun T.idPath(id: String): String = this.directions().pathParameter("id", id).build() diff --git a/build.gradle.kts b/build.gradle.kts index 7e271c54..06aea487 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,71 +1,16 @@ -import io.gitlab.arturbosch.detekt.Detekt -import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jlleitschuh.gradle.ktlint.KtlintExtension - plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.apollo) apply false alias(libs.plugins.build.konfig) apply false - alias(libs.plugins.detekt) + alias(libs.plugins.detekt) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.jetbrains.compose) apply false alias(libs.plugins.kobweb.application) apply false alias(libs.plugins.kobwebx.markdown) apply false alias(libs.plugins.kotlin.multiplatform) apply false - kotlin("plugin.serialization") version libs.versions.kotlin.get() apply false + alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.kover) apply false alias(libs.plugins.ksp) apply false - alias(libs.plugins.ktLint) - id("com.github.johnrengelman.shadow") version "8.1.1" apply false -} - -subprojects { - apply(plugin = "org.jlleitschuh.gradle.ktlint") - configure { - filter { - exclude { element -> element.file.path.contains("/build/") } - } - debug.set(false) - outputToConsole.set(true) - } - - apply(plugin = "io.gitlab.arturbosch.detekt") - detekt { - parallel = true - config.setFrom(files(rootProject.file("detekt.yml"))) - autoCorrect = true - } - - tasks.withType().configureEach { - jvmTarget = libs.versions.jvmTarget.get() - parallel = true - reports { - xml.required.set(false) - html.required.set(false) - txt.required.set(false) - sarif.required.set(false) - } - exclude { it.file.absolutePath.contains("resources/") } - exclude { it.file.absolutePath.contains("build/") } - include("**/*.kt") - } - - tasks.withType().configureEach { - this.jvmTarget = libs.versions.jvmTarget.get() - exclude { it.file.absolutePath.contains("resources/") } - exclude { it.file.absolutePath.contains("build/") } - include("**/*.kt") - } - - tasks.register("detektAll") { - group = "verification" - description = "Runs all detekt tasks" - dependsOn(tasks.withType()) - } - - tasks.withType().configureEach { - kotlinOptions.jvmTarget = libs.versions.jvmTarget.get() - } + alias(libs.plugins.ktLint) apply false } diff --git a/components/localization/src/commonMain/kotlin/component/localization/CommonListState.kt b/components/localization/src/commonMain/kotlin/component/localization/CommonListState.kt index ac4ec630..45da64db 100644 --- a/components/localization/src/commonMain/kotlin/component/localization/CommonListState.kt +++ b/components/localization/src/commonMain/kotlin/component/localization/CommonListState.kt @@ -7,7 +7,7 @@ interface CommonListState { fun setLoading(isLoading: Boolean): ListState { return listState.copy( - isLoading = isLoading + isLoading = isLoading, ) } @@ -15,13 +15,13 @@ interface CommonListState { return listState.copy( items = items, showList = items.isNotEmpty(), - showNoItems = items.isEmpty() + showNoItems = items.isEmpty(), ) } fun setPerPage(perPage: Int): ListState { return listState.copy( - perPage = perPage + perPage = perPage, ) } @@ -31,7 +31,7 @@ interface CommonListState { info = info, pagesNumbers = pageNumbers, showPrevious = info.prev != null, - showNext = info.next != null + showNext = info.next != null, ) } @@ -47,13 +47,10 @@ interface CommonListState { data class ListState( val title: String, val strings: ListStrings = ListStrings(title), - val isLoading: Boolean = true, - val items: List = emptyList(), val showList: Boolean = items.isNotEmpty(), val showNoItems: Boolean = items.isEmpty(), - val info: PageInfo = PageInfo(0, 0, null, null), val pagesNumbers: List = emptyList(), val perPage: Int = 1, @@ -63,7 +60,7 @@ data class ListState( data class ListItem( val id: String, - val name: String + val name: String, ) data class PageInfo( @@ -75,7 +72,7 @@ data class PageInfo( data class PageInput( val page: Int, - val size: Int + val size: Int, ) open class ListStrings( diff --git a/components/localization/src/commonMain/kotlin/component/localization/InputValidator.kt b/components/localization/src/commonMain/kotlin/component/localization/InputValidator.kt index 8bc8b0ba..bb50e2d9 100644 --- a/components/localization/src/commonMain/kotlin/component/localization/InputValidator.kt +++ b/components/localization/src/commonMain/kotlin/component/localization/InputValidator.kt @@ -2,12 +2,28 @@ package component.localization interface InputValidator { fun validateEmail(email: String): String? + fun validatePassword(password: String): String? - fun validateRepeatPassword(password: String, repeatPassword: String): String? - fun validateText(text: String, minLength: Int = 2): String? - fun validatePhone(phone: String, length: Int = 8): String? + + fun validateRepeatPassword( + password: String, + repeatPassword: String, + ): String? + + fun validateText( + text: String, + minLength: Int = 2, + ): String? + + fun validatePhone( + phone: String, + length: Int = 8, + ): String? + fun validateNumberPositive(number: Int): String? + fun validateIsPriceDouble(string: String): String? + fun validateUrl(url: String): String? } @@ -29,14 +45,20 @@ internal class InputValidatorImpl : InputValidator { } } - override fun validateRepeatPassword(password: String, repeatPassword: String): String? { + override fun validateRepeatPassword( + password: String, + repeatPassword: String, + ): String? { return when { password != repeatPassword -> "Passwords do not match" else -> null } } - override fun validateText(text: String, minLength: Int): String? { + override fun validateText( + text: String, + minLength: Int, + ): String? { return when { text.isBlank() -> "Cannot be empty" text.length < minLength -> "Must be at least $minLength characters" @@ -44,7 +66,10 @@ internal class InputValidatorImpl : InputValidator { } } - override fun validatePhone(phone: String, length: Int): String? { + override fun validatePhone( + phone: String, + length: Int, + ): String? { return when { phone.isBlank() -> "Phone cannot be empty" phone.length < length -> "Phone must be at least 8 characters" diff --git a/components/localization/src/commonMain/kotlin/component/localization/LocalizationModule.kt b/components/localization/src/commonMain/kotlin/component/localization/LocalizationModule.kt index c6e1d108..c9aa7e01 100644 --- a/components/localization/src/commonMain/kotlin/component/localization/LocalizationModule.kt +++ b/components/localization/src/commonMain/kotlin/component/localization/LocalizationModule.kt @@ -2,6 +2,7 @@ package component.localization import org.koin.dsl.module -val localizationModule = module { - single { InputValidatorImpl() } -} +val localizationModule = + module { + single { InputValidatorImpl() } + } diff --git a/components/localization/src/commonMain/kotlin/component/localization/StringProvider.kt b/components/localization/src/commonMain/kotlin/component/localization/StringProvider.kt index e83e811a..b687178a 100644 --- a/components/localization/src/commonMain/kotlin/component/localization/StringProvider.kt +++ b/components/localization/src/commonMain/kotlin/component/localization/StringProvider.kt @@ -20,15 +20,22 @@ internal object StringProvider { get() = SupportedLanguage.currentLanguage() // "My name is Adrian and i have 3 cars." -> "My name is %0 and i have %1 cars." - operator fun invoke(key: Strings, params: Array): String { - val stringTemplate = when (currentLanguage) { - SupportedLanguage.English -> englishLanguage(key) - } + operator fun invoke( + key: Strings, + params: Array, + ): String { + val stringTemplate = + when (currentLanguage) { + SupportedLanguage.English -> englishLanguage(key) + } return formatString(stringTemplate, params) } - private fun formatString(template: String, params: Array): String { + private fun formatString( + template: String, + params: Array, + ): String { var result = template params.forEachIndexed { index, param -> result = result.replace("%$index", param.toString()) @@ -39,4 +46,7 @@ internal object StringProvider { internal expect fun getDeviceLanguageCode(): String -fun getString(key: Strings, vararg params: Any): String = StringProvider.invoke(key, params) +fun getString( + key: Strings, + vararg params: Any, +): String = StringProvider.invoke(key, params) diff --git a/components/localization/src/commonMain/kotlin/component/localization/Strings.kt b/components/localization/src/commonMain/kotlin/component/localization/Strings.kt index 5d3be6f8..618c5fc5 100644 --- a/components/localization/src/commonMain/kotlin/component/localization/Strings.kt +++ b/components/localization/src/commonMain/kotlin/component/localization/Strings.kt @@ -450,5 +450,5 @@ enum class Strings { ViewOurFavorites, YouAlsoViewed, Learn, - Availability + Availability, } diff --git a/components/localization/src/commonMain/kotlin/component/localization/languages/English.kt b/components/localization/src/commonMain/kotlin/component/localization/languages/English.kt index b1464de9..a7c50ded 100644 --- a/components/localization/src/commonMain/kotlin/component/localization/languages/English.kt +++ b/components/localization/src/commonMain/kotlin/component/localization/languages/English.kt @@ -7,138 +7,189 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.todayIn import localization.BuildKonfig +@Suppress("CyclomaticComplexMethod", "LongMethod") internal fun englishLanguage( key: Strings, appName: String = BuildKonfig.appName, companyName: String = BuildKonfig.orgName, currentYear: Int = Clock.System.todayIn(TimeZone.currentSystemDefault()).year, -): String = when (key) { - Strings.ShopName -> appName - Strings.AppMotto -> "Here you become different from others" - Strings.CompanyName -> companyName - Strings.AppCopyright -> "© $currentYear $companyName" - Strings.AppVersion -> "0.1.0" - Strings.ContinueWithGoogle -> "Continue with Google" - Strings.ContinueWithFacebook -> "Continue with Facebook" - Strings.Or -> "OR" - Strings.ForgotPassword -> "Forgot your password?" - Strings.DontHaveAccount -> "Don't have an account?" - Strings.SignUp -> "Sign Up" - Strings.SignUpWithGoogle -> "Sign up with Google" - Strings.SignUpWithFacebook -> "Sign up with Facebook" - Strings.Newsletter -> "Sign up for news about sales and new arrivals" - Strings.AlreadyHaveAnAccount -> "Already have an account?" - Strings.PrivacyPolicy -> "Privacy Policy" - Strings.And -> "and" - Strings.TermsOfService -> "Terms of Service" - Strings.BySigningUpAgree -> "By signing up, you agree to our" - Strings.ForgotPasswordDescription -> "Wo worries, we'll send recovery link to your email." - Strings.CheckEmailDescription -> "We have sent a password reset link to" - Strings.PromoText -> "Free shipping on orders over £50" - Strings.HelpAndFaq -> "Help & FAQ" - Strings.CurrencyEnUs -> "EN, $" - Strings.AboutUsSmall -> "About us" - Strings.AboutUs -> "About Us" - Strings.ContactUs -> "Contact Us" - Strings.CyberSecurity -> "Cyber Security" - Strings.FAQs -> "FAQs" - Strings.TrackOrder -> "Track Order" - Strings.IsFeaturedDesc -> "Indicates whether or not the product should be featured." - Strings.AllowReviewsDesc -> "Indicates whether or not a product allows reviews." - Strings.CatalogVisibilityDesc -> "Indicates whether or not the product should be visible in the catalog." - Strings.CategoriesDesc -> "The categories this product is in" - Strings.DescriptionDesc -> "The product's full description." - Strings.ParentCategoryDesc -> "The parent category of the product." - Strings.ToStart -> "to start" - Strings.TagsDesc -> "The tags this product has" - Strings.PriceDesc -> "The current price of the product." - Strings.StockStatusDesc -> "The product's stock status." - Strings.PostStatusDesc -> "The product's current post status." - Strings.IsPurchasableDesc -> "Indicates whether or not the product is currently able to be purchased." - Strings.OnePerOrderDesc -> "Indicates that only one of a product may be held in the order at a time." - Strings.UnsavedChanges -> "Careful! You have unsaved changes!" - Strings.ImagesDesc -> "The images for the product." - Strings.BackorderStatusDesc -> "The status of back-ordering for a product." - Strings.CanBackorderDesc -> "Indicates whether or not a product can be back-ordered." - Strings.IsOnBackorderDesc -> "Indicates whether or not a product is on backorder." - Strings.LowStockThresholdDesc -> "Indicates the threshold for when the low stock notification will be sent to the merchant." - Strings.RemainingStockDesc -> "The number of inventory units remaining for this product." - Strings.TrackQuantityDesc -> "Indicates that a product should use the inventory system." - Strings.RegularPriceDesc -> "The regular price of the product when not discounted." - Strings.SalePriceDesc -> "The price of the product when on sale." - Strings.OnSaleDesc -> "Indicates whether or not the product is currently on sale." - Strings.SaleStartDesc -> "The GMT datetime when the product should start to be on sale." - Strings.SaleEndDesc -> "The GMT datetime when the product should no longer be on sale." - Strings.LengthDesc -> "The length of the product in the store's current units." - Strings.WeightDesc -> "The weight of the product in the store's current units." - Strings.WidthDesc -> "The width of the product in the store's current units." - Strings.RequiresShippingDesc -> "Indicates that the product must be shipped." - Strings.ShippingPreset -> "Choose preset from currently selected category, or pass custom values." - Strings.Kg -> "kg" - Strings.Cm -> "cm" - Strings.CreatedByDesc -> "The user who created the product." - Strings.ShippingPresetDesc -> "The shipping preset for the product, coming from category shipping settings" - Strings.ImproveWithAi -> "Improve with AI" - Strings.DeleteExplain -> "Are you sure you want to delete this? You cannot undo this action." - Strings.ChargeTaxDesc -> "Indicates whether or not the product should have tax charged on it." - Strings.UseGlobalTrackingDesc -> "Indicates whether or not the product should use the global tracking settings." - Strings.NoInsights -> "Insights will display when the product has had recent sales" - Strings.AddressDesc -> "The primary address of this customer" - Strings.MarketingEmailsAgreed -> "Customer agreed to receive marketing emails." - Strings.MarketingSMSAgreed -> "Customer agreed to receive SMS marketing text messages." - Strings.DiscardAllUnsavedChangesDesc -> "If you discard changes, you’ll delete any edits you made since you last saved." - Strings.LanguageDesc -> "This customer will receive notifications in this language." - Strings.MarketingDesc -> "You should ask your customers for permission before you subscribe them to your marketing emails or SMS." - Strings.LeavePageWithUnsavedChanges -> "Leave page with unsaved changes?" - Strings.LeavingThisPageWillDiscardAllUnsavedChanges -> "Leaving this page will discard all unsaved changes." - Strings.Ticker -> "ECO-FRIENDLY CLOTHING. 100% COTTON" - Strings.UnitedKingdom -> "United Kingdom" - Strings.ShippingReturns -> "Shipping & Returns" - Strings.WeWillReply -> "We'll reply ASAP." - Strings.HeightDesc -> "The height of the product in the store's current units." - Strings.NoSpamUnsubscribeAnytime -> "No spam, unsubscribe anytime!" - Strings.YouAreSigningUpToReceiveEmails -> "*You're signing up to receive our emails and can unsubscribe at any time." - Strings.EcoFriendlyClothing -> "Eco-Friendly clothing" - Strings.CategoryNameDescription -> "The name of the category." - Strings.HandmadeTraitDescription -> "Handmade products are made by hand, not by machine, and typically by an individual or a small group of people." - Strings.OrganicTraitDescription -> "Organic products are made from materials that are grown without the use of synthetic chemicals or pesticides." - Strings.EcoFriendlyTraitDescription -> "Eco-friendly products are made in a way that is not harmful to the environment." - Strings.VeganTraitDescription -> "Vegan products are made without the use of animal products or by-products." - Strings.SociallyResponsibleTraitDescription -> "Socially responsible products are made in a way that takes into account the social and environmental impact of their production." - Strings.CharitableTraitDescription -> "Charitable products are made in a way that supports a charitable cause." - Strings.CustomTraitDescription -> "Custom products are made to order according to the customer's specifications." - Strings.UniqueTraitDescription -> "Unique products are one-of-a-kind and cannot be found anywhere else." - Strings.TrendingTraitDescription -> "Trending products are currently popular and in high demand." - Strings.PopularTraitDescription -> "Popular products are well-liked and frequently purchased by customers." - Strings.FeaturedTraitDescription -> "Featured products are highlighted and promoted by the store." - Strings.RecommendedTraitDescription -> "Recommended products are suggested to customers based on their preferences and purchase history." - Strings.SpecialTraitDescription -> "Special products are unique or limited-edition items that are not typically found in the store." - Strings.ExclusiveTraitDescription -> "Exclusive products are only available to a select group of customers." - Strings.LimitedTraitDescription -> "Limited products are only available in limited quantities for a limited time." - Strings.NewArrivalTraitDescription -> "New arrival products are recently added to the store's inventory." - Strings.SeasonalTraitDescription -> "Seasonal products are only available during a specific season or time of year." - Strings.VintageTraitDescription -> "Vintage products are old or second-hand items that are considered to be of high quality or value." - Strings.LuxuryTraitDescription -> "Luxury products are high-end and expensive items that are associated with luxury and sophistication." - Strings.CasualTraitDescription -> "Casual products are comfortable and informal items that are suitable for everyday wear." - Strings.FormalTraitDescription -> "Formal products are elegant and sophisticated items that are suitable for special occasions." - Strings.BusinessCasualTraitDescription -> "Business casual products are professional and stylish items that are suitable for a business casual dress code." - Strings.AthleticTraitDescription -> "Athletic products are designed for sports and physical activity." - Strings.OutdoorTraitDescription -> "Outdoor products are designed for outdoor activities and adventures." - Strings.WaterResistantTraitDescription -> "Water-resistant products are designed to repel water and keep the wearer dry." - Strings.InsulatedTraitDescription -> "Insulated products are designed to provide warmth and protection from the cold." - Strings.BreathableTraitDescription -> "Breathable products are made from materials that allow air to pass through and moisture to evaporate." - Strings.StretchTraitDescription -> "Stretch products are made from stretchy materials that provide flexibility and freedom of movement." - Strings.NonIronTraitDescription -> "Non-iron products are made from materials that do not wrinkle or crease easily and require little to no ironing." - Strings.EasyCareTraitDescription -> "Easy-care products are made from materials that are easy to clean and maintain." - Strings.MachineWashableTraitDescription -> "Machine-washable products can be safely washed in a washing machine." - Strings.DryCleanOnlyTraitDescription -> "Dry-clean-only products must be professionally dry cleaned and cannot be washed in a washing machine." - Strings.OnAllOrdersOver -> "On all orders over \$100" - Strings.OnAllUnopenedUnusedItems -> "On all unopened, unused items" - Strings.ShopWithConfidence -> "Shop with confidence" - Strings.PercentSatisfaction -> "99.9% Satisfaction" - Strings.RatedExcellentByCustomers -> "Rated excellent by our customers" - Strings.PlaysuitsAndRompers -> "Playsuits & Rompers" - Strings.LatestLooksDescription1 -> "We are delighted to announce our latest range." - - else -> key.name.enumCapitalized() -} +): String = + when (key) { + Strings.ShopName -> appName + Strings.AppMotto -> "Here you become different from others" + Strings.CompanyName -> companyName + Strings.AppCopyright -> "© $currentYear $companyName" + Strings.AppVersion -> "0.1.0" + Strings.ContinueWithGoogle -> "Continue with Google" + Strings.ContinueWithFacebook -> "Continue with Facebook" + Strings.Or -> "OR" + Strings.ForgotPassword -> "Forgot your password?" + Strings.DontHaveAccount -> "Don't have an account?" + Strings.SignUp -> "Sign Up" + Strings.SignUpWithGoogle -> "Sign up with Google" + Strings.SignUpWithFacebook -> "Sign up with Facebook" + Strings.Newsletter -> "Sign up for news about sales and new arrivals" + Strings.AlreadyHaveAnAccount -> "Already have an account?" + Strings.PrivacyPolicy -> "Privacy Policy" + Strings.And -> "and" + Strings.TermsOfService -> "Terms of Service" + Strings.BySigningUpAgree -> "By signing up, you agree to our" + Strings.ForgotPasswordDescription -> "Wo worries, we'll send recovery link to your email." + Strings.CheckEmailDescription -> "We have sent a password reset link to" + Strings.PromoText -> "Free shipping on orders over £50" + Strings.HelpAndFaq -> "Help & FAQ" + Strings.CurrencyEnUs -> "EN, $" + Strings.AboutUsSmall -> "About us" + Strings.AboutUs -> "About Us" + Strings.ContactUs -> "Contact Us" + Strings.CyberSecurity -> "Cyber Security" + Strings.FAQs -> "FAQs" + Strings.TrackOrder -> "Track Order" + Strings.IsFeaturedDesc -> "Indicates whether or not the product should be featured." + Strings.AllowReviewsDesc -> "Indicates whether or not a product allows reviews." + Strings.CatalogVisibilityDesc -> "Indicates whether or not the product should be visible in the catalog." + Strings.CategoriesDesc -> "The categories this product is in" + Strings.DescriptionDesc -> "The product's full description." + Strings.ParentCategoryDesc -> "The parent category of the product." + Strings.ToStart -> "to start" + Strings.TagsDesc -> "The tags this product has" + Strings.PriceDesc -> "The current price of the product." + Strings.StockStatusDesc -> "The product's stock status." + Strings.PostStatusDesc -> "The product's current post status." + Strings.IsPurchasableDesc -> "Indicates whether or not the product is currently able to be purchased." + Strings.OnePerOrderDesc -> "Indicates that only one of a product may be held in the order at a time." + Strings.UnsavedChanges -> "Careful! You have unsaved changes!" + Strings.ImagesDesc -> "The images for the product." + Strings.BackorderStatusDesc -> "The status of back-ordering for a product." + Strings.CanBackorderDesc -> "Indicates whether or not a product can be back-ordered." + Strings.IsOnBackorderDesc -> "Indicates whether or not a product is on backorder." + Strings.LowStockThresholdDesc -> + "Indicates the threshold for when the low stock notification will be sent to the merchant." + + Strings.RemainingStockDesc -> "The number of inventory units remaining for this product." + Strings.TrackQuantityDesc -> "Indicates that a product should use the inventory system." + Strings.RegularPriceDesc -> "The regular price of the product when not discounted." + Strings.SalePriceDesc -> "The price of the product when on sale." + Strings.OnSaleDesc -> "Indicates whether or not the product is currently on sale." + Strings.SaleStartDesc -> "The GMT datetime when the product should start to be on sale." + Strings.SaleEndDesc -> "The GMT datetime when the product should no longer be on sale." + Strings.LengthDesc -> "The length of the product in the store's current units." + Strings.WeightDesc -> "The weight of the product in the store's current units." + Strings.WidthDesc -> "The width of the product in the store's current units." + Strings.RequiresShippingDesc -> "Indicates that the product must be shipped." + Strings.ShippingPreset -> "Choose preset from currently selected category, or pass custom values." + Strings.Kg -> "kg" + Strings.Cm -> "cm" + Strings.CreatedByDesc -> "The user who created the product." + Strings.ShippingPresetDesc -> "The shipping preset for the product, coming from category shipping settings" + Strings.ImproveWithAi -> "Improve with AI" + Strings.DeleteExplain -> "Are you sure you want to delete this? You cannot undo this action." + Strings.ChargeTaxDesc -> "Indicates whether or not the product should have tax charged on it." + Strings.UseGlobalTrackingDesc -> "Indicates whether or not the product should use the global tracking settings." + Strings.NoInsights -> "Insights will display when the product has had recent sales" + Strings.AddressDesc -> "The primary address of this customer" + Strings.MarketingEmailsAgreed -> "Customer agreed to receive marketing emails." + Strings.MarketingSMSAgreed -> "Customer agreed to receive SMS marketing text messages." + Strings.DiscardAllUnsavedChangesDesc -> + "If you discard changes, you’ll delete any edits you made since you last saved." + + Strings.LanguageDesc -> "This customer will receive notifications in this language." + Strings.MarketingDesc -> + "You should ask your customers for permission before you subscribe them to your marketing emails or SMS." + + Strings.LeavePageWithUnsavedChanges -> "Leave page with unsaved changes?" + Strings.LeavingThisPageWillDiscardAllUnsavedChanges -> "Leaving this page will discard all unsaved changes." + Strings.Ticker -> "ECO-FRIENDLY CLOTHING. 100% COTTON" + Strings.UnitedKingdom -> "United Kingdom" + Strings.ShippingReturns -> "Shipping & Returns" + Strings.WeWillReply -> "We'll reply ASAP." + Strings.HeightDesc -> "The height of the product in the store's current units." + Strings.NoSpamUnsubscribeAnytime -> "No spam, unsubscribe anytime!" + Strings.YouAreSigningUpToReceiveEmails -> + "*You're signing up to receive our emails and can unsubscribe at any time." + + Strings.EcoFriendlyClothing -> "Eco-Friendly clothing" + Strings.CategoryNameDescription -> "The name of the category." + Strings.HandmadeTraitDescription -> + "Handmade products are made by hand, not by machine, and typically by an individual or a small group of people." + + Strings.OrganicTraitDescription -> + "Organic products are made from materials that are grown without the use of synthetic chemicals or pesticides." + + Strings.EcoFriendlyTraitDescription -> + "Eco-friendly products are made in a way that is not harmful to the environment." + + Strings.VeganTraitDescription -> "Vegan products are made without the use of animal products or by-products." + Strings.SociallyResponsibleTraitDescription -> + "Socially responsible products are made in a way that takes into account the social and environmental " + + "impact of their production." + + Strings.CharitableTraitDescription -> "Charitable products are made in a way that supports a charitable cause." + Strings.CustomTraitDescription -> "Custom products are made to order according to the customer's specifications." + Strings.UniqueTraitDescription -> "Unique products are one-of-a-kind and cannot be found anywhere else." + Strings.TrendingTraitDescription -> "Trending products are currently popular and in high demand." + Strings.PopularTraitDescription -> "Popular products are well-liked and frequently purchased by customers." + Strings.FeaturedTraitDescription -> "Featured products are highlighted and promoted by the store." + Strings.RecommendedTraitDescription -> + "Recommended products are suggested to customers based on their preferences and purchase history." + + Strings.SpecialTraitDescription -> + "Special products are unique or limited-edition items that are not typically found in the store." + + Strings.ExclusiveTraitDescription -> "Exclusive products are only available to a select group of customers." + Strings.LimitedTraitDescription -> "Limited products are only available in limited quantities for a limited time." + Strings.NewArrivalTraitDescription -> "New arrival products are recently added to the store's inventory." + Strings.SeasonalTraitDescription -> "Seasonal products are only available during a specific season or time of year." + Strings.VintageTraitDescription -> + "Vintage products are old or second-hand items that are considered to be of high quality or value." + + Strings.LuxuryTraitDescription -> + "Luxury products are high-end and expensive items that are associated with luxury and sophistication." + + Strings.CasualTraitDescription -> + "Casual products are comfortable and informal items that are suitable for everyday wear." + + Strings.FormalTraitDescription -> + "Formal products are elegant and sophisticated items that are suitable for special occasions." + + Strings.BusinessCasualTraitDescription -> + "Business casual products are professional and stylish items that are suitable for a business casual " + + "dress code." + + Strings.AthleticTraitDescription -> "Athletic products are designed for sports and physical activity." + Strings.OutdoorTraitDescription -> "Outdoor products are designed for outdoor activities and adventures." + Strings.WaterResistantTraitDescription -> + "Water-resistant products are designed to repel water and keep the wearer dry." + + Strings.InsulatedTraitDescription -> + "Insulated products are designed to provide warmth and protection from the cold." + + Strings.BreathableTraitDescription -> + "Breathable products are made from materials that allow air to pass through and moisture to evaporate." + + Strings.StretchTraitDescription -> + "Stretch products are made from stretchy materials that provide flexibility and freedom of movement." + + Strings.NonIronTraitDescription -> + "Non-iron products are made from materials that do not wrinkle or crease easily and require little to " + + "no ironing." + + Strings.EasyCareTraitDescription -> + "Easy-care products are made from materials that are easy to clean and maintain." + + Strings.MachineWashableTraitDescription -> + "Machine-washable products can be safely washed in a washing machine." + + Strings.DryCleanOnlyTraitDescription -> + "Dry-clean-only products must be professionally dry cleaned and cannot be washed in a washing machine." + + Strings.OnAllOrdersOver -> "On all orders over \$100" + Strings.OnAllUnopenedUnusedItems -> "On all unopened, unused items" + Strings.ShopWithConfidence -> "Shop with confidence" + Strings.PercentSatisfaction -> "99.9% Satisfaction" + Strings.RatedExcellentByCustomers -> "Rated excellent by our customers" + Strings.PlaysuitsAndRompers -> "Playsuits & Rompers" + Strings.LatestLooksDescription1 -> "We are delighted to announce our latest range." + + else -> key.name.enumCapitalized() + } diff --git a/components/localization/src/iosMain/kotlin/MainViewController.kt b/components/localization/src/iosMain/kotlin/MainViewController.kt index b023d876..5d441e10 100644 --- a/components/localization/src/iosMain/kotlin/MainViewController.kt +++ b/components/localization/src/iosMain/kotlin/MainViewController.kt @@ -23,43 +23,48 @@ import util.WindowInfo @OptIn(ExperimentalForeignApi::class) @Suppress("unused", "FunctionNaming", "FunctionName") fun MainViewController(window: UIWindow): UIViewController { - val koinApplication = initKoin( - additionalModules = listOf( - module { - single { NotifierManager.initialize(NotificationPlatformConfiguration.Ios) } - }, - ), - ) + val koinApplication = + initKoin( + additionalModules = + listOf( + module { + single { NotifierManager.initialize(NotificationPlatformConfiguration.Ios) } + }, + ), + ) - val uiViewController = ComposeUIViewController { - LaunchedEffect(window.safeAreaInsets) { - window.safeAreaInsets.useContents { - safePaddingValues = PaddingValues( - top = this.top.dp, - bottom = this.bottom.dp, - start = this.left.dp, - end = this.right.dp, - ) + val uiViewController = + ComposeUIViewController { + LaunchedEffect(window.safeAreaInsets) { + window.safeAreaInsets.useContents { + safePaddingValues = + PaddingValues( + top = this.top.dp, + bottom = this.bottom.dp, + start = this.left.dp, + end = this.right.dp, + ) + } } - } - val rememberedWindowInfo by remember(window) { - val windowInfo = window.frame.useContents { - WindowInfo(this.size.width.dp, this.size.height.dp) + val rememberedWindowInfo by remember(window) { + val windowInfo = + window.frame.useContents { + WindowInfo(this.size.width.dp, this.size.height.dp) + } + mutableStateOf(windowInfo) } - mutableStateOf(windowInfo) - } - KoinContext(koinApplication.koin) { - CompositionLocalProvider( - LocalWindow provides rememberedWindowInfo, - ) { - AppTheme { - RootContent() + KoinContext(koinApplication.koin) { + CompositionLocalProvider( + LocalWindow provides rememberedWindowInfo, + ) { + AppTheme { + RootContent() + } } } } - } koinApplication.koin.loadModules( listOf( diff --git a/components/location/src/androidMain/kotlin/location/LocationServiceAndroid.kt b/components/location/src/androidMain/kotlin/location/LocationServiceAndroid.kt index 62067539..152a8aa6 100644 --- a/components/location/src/androidMain/kotlin/location/LocationServiceAndroid.kt +++ b/components/location/src/androidMain/kotlin/location/LocationServiceAndroid.kt @@ -99,7 +99,10 @@ internal class LocationServiceAndroid( return location } - override suspend fun getPlaceDetails(latitude: Double, longitude: Double): Location? { + override suspend fun getPlaceDetails( + latitude: Double, + longitude: Double, + ): Location? { logger.v { "Getting place details for $latitude, $longitude" } val geocoder = Geocoder(context, Locale.ENGLISH) diff --git a/components/location/src/androidMain/kotlin/location/PlatformModule.kt b/components/location/src/androidMain/kotlin/location/PlatformModule.kt index f4fb9b87..5f9cc754 100644 --- a/components/location/src/androidMain/kotlin/location/PlatformModule.kt +++ b/components/location/src/androidMain/kotlin/location/PlatformModule.kt @@ -4,12 +4,13 @@ import co.touchlab.kermit.Logger.Companion.withTag import org.koin.core.module.Module import org.koin.dsl.module -actual val locationModule: Module = module { - single { - LocationServiceAndroid( - logger = withTag(LocationService::class.java.simpleName), - context = get(), - activity = inject(), - ) +actual val locationModule: Module = + module { + single { + LocationServiceAndroid( + logger = withTag(LocationService::class.java.simpleName), + context = get(), + activity = inject(), + ) + } } -} diff --git a/components/location/src/commonMain/kotlin/location/LocationService.kt b/components/location/src/commonMain/kotlin/location/LocationService.kt index 740e44bd..ff18677f 100644 --- a/components/location/src/commonMain/kotlin/location/LocationService.kt +++ b/components/location/src/commonMain/kotlin/location/LocationService.kt @@ -6,8 +6,15 @@ import location.models.Location interface LocationService { fun checkPermission(): PermissionStatus + suspend fun requestPermission() + fun openSettingPage() + suspend fun getCurrentLocation(): LatLng? - suspend fun getPlaceDetails(latitude: Double, longitude: Double): Location? + + suspend fun getPlaceDetails( + latitude: Double, + longitude: Double, + ): Location? } diff --git a/components/location/src/iosMain/kotlin/location/LocationServiceIos.kt b/components/location/src/iosMain/kotlin/location/LocationServiceIos.kt index 79aa1b43..4a96a04a 100644 --- a/components/location/src/iosMain/kotlin/location/LocationServiceIos.kt +++ b/components/location/src/iosMain/kotlin/location/LocationServiceIos.kt @@ -33,36 +33,45 @@ internal class LocationServiceIos(private val logger: Logger) : LocationService private var latLngContinuation: Continuation? = null @OptIn(ExperimentalForeignApi::class) - private val clDelegate = object : CLLocationManagerDelegateProtocol, NSObject() { - override fun locationManager(manager: CLLocationManager, didUpdateLocations: List<*>) { - val updatedLatLng = didUpdateLocations.filterIsInstance() - .takeIf { it.isNotEmpty() } - ?.let { - it.first().coordinate.useContents { - LatLng(latitude, longitude) - } + private val clDelegate = + object : CLLocationManagerDelegateProtocol, NSObject() { + override fun locationManager( + manager: CLLocationManager, + didUpdateLocations: List<*>, + ) { + val updatedLatLng = + didUpdateLocations.filterIsInstance() + .takeIf { it.isNotEmpty() } + ?.let { + it.first().coordinate.useContents { + LatLng(latitude, longitude) + } + } + + if (updatedLatLng != null) { + logger.v { "Updated locations $updatedLatLng" } + latLngContinuation?.resume(updatedLatLng) + latLngContinuation = null + manager.stopUpdatingLocation() } + } - if (updatedLatLng != null) { - logger.v { "Updated locations $updatedLatLng" } - latLngContinuation?.resume(updatedLatLng) + override fun locationManager( + manager: CLLocationManager, + didFailWithError: NSError, + ) { + logger.e { "Failed $didFailWithError" } + latLngContinuation?.resumeWithException(Throwable(didFailWithError.description)) latLngContinuation = null - manager.stopUpdatingLocation() } } - override fun locationManager(manager: CLLocationManager, didFailWithError: NSError) { - logger.e { "Failed $didFailWithError" } - latLngContinuation?.resumeWithException(Throwable(didFailWithError.description)) - latLngContinuation = null + private val clLocationManager = + CLLocationManager().apply { + desiredAccuracy = kCLLocationAccuracyBest + distanceFilter = kCLDistanceFilterNone + delegate = clDelegate } - } - - private val clLocationManager = CLLocationManager().apply { - desiredAccuracy = kCLLocationAccuracyBest - distanceFilter = kCLDistanceFilterNone - delegate = clDelegate - } override suspend fun getCurrentLocation(): LatLng? { logger.v { "Getting location" } @@ -77,7 +86,10 @@ internal class LocationServiceIos(private val logger: Logger) : LocationService } } - override suspend fun getPlaceDetails(latitude: Double, longitude: Double): Location? { + override suspend fun getPlaceDetails( + latitude: Double, + longitude: Double, + ): Location? { val geocoder = CLGeocoder() val location = CLLocation(latitude = latitude, longitude = longitude) diff --git a/components/location/src/iosMain/kotlin/location/PlatformModule.kt b/components/location/src/iosMain/kotlin/location/PlatformModule.kt index e6886e47..8adc234b 100644 --- a/components/location/src/iosMain/kotlin/location/PlatformModule.kt +++ b/components/location/src/iosMain/kotlin/location/PlatformModule.kt @@ -4,10 +4,11 @@ import co.touchlab.kermit.Logger.Companion.withTag import org.koin.core.module.Module import org.koin.dsl.module -actual val locationModule: Module = module { - single(createdAtStart = true) { - LocationServiceIos( - logger = withTag(LocationService::class.simpleName!!), - ) +actual val locationModule: Module = + module { + single(createdAtStart = true) { + LocationServiceIos( + logger = withTag(LocationService::class.simpleName!!), + ) + } } -} diff --git a/components/location/src/jsMain/kotlin/location/LocationServiceJs.kt b/components/location/src/jsMain/kotlin/location/LocationServiceJs.kt index bdc7164d..4d0c0b96 100644 --- a/components/location/src/jsMain/kotlin/location/LocationServiceJs.kt +++ b/components/location/src/jsMain/kotlin/location/LocationServiceJs.kt @@ -16,8 +16,10 @@ internal class LocationServiceJs(private val logger: Logger) : LocationService { return null } - override suspend fun getPlaceDetails(latitude: Double, longitude: Double): Location? { - + override suspend fun getPlaceDetails( + latitude: Double, + longitude: Double, + ): Location? { return null } diff --git a/components/location/src/jsMain/kotlin/location/PlatformModule.kt b/components/location/src/jsMain/kotlin/location/PlatformModule.kt index 5137369a..584dd48b 100644 --- a/components/location/src/jsMain/kotlin/location/PlatformModule.kt +++ b/components/location/src/jsMain/kotlin/location/PlatformModule.kt @@ -4,10 +4,11 @@ import co.touchlab.kermit.Logger.Companion.withTag import org.koin.core.module.Module import org.koin.dsl.module -actual val locationModule: Module = module { - single(createdAtStart = true) { - LocationServiceJs( - logger = withTag(LocationService::class.simpleName!!), - ) +actual val locationModule: Module = + module { + single(createdAtStart = true) { + LocationServiceJs( + logger = withTag(LocationService::class.simpleName!!), + ) + } } -} diff --git a/components/notification/src/androidMain/kotlin/notification/NotificationPublisher.kt b/components/notification/src/androidMain/kotlin/notification/NotificationPublisher.kt index 8361ecf1..4193dae0 100644 --- a/components/notification/src/androidMain/kotlin/notification/NotificationPublisher.kt +++ b/components/notification/src/androidMain/kotlin/notification/NotificationPublisher.kt @@ -8,7 +8,10 @@ import android.content.Intent import org.koin.core.component.KoinComponent internal class NotificationPublisher : BroadcastReceiver(), KoinComponent { - override fun onReceive(context: Context, intent: Intent) { + override fun onReceive( + context: Context, + intent: Intent, + ) { val notificationsManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val notificationId = @@ -19,7 +22,7 @@ internal class NotificationPublisher : BroadcastReceiver(), KoinComponent { @Suppress("DEPRECATION") val notification = intent.getParcelableExtra( - NotificationServiceAndroid.NOTIFICATION_PAYLOAD_NOTIFICATION + NotificationServiceAndroid.NOTIFICATION_PAYLOAD_NOTIFICATION, ) if (notificationType == NotificationServiceAndroid.NOTIFICATION_TYPE_DISMISS) { diff --git a/components/notification/src/androidMain/kotlin/notification/NotificationServiceAndroid.kt b/components/notification/src/androidMain/kotlin/notification/NotificationServiceAndroid.kt index 392e0a61..e4ea99fc 100644 --- a/components/notification/src/androidMain/kotlin/notification/NotificationServiceAndroid.kt +++ b/components/notification/src/androidMain/kotlin/notification/NotificationServiceAndroid.kt @@ -33,7 +33,9 @@ internal class NotificationServiceAndroid( private val localNotificationPermissions: List = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { listOf(Manifest.permission.POST_NOTIFICATIONS) - } else emptyList() + } else { + emptyList() + } init { val importance = NotificationManager.IMPORTANCE_DEFAULT @@ -64,7 +66,6 @@ internal class NotificationServiceAndroid( context.openAppSettingsPage(Permission.LOCAL_NOTIFICATIONS) } - @SuppressLint("MissingPermission") override suspend fun schedule(notificationType: NotificationType) { if (checkPermission().isNotGranted()) { @@ -75,10 +76,11 @@ internal class NotificationServiceAndroid( when (notificationType) { is NotificationType.Immediate -> { val id = Random.nextInt() - val notification = sendNotification( - title = notificationType.title, - body = notificationType.body, - ) + val notification = + sendNotification( + title = notificationType.title, + body = notificationType.body, + ) notificationManager.notify(id, notification) } @@ -106,20 +108,21 @@ internal class NotificationServiceAndroid( title: String, body: String?, ): Notification { - val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).apply { - setSmallIcon(R.drawable.ic_baseline_directions_car_24) - setContentTitle(title) - setContentText(body) - priority = NotificationManagerCompat.IMPORTANCE_HIGH - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - setCategory(Notification.CATEGORY_NAVIGATION) // shows the notification immediately - } else { - setCategory(Notification.CATEGORY_SERVICE) + val builder = + NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).apply { + setSmallIcon(R.drawable.ic_baseline_directions_car_24) + setContentTitle(title) + setContentText(body) + priority = NotificationManagerCompat.IMPORTANCE_HIGH + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + setCategory(Notification.CATEGORY_NAVIGATION) // shows the notification immediately + } else { + setCategory(Notification.CATEGORY_SERVICE) + } + setOngoing(true) + setOnlyAlertOnce(true) + setWhen(currentTimeMillis()) } - setOngoing(true) - setOnlyAlertOnce(true) - setWhen(currentTimeMillis()) - } logger.v { "Scheduling local notification." } return builder.build() @@ -129,9 +132,10 @@ internal class NotificationServiceAndroid( id: Int, intentTransform: Intent.() -> Unit = {}, ): PendingIntent { - val intent = IdentifiableIntent("$id", context, NotificationPublisher::class.java).apply( - intentTransform, - ) + val intent = + IdentifiableIntent("$id", context, NotificationPublisher::class.java).apply( + intentTransform, + ) return PendingIntent.getBroadcast( context, id, diff --git a/components/notification/src/androidMain/kotlin/notification/PlatformModule.kt b/components/notification/src/androidMain/kotlin/notification/PlatformModule.kt index 9af1a330..750b0582 100644 --- a/components/notification/src/androidMain/kotlin/notification/PlatformModule.kt +++ b/components/notification/src/androidMain/kotlin/notification/PlatformModule.kt @@ -7,20 +7,21 @@ import co.touchlab.kermit.Logger.Companion.withTag import org.koin.core.module.Module import org.koin.dsl.module -actual val notificationModule: Module = module { - single { - get().getSystemService(Context.ALARM_SERVICE) as AlarmManager +actual val notificationModule: Module = + module { + single { + get().getSystemService(Context.ALARM_SERVICE) as AlarmManager + } + single { + get().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + single { + NotificationServiceAndroid( + logger = withTag(NotificationService::class.java.simpleName), + context = get(), + activity = inject(), + notificationManager = get(), + alarmManager = get(), + ) + } } - single { - get().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - } - single { - NotificationServiceAndroid( - logger = withTag(NotificationService::class.java.simpleName), - context = get(), - activity = inject(), - notificationManager = get(), - alarmManager = get(), - ) - } -} diff --git a/components/notification/src/commonMain/kotlin/notification/NotificationService.kt b/components/notification/src/commonMain/kotlin/notification/NotificationService.kt index 82628432..30854d64 100644 --- a/components/notification/src/commonMain/kotlin/notification/NotificationService.kt +++ b/components/notification/src/commonMain/kotlin/notification/NotificationService.kt @@ -4,8 +4,12 @@ import core.models.PermissionStatus interface NotificationService { suspend fun checkPermission(): PermissionStatus + suspend fun requestPermission() + fun openSettingPage() + suspend fun schedule(notificationType: NotificationType) + suspend fun cancelNotification(ids: List) } diff --git a/components/notification/src/commonMain/kotlin/notification/NotificationType.kt b/components/notification/src/commonMain/kotlin/notification/NotificationType.kt index 24e29783..b9269cef 100644 --- a/components/notification/src/commonMain/kotlin/notification/NotificationType.kt +++ b/components/notification/src/commonMain/kotlin/notification/NotificationType.kt @@ -20,5 +20,5 @@ sealed interface NotificationType { enum class SoundType { DEFAULT, CRITICAL, - NONE + NONE, } diff --git a/components/notification/src/iosMain/kotlin/notification/NotificationServiceIos.kt b/components/notification/src/iosMain/kotlin/notification/NotificationServiceIos.kt index 962d688c..766d6a79 100644 --- a/components/notification/src/iosMain/kotlin/notification/NotificationServiceIos.kt +++ b/components/notification/src/iosMain/kotlin/notification/NotificationServiceIos.kt @@ -40,15 +40,16 @@ import platform.darwin.NSObject import kotlin.coroutines.suspendCoroutine internal class NotificationServiceIos(private val logger: Logger) : NotificationService { - private val delegate = object : UNUserNotificationCenterDelegateProtocol, NSObject() { - override fun userNotificationCenter( - center: UNUserNotificationCenter, - willPresentNotification: UNNotification, - withCompletionHandler: (UNNotificationPresentationOptions) -> Unit, - ) { - withCompletionHandler(UNNotificationPresentationOptionAlert) + private val delegate = + object : UNUserNotificationCenterDelegateProtocol, NSObject() { + override fun userNotificationCenter( + center: UNUserNotificationCenter, + willPresentNotification: UNNotification, + withCompletionHandler: (UNNotificationPresentationOptions) -> Unit, + ) { + withCompletionHandler(UNNotificationPresentationOptionAlert) + } } - } private val notificationCenter: UNUserNotificationCenter by lazy { UNUserNotificationCenter.currentNotificationCenter().apply { @@ -57,17 +58,18 @@ internal class NotificationServiceIos(private val logger: Logger) : Notification } override suspend fun checkPermission(): PermissionStatus { - val status = suspendCoroutine { continuation -> - notificationCenter.getNotificationSettingsWithCompletionHandler( - mainContinuation { settings: UNNotificationSettings? -> - continuation.resumeWith( - Result.success( - settings?.authorizationStatus ?: UNAuthorizationStatusNotDetermined, - ), - ) - }, - ) - } + val status = + suspendCoroutine { continuation -> + notificationCenter.getNotificationSettingsWithCompletionHandler( + mainContinuation { settings: UNNotificationSettings? -> + continuation.resumeWith( + Result.success( + settings?.authorizationStatus ?: UNAuthorizationStatusNotDetermined, + ), + ) + }, + ) + } return when (status) { UNAuthorizationStatusAuthorized, UNAuthorizationStatusProvisional, @@ -81,32 +83,34 @@ internal class NotificationServiceIos(private val logger: Logger) : Notification } override suspend fun requestPermission() { - val status: UNAuthorizationStatus = suspendCoroutine { continuation -> - notificationCenter.getNotificationSettingsWithCompletionHandler( - mainContinuation { settings: UNNotificationSettings? -> - continuation.resumeWith( - Result.success(settings?.authorizationStatus ?: UNAuthorizationStatusNotDetermined), - ) - }, - ) - } + val status: UNAuthorizationStatus = + suspendCoroutine { continuation -> + notificationCenter.getNotificationSettingsWithCompletionHandler( + mainContinuation { settings: UNNotificationSettings? -> + continuation.resumeWith( + Result.success(settings?.authorizationStatus ?: UNAuthorizationStatusNotDetermined), + ) + }, + ) + } when (status) { UNAuthorizationStatusAuthorized -> return UNAuthorizationStatusNotDetermined -> { - val isSuccess = suspendCoroutine { continuation -> - UNUserNotificationCenter.currentNotificationCenter().requestAuthorizationWithOptions( - UNAuthorizationOptionSound - .or(UNAuthorizationOptionAlert) - .or(UNAuthorizationOptionBadge), - mainContinuation { isOk, error -> - if (isOk && error == null) { - continuation.resumeWith(Result.success(true)) - } else { - continuation.resumeWith(Result.success(false)) - } - }, - ) - } + val isSuccess = + suspendCoroutine { continuation -> + UNUserNotificationCenter.currentNotificationCenter().requestAuthorizationWithOptions( + UNAuthorizationOptionSound + .or(UNAuthorizationOptionAlert) + .or(UNAuthorizationOptionBadge), + mainContinuation { isOk, error -> + if (isOk && error == null) { + continuation.resumeWith(Result.success(true)) + } else { + continuation.resumeWith(Result.success(false)) + } + }, + ) + } if (isSuccess) { requestPermission() } else { @@ -128,10 +132,11 @@ internal class NotificationServiceIos(private val logger: Logger) : Notification val timeNow = Clock.System.now() when (notificationType) { is NotificationType.Immediate -> { - val trigger = UNTimeIntervalNotificationTrigger.triggerWithTimeInterval( - 0.1, - false, - ) + val trigger = + UNTimeIntervalNotificationTrigger.triggerWithTimeInterval( + 0.1, + false, + ) sendNotification( title = notificationType.title, body = notificationType.body, @@ -141,19 +146,21 @@ internal class NotificationServiceIos(private val logger: Logger) : Notification } is NotificationType.Scheduled -> { - val allUnits = NSCalendarUnitSecond or - NSCalendarUnitMinute or - NSCalendarUnitHour or - NSCalendarUnitDay or - NSCalendarUnitMonth or - NSCalendarUnitYear or - NSCalendarUnitTimeZone + val allUnits = + NSCalendarUnitSecond or + NSCalendarUnitMinute or + NSCalendarUnitHour or + NSCalendarUnitDay or + NSCalendarUnitMonth or + NSCalendarUnitYear or + NSCalendarUnitTimeZone val deliveryDate = notificationType.delivery.toNSDate() val dateComponents = NSCalendar.currentCalendar.components(allUnits, deliveryDate) - val trigger = UNCalendarNotificationTrigger.triggerWithDateMatchingComponents( - dateComponents = dateComponents, - repeats = false, - ) + val trigger = + UNCalendarNotificationTrigger.triggerWithDateMatchingComponents( + dateComponents = dateComponents, + repeats = false, + ) sendNotification( title = notificationType.title, body = notificationType.body, @@ -177,17 +184,19 @@ internal class NotificationServiceIos(private val logger: Logger) : Notification soundType: SoundType, trigger: UNNotificationTrigger, ) { - val content = UNMutableNotificationContent().apply { - setTitle(title) - body?.let { setBody(it) } - soundType.toUNNotificationSound()?.let { setSound(it) } - setInterruptionLevel(UNNotificationInterruptionLevel.UNNotificationInterruptionLevelTimeSensitive) - } - val request = UNNotificationRequest.requestWithIdentifier( - identifier = uuid4().toString(), - content = content, - trigger = trigger, - ) + val content = + UNMutableNotificationContent().apply { + setTitle(title) + body?.let { setBody(it) } + soundType.toUNNotificationSound()?.let { setSound(it) } + setInterruptionLevel(UNNotificationInterruptionLevel.UNNotificationInterruptionLevelTimeSensitive) + } + val request = + UNNotificationRequest.requestWithIdentifier( + identifier = uuid4().toString(), + content = content, + trigger = trigger, + ) notificationCenter.addNotificationRequest(request) { error -> if (error == null) { diff --git a/components/notification/src/iosMain/kotlin/notification/PlatformModule.kt b/components/notification/src/iosMain/kotlin/notification/PlatformModule.kt index 8229dc49..56f7109c 100644 --- a/components/notification/src/iosMain/kotlin/notification/PlatformModule.kt +++ b/components/notification/src/iosMain/kotlin/notification/PlatformModule.kt @@ -4,10 +4,11 @@ import co.touchlab.kermit.Logger.Companion.withTag import org.koin.core.module.Module import org.koin.dsl.module -actual val notificationModule: Module = module { - single(createdAtStart = true) { - NotificationServiceIos( - logger = withTag(NotificationService::class.simpleName!!), - ) +actual val notificationModule: Module = + module { + single(createdAtStart = true) { + NotificationServiceIos( + logger = withTag(NotificationService::class.simpleName!!), + ) + } } -} diff --git a/components/notification/src/jsMain/kotlin/notification/NotificationServiceJs.kt b/components/notification/src/jsMain/kotlin/notification/NotificationServiceJs.kt index ada886cc..a64dd938 100644 --- a/components/notification/src/jsMain/kotlin/notification/NotificationServiceJs.kt +++ b/components/notification/src/jsMain/kotlin/notification/NotificationServiceJs.kt @@ -23,5 +23,4 @@ internal class NotificationServiceJs(private val logger: Logger) : NotificationS override suspend fun cancelNotification(ids: List) { logger.v { "Cancelling notification with IDs: [${ids.joinToString()}]" } } - } diff --git a/components/notification/src/jsMain/kotlin/notification/PlatformModule.kt b/components/notification/src/jsMain/kotlin/notification/PlatformModule.kt index 7c11dfdc..82311612 100644 --- a/components/notification/src/jsMain/kotlin/notification/PlatformModule.kt +++ b/components/notification/src/jsMain/kotlin/notification/PlatformModule.kt @@ -4,10 +4,11 @@ import co.touchlab.kermit.Logger.Companion.withTag import org.koin.core.module.Module import org.koin.dsl.module -actual val notificationModule: Module = module { - single(createdAtStart = true) { - NotificationServiceJs( - logger = withTag(NotificationService::class.simpleName!!), - ) +actual val notificationModule: Module = + module { + single(createdAtStart = true) { + NotificationServiceJs( + logger = withTag(NotificationService::class.simpleName!!), + ) + } } -} diff --git a/components/pictures/src/androidMain/kotlin/component/pictures/ImagePickerFragment.kt b/components/pictures/src/androidMain/kotlin/component/pictures/ImagePickerFragment.kt index 687d8df4..43b1230c 100644 --- a/components/pictures/src/androidMain/kotlin/component/pictures/ImagePickerFragment.kt +++ b/components/pictures/src/androidMain/kotlin/component/pictures/ImagePickerFragment.kt @@ -52,13 +52,14 @@ class ImagePickerFragment : Fragment(), KoinComponent { codeCallbackMap[requestCode] = CallbackData.Gallery( - callback + callback, ) - val intent = Intent( - Intent.ACTION_PICK, - MediaStore.Images.Media.EXTERNAL_CONTENT_URI - ) + val intent = + Intent( + Intent.ACTION_PICK, + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + ) startActivityForResult(intent, requestCode) } @@ -69,11 +70,12 @@ class ImagePickerFragment : Fragment(), KoinComponent { codeCallbackMap[requestCode] = CallbackData.Camera( callback, - outputUri + outputUri, ) - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - .putExtra(MediaStore.EXTRA_OUTPUT, outputUri) + val intent = + Intent(MediaStore.ACTION_IMAGE_CAPTURE) + .putExtra(MediaStore.EXTRA_OUTPUT, outputUri) startActivityForResult(intent, requestCode) } @@ -85,11 +87,15 @@ class ImagePickerFragment : Fragment(), KoinComponent { return FileProvider.getUriForFile( androidContext, androidContext.applicationContext.packageName + FILE_PROVIDER_SUFFIX, - tmpFile + tmpFile, ) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + override fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent?, + ) { super.onActivityResult(requestCode, resultCode, data) val callbackData = codeCallbackMap[requestCode] ?: return @@ -125,29 +131,32 @@ class ImagePickerFragment : Fragment(), KoinComponent { ) { val contentResolver = androidContext.contentResolver - val bitmapOptions = contentResolver.openInputStream(uri)?.use { - BitmapUtils.getBitmapOptionsFromStream(it) - } ?: run { - callback.invoke(Result.failure(Exception("No access to the file: $uri"))) - return - } + val bitmapOptions = + contentResolver.openInputStream(uri)?.use { + BitmapUtils.getBitmapOptionsFromStream(it) + } ?: run { + callback.invoke(Result.failure(Exception("No access to the file: $uri"))) + return + } val sampleSize = BitmapUtils.calculateInSampleSize(bitmapOptions, maxImageWidth, maxImageHeight) - val orientation = contentResolver.openInputStream(uri)?.use { - BitmapUtils.getBitmapOrientation(it) - } ?: run { - callback.invoke(Result.failure(Exception("No access to the file: $uri"))) - return - } + val orientation = + contentResolver.openInputStream(uri)?.use { + BitmapUtils.getBitmapOrientation(it) + } ?: run { + callback.invoke(Result.failure(Exception("No access to the file: $uri"))) + return + } - val bitmap = contentResolver.openInputStream(uri)?.use { - BitmapUtils.getNormalizedBitmap(it, orientation, sampleSize) - } ?: run { - callback.invoke(Result.failure(Exception("No access to the file: $uri"))) - return - } + val bitmap = + contentResolver.openInputStream(uri)?.use { + BitmapUtils.getNormalizedBitmap(it, orientation, sampleSize) + } ?: run { + callback.invoke(Result.failure(Exception("No access to the file: $uri"))) + return + } callback.invoke(Result.success(bitmap)) } @@ -170,12 +179,16 @@ class ImagePickerFragment : Fragment(), KoinComponent { private const val ARG_IMG_MAX_WIDTH = "args_img_max_width" private const val ARG_IMG_MAX_HEIGHT = "args_img_max_height" - fun newInstance(maxWidth: Int, maxHeight: Int): ImagePickerFragment { + fun newInstance( + maxWidth: Int, + maxHeight: Int, + ): ImagePickerFragment { val pickerFragment = ImagePickerFragment() - pickerFragment.arguments = Bundle().apply { - putInt(ARG_IMG_MAX_WIDTH, maxWidth) - putInt(ARG_IMG_MAX_HEIGHT, maxHeight) - } + pickerFragment.arguments = + Bundle().apply { + putInt(ARG_IMG_MAX_WIDTH, maxWidth) + putInt(ARG_IMG_MAX_HEIGHT, maxHeight) + } return pickerFragment } } diff --git a/components/pictures/src/androidMain/kotlin/component/pictures/MediaPickerControllerAndroid.kt b/components/pictures/src/androidMain/kotlin/component/pictures/MediaPickerControllerAndroid.kt index 7b947f1e..e84335ee 100644 --- a/components/pictures/src/androidMain/kotlin/component/pictures/MediaPickerControllerAndroid.kt +++ b/components/pictures/src/androidMain/kotlin/component/pictures/MediaPickerControllerAndroid.kt @@ -29,23 +29,24 @@ internal class MediaPickerControllerAndroid( return null } - - val imagePickerFragment: ImagePickerFragment = withContext(Dispatchers.Main) { - ImagePickerFragment.newInstance(1024, 1024).also { - fragmentManager!! - .beginTransaction() - .add(it, this::class.simpleName) - .commitNow() + val imagePickerFragment: ImagePickerFragment = + withContext(Dispatchers.Main) { + ImagePickerFragment.newInstance(1024, 1024).also { + fragmentManager!! + .beginTransaction() + .add(it, this::class.simpleName) + .commitNow() + } } - } - val bitmap = suspendCoroutine { continuation -> - val action: (Result) -> Unit = { continuation.resumeWith(it) } - when (source) { - MediaSource.GALLERY -> imagePickerFragment.pickGalleryImage(action) - MediaSource.CAMERA -> imagePickerFragment.pickCameraImage(action) + val bitmap = + suspendCoroutine { continuation -> + val action: (Result) -> Unit = { continuation.resumeWith(it) } + when (source) { + MediaSource.GALLERY -> imagePickerFragment.pickGalleryImage(action) + MediaSource.CAMERA -> imagePickerFragment.pickCameraImage(action) + } } - } return Bitmap(bitmap) } @@ -53,13 +54,14 @@ internal class MediaPickerControllerAndroid( private fun bind() { this.fragmentManager = activity.value.fragmentManager - val observer = object : LifecycleObserver { - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroyed(source: LifecycleOwner) { - this@MediaPickerControllerAndroid.fragmentManager = null - source.lifecycle.removeObserver(this) + val observer = + object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun onDestroyed(source: LifecycleOwner) { + this@MediaPickerControllerAndroid.fragmentManager = null + source.lifecycle.removeObserver(this) + } } - } MainScope().launch { activity.value.lifecycle.addObserver(observer) } diff --git a/components/pictures/src/androidMain/kotlin/component/pictures/PicturesServiceAndroid.kt b/components/pictures/src/androidMain/kotlin/component/pictures/PicturesServiceAndroid.kt index 1051e6f2..267bb9bf 100644 --- a/components/pictures/src/androidMain/kotlin/component/pictures/PicturesServiceAndroid.kt +++ b/components/pictures/src/androidMain/kotlin/component/pictures/PicturesServiceAndroid.kt @@ -26,7 +26,7 @@ class PicturesServiceAndroid( context = context, activity = activity, logger = logger, - permissions = galleryCompat() + permissions = galleryCompat(), ) } @@ -35,7 +35,7 @@ class PicturesServiceAndroid( context = context, activity = activity, logger = logger, - permissions = listOf(Manifest.permission.CAMERA) + permissions = listOf(Manifest.permission.CAMERA), ) } @@ -65,7 +65,7 @@ private fun galleryCompat() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { listOf( Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO + Manifest.permission.READ_MEDIA_VIDEO, ) } else { listOf(Manifest.permission.READ_EXTERNAL_STORAGE) diff --git a/components/pictures/src/androidMain/kotlin/component/pictures/PlatformModule.kt b/components/pictures/src/androidMain/kotlin/component/pictures/PlatformModule.kt index ae593c1f..47a7e336 100644 --- a/components/pictures/src/androidMain/kotlin/component/pictures/PlatformModule.kt +++ b/components/pictures/src/androidMain/kotlin/component/pictures/PlatformModule.kt @@ -5,29 +5,30 @@ import org.koin.core.qualifier.named import org.koin.dsl.module @Suppress("unused") -actual val picturesModule = module { - single { - MediaPickerControllerAndroid( - logger = withTag(MediaPickerController::class.simpleName!!), - activity = inject(), - ) - } - single { - PicturesServiceAndroid( - logger = withTag(PicturesService::class.simpleName!!), - context = get(), - activity = inject(), - mediaPickerController = get(), - cameraPermissionResultLauncher = get(named(CAMERA_PERMISSION_LAUNCHER)), - ) - } - single(named(ON_CAMERA_PERMISSION_GRANTED)) { - // here we need to handle the result of the camera permission request - // basically open up the camera +actual val picturesModule = + module { + single { + MediaPickerControllerAndroid( + logger = withTag(MediaPickerController::class.simpleName!!), + activity = inject(), + ) + } + single { + PicturesServiceAndroid( + logger = withTag(PicturesService::class.simpleName!!), + context = get(), + activity = inject(), + mediaPickerController = get(), + cameraPermissionResultLauncher = get(named(CAMERA_PERMISSION_LAUNCHER)), + ) + } + single(named(ON_CAMERA_PERMISSION_GRANTED)) { + // here we need to handle the result of the camera permission request + // basically open up the camera - { println("Camera permission granted. This method is injected from Koin") } + { println("Camera permission granted. This method is injected from Koin") } + } } -} const val ON_CAMERA_PERMISSION_GRANTED = "ON_CAM_PERMISSION_GRANTED" const val CAMERA_PERMISSION_LAUNCHER = "CAMERA_PERMISSION_LAUNCHER" diff --git a/components/pictures/src/androidMain/kotlin/component/pictures/model/Bitmap.kt b/components/pictures/src/androidMain/kotlin/component/pictures/model/Bitmap.kt index 83d7319b..88be8440 100644 --- a/components/pictures/src/androidMain/kotlin/component/pictures/model/Bitmap.kt +++ b/components/pictures/src/androidMain/kotlin/component/pictures/model/Bitmap.kt @@ -27,7 +27,9 @@ actual class Bitmap constructor( interface Delegate { fun getAndroidBitmap(): android.graphics.Bitmap + fun getByteArray(): ByteArray + fun getResized(maxSize: Int): Bitmap } @@ -41,7 +43,7 @@ actual class Bitmap constructor( bitmap.compress( android.graphics.Bitmap.CompressFormat.PNG, 100, - byteArrayOutputStream + byteArrayOutputStream, ) return byteArrayOutputStream.toByteArray() } diff --git a/components/pictures/src/androidMain/kotlin/component/pictures/util/BitmapUtils.kt b/components/pictures/src/androidMain/kotlin/component/pictures/util/BitmapUtils.kt index 8918cdad..4d5b9b4d 100644 --- a/components/pictures/src/androidMain/kotlin/component/pictures/util/BitmapUtils.kt +++ b/components/pictures/src/androidMain/kotlin/component/pictures/util/BitmapUtils.kt @@ -22,15 +22,19 @@ object BitmapUtils { orientation: Int, sampleSize: Int? = null, ): Bitmap { - val bitmapOptions = if (sampleSize != null) { - BitmapFactory.Options().apply { - inJustDecodeBounds = false - inSampleSize = sampleSize + val bitmapOptions = + if (sampleSize != null) { + BitmapFactory.Options().apply { + inJustDecodeBounds = false + inSampleSize = sampleSize + } + } else { + null } - } else null - val bitmap = BitmapFactory.decodeStream(bitmapStream, null, bitmapOptions) - ?: throw IOException("Can't decode bitmap stream") + val bitmap = + BitmapFactory.decodeStream(bitmapStream, null, bitmapOptions) + ?: throw IOException("Can't decode bitmap stream") val matrix = Matrix() when (orientation) { @@ -56,7 +60,10 @@ object BitmapUtils { return result } - fun getResizedBitmap(bitmap: Bitmap, maxSize: Int = DEFAULT_MAX_SIZE): Bitmap { + fun getResizedBitmap( + bitmap: Bitmap, + maxSize: Int = DEFAULT_MAX_SIZE, + ): Bitmap { var width = bitmap.width var height = bitmap.height @@ -71,9 +78,7 @@ object BitmapUtils { return Bitmap.createScaledBitmap(bitmap, width, height, true) } - fun getBitmapOptionsFromStream( - inputStream: InputStream, - ): BitmapFactory.Options { + fun getBitmapOptionsFromStream(inputStream: InputStream): BitmapFactory.Options { return BitmapFactory.Options().apply { inJustDecodeBounds = true BitmapFactory.decodeStream(inputStream, null, this) @@ -90,7 +95,6 @@ object BitmapUtils { var inSampleSize = 1 if (height > maxHeight || width > maxWidth) { - val halfHeight: Int = height / 2 val halfWidth: Int = width / 2 diff --git a/components/pictures/src/commonMain/kotlin/component/pictures/PicturesService.kt b/components/pictures/src/commonMain/kotlin/component/pictures/PicturesService.kt index cc14245d..8ff6afb0 100644 --- a/components/pictures/src/commonMain/kotlin/component/pictures/PicturesService.kt +++ b/components/pictures/src/commonMain/kotlin/component/pictures/PicturesService.kt @@ -6,9 +6,14 @@ import core.models.PermissionStatus interface PicturesService { fun checkGalleryPermission(): PermissionStatus + fun checkCameraPermission(): PermissionStatus + suspend fun requestGalleryPermission() + suspend fun requestCameraPermission() + fun openSettings() + suspend fun pickImage(source: MediaSource): Bitmap? } diff --git a/components/pictures/src/commonMain/kotlin/component/pictures/model/Bitmap.kt b/components/pictures/src/commonMain/kotlin/component/pictures/model/Bitmap.kt index 84d45423..fa86c3ba 100644 --- a/components/pictures/src/commonMain/kotlin/component/pictures/model/Bitmap.kt +++ b/components/pictures/src/commonMain/kotlin/component/pictures/model/Bitmap.kt @@ -2,13 +2,14 @@ package component.pictures.model expect class Bitmap { fun toByteArray(): ByteArray + fun toBase64(): String + fun toBase64WithCompress(maxSize: Int): String } private const val BASE64_IMAGE_MIME_PREFIX = "data:image/png;base64," -fun Bitmap.toBase64WithCompressMIME(maxSize: Int) = - "$BASE64_IMAGE_MIME_PREFIX${toBase64WithCompress(maxSize)}" +fun Bitmap.toBase64WithCompressMIME(maxSize: Int) = "$BASE64_IMAGE_MIME_PREFIX${toBase64WithCompress(maxSize)}" fun Bitmap.toBase64MIME() = "$BASE64_IMAGE_MIME_PREFIX${toBase64()}" diff --git a/components/pictures/src/commonMain/kotlin/component/pictures/model/MediaSource.kt b/components/pictures/src/commonMain/kotlin/component/pictures/model/MediaSource.kt index 585fd287..e4cfe82c 100644 --- a/components/pictures/src/commonMain/kotlin/component/pictures/model/MediaSource.kt +++ b/components/pictures/src/commonMain/kotlin/component/pictures/model/MediaSource.kt @@ -2,5 +2,5 @@ package component.pictures.model enum class MediaSource { GALLERY, - CAMERA + CAMERA, } diff --git a/components/pictures/src/commonMain/kotlin/component/pictures/model/MediaType.kt b/components/pictures/src/commonMain/kotlin/component/pictures/model/MediaType.kt index e74d01b5..e25aaf76 100644 --- a/components/pictures/src/commonMain/kotlin/component/pictures/model/MediaType.kt +++ b/components/pictures/src/commonMain/kotlin/component/pictures/model/MediaType.kt @@ -2,5 +2,5 @@ package component.pictures.model enum class MediaType { PHOTO, - VIDEO + VIDEO, } diff --git a/components/pictures/src/iosMain/kotlin/component/pictures/ImagePickerDelegateToContinuation.kt b/components/pictures/src/iosMain/kotlin/component/pictures/ImagePickerDelegateToContinuation.kt index 96de21d7..ad574bd2 100644 --- a/components/pictures/src/iosMain/kotlin/component/pictures/ImagePickerDelegateToContinuation.kt +++ b/components/pictures/src/iosMain/kotlin/component/pictures/ImagePickerDelegateToContinuation.kt @@ -26,7 +26,6 @@ import kotlin.random.Random internal class ImagePickerDelegateToContinuation( private val continuation: Continuation, ) : NSObject(), UINavigationControllerDelegateProtocol, UIImagePickerControllerDelegateProtocol { - override fun imagePickerControllerDidCancel(picker: UIImagePickerController) { picker.dismissModalViewControllerAnimated(true) continuation.resumeWith(Result.failure(Exception("Image picker cancelled"))) @@ -36,70 +35,82 @@ internal class ImagePickerDelegateToContinuation( picker: UIImagePickerController, didFinishPickingMediaWithInfo: Map, ) { - val image = didFinishPickingMediaWithInfo[UIImagePickerControllerEditedImage] as? UIImage - ?: didFinishPickingMediaWithInfo[UIImagePickerControllerOriginalImage] as? UIImage - val mediaUrl = didFinishPickingMediaWithInfo[UIImagePickerControllerMediaURL] as? NSURL - ?: didFinishPickingMediaWithInfo[UIImagePickerControllerImageURL] as? NSURL + val image = + didFinishPickingMediaWithInfo[UIImagePickerControllerEditedImage] as? UIImage + ?: didFinishPickingMediaWithInfo[UIImagePickerControllerOriginalImage] as? UIImage + val mediaUrl = + didFinishPickingMediaWithInfo[UIImagePickerControllerMediaURL] as? NSURL + ?: didFinishPickingMediaWithInfo[UIImagePickerControllerImageURL] as? NSURL val mediaType = didFinishPickingMediaWithInfo[UIImagePickerControllerMediaType] as? String picker.dismissViewControllerAnimated(true) {} - val type = when (mediaType) { - MediaPickerControllerIos.kMovieType, - MediaPickerControllerIos.kVideoType, - -> MediaType.VIDEO + val type = + when (mediaType) { + MediaPickerControllerIos.kMovieType, + MediaPickerControllerIos.kVideoType, + -> MediaType.VIDEO - MediaPickerControllerIos.kImageType -> MediaType.PHOTO + MediaPickerControllerIos.kImageType -> MediaType.PHOTO - else -> { - continuation.resumeWith(Result.failure(IllegalArgumentException("unknown type $mediaType"))) - return + else -> { + continuation.resumeWith(Result.failure(IllegalArgumentException("unknown type $mediaType"))) + return + } } - } if (type == MediaType.VIDEO) { if (mediaUrl == null) { - continuation.resumeWith(Result.failure(Exception("No access to the file. Info: $didFinishPickingMediaWithInfo"))) // TODO write some info + continuation.resumeWith( + Result.failure(Exception("No access to the file. Info: $didFinishPickingMediaWithInfo")), + ) // TODO write some info return } val asset = AVURLAsset(uRL = mediaUrl, options = null) - val media = Media( - name = mediaUrl.relativeString, - path = mediaUrl.path.orEmpty(), - preview = Bitmap(fetchThumbnail(videoAsset = asset)), - type = type - ) + val media = + Media( + name = mediaUrl.relativeString, + path = mediaUrl.path.orEmpty(), + preview = Bitmap(fetchThumbnail(videoAsset = asset)), + type = type, + ) continuation.resumeWith(Result.success(media)) } else { if (image == null) { - continuation.resumeWith(Result.failure(Exception("No access to the file. Info: $didFinishPickingMediaWithInfo"))) // TODO write some info + continuation.resumeWith( + Result.failure(Exception("No access to the file. Info: $didFinishPickingMediaWithInfo")), + ) // TODO write some info return } - val media = Media( - name = mediaUrl?.relativeString ?: Random.nextLong().toString(), - path = mediaUrl?.path.orEmpty(), - preview = Bitmap(image), - type = type - ) + val media = + Media( + name = mediaUrl?.relativeString ?: Random.nextLong().toString(), + path = mediaUrl?.path.orEmpty(), + preview = Bitmap(image), + type = type, + ) continuation.resumeWith(Result.success(media)) } } private fun fetchThumbnail(videoAsset: AVAsset): UIImage { - val imageGenerator = AVAssetImageGenerator( - asset = videoAsset - ) + val imageGenerator = + AVAssetImageGenerator( + asset = videoAsset, + ) imageGenerator.appliesPreferredTrackTransform = true - val cgImage = imageGenerator.copyCGImageAtTime( - requestedTime = CMTimeMake( - value = 0, - timescale = 1 - ), - actualTime = null, - error = null - ) + val cgImage = + imageGenerator.copyCGImageAtTime( + requestedTime = + CMTimeMake( + value = 0, + timescale = 1, + ), + actualTime = null, + error = null, + ) return UIImage(cGImage = cgImage) } } diff --git a/components/pictures/src/iosMain/kotlin/component/pictures/MediaPickerControllerIos.kt b/components/pictures/src/iosMain/kotlin/component/pictures/MediaPickerControllerIos.kt index 6591417a..fe01acd3 100644 --- a/components/pictures/src/iosMain/kotlin/component/pictures/MediaPickerControllerIos.kt +++ b/components/pictures/src/iosMain/kotlin/component/pictures/MediaPickerControllerIos.kt @@ -20,42 +20,45 @@ internal class MediaPickerControllerIos( private val logger: Logger, private val getViewController: () -> UIViewController, ) : MediaPickerController { - override suspend fun pickImage(source: MediaSource): Bitmap = try { - withContext(Dispatchers.Main.immediate) { - logger.d { "Picking image from $source." } - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var delegatePtr: ImagePickerDelegateToContinuation? // strong reference to delegate (view controller have weak ref) + override suspend fun pickImage(source: MediaSource): Bitmap = + try { + withContext(Dispatchers.Main.immediate) { + logger.d { "Picking image from $source." } + // strong reference to delegate (view controller have weak ref) + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + var delegatePtr: ImagePickerDelegateToContinuation? - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var presentationDelegate: AdaptivePresentationDelegateToContinuation? + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + var presentationDelegate: AdaptivePresentationDelegateToContinuation? - val media = suspendCoroutine { continuation -> - val localDelegatePtr = ImagePickerDelegateToContinuation(continuation) - delegatePtr = localDelegatePtr - val localPresentationDelegatePtr = AdaptivePresentationDelegateToContinuation(continuation) - presentationDelegate = localPresentationDelegatePtr + val media = + suspendCoroutine { continuation -> + val localDelegatePtr = ImagePickerDelegateToContinuation(continuation) + delegatePtr = localDelegatePtr + val localPresentationDelegatePtr = AdaptivePresentationDelegateToContinuation(continuation) + presentationDelegate = localPresentationDelegatePtr - val controller = UIImagePickerController() - controller.sourceType = source.toSourceType() - controller.mediaTypes = listOf(kImageType) - controller.delegate = localDelegatePtr - getViewController().presentViewController( - controller, - animated = true, - completion = null - ) - controller.presentationController?.delegate = localPresentationDelegatePtr - } - delegatePtr = null - presentationDelegate = null + val controller = UIImagePickerController() + controller.sourceType = source.toSourceType() + controller.mediaTypes = listOf(kImageType) + controller.delegate = localDelegatePtr + getViewController().presentViewController( + controller, + animated = true, + completion = null, + ) + controller.presentationController?.delegate = localPresentationDelegatePtr + } + delegatePtr = null + presentationDelegate = null - logger.d { "Picked image from $source." } - media.preview + logger.d { "Picked image from $source." } + media.preview + } + } catch (e: Exception) { + logger.e(e) { "Failed to pick image from $source." } + throw e } - } catch (e: Exception) { - logger.e(e) { "Failed to pick image from $source." } - throw e - } private fun MediaSource.toSourceType(): UIImagePickerControllerSourceType = when (this) { diff --git a/components/pictures/src/iosMain/kotlin/component/pictures/PicturesServiceIos.kt b/components/pictures/src/iosMain/kotlin/component/pictures/PicturesServiceIos.kt index 96275073..12136641 100644 --- a/components/pictures/src/iosMain/kotlin/component/pictures/PicturesServiceIos.kt +++ b/components/pictures/src/iosMain/kotlin/component/pictures/PicturesServiceIos.kt @@ -58,11 +58,15 @@ internal class PicturesServiceIos( when (val status: AVAuthorizationStatus = currentCameraAuthorizationStatus()) { AVAuthorizationStatusAuthorized -> return AVAuthorizationStatusNotDetermined -> { - val isGranted: Boolean = suspendCoroutine { continuation -> - AVCaptureDevice.requestAccessForMediaType(avMediaType) { continuation.resume(it) } + val isGranted: Boolean = + suspendCoroutine { continuation -> + AVCaptureDevice.requestAccessForMediaType(avMediaType) { continuation.resume(it) } + } + if (isGranted) { + return + } else { + throw Exception("Permission Camera denied") } - if (isGranted) return - else throw Exception("Permission Camera denied") } AVAuthorizationStatusDenied -> throw Exception("Permission Camera denied /';[p-=0-ol, l") @@ -95,9 +99,10 @@ internal class PicturesServiceIos( return when (status) { PHAuthorizationStatusAuthorized -> return PHAuthorizationStatusNotDetermined -> { - val newStatus = suspendCoroutine { continuation -> - requestGalleryAccess { continuation.resume(it) } - } + val newStatus = + suspendCoroutine { continuation -> + requestGalleryAccess { continuation.resume(it) } + } provideGalleryPermission(newStatus) } @@ -108,7 +113,7 @@ internal class PicturesServiceIos( private fun requestGalleryAccess(callback: (PHAuthorizationStatus) -> Unit) { PHPhotoLibrary.requestAuthorization( - mainContinuation { status: PHAuthorizationStatus -> callback(status) } + mainContinuation { status: PHAuthorizationStatus -> callback(status) }, ) } diff --git a/components/pictures/src/iosMain/kotlin/component/pictures/PlatformModule.kt b/components/pictures/src/iosMain/kotlin/component/pictures/PlatformModule.kt index 0e7094b1..b2422cc6 100644 --- a/components/pictures/src/iosMain/kotlin/component/pictures/PlatformModule.kt +++ b/components/pictures/src/iosMain/kotlin/component/pictures/PlatformModule.kt @@ -4,17 +4,18 @@ import co.touchlab.kermit.Logger.Companion.withTag import org.koin.dsl.module @Suppress("unused") -actual val picturesModule = module { - single { - MediaPickerControllerIos( - logger = withTag(MediaPickerController::class.simpleName!!), - getViewController = get() - ) +actual val picturesModule = + module { + single { + MediaPickerControllerIos( + logger = withTag(MediaPickerController::class.simpleName!!), + getViewController = get(), + ) + } + single { + PicturesServiceIos( + logger = withTag(PicturesService::class.simpleName!!), + mediaPickerController = get(), + ) + } } - single { - PicturesServiceIos( - logger = withTag(PicturesService::class.simpleName!!), - mediaPickerController = get() - ) - } -} diff --git a/components/pictures/src/iosMain/kotlin/component/pictures/model/Bitmap.kt b/components/pictures/src/iosMain/kotlin/component/pictures/model/Bitmap.kt index d2a99f96..b1314e4e 100644 --- a/components/pictures/src/iosMain/kotlin/component/pictures/model/Bitmap.kt +++ b/components/pictures/src/iosMain/kotlin/component/pictures/model/Bitmap.kt @@ -17,11 +17,11 @@ import platform.UIKit.UIImageJPEGRepresentation @OptIn(ExperimentalForeignApi::class) actual class Bitmap(val image: UIImage) { - @ExperimentalUnsignedTypes actual fun toByteArray(): ByteArray { - val imageData = UIImageJPEGRepresentation(image, 0.99) - ?: throw IllegalArgumentException("image data is null") + val imageData = + UIImageJPEGRepresentation(image, 0.99) + ?: throw IllegalArgumentException("image data is null") val bytes = imageData.bytes ?: throw IllegalArgumentException("image bytes is null") val length = imageData.length @@ -30,8 +30,9 @@ actual class Bitmap(val image: UIImage) { } actual fun toBase64(): String { - val imageData = UIImageJPEGRepresentation(image, 0.99) - ?: throw IllegalArgumentException("image data is null") + val imageData = + UIImageJPEGRepresentation(image, 0.99) + ?: throw IllegalArgumentException("image data is null") return imageData.base64EncodedStringWithOptions(0u) } @@ -48,10 +49,11 @@ actual class Bitmap(val image: UIImage) { UIGraphicsBeginImageContextWithOptions(CGSizeMake(newWidth, newHeight), false, 0.0) image.drawInRect(CGRectMake(0.0, 0.0, newWidth, newHeight)) val newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext(); + UIGraphicsEndImageContext() - val imageData = UIImageJPEGRepresentation(newImage!!, 0.99) - ?: throw IllegalArgumentException("image data is null") + val imageData = + UIImageJPEGRepresentation(newImage!!, 0.99) + ?: throw IllegalArgumentException("image data is null") return imageData.base64EncodedStringWithOptions(0u) } diff --git a/components/pictures/src/jsMain/kotlin/component/pictures/PlatformModule.kt b/components/pictures/src/jsMain/kotlin/component/pictures/PlatformModule.kt index 64465a88..feb20150 100644 --- a/components/pictures/src/jsMain/kotlin/component/pictures/PlatformModule.kt +++ b/components/pictures/src/jsMain/kotlin/component/pictures/PlatformModule.kt @@ -4,10 +4,11 @@ import co.touchlab.kermit.Logger.Companion.withTag import org.koin.dsl.module @Suppress("unused") -actual val picturesModule = module { - single { - PicturesServiceIos( - logger = withTag(PicturesService::class.simpleName!!), - ) +actual val picturesModule = + module { + single { + PicturesServiceIos( + logger = withTag(PicturesService::class.simpleName!!), + ) + } } -} diff --git a/convention-plugins/build.gradle.kts b/convention-plugins/build.gradle.kts index dcaf5675..a1f81ab2 100644 --- a/convention-plugins/build.gradle.kts +++ b/convention-plugins/build.gradle.kts @@ -5,5 +5,10 @@ plugins { dependencies { implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) implementation(libs.androidx.gradle) + implementation(libs.detekt) implementation(libs.kotlin.gradle) + implementation(libs.kover) + implementation(libs.ksp) + implementation(libs.ktLint) + implementation(libs.spotless) } diff --git a/convention-plugins/src/main/kotlin/common.gradle.kts b/convention-plugins/src/main/kotlin/common.gradle.kts index 6c970105..41b23743 100644 --- a/convention-plugins/src/main/kotlin/common.gradle.kts +++ b/convention-plugins/src/main/kotlin/common.gradle.kts @@ -1,9 +1,11 @@ import org.gradle.accessors.dm.LibrariesForLibs import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("multiplatform") id("com.android.library") + id("lint") } val libs = the() @@ -44,7 +46,7 @@ kotlin { } android { - namespace = group.toString() + namespace = "${libs.versions.packageName.get()}.$group" compileSdk = ProjectConfig.Android.compileSdk defaultConfig { minSdk = ProjectConfig.Android.minSdk @@ -54,3 +56,7 @@ android { targetCompatibility = ProjectConfig.Kotlin.javaVersion } } + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = libs.versions.jvmTarget.get() +} diff --git a/convention-plugins/src/main/kotlin/lint.gradle.kts b/convention-plugins/src/main/kotlin/lint.gradle.kts new file mode 100644 index 00000000..c9842cb2 --- /dev/null +++ b/convention-plugins/src/main/kotlin/lint.gradle.kts @@ -0,0 +1,65 @@ +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask +import org.gradle.accessors.dm.LibrariesForLibs + +plugins { + kotlin("multiplatform") + id("com.diffplug.spotless") + id("io.gitlab.arturbosch.detekt") + id("org.jlleitschuh.gradle.ktlint") +} + +val libs = the() + +dependencies { + detektPlugins(libs.arrow.detekt.rules) +} + +ktlint { + filter { + exclude { element -> element.file.path.contains("/build/") } + } + debug.set(false) + outputToConsole.set(true) +} + +spotless { + kotlin { + target("**/*.kt") + targetExclude("${layout.buildDirectory.asFile.get()}/**/*.kt") + ktlint().editorConfigOverride(mapOf("no-line-break-before-assignment" to "disabled")) + } + + kotlinGradle { + target("*.gradle.kts") + ktlint() + } +} + +detekt { + parallel = true + config.setFrom(files(rootProject.file("detekt.yml"))) + autoCorrect = true +} + +tasks { + withType().configureEach { + jvmTarget = libs.versions.jvmTarget.get() + parallel = true + reports { + xml.required.set(false) + html.required.set(false) + txt.required.set(false) + sarif.required.set(false) + } + exclude { it.file.absolutePath.contains("resources/") } + exclude { it.file.absolutePath.contains("build/") } + include("**/*.kt") + } + withType().configureEach { + jvmTarget = libs.versions.jvmTarget.get() + exclude { it.file.absolutePath.contains("resources/") } + exclude { it.file.absolutePath.contains("build/") } + include("**/*.kt") + } +} diff --git a/core/src/androidMain/kotlin/core/util/Permissions.kt b/core/src/androidMain/kotlin/core/util/Permissions.kt deleted file mode 100644 index 283d0743..00000000 --- a/core/src/androidMain/kotlin/core/util/Permissions.kt +++ /dev/null @@ -1,97 +0,0 @@ -package core.util - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.provider.Settings -import androidx.core.app.ActivityCompat -import co.touchlab.kermit.Logger -import core.models.Permission -import core.models.PermissionStatus -import org.koin.core.error.NoDefinitionFoundException - -/** - * Opens the settings page for the given permission. - */ -fun Context.openPage( - action: String, - newData: Uri? = null, - onError: (Exception) -> Unit, -) { - try { - val intent = Intent(action).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - newData?.let { data = it } - } - startActivity(intent) - } catch (e: ActivityNotFoundException) { - onError(e) - } -} - -/** - * Checks the given permissions. - */ -fun checkPermissions( - context: Context, - activity: Lazy, - logger: Logger, - permissions: List, -): PermissionStatus { - permissions.ifEmpty { return PermissionStatus.GRANTED } // no permissions needed - val status: List = permissions.map { - context.checkSelfPermission(it) - } - val isAllGranted: Boolean = status.all { it == PackageManager.PERMISSION_GRANTED } - if (isAllGranted) return PermissionStatus.GRANTED - - // SDK starts checking permissions before activity is available, so until we have activity - // we can't check permission rationale, so we return NOT_DETERMINED. It makes not difference - // for AppState, because permission is not granted anyway. Code below matters only in the UI, - // but the Activity is ready at this point. - val isAllRequestRationale: Boolean = try { - permissions.all { - !activity.value.shouldShowRequestPermissionRationale(it) - } - } catch (e: ActivityNotFoundException) { - logger.e { "Failed to check permission rationale. No Activity." } - true - } catch (e: NoDefinitionFoundException) { - logger.e { "Failed to check permission rationale. No Activity." } - true - } - return if (isAllRequestRationale) PermissionStatus.NOT_DETERMINED - else PermissionStatus.DENIED -} - -internal const val REQUEST_CODE = 100 - -/** - * Requests the given permissions. - */ -fun Activity.providePermissions( - permissions: List, - onError: (Throwable) -> Unit, -) { - try { - ActivityCompat.requestPermissions( - this, permissions.toTypedArray(), REQUEST_CODE, - ) - } catch (e: ActivityNotFoundException) { - onError(e) - } -} - -/** - * Opens the settings page for the given permission. - */ -fun Context.openAppSettingsPage(permission: Permission) { - openPage( - action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - newData = Uri.parse("package:$packageName"), - onError = { throw Exception("Cannot open settings for permission ${permission.name}.") }, - ) -} diff --git a/data/src/androidMain/kotlin/data/PlatformModule.kt b/data/src/androidMain/kotlin/data/PlatformModule.kt index 3df5bd66..509097f6 100644 --- a/data/src/androidMain/kotlin/data/PlatformModule.kt +++ b/data/src/androidMain/kotlin/data/PlatformModule.kt @@ -12,30 +12,32 @@ import org.koin.core.qualifier.named import org.koin.dsl.module @Suppress("unused") -internal actual val platformModule: Module = module { - single(named(NormalizedCacheType.SQL)) { - SqlNormalizedCacheFactory( - context = get(), - name = BuildKonfig.dbName, - ) - } - single(named(SettingsType.SETTINGS_NON_ENCRYPTED.name)) { - SharedPreferencesSettings.Factory(get()).create(SettingsType.SETTINGS_NON_ENCRYPTED.name) - } +internal actual val platformModule: Module = + module { + single(named(NormalizedCacheType.SQL)) { + SqlNormalizedCacheFactory( + context = get(), + name = BuildKonfig.dbName, + ) + } + single(named(SettingsType.SETTINGS_NON_ENCRYPTED.name)) { + SharedPreferencesSettings.Factory(get()).create(SettingsType.SETTINGS_NON_ENCRYPTED.name) + } - single(named(SettingsType.SETTINGS_ENCRYPTED.name)) { - val prefs = EncryptedSharedPreferences.create( - get(), - SettingsType.SETTINGS_ENCRYPTED.name, - MasterKey.Builder(get()) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build(), - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, - ) - SharedPreferencesSettings( - delegate = prefs, - ) + single(named(SettingsType.SETTINGS_ENCRYPTED.name)) { + val prefs = + EncryptedSharedPreferences.create( + get(), + SettingsType.SETTINGS_ENCRYPTED.name, + MasterKey.Builder(get()) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + SharedPreferencesSettings( + delegate = prefs, + ) + } + factory { CIO.create() } } - factory { CIO.create() } -} diff --git a/data/src/androidMain/kotlin/data/utils/ImageFile.android.kt b/data/src/androidMain/kotlin/data/utils/ImageFile.android.kt index 41fb3f94..defae7d3 100644 --- a/data/src/androidMain/kotlin/data/utils/ImageFile.android.kt +++ b/data/src/androidMain/kotlin/data/utils/ImageFile.android.kt @@ -5,9 +5,10 @@ import android.net.Uri actual typealias ImageFile = ImageUri -actual suspend fun ImageFile.toByteArray() = contentResolver.openInputStream(uri)?.use { - it.readBytes() -} ?: throw IllegalStateException("Couldn't open inputStream $uri") +actual suspend fun ImageFile.toByteArray() = + contentResolver.openInputStream(uri)?.use { + it.readBytes() + } ?: throw IllegalStateException("Couldn't open inputStream $uri") class ImageUri(val uri: Uri, val contentResolver: ContentResolver) diff --git a/data/src/commonMain/kotlin/data/DataModule.kt b/data/src/commonMain/kotlin/data/DataModule.kt index e80e7056..40b1c419 100644 --- a/data/src/commonMain/kotlin/data/DataModule.kt +++ b/data/src/commonMain/kotlin/data/DataModule.kt @@ -33,83 +33,84 @@ import org.koin.core.module.Module import org.koin.core.qualifier.named import org.koin.dsl.module -val dataModule = module { - includes( - platformModule, - apolloModule, - ) +val dataModule = + module { + includes( + platformModule, + apolloModule, + ) - single { - HttpClient(get()) { - install(Logging) { - level = LogLevel.ALL - } - install(ContentNegotiation) { - json( - Json { - ignoreUnknownKeys = true - encodeDefaults = true - isLenient = true - } - ) + single { + HttpClient(get()) { + install(Logging) { + level = LogLevel.ALL + } + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + encodeDefaults = true + isLenient = true + }, + ) + } } } - } - single { - DebugServiceImpl( - apolloClient = get(), - ) - } - single { - AuthServiceImpl( - logger = withTag(AuthService::class.simpleName!!), - apolloClient = get(), - settings = get(named(SettingsType.SETTINGS_ENCRYPTED.name)), - ) - } - single { - UserServiceImpl( - apolloClient = get(), - settings = get(named(SettingsType.SETTINGS_ENCRYPTED.name)), - ) - } - single { - ProductServiceImpl( - apolloClient = get(), - ) - } - single { - AdminServiceImpl( - apolloClient = get(), - ) - } - single { - CategoryServiceImpl( - apolloClient = get(), - ) - } - single { - TagServiceImpl( - apolloClient = get(), - ) - } - single { - OrderServiceImpl( - apolloClient = get(), - ) - } - single { - ConfigServiceImpl( - apolloClient = get(), - ) - } - single { - PaymentServiceImpl( - apolloClient = get(), - ) + single { + DebugServiceImpl( + apolloClient = get(), + ) + } + single { + AuthServiceImpl( + logger = withTag(AuthService::class.simpleName!!), + apolloClient = get(), + settings = get(named(SettingsType.SETTINGS_ENCRYPTED.name)), + ) + } + single { + UserServiceImpl( + apolloClient = get(), + settings = get(named(SettingsType.SETTINGS_ENCRYPTED.name)), + ) + } + single { + ProductServiceImpl( + apolloClient = get(), + ) + } + single { + AdminServiceImpl( + apolloClient = get(), + ) + } + single { + CategoryServiceImpl( + apolloClient = get(), + ) + } + single { + TagServiceImpl( + apolloClient = get(), + ) + } + single { + OrderServiceImpl( + apolloClient = get(), + ) + } + single { + ConfigServiceImpl( + apolloClient = get(), + ) + } + single { + PaymentServiceImpl( + apolloClient = get(), + ) + } } -} internal expect val platformModule: Module diff --git a/data/src/commonMain/kotlin/data/apollo/ApolloModule.kt b/data/src/commonMain/kotlin/data/apollo/ApolloModule.kt index 1dc95b66..2339c88a 100644 --- a/data/src/commonMain/kotlin/data/apollo/ApolloModule.kt +++ b/data/src/commonMain/kotlin/data/apollo/ApolloModule.kt @@ -13,33 +13,35 @@ import kotlinx.coroutines.Dispatchers import org.koin.core.qualifier.named import org.koin.dsl.module -internal val apolloModule = module { - single(named(NormalizedCacheType.MEMORY)) { - MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024) - } - single(named(NormalizedCacheType.BOTH)) { - val memoryCache = get(named(NormalizedCacheType.MEMORY)) - val sqlCache = get(named(NormalizedCacheType.SQL)) - memoryCache.chain(sqlCache) - } +internal val apolloModule = + module { + single(named(NormalizedCacheType.MEMORY)) { + MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024) + } + single(named(NormalizedCacheType.BOTH)) { + val memoryCache = get(named(NormalizedCacheType.MEMORY)) + val sqlCache = get(named(NormalizedCacheType.SQL)) + memoryCache.chain(sqlCache) + } - single { - AuthorizationInterceptor( - logger = withTag(AuthorizationInterceptor::class.simpleName!!), - settings = get(named(SettingsType.SETTINGS_ENCRYPTED.name)), - ) - } - single { - val normalizedCacheFactory = if (currentPlatform == Platform.JS) { - get(named(NormalizedCacheType.MEMORY)) - } else { - get(named(NormalizedCacheType.BOTH)) + single { + AuthorizationInterceptor( + logger = withTag(AuthorizationInterceptor::class.simpleName!!), + settings = get(named(SettingsType.SETTINGS_ENCRYPTED.name)), + ) + } + single { + val normalizedCacheFactory = + if (currentPlatform == Platform.JS) { + get(named(NormalizedCacheType.MEMORY)) + } else { + get(named(NormalizedCacheType.BOTH)) + } + ApolloProviderImpl( + baseUrlGraphQl = BuildKonfig.serverUrlGraphQl, + authorizationInterceptor = get(), + normalizedCacheFactory = normalizedCacheFactory, + dispatcher = Dispatchers.Default, + ).provide() } - ApolloProviderImpl( - baseUrlGraphQl = BuildKonfig.serverUrlGraphQl, - authorizationInterceptor = get(), - normalizedCacheFactory = normalizedCacheFactory, - dispatcher = Dispatchers.Default, - ).provide() } -} diff --git a/data/src/commonMain/kotlin/data/apollo/ApolloProvider.kt b/data/src/commonMain/kotlin/data/apollo/ApolloProvider.kt index c7c1627c..425eeaad 100644 --- a/data/src/commonMain/kotlin/data/apollo/ApolloProvider.kt +++ b/data/src/commonMain/kotlin/data/apollo/ApolloProvider.kt @@ -19,7 +19,8 @@ internal class ApolloProviderImpl( private val authorizationInterceptor: AuthorizationInterceptor, private val dispatcher: CoroutineDispatcher, ) : ApolloProvider { - override fun provide(): ApolloClient = ApolloClient.Builder() + override fun provide(): ApolloClient = + ApolloClient.Builder() // .subscriptionNetworkTransport( // WebSocketNetworkTransport.Builder() // .protocol(GraphQLWsProtocol.Factory()) @@ -32,13 +33,13 @@ internal class ApolloProviderImpl( // .idleTimeoutMillis(30.minutes.inWholeMilliseconds) // .build(), // ) - .serverUrl(baseUrlGraphQl) - .httpEngine(KtorHttpEngine()) - .addHttpInterceptor(authorizationInterceptor) - .addHttpInterceptor(LoggingInterceptor()) - .normalizedCache(normalizedCacheFactory) - .dispatcher(dispatcher) + .serverUrl(baseUrlGraphQl) + .httpEngine(KtorHttpEngine()) + .addHttpInterceptor(authorizationInterceptor) + .addHttpInterceptor(LoggingInterceptor()) + .normalizedCache(normalizedCacheFactory) + .dispatcher(dispatcher) // .autoPersistedQueries() // .httpBatching() - .build() + .build() } diff --git a/data/src/commonMain/kotlin/data/service/AdminService.kt b/data/src/commonMain/kotlin/data/service/AdminService.kt index 2fc60a62..db37792e 100644 --- a/data/src/commonMain/kotlin/data/service/AdminService.kt +++ b/data/src/commonMain/kotlin/data/service/AdminService.kt @@ -14,8 +14,12 @@ import data.utils.handle interface AdminService { suspend fun getStats(): Either + suspend fun generateData(input: GenerateDataInput): Either - suspend fun deleteGeneratedData(input: DeleteGenerateDataInput): Either + + suspend fun deleteGeneratedData( + input: DeleteGenerateDataInput, + ): Either } internal class AdminServiceImpl(private val apolloClient: ApolloClient) : AdminService { @@ -31,7 +35,7 @@ internal class AdminServiceImpl(private val apolloClient: ApolloClient) : AdminS .handle() override suspend fun deleteGeneratedData( - input: DeleteGenerateDataInput + input: DeleteGenerateDataInput, ): Either = apolloClient.mutation(DeleteGeneratedDataMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) diff --git a/data/src/commonMain/kotlin/data/service/AuthService.kt b/data/src/commonMain/kotlin/data/service/AuthService.kt index b239c18f..0efaa1c1 100644 --- a/data/src/commonMain/kotlin/data/service/AuthService.kt +++ b/data/src/commonMain/kotlin/data/service/AuthService.kt @@ -25,11 +25,24 @@ import kotlinx.coroutines.isActive interface AuthService { val userId: String? val userRole: Role? - suspend fun login(email: String, password: String): Either - suspend fun register(email: String, password: String, name: String): Either + + suspend fun login( + email: String, + password: String, + ): Either + + suspend fun register( + email: String, + password: String, + name: String, + ): Either + suspend fun forgotPassword(email: String): Either + suspend fun signOut() + fun isAuth(): Boolean + suspend fun observeToken(): Flow } @@ -43,10 +56,13 @@ internal class AuthServiceImpl( private var token by settings.nullableString() - private var _role by settings.nullableString() - override val userRole: Role? get() = _role?.let { Role.valueOf(it) } + private var role by settings.nullableString() + override val userRole: Role? get() = role?.let { Role.valueOf(it) } - override suspend fun login(email: String, password: String): Either { + override suspend fun login( + email: String, + password: String, + ): Either { val authInput = LoginInput(email = email, password = password) return apolloClient.mutation(LoginMutation(authInput)).handle { saveData(it.login.userMinimal.id, it.login.token, it.login.userMinimal.role) @@ -56,7 +72,7 @@ internal class AuthServiceImpl( override suspend fun register( email: String, password: String, - name: String + name: String, ): Either { val authInput = RegisterInput(email = email, password = password, name = name) return apolloClient.mutation(RegisterMutation(authInput)).handle { @@ -79,21 +95,26 @@ internal class AuthServiceImpl( return isAuth } - override suspend fun observeToken(): Flow = flow { - while (currentCoroutineContext().isActive) { - emit(token) - delay(16) - } - } - .distinctUntilChanged() - .onEach { - logger.d("userId=$userId,\nrole=$userRole,\ntoken=$token") + override suspend fun observeToken(): Flow = + flow { + while (currentCoroutineContext().isActive) { + emit(token) + delay(16) + } } + .distinctUntilChanged() + .onEach { + logger.d("userId=$userId,\nrole=$userRole,\ntoken=$token") + } - private fun saveData(userId: String?, token: String?, role: Role?): String? { + private fun saveData( + userId: String?, + token: String?, + role: Role?, + ): String? { logger.d { "Saving data:\nuserId=$userId,\nrole=$role,\ntoken=$token" } _userId = userId - _role = role?.name + this.role = role?.name this.token = token return this.token } diff --git a/data/src/commonMain/kotlin/data/service/CategoryService.kt b/data/src/commonMain/kotlin/data/service/CategoryService.kt index b00b67b7..b2988bb9 100644 --- a/data/src/commonMain/kotlin/data/service/CategoryService.kt +++ b/data/src/commonMain/kotlin/data/service/CategoryService.kt @@ -36,8 +36,11 @@ interface CategoryService { ): Either suspend fun getById(id: String): Either + suspend fun getCategoriesAllMinimal(): Either + suspend fun deleteById(id: String): Either + suspend fun update( id: String, name: String?, @@ -71,13 +74,14 @@ internal class CategoryServiceImpl(private val apolloClient: ApolloClient) : Cat sortBy: String?, sortDirection: SortDirection?, ): Either { - val pageInput = PageInput( - page = page, - size = size, - query = query.skipIfNull(), - sortBy = sortBy.skipIfNull(), - sortDirection = sortDirection.skipIfNull(), - ) + val pageInput = + PageInput( + page = page, + size = size, + query = query.skipIfNull(), + sortBy = sortBy.skipIfNull(), + sortDirection = sortDirection.skipIfNull(), + ) return apolloClient.query(GetCategoriesAsPageQuery(pageInput)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -111,26 +115,30 @@ internal class CategoryServiceImpl(private val apolloClient: ApolloClient) : Cat height: String?, requiresShipping: Boolean?, ): Either { - val shippingPreset = if ( - weight != null || length != null || width != null || height != null || requiresShipping != null - ) { - Optional.present( - ShippingPresetInput( - weight = weight.skipIfNull(), - length = length.skipIfNull(), - width = width.skipIfNull(), - height = height.skipIfNull(), - requiresShipping = requiresShipping.skipIfNull(), + val shippingPreset = + if ( + weight != null || length != null || width != null || height != null || requiresShipping != null + ) { + Optional.present( + ShippingPresetInput( + weight = weight.skipIfNull(), + length = length.skipIfNull(), + width = width.skipIfNull(), + height = height.skipIfNull(), + requiresShipping = requiresShipping.skipIfNull(), + ), ) + } else { + Optional.absent() + } + + val input = + CategoryUpdateInput( + id = id, + name = name.skipIfNull(), + display = display.skipIfNull(), + shippingPreset = shippingPreset, ) - } else Optional.absent() - - val input = CategoryUpdateInput( - id = id, - name = name.skipIfNull(), - display = display.skipIfNull(), - shippingPreset = shippingPreset, - ) return apolloClient.mutation(UpdateCategoryMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -141,11 +149,12 @@ internal class CategoryServiceImpl(private val apolloClient: ApolloClient) : Cat blob: String, type: MediaType, ): Either { - val input = AddMediaToCategoryInput( - categoryId = categoryId, - blob = BlobInput(blob), - type = type, - ) + val input = + AddMediaToCategoryInput( + categoryId = categoryId, + blob = BlobInput(blob), + type = type, + ) return apolloClient.mutation(AddCategoryImageMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() diff --git a/data/src/commonMain/kotlin/data/service/ConfigService.kt b/data/src/commonMain/kotlin/data/service/ConfigService.kt index 54140ba6..2907109b 100644 --- a/data/src/commonMain/kotlin/data/service/ConfigService.kt +++ b/data/src/commonMain/kotlin/data/service/ConfigService.kt @@ -32,8 +32,11 @@ import data.utils.skipIfNull interface ConfigService { suspend fun fetchConfig(): Either + suspend fun createConfig(input: CreateConfigInput): Either + suspend fun getLandingConfig(): Either + suspend fun getCatalogConfig(): Either suspend fun updateConfig( @@ -73,9 +76,10 @@ interface ConfigService { } internal class ConfigServiceImpl(private val apolloClient: ApolloClient) : ConfigService { - override suspend fun fetchConfig(): Either = apolloClient - .query(GetConfigQuery(Optional.present(null))) - .handle() + override suspend fun fetchConfig(): Either = + apolloClient + .query(GetConfigQuery(Optional.present(null))) + .handle() override suspend fun createConfig(input: CreateConfigInput): Either { return apolloClient.mutation(CreateConfigMutation(input)).handle() @@ -109,72 +113,89 @@ internal class ConfigServiceImpl(private val apolloClient: ApolloClient) : Confi topCategoriesSectionMiddle: TopCategoryItemInput?, topCategoriesRight: TopCategoryItemInput?, ): Either { - val contactInfoUpdateInput = if ( - companyName == null && - companyWebsite == null && - email == null && - phone == null - ) null else { - ContactInfoUpdateInput( - companyName = companyName.skipIfNull(), - companyWebsite = companyWebsite.skipIfNull(), - email = email.skipIfNull(), - phone = phone.skipIfNull(), - ) - } - val openingTimesUpdateInput = if ( - dayFrom == null && - dayTo == null && - open == null && - close == null - ) null else { - OpeningTimesUpdateInput( - dayFrom = dayFrom.skipIfNull(), - dayTo = dayTo.skipIfNull(), - open = open.skipIfNull(), - close = close.skipIfNull(), - ) - } - val companyInfoInput = if ( - contactInfoUpdateInput == null && - openingTimesUpdateInput == null - ) null else { - CompanyInfoUpdateInput( - contactInfoInput = contactInfoUpdateInput.skipIfNull(), - openingTimesInput = openingTimesUpdateInput.skipIfNull(), + val contactInfoUpdateInput = + if ( + companyName == null && + companyWebsite == null && + email == null && + phone == null + ) { + null + } else { + ContactInfoUpdateInput( + companyName = companyName.skipIfNull(), + companyWebsite = companyWebsite.skipIfNull(), + email = email.skipIfNull(), + phone = phone.skipIfNull(), + ) + } + val openingTimesUpdateInput = + if ( + dayFrom == null && + dayTo == null && + open == null && + close == null + ) { + null + } else { + OpeningTimesUpdateInput( + dayFrom = dayFrom.skipIfNull(), + dayTo = dayTo.skipIfNull(), + open = open.skipIfNull(), + close = close.skipIfNull(), + ) + } + val companyInfoInput = + if ( + contactInfoUpdateInput == null && + openingTimesUpdateInput == null + ) { + null + } else { + CompanyInfoUpdateInput( + contactInfoInput = contactInfoUpdateInput.skipIfNull(), + openingTimesInput = openingTimesUpdateInput.skipIfNull(), + ) + } + val footerConfigUpdateInput = + if ( + showStartChat == null && + showOpeningTimes == null && + showCareer == null && + showCyberSecurity == null && + showPress == null + ) { + null + } else { + FooterConfigUpdateInput( + showStartChat = showStartChat.skipIfNull(), + showOpeningTimes = showOpeningTimes.skipIfNull(), + showCareer = showCareer.skipIfNull(), + showCyberSecurity = showCyberSecurity.skipIfNull(), + showPress = showPress.skipIfNull(), + ) + } + val bannerSectionInput = + if (topCategoriesSectionLeft == null && topCategoriesRight == null) { + null + } else { + TopCategoriesSectionUpdateInput( + leftInput = topCategoriesSectionLeft.skipIfNull(), + rightInput = topCategoriesRight.skipIfNull(), + ) + } + val landingConfigInput = + LandingConfigUpdateInput( + slideshowItems = slideshowItems.skipIfNull(), + topCategoriesSectionInput = bannerSectionInput.skipIfNull(), ) - } - val footerConfigUpdateInput = if ( - showStartChat == null && - showOpeningTimes == null && - showCareer == null && - showCyberSecurity == null && - showPress == null - ) null else { - FooterConfigUpdateInput( - showStartChat = showStartChat.skipIfNull(), - showOpeningTimes = showOpeningTimes.skipIfNull(), - showCareer = showCareer.skipIfNull(), - showCyberSecurity = showCyberSecurity.skipIfNull(), - showPress = showPress.skipIfNull(), + val input = + ConfigUpdateInput( + id = configId, + companyInfoInput = companyInfoInput.skipIfNull(), + footerConfigInput = footerConfigUpdateInput.skipIfNull(), + landingConfigInput = landingConfigInput.skipIfNull(), ) - } - val bannerSectionInput = if (topCategoriesSectionLeft == null && topCategoriesRight == null) null else { - TopCategoriesSectionUpdateInput( - leftInput = topCategoriesSectionLeft.skipIfNull(), - rightInput = topCategoriesRight.skipIfNull(), - ) - } - val landingConfigInput = LandingConfigUpdateInput( - slideshowItems = slideshowItems.skipIfNull(), - topCategoriesSectionInput = bannerSectionInput.skipIfNull(), - ) - val input = ConfigUpdateInput( - id = configId, - companyInfoInput = companyInfoInput.skipIfNull(), - footerConfigInput = footerConfigUpdateInput.skipIfNull(), - landingConfigInput = landingConfigInput.skipIfNull(), - ) return apolloClient.mutation(UpdateConfigMutation(input)).handle() } @@ -184,12 +205,13 @@ internal class ConfigServiceImpl(private val apolloClient: ApolloClient) : Confi blob: BlobInput, mediaType: MediaType, ): Either { - val input = ConfigCollageMediaUploadInput( - configId = configId, - imageId = imageId, - blob = blob, - mediaType = mediaType, - ) + val input = + ConfigCollageMediaUploadInput( + configId = configId, + imageId = imageId, + blob = blob, + mediaType = mediaType, + ) return apolloClient.mutation(UploadConfigCollageImageMutation(input)).handle() } @@ -197,14 +219,15 @@ internal class ConfigServiceImpl(private val apolloClient: ApolloClient) : Confi configId: String, side: Side, blob: BlobInput, - mediaType: MediaType + mediaType: MediaType, ): Either { - val input = ConfigBannerMediaUploadInput( - configId = configId, - side = side, - blob = blob, - mediaType = mediaType, - ) + val input = + ConfigBannerMediaUploadInput( + configId = configId, + side = side, + blob = blob, + mediaType = mediaType, + ) return apolloClient.mutation(UploadConfigBannerImageMutation(input)).handle() } } diff --git a/data/src/commonMain/kotlin/data/service/DebugService.kt b/data/src/commonMain/kotlin/data/service/DebugService.kt index 83670552..cdd0c042 100644 --- a/data/src/commonMain/kotlin/data/service/DebugService.kt +++ b/data/src/commonMain/kotlin/data/service/DebugService.kt @@ -26,7 +26,8 @@ internal class DebugServiceImpl(private val apolloClient: ApolloClient) : DebugS // return apolloClient.query(DeleteGeneratedUsersQuery()).handle() // } - override suspend fun clearCache(): Either = either { - apolloClient.apolloStore.clearAll() - } + override suspend fun clearCache(): Either = + either { + apolloClient.apolloStore.clearAll() + } } diff --git a/data/src/commonMain/kotlin/data/service/OrderService.kt b/data/src/commonMain/kotlin/data/service/OrderService.kt index 6102b2ad..2c639253 100644 --- a/data/src/commonMain/kotlin/data/service/OrderService.kt +++ b/data/src/commonMain/kotlin/data/service/OrderService.kt @@ -32,7 +32,9 @@ interface OrderService { ): Either suspend fun getById(id: String): Flow> + suspend fun deleteById(id: String): Either + suspend fun update( id: String, name: String?, @@ -54,13 +56,14 @@ internal class OrderServiceImpl(private val apolloClient: ApolloClient) : OrderS sortBy: String?, sortDirection: SortDirection?, ): Either { - val pageInput = PageInput( - page = page, - size = size, - query = query.skipIfNull(), - sortBy = sortBy.skipIfNull(), - sortDirection = sortDirection.skipIfNull(), - ) + val pageInput = + PageInput( + page = page, + size = size, + query = query.skipIfNull(), + sortBy = sortBy.skipIfNull(), + sortDirection = sortDirection.skipIfNull(), + ) return apolloClient.query(GetCategoriesAsPageQuery(pageInput)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -83,10 +86,11 @@ internal class OrderServiceImpl(private val apolloClient: ApolloClient) : OrderS id: String, name: String?, ): Either { - val input = CategoryUpdateInput( - id = id, - name = name.skipIfNull(), - ) + val input = + CategoryUpdateInput( + id = id, + name = name.skipIfNull(), + ) return apolloClient.mutation(UpdateCategoryMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() diff --git a/data/src/commonMain/kotlin/data/service/PaymentService.kt b/data/src/commonMain/kotlin/data/service/PaymentService.kt index 3cb3cc64..a517c3b1 100644 --- a/data/src/commonMain/kotlin/data/service/PaymentService.kt +++ b/data/src/commonMain/kotlin/data/service/PaymentService.kt @@ -15,7 +15,7 @@ interface PaymentService { internal class PaymentServiceImpl(private val apolloClient: ApolloClient) : PaymentService { override suspend fun getPaymentMethods( - platforms: List + platforms: List, ): Either { return apolloClient.query(GetPaymentMethodsQuery(platforms)) .fetchPolicy(FetchPolicy.NetworkOnly) diff --git a/data/src/commonMain/kotlin/data/service/ProductService.kt b/data/src/commonMain/kotlin/data/service/ProductService.kt index 050b7714..9d82133d 100644 --- a/data/src/commonMain/kotlin/data/service/ProductService.kt +++ b/data/src/commonMain/kotlin/data/service/ProductService.kt @@ -46,6 +46,7 @@ import data.utils.skipIfNull interface ProductService { suspend fun create(name: String): Either + suspend fun getAdminProductsAsPage( page: Int, size: Int, @@ -63,10 +64,11 @@ interface ProductService { priceFrom: Double?, priceTo: Double?, sortBy: ProductsSort?, - traits: List? + traits: List?, ): Either suspend fun getProductById(id: String): Either + suspend fun getCurrentCatalogFilterOptions( categories: List?, colors: List?, @@ -77,7 +79,9 @@ interface ProductService { ): Either suspend fun getAllCatalogFilterOptions(): Either + suspend fun delete(id: String): Either + suspend fun updateProduct( id: String, name: String?, @@ -113,24 +117,27 @@ interface ProductService { suspend fun deleteImage( productId: String, - imageId: String + imageId: String, ): Either suspend fun getTrendingNowProducts(): Either + suspend fun getRecommendedProducts(): Either + suspend fun getSimilarProducts(): Either + suspend fun getTopSellingProducts(): Either } internal class ProductServiceImpl(private val apolloClient: ApolloClient) : ProductService { override suspend fun deleteImage( productId: String, - imageId: String + imageId: String, ): Either = apolloClient.mutation( AdminDeleteProductMediaMutation( - ProductMediaDeleteInput(mediaId = productId, productId = imageId) - ) + ProductMediaDeleteInput(mediaId = productId, productId = imageId), + ), ).handle() override suspend fun getTrendingNowProducts(): Either { @@ -162,11 +169,12 @@ internal class ProductServiceImpl(private val apolloClient: ApolloClient) : Prod blobInput: BlobInput, mediaType: MediaType, ): Either { - val input = ProductMediaUploadInput( - productId = productId, - blob = blobInput, - mediaType = mediaType, - ) + val input = + ProductMediaUploadInput( + productId = productId, + blob = blobInput, + mediaType = mediaType, + ) return apolloClient.mutation(AdminProductUploadImageMutation(input)).handle() } @@ -196,62 +204,72 @@ internal class ProductServiceImpl(private val apolloClient: ApolloClient) : Prod width: String?, variants: List?, ): Either { - val inventory = if ( - backorderStatus != null || lowStockThreshold != null || remainingStock != null || stockStatus != null || - trackQuantity != null - ) { - Optional.present( - InventoryUpdateInput( - backorderStatus = backorderStatus.skipIfNull(), - lowStockThreshold = lowStockThreshold.skipIfNull(), - remainingStock = remainingStock.skipIfNull(), - trackQuantity = trackQuantity.skipIfNull(), + val inventory = + if ( + backorderStatus != null || lowStockThreshold != null || remainingStock != null || stockStatus != null || + trackQuantity != null + ) { + Optional.present( + InventoryUpdateInput( + backorderStatus = backorderStatus.skipIfNull(), + lowStockThreshold = lowStockThreshold.skipIfNull(), + remainingStock = remainingStock.skipIfNull(), + trackQuantity = trackQuantity.skipIfNull(), + ), ) - ) - } else Optional.absent() - - val productPrice = if (salePrice != null || regularPrice != null || chargeTax != null) { - Optional.present( - PricingUpdateInput( - salePrice = salePrice.skipIfNull(), - regularPrice = regularPrice.skipIfNull(), - chargeTax = chargeTax.skipIfNull(), + } else { + Optional.absent() + } + + val productPrice = + if (salePrice != null || regularPrice != null || chargeTax != null) { + Optional.present( + PricingUpdateInput( + salePrice = salePrice.skipIfNull(), + regularPrice = regularPrice.skipIfNull(), + chargeTax = chargeTax.skipIfNull(), + ), ) - ) - } else Optional.absent() - - val shipping = if ( - presetId != null || height != null || length != null || isPhysicalProduct != null || weight != null || - width != null - ) { - Optional.present( - ShippingUpdateInput( - presetId = presetId.skipIfNull(), - height = height.skipIfNull(), - length = length.skipIfNull(), - isPhysicalProduct = isPhysicalProduct.skipIfNull(), - weight = weight.skipIfNull(), - width = width.skipIfNull(), + } else { + Optional.absent() + } + + val shipping = + if ( + presetId != null || height != null || length != null || isPhysicalProduct != null || weight != null || + width != null + ) { + Optional.present( + ShippingUpdateInput( + presetId = presetId.skipIfNull(), + height = height.skipIfNull(), + length = length.skipIfNull(), + isPhysicalProduct = isPhysicalProduct.skipIfNull(), + weight = weight.skipIfNull(), + width = width.skipIfNull(), + ), ) + } else { + Optional.absent() + } + + val input = + ProductUpdateInput( + id = id, + name = name.skipIfNull(), + description = description.skipIfNull(), + isFeatured = isFeatured.skipIfNull(), + allowReviews = allowReviews.skipIfNull(), + categoryId = categoryId.skipIfNull(), + tags = tags.skipIfNull(), + postStatus = postStatus.skipIfNull(), + media = Optional.absent(), + inventory = inventory, + traits = traits.skipIfNull(), + pricing = productPrice, + shipping = shipping, + variants = variants.skipIfNull(), ) - } else Optional.absent() - - val input = ProductUpdateInput( - id = id, - name = name.skipIfNull(), - description = description.skipIfNull(), - isFeatured = isFeatured.skipIfNull(), - allowReviews = allowReviews.skipIfNull(), - categoryId = categoryId.skipIfNull(), - tags = tags.skipIfNull(), - postStatus = postStatus.skipIfNull(), - media = Optional.absent(), - inventory = inventory, - traits = traits.skipIfNull(), - pricing = productPrice, - shipping = shipping, - variants = variants.skipIfNull(), - ) return apolloClient.mutation(AdminProductUpdateMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) @@ -270,13 +288,14 @@ internal class ProductServiceImpl(private val apolloClient: ApolloClient) : Prod sortBy: String?, sortDirection: SortDirection?, ): Either { - val pageInput = PageInput( - page = page, - size = size, - query = query.skipIfNull(), - sortBy = sortBy.skipIfNull(), - sortDirection = sortDirection.skipIfNull(), - ) + val pageInput = + PageInput( + page = page, + size = size, + query = query.skipIfNull(), + sortBy = sortBy.skipIfNull(), + sortDirection = sortDirection.skipIfNull(), + ) return apolloClient.query(AdminGetAllProductsPageQuery(pageInput)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -291,22 +310,24 @@ internal class ProductServiceImpl(private val apolloClient: ApolloClient) : Prod priceFrom: Double?, priceTo: Double?, sortBy: ProductsSort?, - traits: List? + traits: List?, ): Either { - val pageInput = CatalogPageInput( - page = page, - size = Optional.present(9), - query = query.skipIfNull(), - filters = CurrentCatalogFilterInput( - categories = categories.skipIfNull(), - colors = colors.skipIfNull(), - sizes = sizes.skipIfNull(), - priceFrom = priceFrom.skipIfNull(), - priceTo = priceTo.skipIfNull(), - traits = traits.skipIfNull(), - ), - sortBy = sortBy.skipIfNull(), - ) + val pageInput = + CatalogPageInput( + page = page, + size = Optional.present(9), + query = query.skipIfNull(), + filters = + CurrentCatalogFilterInput( + categories = categories.skipIfNull(), + colors = colors.skipIfNull(), + sizes = sizes.skipIfNull(), + priceFrom = priceFrom.skipIfNull(), + priceTo = priceTo.skipIfNull(), + traits = traits.skipIfNull(), + ), + sortBy = sortBy.skipIfNull(), + ) return apolloClient.query(GetCatalogPageQuery(pageInput)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -325,14 +346,15 @@ internal class ProductServiceImpl(private val apolloClient: ApolloClient) : Prod sizes: List?, traits: List?, ): Either { - val input = CurrentCatalogFilterInput( - categories = categories.skipIfNull(), - colors = colors.skipIfNull(), - priceFrom = priceFrom.skipIfNull(), - priceTo = priceTo.skipIfNull(), - sizes = sizes.skipIfNull(), - traits = traits.skipIfNull(), - ) + val input = + CurrentCatalogFilterInput( + categories = categories.skipIfNull(), + colors = colors.skipIfNull(), + priceFrom = priceFrom.skipIfNull(), + priceTo = priceTo.skipIfNull(), + sizes = sizes.skipIfNull(), + traits = traits.skipIfNull(), + ) return apolloClient.query(GetCurrentCatalogFilterOptionsQuery(input)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() diff --git a/data/src/commonMain/kotlin/data/service/TagService.kt b/data/src/commonMain/kotlin/data/service/TagService.kt index 83169c00..12025870 100644 --- a/data/src/commonMain/kotlin/data/service/TagService.kt +++ b/data/src/commonMain/kotlin/data/service/TagService.kt @@ -30,8 +30,11 @@ interface TagService { ): Either suspend fun getById(id: String): Either + suspend fun getTagsAllMinimal(): Either + suspend fun deleteById(id: String): Either + suspend fun update( id: String, name: String?, @@ -53,13 +56,14 @@ internal class TagServiceImpl(private val apolloClient: ApolloClient) : TagServi sortBy: String?, sortDirection: SortDirection?, ): Either { - val pageInput = PageInput( - page = page, - size = size, - query = query.skipIfNull(), - sortBy = sortBy.skipIfNull(), - sortDirection = sortDirection.skipIfNull(), - ) + val pageInput = + PageInput( + page = page, + size = size, + query = query.skipIfNull(), + sortBy = sortBy.skipIfNull(), + sortDirection = sortDirection.skipIfNull(), + ) return apolloClient.query(GetTagsAsPageQuery(pageInput)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -87,10 +91,11 @@ internal class TagServiceImpl(private val apolloClient: ApolloClient) : TagServi id: String, name: String?, ): Either { - val input = TagUpdateInput( - id = id, - name = name.skipIfNull(), - ) + val input = + TagUpdateInput( + id = id, + name = name.skipIfNull(), + ) return apolloClient.mutation(UpdateTagMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() diff --git a/data/src/commonMain/kotlin/data/service/UserService.kt b/data/src/commonMain/kotlin/data/service/UserService.kt index 0b8eb6d4..bd5f4a95 100644 --- a/data/src/commonMain/kotlin/data/service/UserService.kt +++ b/data/src/commonMain/kotlin/data/service/UserService.kt @@ -32,6 +32,7 @@ interface UserService { ): Either suspend fun getById(id: String): Either + suspend fun getAsPage( page: Int, size: Int, @@ -71,9 +72,10 @@ interface UserService { ): Either suspend fun getCart(): Either + suspend fun removeItemFromCart( productId: String, - variantId: String + variantId: String, ): Either } @@ -88,11 +90,12 @@ internal class UserServiceImpl( firstName: String, lastName: String, ): Either { - val input = UserCreateInput( - email = email, - firstName = Optional.present(firstName), - lastName = Optional.present(lastName), - ) + val input = + UserCreateInput( + email = email, + firstName = Optional.present(firstName), + lastName = Optional.present(lastName), + ) return apolloClient.mutation(CreateCustomerMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -111,13 +114,14 @@ internal class UserServiceImpl( sortBy: String?, sortDirection: SortDirection?, ): Either { - val pageInput = PageInput( - page = page, - size = size, - query = query.skipIfNull(), - sortBy = sortBy.skipIfNull(), - sortDirection = sortDirection.skipIfNull(), - ) + val pageInput = + PageInput( + page = page, + size = size, + query = query.skipIfNull(), + sortBy = sortBy.skipIfNull(), + sortDirection = sortDirection.skipIfNull(), + ) return apolloClient.query(GetAllCustomersAsPageQuery(pageInput)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -150,26 +154,27 @@ internal class UserServiceImpl( marketingSms: Boolean?, password: String?, ): Either { - val input = UserUpdateInput( - id = id, - email = email.skipIfNull(), - password = password.skipIfNull(), - address = address.skipIfNull(), - addressFirstName = addressFirstName.skipIfNull(), - addressLastName = addressLastName.skipIfNull(), - apartment = apartment.skipIfNull(), - city = city.skipIfNull(), - collectTax = collectTax.skipIfNull(), - company = company.skipIfNull(), - country = country.skipIfNull(), - detailsFirstName = detailsFirstName.skipIfNull(), - detailsLastName = detailsLastName.skipIfNull(), - marketingEmails = marketingEmails.skipIfNull(), - marketingSms = marketingSms.skipIfNull(), - detailsPhone = detailPhone.skipIfNull(), - postcode = postcode.skipIfNull(), - addressPhone = addressPhone.skipIfNull(), - ) + val input = + UserUpdateInput( + id = id, + email = email.skipIfNull(), + password = password.skipIfNull(), + address = address.skipIfNull(), + addressFirstName = addressFirstName.skipIfNull(), + addressLastName = addressLastName.skipIfNull(), + apartment = apartment.skipIfNull(), + city = city.skipIfNull(), + collectTax = collectTax.skipIfNull(), + company = company.skipIfNull(), + country = country.skipIfNull(), + detailsFirstName = detailsFirstName.skipIfNull(), + detailsLastName = detailsLastName.skipIfNull(), + marketingEmails = marketingEmails.skipIfNull(), + marketingSms = marketingSms.skipIfNull(), + detailsPhone = detailPhone.skipIfNull(), + postcode = postcode.skipIfNull(), + addressPhone = addressPhone.skipIfNull(), + ) return apolloClient.mutation(UpdateCustomerMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) @@ -179,13 +184,14 @@ internal class UserServiceImpl( override suspend fun addProductToCart( productId: String, variantId: String, - quantity: Int + quantity: Int, ): Either { - val input = AddToCartInput( - productId = productId, - variantId = variantId, - quantity = quantity, - ) + val input = + AddToCartInput( + productId = productId, + variantId = variantId, + quantity = quantity, + ) return apolloClient.mutation(AddToCartMutation(input)) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() @@ -200,14 +206,14 @@ internal class UserServiceImpl( override suspend fun removeItemFromCart( productId: String, - variantId: String + variantId: String, ): Either { return apolloClient.mutation( RemoveItemFromUserCartMutation( Optional.present(guestCartId), productId, - variantId - ) + variantId, + ), ) .fetchPolicy(FetchPolicy.NetworkOnly) .handle() diff --git a/data/src/commonMain/kotlin/data/utils/Apollo.kt b/data/src/commonMain/kotlin/data/utils/Apollo.kt index ccc296d8..27d48a29 100644 --- a/data/src/commonMain/kotlin/data/utils/Apollo.kt +++ b/data/src/commonMain/kotlin/data/utils/Apollo.kt @@ -20,37 +20,39 @@ import org.koin.core.component.get */ internal suspend fun ApolloCall.handle( block: suspend (T) -> Unit = {}, -): Either = either { - val operationName = this@handle.operation.name() - val response = this@handle.execute() +): Either = + either { + val operationName = this@handle.operation.name() + val response = this@handle.execute() - ensure(response.errors.isNullOrEmpty()) { - val errorsAsString = response.errors?.joinToString { it.message } ?: "" + ensure(response.errors.isNullOrEmpty()) { + val errorsAsString = response.errors?.joinToString { it.message } ?: "" - if ("Forbidden" in errorsAsString || "Unauthorized" in errorsAsString) { - val authService = object : KoinComponent {} - authService.get().signOut() - raise(Unauthorized) - } + if ("Forbidden" in errorsAsString || "Unauthorized" in errorsAsString) { + val authService = object : KoinComponent {} + authService.get().signOut() + raise(Unauthorized) + } - raise(RequestError(operationName, "$operationName: $errorsAsString")) - } + raise(RequestError(operationName, "$operationName: $errorsAsString")) + } - response.exception?.let { raise(RequestError(operationName, it.message)) } + response.exception?.let { raise(RequestError(operationName, it.message)) } - val data = response.data ?: raise(NoData(operationName)) - block(data) - data -} + val data = response.data ?: raise(NoData(operationName)) + block(data) + data + } -internal fun ApolloResponse.handle(): Either = either { - val operationName = this@handle.operation.name() - ensure(!errors.isNullOrEmpty()) { - val errorsAsString = errors?.joinToString { it.message } - raise(RequestError(operationName, "$operationName: $errorsAsString")) +internal fun ApolloResponse.handle(): Either = + either { + val operationName = this@handle.operation.name() + ensure(!errors.isNullOrEmpty()) { + val errorsAsString = errors?.joinToString { it.message } + raise(RequestError(operationName, "$operationName: $errorsAsString")) + } + data ?: raise(NoData(operationName)) } - data ?: raise(NoData(operationName)) -} /** * Use this if you want to skip sending the value if value is null. diff --git a/data/src/commonTest/kotlin/data/auth/LoginTests.kt b/data/src/commonTest/kotlin/data/auth/LoginTests.kt index b54f013f..0261eb46 100644 --- a/data/src/commonTest/kotlin/data/auth/LoginTests.kt +++ b/data/src/commonTest/kotlin/data/auth/LoginTests.kt @@ -12,31 +12,36 @@ import kotlin.test.assertEquals @OptIn(ApolloExperimental::class) class LoginTests { - private val apolloClient: ApolloClient = ApolloClient.Builder() - .networkTransport(QueueTestNetworkTransport()) - .build() + private val apolloClient: ApolloClient = + ApolloClient.Builder() + .networkTransport(QueueTestNetworkTransport()) + .build() @Test - fun exampleApolloTest() = runTest { - val authInput = AuthInput( - email = "nataliashop@nataliashop.com", - password = "P@ss1234", - ) - val testQuery = LoginMutation(authInput) - val testData = LoginMutation.Data( - LoginMutation.Login( - token = "test_token", - userMinimal = LoginMutation.UserMinimal( - id = "test_id", - email = "test_email", - __typename = "UserMinimal", - ), - ), - ) + fun exampleApolloTest() = + runTest { + val authInput = + AuthInput( + email = "nataliashop@nataliashop.com", + password = "P@ss1234", + ) + val testQuery = LoginMutation(authInput) + val testData = + LoginMutation.Data( + LoginMutation.Login( + token = "test_token", + userMinimal = + LoginMutation.UserMinimal( + id = "test_id", + email = "test_email", + __typename = "UserMinimal", + ), + ), + ) - apolloClient.enqueueTestResponse(testQuery, testData) + apolloClient.enqueueTestResponse(testQuery, testData) - val actual = apolloClient.mutation(testQuery).execute().data!! - assertEquals(testData.login.token, actual.login.token) - } + val actual = apolloClient.mutation(testQuery).execute().data!! + assertEquals(testData.login.token, actual.login.token) + } } diff --git a/data/src/iosMain/kotlin/data/PlatformModule.kt b/data/src/iosMain/kotlin/data/PlatformModule.kt index e612d771..7b376dc8 100644 --- a/data/src/iosMain/kotlin/data/PlatformModule.kt +++ b/data/src/iosMain/kotlin/data/PlatformModule.kt @@ -23,20 +23,21 @@ import platform.Security.kSecAttrService ExperimentalSettingsImplementation::class, ) @Suppress("unused") -internal actual val platformModule: Module = module { - single(named(NormalizedCacheType.SQL)) { - SqlNormalizedCacheFactory( - name = BuildKonfig.dbName, - ) +internal actual val platformModule: Module = + module { + single(named(NormalizedCacheType.SQL)) { + SqlNormalizedCacheFactory( + name = BuildKonfig.dbName, + ) + } + single(named(SettingsType.SETTINGS_NON_ENCRYPTED.name)) { + NSUserDefaultsSettings.Factory().create(SettingsType.SETTINGS_NON_ENCRYPTED.name) + } + single(named(SettingsType.SETTINGS_ENCRYPTED.name)) { + KeychainSettings( + kSecAttrService to CFBridgingRetain(SettingsType.SETTINGS_ENCRYPTED.name), + kSecAttrAccessible to kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + ) + } + factory { Darwin.create() } } - single(named(SettingsType.SETTINGS_NON_ENCRYPTED.name)) { - NSUserDefaultsSettings.Factory().create(SettingsType.SETTINGS_NON_ENCRYPTED.name) - } - single(named(SettingsType.SETTINGS_ENCRYPTED.name)) { - KeychainSettings( - kSecAttrService to CFBridgingRetain(SettingsType.SETTINGS_ENCRYPTED.name), - kSecAttrAccessible to kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, - ) - } - factory { Darwin.create() } -} diff --git a/data/src/iosMain/kotlin/data/utils/ImageFile.kt b/data/src/iosMain/kotlin/data/utils/ImageFile.kt index 5447101a..f608f93f 100644 --- a/data/src/iosMain/kotlin/data/utils/ImageFile.kt +++ b/data/src/iosMain/kotlin/data/utils/ImageFile.kt @@ -14,8 +14,9 @@ actual suspend fun ImageFile.toByteArray() = UIImagePNGRepresentation(this)?.toByteArray() ?: emptyArray().toByteArray() @OptIn(ExperimentalForeignApi::class) -private fun NSData.toByteArray(): ByteArray = ByteArray(length.toInt()).apply { - usePinned { - memcpy(it.addressOf(0), bytes, length) +private fun NSData.toByteArray(): ByteArray = + ByteArray(length.toInt()).apply { + usePinned { + memcpy(it.addressOf(0), bytes, length) + } } -} diff --git a/data/src/jsMain/kotlin/data/PlatformModule.kt b/data/src/jsMain/kotlin/data/PlatformModule.kt index 0d8a6bb0..12f914af 100644 --- a/data/src/jsMain/kotlin/data/PlatformModule.kt +++ b/data/src/jsMain/kotlin/data/PlatformModule.kt @@ -8,12 +8,13 @@ import org.koin.core.qualifier.named import org.koin.dsl.module @Suppress("unused") -internal actual val platformModule: Module = module { - single(named(SettingsType.SETTINGS_NON_ENCRYPTED.name)) { - StorageSettings() +internal actual val platformModule: Module = + module { + single(named(SettingsType.SETTINGS_NON_ENCRYPTED.name)) { + StorageSettings() + } + single(named(SettingsType.SETTINGS_ENCRYPTED.name)) { + StorageSettings() + } + factory { Js.create() } } - single(named(SettingsType.SETTINGS_ENCRYPTED.name)) { - StorageSettings() - } - factory { Js.create() } -} diff --git a/data/src/jsMain/kotlin/data/utils/ImageFile.kt b/data/src/jsMain/kotlin/data/utils/ImageFile.kt index 479b9296..2acf5dbb 100644 --- a/data/src/jsMain/kotlin/data/utils/ImageFile.kt +++ b/data/src/jsMain/kotlin/data/utils/ImageFile.kt @@ -14,16 +14,17 @@ actual fun ImageFile.toByteArray(): ByteArray { // readAsByteArray(this).await() } -fun readAsByteArray(file: File): Promise = Promise { resolve, reject -> - val reader = FileReader() - reader.onloadend = { _ -> - resolve(Uint8Array(reader.result as ArrayBuffer).toByteArray()) +fun readAsByteArray(file: File): Promise = + Promise { resolve, reject -> + val reader = FileReader() + reader.onloadend = { _ -> + resolve(Uint8Array(reader.result as ArrayBuffer).toByteArray()) + } + reader.onerror = { _ -> + reject(Throwable("Failed to read file")) + } + reader.readAsArrayBuffer(file) } - reader.onerror = { _ -> - reject(Throwable("Failed to read file")) - } - reader.readAsArrayBuffer(file) -} fun Uint8Array.toByteArray(): ByteArray { val byteArray = ByteArray(length) diff --git a/detekt.yml b/detekt.yml index 20d011f4..3fed0185 100644 --- a/detekt.yml +++ b/detekt.yml @@ -105,7 +105,7 @@ complexity: includePrivateDeclarations: false ignoreOverloaded: false CyclomaticComplexMethod: - active: true + active: false threshold: 15 ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false @@ -124,11 +124,11 @@ complexity: active: false ignoredLabels: [ ] LargeClass: - active: true + active: false threshold: 600 LongMethod: - active: true - threshold: 60 + active: false + threshold: 100 LongParameterList: active: false functionThreshold: 30 @@ -607,7 +607,7 @@ style: active: true maxJumpCount: 1 MagicNumber: - active: true + active: false excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] ignoreNumbers: - '-1' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac5b1c6b..af8ebd97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -85,6 +85,8 @@ compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } + kamel-image = { module = "media.kamel:kamel-image", version.ref = "kamelImage" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } @@ -108,6 +110,12 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } +kover = { module = "org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin", version.ref = "kover" } + +ksp = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } + +ktLint = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktLint" } + ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }