diff --git a/composeApp/composeApp.podspec b/composeApp/composeApp.podspec index 920d45e9425b52c6208ad32017e41ecfd7287d5a..bed87b28debff418ca79cf502293d890529af5d0 100644 --- a/composeApp/composeApp.podspec +++ b/composeApp/composeApp.podspec @@ -35,7 +35,5 @@ Pod::Spec.new do |spec| SCRIPT } ] - spec.resource_bundles = { - 'LibresComposeApp' => ['build/generated/libres/apple/resources/images/LibresComposeApp.xcassets'] - } + end \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/Color.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/Color.kt index 0ee68bfdda45c3ad3ebac7b8cfb82ddd77dde7db..1a37317204d6a64f8d9b32ae06f0ae9944b606cb 100644 --- a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/Color.kt +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/Color.kt @@ -217,3 +217,5 @@ internal val textInBorderPurple = Color(0xFF5800CB) internal val borderPurple = Color(0xFF6F01FF) internal val textGrayColor = Color(0x80000000) +internal val companyColor = Color(0xFF323E48) + diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/GoogleSignInButton.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/GoogleSignInButton.kt index 762d825e74fd9d5a3f131126127ffbdd45e94f04..a2bf7849de3751093038b30fb0299386c3092166 100644 --- a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/GoogleSignInButton.kt +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/GoogleSignInButton.kt @@ -1,6 +1,7 @@ package band.effective.office.elevator.components import androidx.compose.foundation.Image +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -22,11 +23,19 @@ import dev.icerock.moko.resources.compose.stringResource internal fun GoogleSignInButton(modifier: Modifier, onClick: () -> Unit) { Button( onClick = onClick, - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(6.dp), + modifier = modifier.fillMaxWidth() + .border(2.dp, MaterialTheme.colors.secondary, RoundedCornerShape(40.dp)), + shape = RoundedCornerShape(40.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.surface, - contentColor = MaterialTheme.colors.onSurface + backgroundColor = MaterialTheme.colors.background, + contentColor = MaterialTheme.colors.secondary + ), + elevation = ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + disabledElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp ) ) { Image( @@ -35,6 +44,10 @@ internal fun GoogleSignInButton(modifier: Modifier, onClick: () -> Unit) { modifier = Modifier.size(32.dp) ) Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(MainRes.strings.sign_in_google), modifier = Modifier.padding(6.dp)) + Text( + text = stringResource(MainRes.strings.sign_in_google), + modifier = Modifier.padding(6.dp), + style = MaterialTheme.typography.button + ) } } diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/OutlinedTextFieldColorsSetup.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/OutlinedTextFieldColorsSetup.kt new file mode 100644 index 0000000000000000000000000000000000000000..c3a3e76ba239f1adef72167b9896f4a06575ff81 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/OutlinedTextFieldColorsSetup.kt @@ -0,0 +1,33 @@ +package band.effective.office.elevator.components + +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import band.effective.office.elevator.ExtendedTheme +import band.effective.office.elevator.textGrayColor +import band.effective.office.elevator.theme_light_primary_stroke + +@Composable +fun OutlinedTextColorsSetup() = TextFieldDefaults.outlinedTextFieldColors( +// region::Border + focusedBorderColor = theme_light_primary_stroke, + unfocusedBorderColor = textGrayColor, + disabledBorderColor = textGrayColor, + errorBorderColor = ExtendedTheme.colors.error, +// endregion + +// region::Trailing icon + trailingIconColor = Color.Black, + disabledTrailingIconColor = textGrayColor, + errorTrailingIconColor = Color.Black, +// endregion + +// region::Leading icon + disabledLeadingIconColor = textGrayColor, +// endregion + +// region::Cursor colors + cursorColor = theme_light_primary_stroke, + errorCursorColor = ExtendedTheme.colors.error +// endregion +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/PrimaryButton.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/PrimaryButton.kt new file mode 100644 index 0000000000000000000000000000000000000000..68dafca960a551da25bd570a775ebefb4baa7807 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/components/PrimaryButton.kt @@ -0,0 +1,59 @@ +package band.effective.office.elevator.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonColors +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.ButtonElevation +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit + +@Composable +fun PrimaryButton( + text: String, + modifier: Modifier = Modifier, + cornerValue: Dp, + contentTextSize: TextUnit, + paddingValues: PaddingValues = PaddingValues(), + border: BorderStroke? = null, + elevation: ButtonElevation = ButtonDefaults.elevation(), + colors: ButtonColors = ButtonDefaults.buttonColors(), + onButtonClick: () -> Unit +) { + Button( + modifier = Modifier + .fillMaxWidth() + .then(modifier), + border = border, + elevation = elevation, + shape = RoundedCornerShape(cornerValue), + colors = colors, + contentPadding = paddingValues, + onClick = onButtonClick + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), contentAlignment = Alignment.Center + ) { + Text( + text = text, + fontSize = contentTextSize, + style = MaterialTheme.typography.button, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/Authorization.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/Authorization.kt new file mode 100644 index 0000000000000000000000000000000000000000..75a014169da5e1259cb26aea247a07059642e568 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/Authorization.kt @@ -0,0 +1,27 @@ +package band.effective.office.elevator.ui.authorization + +import androidx.compose.runtime.Composable +import band.effective.office.elevator.ui.authorization.authorization_google.AuthorizationGoogleScreen +import band.effective.office.elevator.ui.authorization.authorization_phone.AuthorizationPhoneScreen +import band.effective.office.elevator.ui.authorization.authorization_profile.AuthorizationProfileScreen +import band.effective.office.elevator.ui.authorization.authorization_telegram.AuthorizationTelegramScreen +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.plus +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.scale +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation + +@Composable +fun AuthorizationScreen(component: AuthorizationComponent) { + Children( + stack = component.childStack, + animation = stackAnimation(fade() + scale()), + ) { + when (val child = it.instance) { + is AuthorizationComponent.Child.GoogleAuthChild -> AuthorizationGoogleScreen(child.component) + is AuthorizationComponent.Child.PhoneAuthChild -> AuthorizationPhoneScreen(child.component) + is AuthorizationComponent.Child.ProfileAuthChild -> AuthorizationProfileScreen(child.component) + is AuthorizationComponent.Child.TelegramAuthChild -> AuthorizationTelegramScreen(child.component) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/AuthorizationComponent.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/AuthorizationComponent.kt index b731befeef59cf7c4d63a87a223d70b6ca36e080..27a07e318f9ad7244e63ae4b7756c3ea4bf9c3f7 100644 --- a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/AuthorizationComponent.kt +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/AuthorizationComponent.kt @@ -1,39 +1,143 @@ package band.effective.office.elevator.ui.authorization -import band.effective.office.elevator.ui.authorization.store.AuthorizationStore -import band.effective.office.elevator.ui.authorization.store.AuthorizationStoreFactory +import band.effective.office.elevator.ui.authorization.authorization_google.AuthorizationGoogleComponent +import band.effective.office.elevator.ui.authorization.authorization_phone.AuthorizationPhoneComponent +import band.effective.office.elevator.ui.authorization.authorization_profile.AuthorizationProfileComponent +import band.effective.office.elevator.ui.authorization.authorization_telegram.AuthorizationTelegramComponent +import band.effective.office.elevator.ui.models.validator.Validator import com.arkivanov.decompose.ComponentContext -import com.arkivanov.mvikotlin.core.instancekeeper.getStore +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.bringToFront +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.replaceAll +import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.mvikotlin.core.store.StoreFactory -import com.arkivanov.mvikotlin.extensions.coroutines.labels -import kotlinx.coroutines.flow.Flow class AuthorizationComponent( componentContext: ComponentContext, - storeFactory: StoreFactory, - private val output: (Output) -> Unit -) : ComponentContext by componentContext { - - private val authorizationStore = - instanceKeeper.getStore { - AuthorizationStoreFactory( - storeFactory = storeFactory - ).create() + private val storeFactory: StoreFactory, + private val openContentFlow: () -> Unit +) : + ComponentContext by componentContext { + + private val validator: Validator = Validator() + private val navigation = StackNavigation() + + private val stack = childStack( + source = navigation, + initialStack = { listOf(AuthorizationComponent.Config.GoogleAuth) }, + childFactory = ::child, + handleBackButton = true + ) + + val childStack: Value> = stack + + private fun child( + config: AuthorizationComponent.Config, + componentContext: ComponentContext + ): AuthorizationComponent.Child = + when (config) { + is Config.GoogleAuth -> Child.GoogleAuthChild( + AuthorizationGoogleComponent( + componentContext, + storeFactory, + ::googleAuthOutput + ) + ) + + is Config.PhoneAuth -> Child.PhoneAuthChild( + AuthorizationPhoneComponent( + componentContext, + storeFactory, + validator, + ::phoneAuthOutput + ) + ) + + is Config.ProfileAuth -> Child.ProfileAuthChild( + AuthorizationProfileComponent( + componentContext, + storeFactory, + validator, + ::profileAuthOutput + ) + ) + + is Config.TelegramAuth -> Child.TelegramAuthChild( + AuthorizationTelegramComponent( + componentContext, + storeFactory, + validator, + ::telegramAuthOutput + ) + ) } - val label: Flow = authorizationStore.labels + private fun googleAuthOutput(output: AuthorizationGoogleComponent.Output) { + when (output) { + AuthorizationGoogleComponent.Output.OpenAuthorizationPhoneScreen -> navigation.replaceAll( + Config.PhoneAuth + ) + else -> {} + } + } - fun onEvent(event: AuthorizationStore.Intent) { - authorizationStore.accept(event) + private fun phoneAuthOutput(output: AuthorizationPhoneComponent.Output) { + when (output) { + AuthorizationPhoneComponent.Output.OpenProfileScreen -> navigation.bringToFront( + AuthorizationComponent.Config.ProfileAuth + ) + + AuthorizationPhoneComponent.Output.OpenGoogleScreen -> navigation.bringToFront( + AuthorizationComponent.Config.GoogleAuth + ) + } } - fun onOutput(output: Output) { - output(output) + private fun profileAuthOutput(output: AuthorizationProfileComponent.Output) { + when (output) { + AuthorizationProfileComponent.Output.OpenPhoneScreen -> navigation.bringToFront( + Config.PhoneAuth + ) + + AuthorizationProfileComponent.Output.OpenTGScreen -> navigation.bringToFront( + Config.TelegramAuth + ) + } + } + + private fun telegramAuthOutput(output: AuthorizationTelegramComponent.Output) { + when (output) { + AuthorizationTelegramComponent.Output.OpenProfileScreen -> navigation.bringToFront( + Config.ProfileAuth + ) + + AuthorizationTelegramComponent.Output.OpenContentFlow -> openContentFlow() + } } - sealed class Output { - object OpenMainScreen : Output() + sealed class Child { + class GoogleAuthChild(val component: AuthorizationGoogleComponent) : Child() + class PhoneAuthChild(val component: AuthorizationPhoneComponent) : Child() + class ProfileAuthChild(val component: AuthorizationProfileComponent) : Child() + class TelegramAuthChild(val component: AuthorizationTelegramComponent) : Child() } -} + sealed class Config : Parcelable { + @Parcelize + object GoogleAuth : Config() + + @Parcelize + object PhoneAuth : Config() + + @Parcelize + object ProfileAuth : Config() + + @Parcelize + object TelegramAuth : Config() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/AuthorizationScreen.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/AuthorizationScreen.kt deleted file mode 100644 index 87331e2ed7b795850339653032c5c20247b88553..0000000000000000000000000000000000000000 --- a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/AuthorizationScreen.kt +++ /dev/null @@ -1,43 +0,0 @@ -package band.effective.office.elevator.ui.authorization - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import band.effective.office.elevator.components.GoogleSignInButton -import band.effective.office.elevator.expects.showToast -import band.effective.office.elevator.ui.authorization.store.AuthorizationStore -import dev.icerock.moko.resources.desc.StringDesc - - -@Composable -fun AuthorizationScreen(component: AuthorizationComponent) { - - LaunchedEffect(component) { - component.label.collect { label -> - when (label) { - is AuthorizationStore.Label.AuthorizationFailure -> showToast(label.message) - AuthorizationStore.Label.AuthorizationSuccess -> component.onOutput( - AuthorizationComponent.Output.OpenMainScreen - ) - } - } - } - - AuthorizationScreenContent(onEvent = component::onEvent) -} - - -@Composable -private fun AuthorizationScreenContent(onEvent: (AuthorizationStore.Intent) -> Unit) { - Box(modifier = Modifier.fillMaxSize().padding(16.dp)) { - GoogleSignInButton( - modifier = Modifier.align(Alignment.Center), - onClick = { onEvent(AuthorizationStore.Intent.SignInButtonClicked) }) - } -} - diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/AuthorizationGoogleComponent.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/AuthorizationGoogleComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..7229837739be29a89c27ab2661fc2c2a35d7d37e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/AuthorizationGoogleComponent.kt @@ -0,0 +1,40 @@ +package band.effective.office.elevator.ui.authorization.authorization_google + +import band.effective.office.elevator.ui.authorization.authorization_google.store.AuthorizationGoogleStore +import band.effective.office.elevator.ui.authorization.authorization_google.store.AuthorizationGoogleStoreFactory +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.mvikotlin.core.instancekeeper.getStore +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.labels +import kotlinx.coroutines.flow.Flow + +class AuthorizationGoogleComponent( + componentContext: ComponentContext, + storeFactory: StoreFactory, + private val output: (Output) -> Unit +) : ComponentContext by componentContext { + + private val authorizationStore = + instanceKeeper.getStore { + AuthorizationGoogleStoreFactory( + storeFactory = storeFactory + ).create() + } + + val label: Flow = authorizationStore.labels + + + fun onEvent(event: AuthorizationGoogleStore.Intent) { + authorizationStore.accept(event) + } + + fun onOutput(output: Output) { + output(output) + } + + sealed class Output { + object OpenAuthorizationPhoneScreen : Output() +// object OpenMainScreen : Output() + } + +} diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/AuthorizationGoogleScreen.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/AuthorizationGoogleScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..b530a38b0505d00ee6adf106c0917116042cbb3b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/AuthorizationGoogleScreen.kt @@ -0,0 +1,82 @@ +package band.effective.office.elevator.ui.authorization.authorization_google + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import band.effective.office.elevator.MainRes +import band.effective.office.elevator.companyColor +import band.effective.office.elevator.components.GoogleSignInButton +import band.effective.office.elevator.expects.showToast +import band.effective.office.elevator.ui.authorization.authorization_google.store.AuthorizationGoogleStore +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + + +@Composable +fun AuthorizationGoogleScreen(component: AuthorizationGoogleComponent) { + + LaunchedEffect(component) { + component.label.collect { label -> + when (label) { + is AuthorizationGoogleStore.Label.AuthorizationFailure -> showToast(label.message) + AuthorizationGoogleStore.Label.AuthorizationSuccess -> component.onOutput( + AuthorizationGoogleComponent.Output.OpenAuthorizationPhoneScreen + ) + } + } + } + + AuthorizationGoogleScreenContent(onEvent = component::onEvent) +} + + +@Composable +private fun AuthorizationGoogleScreenContent(onEvent: (AuthorizationGoogleStore.Intent) -> Unit) { + Box(modifier = Modifier.fillMaxSize().padding(16.dp)) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceAround, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painterResource(MainRes.images.effective_logo), + contentDescription = "Effective logo", + modifier = Modifier.size(80.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(MainRes.strings.company_name), + color = companyColor, + style = MaterialTheme.typography.h4 + ) + } + + GoogleSignInButton( + modifier = Modifier, + onClick = { onEvent(AuthorizationGoogleStore.Intent.SignInButtonClicked) }) + } + } +} + diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/store/AuthorizationGoogleStore.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/store/AuthorizationGoogleStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..df55a0df3d4ec641742899d608ef11631358ac22 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/store/AuthorizationGoogleStore.kt @@ -0,0 +1,19 @@ +package band.effective.office.elevator.ui.authorization.authorization_google.store + +import com.arkivanov.mvikotlin.core.store.Store + +interface AuthorizationGoogleStore : + Store { + + sealed interface Intent { + object SignInButtonClicked : Intent + } + + class State + + sealed interface Label { + object AuthorizationSuccess : Label + + data class AuthorizationFailure(val message: String) : Label + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/store/AuthorizationStoreImpl.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/store/AuthorizationGoogleStoreImpl.kt similarity index 74% rename from composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/store/AuthorizationStoreImpl.kt rename to composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/store/AuthorizationGoogleStoreImpl.kt index c9e4bb9f9e35536195659c52d529fc0385d846bd..9470f5c5db549759af9b231f14f4ca2d3b76d419 100644 --- a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/store/AuthorizationStoreImpl.kt +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_google/store/AuthorizationGoogleStoreImpl.kt @@ -1,10 +1,10 @@ -package band.effective.office.elevator.ui.authorization.store +package band.effective.office.elevator.ui.authorization.authorization_google.store import band.effective.office.elevator.domain.GoogleSignIn import band.effective.office.elevator.domain.SignInResultCallback -import band.effective.office.elevator.ui.authorization.store.AuthorizationStore.Intent -import band.effective.office.elevator.ui.authorization.store.AuthorizationStore.Label -import band.effective.office.elevator.ui.authorization.store.AuthorizationStore.State +import band.effective.office.elevator.ui.authorization.authorization_google.store.AuthorizationGoogleStore.Intent +import band.effective.office.elevator.ui.authorization.authorization_google.store.AuthorizationGoogleStore.Label +import band.effective.office.elevator.ui.authorization.authorization_google.store.AuthorizationGoogleStore.State import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.utils.ExperimentalMviKotlinApi @@ -14,15 +14,15 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject -internal class AuthorizationStoreFactory( +internal class AuthorizationGoogleStoreFactory( private val storeFactory: StoreFactory ) : KoinComponent { private val signInClient: GoogleSignIn by inject() @OptIn(ExperimentalMviKotlinApi::class) - fun create(): AuthorizationStore = - object : AuthorizationStore, Store by storeFactory.create( + fun create(): AuthorizationGoogleStore = + object : AuthorizationGoogleStore, Store by storeFactory.create( name = "AuthorizationStore", initialState = State(), bootstrapper = coroutineBootstrapper { diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/AuthorizationPhoneComponent.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/AuthorizationPhoneComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..02d0274559cbca6eee3de4422766ddf26e202415 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/AuthorizationPhoneComponent.kt @@ -0,0 +1,49 @@ +package band.effective.office.elevator.ui.authorization.authorization_phone + +import band.effective.office.elevator.ui.authorization.authorization_phone.store.AuthorizationPhoneStore +import band.effective.office.elevator.ui.authorization.authorization_phone.store.AuthorizationPhoneStoreFactory +import band.effective.office.elevator.ui.models.validator.Validator +import band.effective.office.elevator.ui.profile.editProfile.store.ProfileEditStore +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.mvikotlin.core.instancekeeper.getStore +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.labels +import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +class AuthorizationPhoneComponent( + componentContext: ComponentContext, + private val storeFactory: StoreFactory, + private val validator: Validator, + private val output: (AuthorizationPhoneComponent.Output) -> Unit +) : ComponentContext by componentContext { + + private val authorizationPhoneStore = + instanceKeeper.getStore { + AuthorizationPhoneStoreFactory( + storeFactory = storeFactory, + validator + ).create() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val phone: StateFlow = authorizationPhoneStore.stateFlow + + val label: Flow = authorizationPhoneStore.labels + + fun onEvent(event: AuthorizationPhoneStore.Intent) { + authorizationPhoneStore.accept(event) + } + + fun onOutput(output: Output) { + output(output) + } + + sealed class Output { + object OpenProfileScreen : Output() + + object OpenGoogleScreen : Output() + } +} diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/AuthorizationPhoneScreen.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/AuthorizationPhoneScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..95004132f45cac33c4c9c08930954fb0de18b4f5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/AuthorizationPhoneScreen.kt @@ -0,0 +1,247 @@ +package band.effective.office.elevator.ui.authorization.authorization_phone + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material3.Divider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import band.effective.office.elevator.ExtendedTheme +import band.effective.office.elevator.MainRes +import band.effective.office.elevator.components.OutlinedTextColorsSetup +import band.effective.office.elevator.components.PrimaryButton +import band.effective.office.elevator.ui.models.PhoneMaskTransformation +import band.effective.office.elevator.expects.showToast +import band.effective.office.elevator.textGrayColor +import band.effective.office.elevator.theme_light_primary_stroke +import band.effective.office.elevator.ui.authorization.authorization_phone.store.AuthorizationPhoneStore +import band.effective.office.elevator.ui.authorization.components.AuthSubTitle +import band.effective.office.elevator.ui.authorization.components.AuthTabRow +import band.effective.office.elevator.ui.authorization.components.AuthTitle +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun AuthorizationPhoneScreen(component: AuthorizationPhoneComponent) { + + val state by component.phone.collectAsState() + val errorMessage = stringResource(MainRes.strings.number_format_error) + + LaunchedEffect(component) { + component.label.collect { label -> + when (label) { + AuthorizationPhoneStore.Label.AuthorizationPhoneFailure -> { + showToast(errorMessage) + } + + AuthorizationPhoneStore.Label.AuthorizationPhoneSuccess -> component.onOutput( + AuthorizationPhoneComponent.Output.OpenProfileScreen + ) + + AuthorizationPhoneStore.Label.ReturnInGoogleAuthorization -> component.onOutput( + AuthorizationPhoneComponent.Output.OpenGoogleScreen + ) + } + } + } + + AuthorizationPhoneComponent(onEvent = component::onEvent, state) +} + +@Composable +private fun AuthorizationPhoneComponent( + onEvent: (AuthorizationPhoneStore.Intent) -> Unit, + state: AuthorizationPhoneStore.State +) { + val elevation = ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + disabledElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp + ) + + val closeIcon = remember { mutableStateOf(false) } + val borderColor = remember { mutableStateOf(textGrayColor) } + val leadingColor = remember { mutableStateOf(textGrayColor) } + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding( + horizontal = 16.dp, + vertical = 48.dp + ), + ) { + IconButton( + modifier = Modifier.size(size = 48.dp), + onClick = { + onEvent(AuthorizationPhoneStore.Intent.BackButtonClicked) + }) { + Icon( + imageVector = Icons.Rounded.ArrowBack, + tint = Color.Black, + contentDescription = "back screen arrow" + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + AuthTabRow(0) + + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + AuthTitle( + text = stringResource(MainRes.strings.input_number), + modifier = Modifier.padding(bottom = 7.dp), + textAlign = TextAlign.Start + ) + + AuthSubTitle( + text = stringResource(MainRes.strings.select_number), + modifier = Modifier.padding(bottom = 24.dp), + textAlign = TextAlign.Start + ) + + OutlinedTextField( + value = state.phoneNumber, + onValueChange = { + if (it.isNotEmpty()) { + closeIcon.value = true + leadingColor.value = Color.Black + borderColor.value = theme_light_primary_stroke + } else { + borderColor.value = textGrayColor + closeIcon.value = false + leadingColor.value = textGrayColor + } + onEvent( + AuthorizationPhoneStore.Intent.PhoneNumberChanged(phoneNumber = it) + ) + }, + visualTransformation = PhoneMaskTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + textStyle = MaterialTheme.typography.body1, + colors = OutlinedTextColorsSetup(), + placeholder = { + Text( + text = stringResource(MainRes.strings.number_hint), + style = MaterialTheme.typography.button, + color = textGrayColor + ) + }, + isError = state.isErrorPhoneNumber, + singleLine = true, + trailingIcon = { + if (closeIcon.value) { + IconButton(onClick = { + onEvent( + AuthorizationPhoneStore.Intent.PhoneNumberChanged(phoneNumber = "") + ) + closeIcon.value = false + leadingColor.value = textGrayColor + }) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = "clear text field", + ) + } + } + }, + shape = RoundedCornerShape(12.dp), + leadingIcon = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Text( + text = stringResource(MainRes.strings.phone_plus_seven), + style = MaterialTheme.typography.button, + color = leadingColor.value + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Divider( + modifier = Modifier + .height(20.dp) + .width(2.dp) + .clip(RoundedCornerShape(4.dp)) + .background(if (state.isErrorPhoneNumber) ExtendedTheme.colors.error else borderColor.value) + .padding(vertical = 14.dp) + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .onFocusChanged { + if (it.isFocused) { + borderColor.value = theme_light_primary_stroke + } else { + borderColor.value = textGrayColor + leadingColor.value = textGrayColor + } + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PrimaryButton( + text = stringResource(MainRes.strings._continue), + cornerValue = 40.dp, + contentTextSize = 16.sp, + paddingValues = PaddingValues(all = 10.dp), + elevation = elevation, + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.primary + ), + border = null, + onButtonClick = { + onEvent(AuthorizationPhoneStore.Intent.ContinueButtonClicked) + } + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/store/AuthorizationPhoneStore.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/store/AuthorizationPhoneStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..906ee2736eeb500156c2c648d011d07ea28b31d1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/store/AuthorizationPhoneStore.kt @@ -0,0 +1,28 @@ +package band.effective.office.elevator.ui.authorization.authorization_phone.store + +import com.arkivanov.mvikotlin.core.store.Store + +interface AuthorizationPhoneStore : + Store { + + sealed interface Intent { + data class PhoneNumberChanged(val phoneNumber: String) : Intent + object ContinueButtonClicked : Intent + object BackButtonClicked : Intent + } + + data class State( + var phoneNumber: String = "", + var isErrorPhoneNumber: Boolean = false, + var isLoading: Boolean = false, + var isError: Boolean = false + ) + + sealed interface Label { + object AuthorizationPhoneSuccess : Label + + object AuthorizationPhoneFailure : Label + + object ReturnInGoogleAuthorization : Label + } +} diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/store/AuthorizationPhoneStoreImpl.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/store/AuthorizationPhoneStoreImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..1eeeec3102a3098307db0cc9f94707bdb76056b1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_phone/store/AuthorizationPhoneStoreImpl.kt @@ -0,0 +1,91 @@ +package band.effective.office.elevator.ui.authorization.authorization_phone.store + +import band.effective.office.elevator.ui.authorization.authorization_phone.store.AuthorizationPhoneStore.* +import band.effective.office.elevator.ui.models.validator.Validator +import com.arkivanov.mvikotlin.core.store.Reducer +import com.arkivanov.mvikotlin.core.store.Store +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor +import org.koin.core.component.KoinComponent + +internal class AuthorizationPhoneStoreFactory( + private val storeFactory: StoreFactory, + private val validator: Validator +) : + KoinComponent { + + fun create(): AuthorizationPhoneStore = + object : AuthorizationPhoneStore, + Store by storeFactory.create( + name = "Authorization phone", + initialState = State(), + executorFactory = ::ExecutorImpl, + reducer = ReducerImpl + ) { + } + + private sealed interface Action { + + } + + sealed interface Msg { + data class Error( + val error: Boolean + ) : Msg + + data class Data( + val phoneNumber: String, + ) : Msg + } + + private object ReducerImpl : Reducer { + override fun State.reduce(msg: Msg): AuthorizationPhoneStore.State = + when (msg) { + is Msg.Data -> copy( + phoneNumber = msg.phoneNumber, + ) + + is Msg.Error -> copy( + isErrorPhoneNumber = msg.error + ) + } + } + + private inner class ExecutorImpl : CoroutineExecutor() { + + override fun executeIntent(intent: Intent, getState: () -> State) = + when (intent) { + Intent.BackButtonClicked -> back() + Intent.ContinueButtonClicked -> checkPhoneNumber(getState().phoneNumber) + is Intent.PhoneNumberChanged -> + dispatch( + Msg.Data( + phoneNumber = intent.phoneNumber + ) + ) + + } + + private fun checkPhoneNumber(phoneNumber: String) { + if (validator.checkPhone(phoneNumber)) { + publish(AuthorizationPhoneStore.Label.AuthorizationPhoneSuccess) + dispatch( + AuthorizationPhoneStoreFactory.Msg.Error( + error = false + ) + ) + } else { + dispatch( + AuthorizationPhoneStoreFactory.Msg.Error( + error = true + ) + ) + publish(AuthorizationPhoneStore.Label.AuthorizationPhoneFailure) + } + } + + private fun back() { + publish(AuthorizationPhoneStore.Label.ReturnInGoogleAuthorization) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/AuthorizationProfileComponent.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/AuthorizationProfileComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..0eb3e9a36ebbd698c5bbf06bb9a4bd2bb9ca0d54 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/AuthorizationProfileComponent.kt @@ -0,0 +1,48 @@ +package band.effective.office.elevator.ui.authorization.authorization_profile + +import band.effective.office.elevator.ui.authorization.authorization_profile.store.AuthorizationProfileStore +import band.effective.office.elevator.ui.authorization.authorization_profile.store.AuthorizationProfileStoreFactory +import band.effective.office.elevator.ui.models.validator.Validator +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.mvikotlin.core.instancekeeper.getStore +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.labels +import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +class AuthorizationProfileComponent( + componentContext: ComponentContext, + private val storeFactory: StoreFactory, + private val validator: Validator, + private val output: (AuthorizationProfileComponent.Output) -> Unit +) : ComponentContext by componentContext { + + private val authorizationProfileStore = + instanceKeeper.getStore { + AuthorizationProfileStoreFactory( + storeFactory = storeFactory, + validator + ).create() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val user: StateFlow = authorizationProfileStore.stateFlow + + val label: Flow = authorizationProfileStore.labels + + fun onEvent(event: AuthorizationProfileStore.Intent) { + authorizationProfileStore.accept(event) + } + + fun onOutput(output: Output) { + output(output) + } + + sealed class Output { + object OpenTGScreen : Output() + + object OpenPhoneScreen : Output() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/AuthorizationProfileScreen.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/AuthorizationProfileScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..865b9f911fd62ca8953ecd99423eba52c7feb53a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/AuthorizationProfileScreen.kt @@ -0,0 +1,338 @@ +package band.effective.office.elevator.ui.authorization.authorization_profile + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Person +import androidx.compose.material.icons.rounded.Work +import androidx.compose.material3.Divider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import band.effective.office.elevator.ExtendedTheme +import band.effective.office.elevator.MainRes +import band.effective.office.elevator.components.OutlinedTextColorsSetup +import band.effective.office.elevator.components.PrimaryButton +import band.effective.office.elevator.expects.showToast +import band.effective.office.elevator.getDefaultFont +import band.effective.office.elevator.textGrayColor +import band.effective.office.elevator.theme_light_primary_stroke +import band.effective.office.elevator.ui.authorization.authorization_phone.store.AuthorizationPhoneStore +import band.effective.office.elevator.ui.authorization.authorization_profile.store.AuthorizationProfileStore +import band.effective.office.elevator.ui.authorization.components.AuthSubTitle +import band.effective.office.elevator.ui.authorization.components.AuthTabRow +import band.effective.office.elevator.ui.authorization.components.AuthTitle +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun AuthorizationProfileScreen(component: AuthorizationProfileComponent) { + + val state by component.user.collectAsState() + val errorMessage = stringResource(MainRes.strings.profile_format_error) + + LaunchedEffect(component) { + component.label.collect { label -> + when (label) { + AuthorizationProfileStore.Label.AuthorizationProfileFailure -> { + showToast(errorMessage) + } + + AuthorizationProfileStore.Label.AuthorizationProfileSuccess -> component.onOutput( + AuthorizationProfileComponent.Output.OpenTGScreen + ) + + AuthorizationProfileStore.Label.ReturnInPhoneAuthorization -> component.onOutput( + AuthorizationProfileComponent.Output.OpenPhoneScreen + ) + } + } + } + + AuthorizationProfileComponent(onEvent = component::onEvent, state) +} + +@Composable +fun AuthorizationProfileComponent( + onEvent: (AuthorizationProfileStore.Intent) -> Unit, + state: AuthorizationProfileStore.State +) { + + val closeIcon1 = remember { mutableStateOf(false) } + val borderColor1 = remember { mutableStateOf(textGrayColor) } + val leadingColor1 = remember { mutableStateOf(textGrayColor) } + + val closeIcon2 = remember { mutableStateOf(false) } + val borderColor2 = remember { mutableStateOf(textGrayColor) } + val leadingColor2 = remember { mutableStateOf(textGrayColor) } + + val elevation = ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + disabledElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp + ) + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding( + horizontal = 16.dp, + vertical = 48.dp + ), + ) { + IconButton( + modifier = Modifier.size(size = 48.dp), + onClick = { + onEvent(AuthorizationProfileStore.Intent.BackButtonClicked) + }) { + Icon( + imageVector = Icons.Rounded.ArrowBack, + tint = Color.Black, + contentDescription = "image_back" + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + AuthTabRow(1) + + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + AuthTitle( + text = stringResource(MainRes.strings.input_profile), + modifier = Modifier.padding(bottom = 7.dp), + textAlign = TextAlign.Start + ) + + AuthSubTitle( + text = stringResource(MainRes.strings.select_profile), + modifier = Modifier.padding(bottom = 24.dp), + textAlign = TextAlign.Start + ) + +// NAME + OutlinedTextField( + value = state.name, + onValueChange = { + if (it.isNotEmpty()) { + closeIcon1.value = true + leadingColor1.value = Color.Black + borderColor1.value = theme_light_primary_stroke + } else { + borderColor1.value = textGrayColor + closeIcon1.value = false + leadingColor1.value = textGrayColor + } + + onEvent(AuthorizationProfileStore.Intent.NameChanged(name = it)) + }, + textStyle = MaterialTheme.typography.body1, + colors = OutlinedTextColorsSetup(), + placeholder = { + Text( + text = stringResource(MainRes.strings.profile_hint), + style = MaterialTheme.typography.button, + color = textGrayColor + ) + }, + isError = state.isErrorName, + singleLine = true, + trailingIcon = { + if (closeIcon1.value) { + IconButton(onClick = { + closeIcon1.value = false + leadingColor1.value = textGrayColor + onEvent(AuthorizationProfileStore.Intent.NameChanged(name = "")) + }) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = "clear text field", + ) + } + } + }, + shape = RoundedCornerShape(12.dp), + leadingIcon = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Icon( + imageVector = Icons.Rounded.Person, + contentDescription = "name icon", + tint = leadingColor1.value + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Divider( + modifier = Modifier + .height(20.dp) + .width(2.dp) + .clip(RoundedCornerShape(4.dp)) + .background(if (state.isErrorName) ExtendedTheme.colors.error else borderColor1.value) + .padding(vertical = 14.dp) + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .onFocusChanged { + if (it.isFocused) { + borderColor1.value = theme_light_primary_stroke + } else { + borderColor1.value = textGrayColor + leadingColor1.value = textGrayColor + } + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + +// POST + OutlinedTextField( + value = state.post, + onValueChange = { + if (it.isNotEmpty()) { + closeIcon2.value = true + leadingColor2.value = Color.Black + borderColor2.value = theme_light_primary_stroke + } else { + borderColor2.value = textGrayColor + closeIcon2.value = false + leadingColor2.value = textGrayColor + } + + onEvent(AuthorizationProfileStore.Intent.PostChanged(post = it)) + }, + textStyle = MaterialTheme.typography.body1, + colors = OutlinedTextColorsSetup(), + placeholder = { + Text( + text = stringResource(MainRes.strings.profile_hint_), + style = MaterialTheme.typography.button, + color = textGrayColor + ) + }, + isError = state.isErrorPost, + singleLine = true, + trailingIcon = { + if (closeIcon2.value) { + IconButton(onClick = { + closeIcon2.value = false + leadingColor2.value = textGrayColor + onEvent(AuthorizationProfileStore.Intent.PostChanged(post = "")) + }) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = "clear text field with post text", + ) + } + } + }, + shape = RoundedCornerShape(12.dp), + leadingIcon = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Icon( + imageVector = Icons.Rounded.Work, + contentDescription = "post icon", + tint = leadingColor2.value + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Divider( + modifier = Modifier + .height(20.dp) + .width(2.dp) + .clip(RoundedCornerShape(4.dp)) + .background(if (state.isErrorPost) ExtendedTheme.colors.error else borderColor2.value) + .padding(vertical = 14.dp) + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .onFocusChanged { + if (it.isFocused) { + borderColor2.value = theme_light_primary_stroke + } else { + borderColor2.value = textGrayColor + leadingColor2.value = textGrayColor + } + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PrimaryButton( + text = stringResource(MainRes.strings._continue), + cornerValue = 40.dp, + contentTextSize = 16.sp, + paddingValues = PaddingValues(all = 10.dp), + elevation = elevation, + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.primary + ), + border = null, + onButtonClick = { + onEvent(AuthorizationProfileStore.Intent.ContinueButtonClicked) + } + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/store/AuthorizationProfileStore.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/store/AuthorizationProfileStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..6134dea14e7cab7fb33121bbfd767a917672e8c8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/store/AuthorizationProfileStore.kt @@ -0,0 +1,31 @@ +package band.effective.office.elevator.ui.authorization.authorization_profile.store + +import com.arkivanov.mvikotlin.core.store.Store + +interface AuthorizationProfileStore : + Store { + + sealed interface Intent { + data class NameChanged(val name: String) : Intent + data class PostChanged(val post: String) : Intent + object ContinueButtonClicked : Intent + object BackButtonClicked : Intent + } + + data class State( + var name: String = "", + var post: String = "", + var isLoading: Boolean = false, + var isError: Boolean = false, + var isErrorName: Boolean = false, + var isErrorPost: Boolean = false + ) + + sealed interface Label { + object AuthorizationProfileSuccess : Label + + object AuthorizationProfileFailure : Label + + object ReturnInPhoneAuthorization : Label + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/store/AuthorizationProfileStoreImpl.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/store/AuthorizationProfileStoreImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..79c945bda78614144c51ccc2bcea376583c4702b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_profile/store/AuthorizationProfileStoreImpl.kt @@ -0,0 +1,100 @@ +package band.effective.office.elevator.ui.authorization.authorization_profile.store + +import band.effective.office.elevator.ui.models.validator.Validator +import com.arkivanov.mvikotlin.core.store.Reducer +import com.arkivanov.mvikotlin.core.store.Store +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor +import org.koin.core.component.KoinComponent + +class AuthorizationProfileStoreFactory( + private val storeFactory: StoreFactory, + private val validator: Validator +) : + KoinComponent { + + fun create(): AuthorizationProfileStore = + object : AuthorizationProfileStore, + Store by storeFactory.create( + name = "Authorization profile", + initialState = AuthorizationProfileStore.State(), + executorFactory = ::ExecutorImpl, + reducer = ReducerImpl + ) { + } + + private sealed interface Msg { + data class NameData( + var name: String, + var isNameError: Boolean + ) : Msg + + data class PostData( + var post: String, + var isPostError: Boolean + ) : Msg + } + + private sealed interface Action { + + } + + private object ReducerImpl : + Reducer { + override fun AuthorizationProfileStore.State.reduce(msg: AuthorizationProfileStoreFactory.Msg): AuthorizationProfileStore.State = + when (msg) { + is Msg.NameData -> copy( + name = msg.name, + isErrorName = msg.isNameError + ) + + is Msg.PostData -> copy( + post = msg.post, + isErrorPost = msg.isPostError + ) + } + } + + private inner class ExecutorImpl : + CoroutineExecutor() { + override fun executeIntent( + intent: AuthorizationProfileStore.Intent, + getState: () -> AuthorizationProfileStore.State + ) = + when (intent) { + AuthorizationProfileStore.Intent.BackButtonClicked -> back() + AuthorizationProfileStore.Intent.ContinueButtonClicked -> checkUserdata( + getState().name, + getState().post + ) + + is AuthorizationProfileStore.Intent.PostChanged -> with(intent.post) { + dispatch( + AuthorizationProfileStoreFactory.Msg.PostData( + post = this, + isPostError = validator.checkPost(this) + ) + ) + } + + is AuthorizationProfileStore.Intent.NameChanged -> dispatch( + AuthorizationProfileStoreFactory.Msg.NameData( + name = intent.name, + isNameError = validator.checkName(intent.name) + ) + ) + } + + private fun checkUserdata(name: String, post: String) { + if (!validator.checkName(name) && !validator.checkPost(post)) { + publish(AuthorizationProfileStore.Label.AuthorizationProfileSuccess) + } else { + publish(AuthorizationProfileStore.Label.AuthorizationProfileFailure) + } + } + + private fun back() { + publish(AuthorizationProfileStore.Label.ReturnInPhoneAuthorization) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/AuthorizationTelegramComponent.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/AuthorizationTelegramComponent.kt new file mode 100644 index 0000000000000000000000000000000000000000..3a1f0448bcfeb80547b92fbf7000cf0c9e32d384 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/AuthorizationTelegramComponent.kt @@ -0,0 +1,48 @@ +package band.effective.office.elevator.ui.authorization.authorization_telegram + +import band.effective.office.elevator.ui.authorization.authorization_telegram.store.AuthorizationTelegramStore +import band.effective.office.elevator.ui.authorization.authorization_telegram.store.AuthorizationTelegramStoreFactory +import band.effective.office.elevator.ui.models.validator.Validator +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.mvikotlin.core.instancekeeper.getStore +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.labels +import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +class AuthorizationTelegramComponent( + componentContext: ComponentContext, + private val storeFactory: StoreFactory, + private val validator: Validator, + private val output: (Output) -> Unit +) : ComponentContext by componentContext { + + private val authorizationTelegramStore = + instanceKeeper.getStore { + AuthorizationTelegramStoreFactory( + storeFactory = storeFactory, + validator + ).create() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val nick: StateFlow = authorizationTelegramStore.stateFlow + + val label: Flow = authorizationTelegramStore.labels + + fun onEvent(event: AuthorizationTelegramStore.Intent) { + authorizationTelegramStore.accept(event) + } + + fun onOutput(output: Output) { + output(output) + } + + sealed class Output { + object OpenContentFlow : Output() + + object OpenProfileScreen : Output() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/AuthorizationTelegramScreen.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/AuthorizationTelegramScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..e14d29fe346f477d4d178673ed7fe95088daa0a1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/AuthorizationTelegramScreen.kt @@ -0,0 +1,244 @@ +package band.effective.office.elevator.ui.authorization.authorization_telegram + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material3.Divider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import band.effective.office.elevator.ExtendedTheme +import band.effective.office.elevator.MainRes +import band.effective.office.elevator.components.OutlinedTextColorsSetup +import band.effective.office.elevator.components.PrimaryButton +import band.effective.office.elevator.expects.showToast +import band.effective.office.elevator.textGrayColor +import band.effective.office.elevator.theme_light_primary_stroke +import band.effective.office.elevator.ui.authorization.authorization_telegram.store.AuthorizationTelegramStore +import band.effective.office.elevator.ui.authorization.components.AuthSubTitle +import band.effective.office.elevator.ui.authorization.components.AuthTabRow +import band.effective.office.elevator.ui.authorization.components.AuthTitle +import dev.icerock.moko.resources.compose.stringResource + + +@Composable +fun AuthorizationTelegramScreen(component: AuthorizationTelegramComponent) { + + val state by component.nick.collectAsState() + val errorMessage = stringResource(MainRes.strings.telegram_format_error) + + LaunchedEffect(component) { + component.label.collect { label -> + when (label) { + AuthorizationTelegramStore.Label.AuthorizationTelegramFailure -> { + showToast(errorMessage) + } + + AuthorizationTelegramStore.Label.AuthorizationTelegramSuccess -> component.onOutput( + AuthorizationTelegramComponent.Output.OpenContentFlow + ) + + AuthorizationTelegramStore.Label.ReturnInProfileAuthorization -> component.onOutput( + AuthorizationTelegramComponent.Output.OpenProfileScreen + ) + } + } + } + + AuthorizationTelegramComponent(onEvent = component::onEvent, state) +} + +@Composable +private fun AuthorizationTelegramComponent( + onEvent: (AuthorizationTelegramStore.Intent) -> Unit, + state: AuthorizationTelegramStore.State +) { + val elevation = ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + disabledElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp + ) + + val closeIcon = remember { mutableStateOf(false) } + val borderColor = remember { mutableStateOf(textGrayColor) } + val leadingColor = remember { mutableStateOf(textGrayColor) } + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding( + horizontal = 16.dp, + vertical = 48.dp + ), + ) { + IconButton( + modifier = Modifier.size(size = 48.dp), + onClick = { + onEvent(AuthorizationTelegramStore.Intent.BackButtonClicked) + }) { + Icon( + imageVector = Icons.Rounded.ArrowBack, + tint = Color.Black, + contentDescription = "back screen arrow" + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + AuthTabRow(2) + + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + AuthTitle( + text = stringResource(MainRes.strings.input_employee), + modifier = Modifier.padding(bottom = 7.dp), + textAlign = TextAlign.Start + ) + + AuthSubTitle( + text = stringResource(MainRes.strings.select_employee), + modifier = Modifier.padding(bottom = 24.dp), + textAlign = TextAlign.Start + ) + + OutlinedTextField( + value = state.nick, + onValueChange = { + if (it.isNotEmpty()) { + closeIcon.value = true + leadingColor.value = Color.Black + borderColor.value = theme_light_primary_stroke + } else { + borderColor.value = textGrayColor + closeIcon.value = false + leadingColor.value = textGrayColor + } + onEvent(AuthorizationTelegramStore.Intent.NickChanged(name = it)) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + textStyle = MaterialTheme.typography.body1, + colors = OutlinedTextColorsSetup(), + placeholder = { + Text( + text = stringResource(MainRes.strings.employee_hint), + color = textGrayColor, + style = MaterialTheme.typography.button + ) + }, + isError = state.isErrorNick, + singleLine = true, + trailingIcon = { + if (closeIcon.value) { + IconButton(onClick = { + onEvent( + AuthorizationTelegramStore.Intent.NickChanged(name = "") + ) + closeIcon.value = false + leadingColor.value = textGrayColor + }) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = "clear text field", + ) + } + } + }, + shape = RoundedCornerShape(12.dp), + leadingIcon = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Text( + text = stringResource(MainRes.strings.telegram_symbol), + style = MaterialTheme.typography.button, + color = leadingColor.value + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Divider( + modifier = Modifier + .height(20.dp) + .width(2.dp) + .clip(RoundedCornerShape(4.dp)) + .background(if (state.isErrorNick) ExtendedTheme.colors.error else borderColor.value) + .padding(vertical = 14.dp) + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .onFocusChanged { + if (it.isFocused) { + borderColor.value = theme_light_primary_stroke + } else { + borderColor.value = textGrayColor + leadingColor.value = textGrayColor + } + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PrimaryButton( + text = stringResource(MainRes.strings._continue), + cornerValue = 40.dp, + contentTextSize = 16.sp, + paddingValues = PaddingValues(all = 10.dp), + elevation = elevation, + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.primary + ), + border = null, + onButtonClick = { + onEvent(AuthorizationTelegramStore.Intent.ContinueButtonClicked) + } + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/store/AuthorizationTelegramStore.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/store/AuthorizationTelegramStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..320eb3e8d79a5f7f45a6c2dc3f40db44d5920358 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/store/AuthorizationTelegramStore.kt @@ -0,0 +1,28 @@ +package band.effective.office.elevator.ui.authorization.authorization_telegram.store + +import com.arkivanov.mvikotlin.core.store.Store + +interface AuthorizationTelegramStore : + Store { + + sealed interface Intent { + data class NickChanged(val name: String) : Intent + object ContinueButtonClicked : Intent + object BackButtonClicked : Intent + } + + data class State( + var nick: String = "", + var isErrorNick: Boolean = false, + var isLoading: Boolean = false, + var isError: Boolean = false + ) + + sealed interface Label { + object AuthorizationTelegramSuccess : Label + + object AuthorizationTelegramFailure : Label + + object ReturnInProfileAuthorization : Label + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/store/AuthorizationTelegramStoreImpl.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/store/AuthorizationTelegramStoreImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..72c2fc4a3735f79fc032964696439234cb2b12ee --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/authorization_telegram/store/AuthorizationTelegramStoreImpl.kt @@ -0,0 +1,82 @@ +package band.effective.office.elevator.ui.authorization.authorization_telegram.store + +import band.effective.office.elevator.ui.models.validator.Validator +import com.arkivanov.mvikotlin.core.store.Reducer +import com.arkivanov.mvikotlin.core.store.Store +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor +import org.koin.core.component.KoinComponent + +class AuthorizationTelegramStoreFactory( + private val storeFactory: StoreFactory, + private val validator: Validator +) : + KoinComponent { + + fun create(): AuthorizationTelegramStore = + object : AuthorizationTelegramStore, + Store by storeFactory.create( + name = "Authorization telegram", + initialState = AuthorizationTelegramStore.State(), + executorFactory = ::ExecutorImpl, + reducer = ReducerImpl + ) { + } + + sealed interface Msg { + data class Data( + val nickName: String, + val isErrorNickName: Boolean, + ) : Msg + } + + private sealed interface Action { + + } + + private object ReducerImpl : + Reducer { + override fun AuthorizationTelegramStore.State.reduce(msg: AuthorizationTelegramStoreFactory.Msg): AuthorizationTelegramStore.State = + when (msg) { + is Msg.Data -> copy( + nick = msg.nickName, + isErrorNick = msg.isErrorNickName + ) + } + } + + private inner class ExecutorImpl : + CoroutineExecutor() { + override fun executeIntent( + intent: AuthorizationTelegramStore.Intent, + getState: () -> AuthorizationTelegramStore.State + ) = + when (intent) { + AuthorizationTelegramStore.Intent.BackButtonClicked -> back() + AuthorizationTelegramStore.Intent.ContinueButtonClicked -> checkTelegramNick( + getState().nick + ) + + is AuthorizationTelegramStore.Intent.NickChanged -> dispatch( + Msg.Data( + nickName = intent.name, + isErrorNickName = !isErrorNickName(intent.name) + ) + ) + } + + private fun isErrorNickName(nickname: String) = validator.checkTelegramNick(nickname) + + private fun checkTelegramNick(telegramNick: String) { + if (validator.checkTelegramNick(telegramNick)) + publish(AuthorizationTelegramStore.Label.AuthorizationTelegramSuccess) + else + publish(AuthorizationTelegramStore.Label.AuthorizationTelegramFailure) + + } + + private fun back() { + publish(AuthorizationTelegramStore.Label.ReturnInProfileAuthorization) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthSubTitile.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthSubTitile.kt new file mode 100644 index 0000000000000000000000000000000000000000..eda5a7f7c21cc74bf481c89ace03d54b7a78df7d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthSubTitile.kt @@ -0,0 +1,37 @@ +package band.effective.office.elevator.ui.authorization.components + +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import band.effective.office.elevator.getDefaultFont +import band.effective.office.elevator.textGrayColor +import band.effective.office.elevator.theme_light_tertiary_color + +@Composable +fun AuthSubTitle( + text: String, + modifier: Modifier = Modifier, + textAlign: TextAlign?, +) { + Text( + text = text, + modifier = Modifier + .wrapContentSize() + .then(modifier), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 20.8.sp, + fontFamily = getDefaultFont(), + fontWeight = FontWeight(500), + color = textGrayColor, + letterSpacing = 0.1.sp, + ), + textAlign = textAlign + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTabItem.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTabItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..a6d08c38f20f17c93440f04a8d1440d9f73c4be5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTabItem.kt @@ -0,0 +1,52 @@ +package band.effective.office.elevator.ui.authorization.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.Tab +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import band.effective.office.elevator.textGrayColor +import band.effective.office.elevator.theme_light_secondary_icon_color +import band.effective.office.elevator.theme_light_tertiary_icon_color + +@Composable +fun AuthTabItem(selected: Boolean, index: Int) { + + val selectedColor = remember { mutableStateOf(theme_light_secondary_icon_color) } + val unselectedColor = remember { mutableStateOf(Color(0x66000000)) } + + Tab( + selectedContentColor = selectedColor.value, + unselectedContentColor = unselectedColor.value, + selected = selected, + onClick = { }, + enabled = false, + modifier = Modifier + .height(4.dp), + icon = { + Box( + modifier = Modifier.padding( + when (index) { + 0 -> PaddingValues(start = 6.dp) + 1 -> PaddingValues(horizontal = 6.dp) + else -> PaddingValues(end = 6.dp) + } + ).clip(RoundedCornerShape(8.dp)) + ) { + Divider( + modifier = Modifier.height(4.dp), + color = if (selected) selectedColor.value else unselectedColor.value + ) + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTabRow.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTabRow.kt new file mode 100644 index 0000000000000000000000000000000000000000..2c63645bb09c73dc4664a614f7ddfb4469131449 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTabRow.kt @@ -0,0 +1,43 @@ +package band.effective.office.elevator.ui.authorization.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.height +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun AuthTabRow(selectedIndex: Int) { + val i = remember { mutableStateOf(selectedIndex) } + val tabs = listOf("1", "2", "3") + + TabRow( + selectedTabIndex = i.value, + divider = { + Color.Transparent + }, + indicator = { pos -> + TabRowDefaults.Indicator( + modifier = Modifier + .background(Color.Transparent) + .height(0.dp), + color = Color.Transparent + ) + }, + backgroundColor = Color.Transparent, + contentColor = Color.Transparent, + modifier = Modifier + .background(Color.Transparent) + .clickable(enabled = false, onClick = {}) + ) { + tabs.forEachIndexed { index, s -> + AuthTabItem(i.value == index, index) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTitile.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTitile.kt new file mode 100644 index 0000000000000000000000000000000000000000..94ffbc2b651c5ee292f1b7fdb33546a893c6c4e1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/components/AuthTitile.kt @@ -0,0 +1,26 @@ +package band.effective.office.elevator.ui.authorization.components + +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign + +@Composable +fun AuthTitle( + text: String, + modifier: Modifier = Modifier, + textAlign: TextAlign?, +) { + Text( + text = text, + modifier = Modifier + .wrapContentSize() + .then(modifier), + style = MaterialTheme.typography.h5, + color = Color(0xFF5800CB), + textAlign = textAlign, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/store/AuthorizationStore.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/store/AuthorizationStore.kt deleted file mode 100644 index c56fd1931ea9d398d61525c38fad0423b66497ac..0000000000000000000000000000000000000000 --- a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/authorization/store/AuthorizationStore.kt +++ /dev/null @@ -1,22 +0,0 @@ -package band.effective.office.elevator.ui.authorization.store - -import androidx.compose.runtime.Composable -import com.arkivanov.mvikotlin.core.store.Store -import dev.icerock.moko.resources.StringResource -import dev.icerock.moko.resources.desc.StringDesc - -interface AuthorizationStore : - Store { - - sealed interface Intent { - object SignInButtonClicked : Intent - } - - class State - - sealed interface Label { - object AuthorizationSuccess : Label - - data class AuthorizationFailure(val message: String) : Label - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/PhoneMaskTransformation.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/PhoneMaskTransformation.kt new file mode 100644 index 0000000000000000000000000000000000000000..ffb1a3fe9fca75f339f55a14ce2b9d7407584885 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/PhoneMaskTransformation.kt @@ -0,0 +1,43 @@ +package band.effective.office.elevator.ui.models + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +class PhoneMaskTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + return maskFilter(text) + } + + private fun maskFilter(text: AnnotatedString): TransformedText { + + // 000-000-00-00 + val trimmed = if (text.text.length >= 10) text.text.substring(0..9) else text.text + var out = "" + + for (i in trimmed.indices) { + out += trimmed[i] + if (i == 2 || i == 5) out += "-" + if (i == 7) out += "-" + } + + val numberOffsetTranslator = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + if (offset <= 2) return offset + if (offset <= 5) return offset + 1 + if (offset <= 7) return offset + 2 + return 11 + } + + override fun transformedToOriginal(offset: Int): Int { + if (offset <= 3) return offset + if (offset <= 7) return offset - 1 + if (offset <= 10) return offset - 2 + return 9 + } + } + + return TransformedText(AnnotatedString(out), numberOffsetTranslator) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/validator/Validator.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/validator/Validator.kt new file mode 100644 index 0000000000000000000000000000000000000000..9e04b55cc18acfe463afc133dde74e9ed42bad0a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/validator/Validator.kt @@ -0,0 +1,13 @@ +package band.effective.office.elevator.ui.models.validator + +open class Validator : ValidatorMethods { + + override fun checkPhone(phoneNumber: String): Boolean = phoneNumber.length == 10 + + override fun checkName(name: String): Boolean = name.isEmpty() + + override fun checkPost(post: String): Boolean = post.isEmpty() + + override fun checkTelegramNick(telegramNick: String): Boolean = + telegramNick.isNotEmpty() && !telegramNick.contains(char = '@', ignoreCase = true) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/validator/ValidatorMethods.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/validator/ValidatorMethods.kt new file mode 100644 index 0000000000000000000000000000000000000000..93975342c9d73e5f37d4f8c1dcb03ecb4b8c6b5d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/models/validator/ValidatorMethods.kt @@ -0,0 +1,8 @@ +package band.effective.office.elevator.ui.models.validator + +interface ValidatorMethods { + fun checkPhone(phoneNumber : String) : Boolean + fun checkName(name : String) : Boolean + fun checkPost(post : String) : Boolean + fun checkTelegramNick(telegramNick : String) : Boolean +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/root/RootComponent.kt b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/root/RootComponent.kt index 1089d8d779b38de93c63f5b8395dd07c927bafe7..0bbaaa678e7dac3fccbbfa8eaba67b54532ad6ee 100644 --- a/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/root/RootComponent.kt +++ b/composeApp/src/commonMain/kotlin/band/effective/office/elevator/ui/root/RootComponent.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.Flow class RootComponent internal constructor( componentContext: ComponentContext, private val storeFactory: DefaultStoreFactory, - private val authorization: (ComponentContext, (AuthorizationComponent.Output) -> Unit) -> AuthorizationComponent, + private val authorization: (ComponentContext, () -> Unit) -> AuthorizationComponent, private val content: (ComponentContext, () -> Unit) -> ContentComponent, ) : ComponentContext by componentContext { @@ -54,7 +54,11 @@ class RootComponent internal constructor( componentContext, storeFactory, authorization = { childContext, output -> - AuthorizationComponent(childContext, storeFactory, output) + AuthorizationComponent( + childContext, + storeFactory, + openContentFlow = output + ) }, content = { childContext, onSignOut -> ContentComponent( @@ -62,16 +66,14 @@ class RootComponent internal constructor( storeFactory, openAuthorizationFlow = onSignOut ) - }) + }, + ) private fun child(config: Config, componentContext: ComponentContext): Child = when (config) { - is Config.Authorization -> Child.AuthorizationChild( - authorization( - componentContext, - ::onAuthorizationOutput - ) - ) + is Config.Authorization -> Child.AuthorizationChild(authorization(componentContext){ + navigation.replaceAll(Config.Content) + }) is Config.Content -> Child.ContentChild(content(componentContext) { navigation.replaceAll(Config.Authorization) @@ -80,17 +82,11 @@ class RootComponent internal constructor( Config.Undefined -> Child.Undefined } - - private fun onAuthorizationOutput(output: AuthorizationComponent.Output) { - when (output) { - AuthorizationComponent.Output.OpenMainScreen -> navigation.replaceAll(Config.Content) - } - } - fun onOutput(output: Output) { when (output) { Output.OpenContent -> navigation.replaceAll(Config.Content) Output.OpenAuthorizationFlow -> navigation.replaceAll(Config.Authorization) + else -> {} } } diff --git a/composeApp/src/commonMain/resources/MR/base/strings_ru.xml b/composeApp/src/commonMain/resources/MR/base/strings_ru.xml index 9ea0f16dab3364aab450b38ea2a3ac306b3d6ee8..7da5d248bc5d748814dff043262126352975530b 100644 --- a/composeApp/src/commonMain/resources/MR/base/strings_ru.xml +++ b/composeApp/src/commonMain/resources/MR/base/strings_ru.xml @@ -1,8 +1,9 @@ Office Elevator - Войти в аккаунт Effective + Авторизация через Google Что-то пошло не так. Пожалуйста попробуйте еще раз + effective\noffice Иванов Иван Смирнов Андрей Васильев Василий @@ -14,6 +15,24 @@ Будет сегодня Нет бронирований + + Продолжить + + Введите номер + Укажите номер, по которому коллеги смогли бы перевести вам деньги по СБП + Номер телефона + + Заполните профиль + Это необходимо для того, чтобы сотрудники смогли вас узнать + Фамилия Имя + Должность + + Укажите контакты + Это необходимо для того, чтобы коллегам было удобно связаться с вами + Никнейм в Telegram + +7 + @ + Лифт Поднять @@ -34,6 +53,9 @@ Бронирование Сотрудники + + Продолжить + Для работы приложения нужно авторизоваться Проблема с интернет соединением @@ -42,6 +64,9 @@ Вы используете неправильный аккаунт Время вышло. Попробуйте снова Ошибка сервера ${code} + Неверный формат номера телефона + Заполните профиль + Укажите ник в телеграмме Лифт успешно вызван! diff --git a/composeApp/src/commonMain/resources/MR/en/strings_en.xml b/composeApp/src/commonMain/resources/MR/en/strings_en.xml index 82366ab0428fa50391221614f0daba9ad948e351..cd510b24d25e0806838df43c5ad351912297c44a 100644 --- a/composeApp/src/commonMain/resources/MR/en/strings_en.xml +++ b/composeApp/src/commonMain/resources/MR/en/strings_en.xml @@ -3,6 +3,7 @@ Office Elevator Sign in to Effective email Something went wrong. Please try again + effective\noffice Ivanov Ivan Smirnov Andrey Vasilyev Vasiliy @@ -14,6 +15,19 @@ Will be today No reservations + + Continue + Enter number + Specify a number to which colleagues could transfer money to you using SBP + Phone number + Fill in your profile + This is necessary for employees to recognize you + Last Name First Name + Occupation + Specify contacts + This is necessary for colleagues to be able to contact you conveniently + Telegram nickname + By date Show on map Book a seat @@ -23,6 +37,9 @@ Cansel Ok + + Continue + Profile Main @@ -40,6 +57,11 @@ You are using the wrong account Request time is up. Try again Server error ${code} + Invalid phone number format + Enter your profile + Specify nickname in telegram + +7 + @ Successfully! diff --git a/composeApp/src/commonMain/resources/MR/images/effective_logo@0.75x.png b/composeApp/src/commonMain/resources/MR/images/effective_logo@0.75x.png new file mode 100644 index 0000000000000000000000000000000000000000..008f8c920f5136b5f6d69646deb7826f3db2e1c0 Binary files /dev/null and b/composeApp/src/commonMain/resources/MR/images/effective_logo@0.75x.png differ diff --git a/composeApp/src/commonMain/resources/MR/images/effective_logo@1.5x.png b/composeApp/src/commonMain/resources/MR/images/effective_logo@1.5x.png new file mode 100644 index 0000000000000000000000000000000000000000..008f8c920f5136b5f6d69646deb7826f3db2e1c0 Binary files /dev/null and b/composeApp/src/commonMain/resources/MR/images/effective_logo@1.5x.png differ diff --git a/composeApp/src/commonMain/resources/MR/images/effective_logo@1x.png b/composeApp/src/commonMain/resources/MR/images/effective_logo@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..008f8c920f5136b5f6d69646deb7826f3db2e1c0 Binary files /dev/null and b/composeApp/src/commonMain/resources/MR/images/effective_logo@1x.png differ diff --git a/composeApp/src/commonMain/resources/MR/images/effective_logo@2x.png b/composeApp/src/commonMain/resources/MR/images/effective_logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..008f8c920f5136b5f6d69646deb7826f3db2e1c0 Binary files /dev/null and b/composeApp/src/commonMain/resources/MR/images/effective_logo@2x.png differ diff --git a/composeApp/src/commonMain/resources/MR/images/effective_logo@3x.png b/composeApp/src/commonMain/resources/MR/images/effective_logo@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..008f8c920f5136b5f6d69646deb7826f3db2e1c0 Binary files /dev/null and b/composeApp/src/commonMain/resources/MR/images/effective_logo@3x.png differ diff --git a/composeApp/src/commonMain/resources/MR/images/effective_logo@4x.png b/composeApp/src/commonMain/resources/MR/images/effective_logo@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..008f8c920f5136b5f6d69646deb7826f3db2e1c0 Binary files /dev/null and b/composeApp/src/commonMain/resources/MR/images/effective_logo@4x.png differ diff --git a/composeApp/src/commonMain/resources/MR/images/ic_bag.svg b/composeApp/src/commonMain/resources/MR/images/ic_bag.svg new file mode 100644 index 0000000000000000000000000000000000000000..4264454ba7bf89129b85debbe147a34936d3e496 --- /dev/null +++ b/composeApp/src/commonMain/resources/MR/images/ic_bag.svg @@ -0,0 +1,3 @@ + + + diff --git a/composeApp/src/commonMain/resources/MR/images/ic_person.svg b/composeApp/src/commonMain/resources/MR/images/ic_person.svg new file mode 100644 index 0000000000000000000000000000000000000000..f0525e13c25a7c15bf5040d0b8429edc4be82ce5 --- /dev/null +++ b/composeApp/src/commonMain/resources/MR/images/ic_person.svg @@ -0,0 +1,3 @@ + + +