From 97e162707b5651fa0e5683a549f6797e14d080df Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 14 Oct 2024 11:32:31 +0500 Subject: [PATCH] global: fix forgot-password --- .../src/main/java/app/tourism/AuthActivity.kt | 6 +- .../src/main/java/app/tourism/Constants.kt | 2 +- .../app/tourism/data/dto/auth/EmailBodyDto.kt | 3 + .../app/tourism/data/remote/TourismApi.kt | 4 + .../data/repositories/AuthRepository.kt | 9 + .../auth/sign_in/ForgotPasswordViewModel.kt | 60 ++++++ .../ui/screens/auth/sign_in/SignInScreen.kt | 104 +++++++++- .../src/main/res/drawable/blur_background.xml | 10 - .../app/src/main/res/values-ru/strings.xml | 2 + android/app/src/main/res/values/strings.xml | 2 + .../en-GB.lproj/Localizable.strings | 4 + .../en.lproj/Localizable.strings | 4 + .../ru.lproj/Localizable.strings | 4 + iphone/Maps/Maps.xcodeproj/project.pbxproj | 8 + .../Tourism/Data/Network/APIEndpoints.swift | 1 + .../Data/Network/DTO/Auth/EmailBodyDto.swift | 5 + .../Data/Network/Services/AuthService.swift | 5 + .../Repositories/AuthRepositoryImpl.swift | 5 + .../Domain/Repositories/AuthRepository.swift | 1 + .../ForgotPasswordViewController.swift | 179 ++++++++++++++++++ 20 files changed, 396 insertions(+), 22 deletions(-) create mode 100644 android/app/src/main/java/app/tourism/data/dto/auth/EmailBodyDto.kt create mode 100644 android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/ForgotPasswordViewModel.kt delete mode 100644 android/app/src/main/res/drawable/blur_background.xml create mode 100644 iphone/Maps/Tourism/Data/Network/DTO/Auth/EmailBodyDto.swift create mode 100644 iphone/Maps/Tourism/Presentation/Auth/Screens/ForgotPasswordViewController.swift diff --git a/android/app/src/main/java/app/tourism/AuthActivity.kt b/android/app/src/main/java/app/tourism/AuthActivity.kt index de3d3d6289..1a4a575b82 100644 --- a/android/app/src/main/java/app/tourism/AuthActivity.kt +++ b/android/app/src/main/java/app/tourism/AuthActivity.kt @@ -30,9 +30,11 @@ class AuthActivity : ComponentActivity() { lifecycleScope.launch { placesRepository.downloadAllData() } + + val blackest = resources.getColor(R.color.button_text) // yes, I know enableEdgeToEdge( - statusBarStyle = SystemBarStyle.dark(resources.getColor(R.color.black_primary)), - navigationBarStyle = SystemBarStyle.dark(resources.getColor(R.color.black_primary)) + statusBarStyle = SystemBarStyle.dark(blackest), + navigationBarStyle = SystemBarStyle.dark(blackest) ) setContent { OrganicMapsTheme() { diff --git a/android/app/src/main/java/app/tourism/Constants.kt b/android/app/src/main/java/app/tourism/Constants.kt index 18ad7fecd0..7ef8523307 100644 --- a/android/app/src/main/java/app/tourism/Constants.kt +++ b/android/app/src/main/java/app/tourism/Constants.kt @@ -56,7 +56,7 @@ fun Modifier.drawOverlayForTextBehind() = brush = Brush.verticalGradient( colors = listOf( Color.Transparent, - Color.Black.copy(alpha = 0.8f), + Color.Black.copy(alpha = 0.9f), ) ) ) diff --git a/android/app/src/main/java/app/tourism/data/dto/auth/EmailBodyDto.kt b/android/app/src/main/java/app/tourism/data/dto/auth/EmailBodyDto.kt new file mode 100644 index 0000000000..052140fed6 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/auth/EmailBodyDto.kt @@ -0,0 +1,3 @@ +package app.tourism.data.dto.auth + +data class EmailBodyDto(val email: String) diff --git a/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt b/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt index afe922ecf4..8cb5a0ca10 100644 --- a/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt +++ b/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt @@ -5,6 +5,7 @@ import app.tourism.data.dto.CategoryDto import app.tourism.data.dto.FavoritesDto import app.tourism.data.dto.FavoritesIdsDto import app.tourism.data.dto.auth.AuthResponseDto +import app.tourism.data.dto.auth.EmailBodyDto import app.tourism.data.dto.place.ReviewDto import app.tourism.data.dto.place.ReviewIdsDto import app.tourism.data.dto.place.ReviewsDto @@ -48,6 +49,9 @@ interface TourismApi { @POST("logout") suspend fun signOut(): Response + + @POST("forgot-password") + suspend fun sendEmailForPasswordReset(@Body emailBody: EmailBodyDto): Response // endregion auth // region profile diff --git a/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt index db67281abc..9f5a968bfc 100644 --- a/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt +++ b/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt @@ -1,6 +1,7 @@ package app.tourism.data.repositories import android.content.Context +import app.tourism.data.dto.auth.EmailBodyDto import app.tourism.data.remote.TourismApi import app.tourism.data.remote.handleGenericCall import app.tourism.domain.models.SimpleResponse @@ -42,4 +43,12 @@ class AuthRepository(private val api: TourismApi, private val context: Context) context ) } + + fun sendEmailForPasswordReset(email: String) = flow { + handleGenericCall( + call = { api.sendEmailForPasswordReset(EmailBodyDto(email)) }, + mapper = { it }, + context + ) + } } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/ForgotPasswordViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/ForgotPasswordViewModel.kt new file mode 100644 index 0000000000..ddaa398243 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/ForgotPasswordViewModel.kt @@ -0,0 +1,60 @@ +package app.tourism.ui.screens.auth.sign_in + +import android.content.Context +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.organicmaps.R +import app.tourism.data.repositories.AuthRepository +import app.tourism.domain.models.SimpleResponse +import app.tourism.domain.models.resource.Resource +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ForgotPasswordViewModel @Inject constructor( + @ApplicationContext val context: Context, + private val authRepository: AuthRepository, +) : ViewModel() { + private val uiChannel = Channel() + val uiEventsChannelFlow = uiChannel.receiveAsFlow() + + private val _email = MutableStateFlow("") + val email = _email.asStateFlow() + + fun setEmail(value: String) { + _email.value = value + } + + private val _forgotPasswordResponse = + MutableStateFlow>(Resource.Idle()) + val forgotPasswordResponse = _forgotPasswordResponse.asStateFlow() + + fun sendEmailForPasswordReset() { + viewModelScope.launch { + authRepository.sendEmailForPasswordReset(email.value) + .collectLatest { resource -> + _forgotPasswordResponse.value = resource + + if (resource is Resource.Success) { + uiChannel.send(ForgotPasswordUiEvent.PopDialog) + uiChannel.send(ForgotPasswordUiEvent.ShowToast(context.getString(R.string.we_sent_you_password_reset_email))) + } else if (resource is Resource.Error) { + uiChannel.send(ForgotPasswordUiEvent.ShowToast(resource.message ?: "")) + } + } + } + } +} + +sealed interface ForgotPasswordUiEvent { + data object PopDialog : ForgotPasswordUiEvent + data class ShowToast(val message: String) : ForgotPasswordUiEvent +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt index 23d43fb14f..1a37768d5a 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt @@ -1,12 +1,12 @@ package app.tourism.ui.screens.auth.sign_in import PasswordEditText -import android.view.RoundedCorner import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -17,11 +17,14 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -31,16 +34,19 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.hilt.navigation.compose.hiltViewModel import app.organicmaps.R -import app.tourism.BASE_URL import app.tourism.Constants import app.tourism.domain.models.resource.Resource import app.tourism.drawDarkContainerBehind import app.tourism.drawOverlayForTextBehind import app.tourism.ui.ObserveAsEvents +import app.tourism.ui.common.HorizontalSpace import app.tourism.ui.common.VerticalSpace import app.tourism.ui.common.buttons.PrimaryButton +import app.tourism.ui.common.buttons.SecondaryButton import app.tourism.ui.common.nav.BackButton import app.tourism.ui.common.textfields.AuthEditText import app.tourism.ui.theme.TextStyles @@ -56,7 +62,9 @@ fun SignInScreen( val context = LocalContext.current val focusManager = LocalFocusManager.current - val userName = vm.email.collectAsState().value + var showForgotPasswordDialog by remember { mutableStateOf(false) } + + val email = vm.email.collectAsState().value val password = vm.password.collectAsState().value val signInResponse = vm.signInResponse.collectAsState().value @@ -105,7 +113,7 @@ fun SignInScreen( ) VerticalSpace(height = 32.dp) AuthEditText( - value = userName, + value = email, onValueChange = { vm.setEmail(it) }, hint = stringResource(id = R.string.email), keyboardActions = KeyboardActions( @@ -134,10 +142,7 @@ fun SignInScreen( TextButton( onClick = { - openUrlInBrowser( - context, - "$BASE_URL/forgot-password" - ) + showForgotPasswordDialog = true }, ) { Text( @@ -161,6 +166,87 @@ fun SignInScreen( color = Color.White, style = TextStyles.h4.copy() ) + + if (showForgotPasswordDialog) { + Dialog( + onDismissRequest = { + showForgotPasswordDialog = false + }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + ForgotPasswordDialog(dismissDialog = { + showForgotPasswordDialog = false + }) + } + } } } +@Composable +fun ForgotPasswordDialog( + vm: ForgotPasswordViewModel = hiltViewModel(), + dismissDialog: () -> Unit +) { + val context = LocalContext.current + + val email = vm.email.collectAsState().value + + val forgotPasswordResponse = vm.forgotPasswordResponse.collectAsState().value + + ObserveAsEvents(flow = vm.uiEventsChannelFlow) { event -> + when (event) { + is ForgotPasswordUiEvent.PopDialog -> dismissDialog() + is ForgotPasswordUiEvent.ShowToast -> context.showToast(event.message) + } + } + + Column( + modifier = Modifier + .padding(16.dp) + .clip(RoundedCornerShape(16.dp)) + .background(color = Color.Black) + .padding(32.dp) + ) { + Text( + text = stringResource(R.string.send_email_for_password_reset), + style = TextStyles.h3, + color = Color.White, + textAlign = TextAlign.Center + ) + + AuthEditText( + value = email, + onValueChange = { vm.setEmail(it) }, + hint = stringResource(id = R.string.email), + keyboardActions = KeyboardActions( + onDone = { + vm.sendEmailForPasswordReset() + }, + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + ) + VerticalSpace(32.dp) + + Row { + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + label = stringResource(id = R.string.send), + isLoading = forgotPasswordResponse is Resource.Loading, + onClick = { + vm.sendEmailForPasswordReset() + }, + ) + HorizontalSpace(16.dp) + + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + label = stringResource(id = R.string.cancel), + onClick = dismissDialog, + ) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/blur_background.xml b/android/app/src/main/res/drawable/blur_background.xml deleted file mode 100644 index 42645d2f39..0000000000 --- a/android/app/src/main/res/drawable/blur_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 62b3ae97c9..55e8177d68 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -450,6 +450,8 @@ Войти в OpenStreetMap Пароль Забыли пароль? + Отправьте свой email, чтобы мы отправили вам ссылку для восстановления пароля + Мы отправили вам письмо для восстановления пароля Выйти Редактировать место Добавить язык diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index d7096f344d..da87dbba8f 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -472,6 +472,8 @@ Login to OpenStreetMap Password Forgot your password? + Send your email, so you receive a link for password reset + We sent you email for password reset\n Log Out Edit Place Add a language diff --git a/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings index 3f52e202f9..97e8ba7dbf 100644 --- a/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings @@ -3981,6 +3981,10 @@ "tourism_forgot_password" = "Forgot password?"; +"send_email_for_password_reset" = "Send your email, so you receive a link for password reset"; + +"we_sent_you_password_reset_email" = "We sent you email for password reset"; + "home" = "Home"; "favorites" = "Favorites"; diff --git a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings index e52b2b0741..49e5990b78 100644 --- a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings @@ -3981,6 +3981,10 @@ "tourism_forgot_password" = "Forgot password?"; +"send_email_for_password_reset" = "Send your email, so you receive a link for password reset"; + +"we_sent_you_password_reset_email" = "We sent you email for password reset"; + "home" = "Home"; "favorites" = "Favorites"; diff --git a/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings index cae011b8b4..417120b3f5 100644 --- a/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings @@ -3981,6 +3981,10 @@ "tourism_forgot_password" = "Забыли пароль?"; +"send_email_for_password_reset" = "Отправьте свой email, чтобы мы отправили вам ссылку для восстановления пароля"; + +"we_sent_you_password_reset_email" = "Мы отправили вам письмо для восстановления пароля"; + "home" = "Главная"; "favorites" = "Избранное"; diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index e4d427d26f..7c587ef287 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -590,6 +590,8 @@ CE6450202C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */; }; CE6450242C9772310075A59B /* DownloadProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6450232C9772310075A59B /* DownloadProgress.swift */; }; CE6450282C99572F0075A59B /* ImageStoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6450272C99572F0075A59B /* ImageStoreUtils.swift */; }; + CE8982032CB9588E00FC2D2E /* EmailBodyDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8982022CB9588E00FC2D2E /* EmailBodyDto.swift */; }; + CE8982052CBCD46300FC2D2E /* ForgotPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8982042CBCD46300FC2D2E /* ForgotPasswordViewController.swift */; }; CEA45BC42C9AE01000ABE6B2 /* DataSyncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */; }; CED0E00E2C8ACBCA008C61CA /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = CED0E00D2C8ACBCA008C61CA /* SDWebImageSwiftUI */; }; CED0E0112C8ACBE1008C61CA /* CountryPickerView in Frameworks */ = {isa = PBXBuildFile; productRef = CED0E0102C8ACBE1008C61CA /* CountryPickerView */; }; @@ -1645,6 +1647,8 @@ CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceControllerTesterBro.swift; sourceTree = ""; }; CE6450232C9772310075A59B /* DownloadProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgress.swift; sourceTree = ""; }; CE6450272C99572F0075A59B /* ImageStoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStoreUtils.swift; sourceTree = ""; }; + CE8982022CB9588E00FC2D2E /* EmailBodyDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailBodyDto.swift; sourceTree = ""; }; + CE8982042CBCD46300FC2D2E /* ForgotPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForgotPasswordViewController.swift; sourceTree = ""; }; CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSyncer.swift; sourceTree = ""; }; CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; CED0E0182C8AD57C008C61CA /* EmptyUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUI.swift; sourceTree = ""; }; @@ -3158,6 +3162,7 @@ 5260D3D72C64F8BC00C673B4 /* AuthResponseDTO.swift */, 52ED919E2C71F718000EE25B /* SignUpRequestDTO.swift */, 529A5F3E2C86E09B004FE4A1 /* HashDTO.swift */, + CE8982022CB9588E00FC2D2E /* EmailBodyDto.swift */, ); path = Auth; sourceTree = ""; @@ -3468,6 +3473,7 @@ 52E2D3A32C59F9CE00A8843A /* WelcomeViewController.swift */, 52B573EB2C61E1C10047FAC9 /* SignInViewController.swift */, 52B573F12C61E8980047FAC9 /* SignUpViewController.swift */, + CE8982042CBCD46300FC2D2E /* ForgotPasswordViewController.swift */, ); path = Screens; sourceTree = ""; @@ -5242,6 +5248,7 @@ 99C9642B2428C0F700E41723 /* PlacePageHeaderViewController.swift in Sources */, F6FE3C391CC50FFD00A73196 /* MWMPlaceDoesntExistAlert.m in Sources */, F6E2FDFE1E097BA00083EBEC /* MWMOpeningHoursClosedSpanTableViewCell.mm in Sources */, + CE8982032CB9588E00FC2D2E /* EmailBodyDto.swift in Sources */, 34B846A12029DCC10081ECCD /* BMCCategoriesHeader.swift in Sources */, 99A614D523C8911A00D8D8D0 /* AuthStyleSheet.swift in Sources */, 99A906F123FA946E0005872B /* DifficultyViewRenderer.swift in Sources */, @@ -5450,6 +5457,7 @@ 3486B5191E27AD3B0069C126 /* MWMFrameworkListener.mm in Sources */, 3404756B1E081A4600C92850 /* MWMSearch+CoreSpotlight.mm in Sources */, CD9AD96C2281B56900EC174A /* CPViewPortState.swift in Sources */, + CE8982052CBCD46300FC2D2E /* ForgotPasswordViewController.swift in Sources */, EDE243DD2B6D2E640057369B /* AboutController.swift in Sources */, 3404755C1E081A4600C92850 /* MWMLocationManager.mm in Sources */, 3454D7BC1E07F045004AF2AD /* CLLocation+Mercator.mm in Sources */, diff --git a/iphone/Maps/Tourism/Data/Network/APIEndpoints.swift b/iphone/Maps/Tourism/Data/Network/APIEndpoints.swift index 66718b2dac..e62a5a93fe 100644 --- a/iphone/Maps/Tourism/Data/Network/APIEndpoints.swift +++ b/iphone/Maps/Tourism/Data/Network/APIEndpoints.swift @@ -5,6 +5,7 @@ struct APIEndpoints { static let signInUrl = "\(BASE_URL)login" static let signUpUrl = "\(BASE_URL)register" static let signOutUrl = "\(BASE_URL)logout" + static let forgotPassword = "\(BASE_URL)forgot-password" // MARK: - Profile static let getUserUrl = "\(BASE_URL)user" diff --git a/iphone/Maps/Tourism/Data/Network/DTO/Auth/EmailBodyDto.swift b/iphone/Maps/Tourism/Data/Network/DTO/Auth/EmailBodyDto.swift new file mode 100644 index 0000000000..0dd3a0363b --- /dev/null +++ b/iphone/Maps/Tourism/Data/Network/DTO/Auth/EmailBodyDto.swift @@ -0,0 +1,5 @@ +import Foundation + +struct EmailBodyDto: Codable { + let email: String +} diff --git a/iphone/Maps/Tourism/Data/Network/Services/AuthService.swift b/iphone/Maps/Tourism/Data/Network/Services/AuthService.swift index 2055376142..6aac6547b6 100644 --- a/iphone/Maps/Tourism/Data/Network/Services/AuthService.swift +++ b/iphone/Maps/Tourism/Data/Network/Services/AuthService.swift @@ -5,6 +5,7 @@ protocol AuthService { func signIn(body: SignInRequestDTO) -> AnyPublisher func signUp(body: SignUpRequestDTO) -> AnyPublisher func signOut() -> AnyPublisher + func sendEmailForPasswordReset(email: String) -> AnyPublisher } class AuthServiceImpl: AuthService { @@ -20,4 +21,8 @@ class AuthServiceImpl: AuthService { func signOut() -> AnyPublisher { return CombineNetworkHelper.postWithoutBody(path: APIEndpoints.signOutUrl) } + + func sendEmailForPasswordReset(email: String) -> AnyPublisher { + return CombineNetworkHelper.post(path: APIEndpoints.forgotPassword, body: EmailBodyDto(email: email)) + } } diff --git a/iphone/Maps/Tourism/Data/Repositories/AuthRepositoryImpl.swift b/iphone/Maps/Tourism/Data/Repositories/AuthRepositoryImpl.swift index 8b0c4bc2fb..431678b050 100644 --- a/iphone/Maps/Tourism/Data/Repositories/AuthRepositoryImpl.swift +++ b/iphone/Maps/Tourism/Data/Repositories/AuthRepositoryImpl.swift @@ -23,4 +23,9 @@ class AuthRepositoryImpl: AuthRepository { return authService.signOut() .eraseToAnyPublisher() } + + func sendEmailForPasswordReset(email: String) -> AnyPublisher { + return authService.sendEmailForPasswordReset(email: email) + .eraseToAnyPublisher() + } } diff --git a/iphone/Maps/Tourism/Domain/Repositories/AuthRepository.swift b/iphone/Maps/Tourism/Domain/Repositories/AuthRepository.swift index ed7775ab7e..805bda8c30 100644 --- a/iphone/Maps/Tourism/Domain/Repositories/AuthRepository.swift +++ b/iphone/Maps/Tourism/Domain/Repositories/AuthRepository.swift @@ -5,4 +5,5 @@ protocol AuthRepository { func signIn(body: SignInRequest) -> AnyPublisher func signUp(body: SignUpRequest) -> AnyPublisher func signOut() -> AnyPublisher + func sendEmailForPasswordReset(email: String) -> AnyPublisher } diff --git a/iphone/Maps/Tourism/Presentation/Auth/Screens/ForgotPasswordViewController.swift b/iphone/Maps/Tourism/Presentation/Auth/Screens/ForgotPasswordViewController.swift new file mode 100644 index 0000000000..0e53b92e7b --- /dev/null +++ b/iphone/Maps/Tourism/Presentation/Auth/Screens/ForgotPasswordViewController.swift @@ -0,0 +1,179 @@ +import UIKit +import Combine + +class ForgotPasswordViewController: UIViewController { + + + private var cancellables = Set() + private var authRepository = AuthRepositoryImpl(authService: AuthServiceImpl()) + + private let backButton: BackButton = { + let backButton = BackButton() + backButton.translatesAutoresizingMaskIntoConstraints = false + return backButton + }() + + private let backgroundImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "splash_background") + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let containerView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = 16 + return view + }() + + private let blurView: UIVisualEffectView = { + let blurEffect = UIBlurEffect(style: .light) + let blurView = UIVisualEffectView(effect: blurEffect) + blurView.translatesAutoresizingMaskIntoConstraints = false + blurView.layer.cornerRadius = 16 + blurView.clipsToBounds = true + return blurView + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = L("send_email_for_password_reset") + UIKitFont.applyStyle(to: label, style: UIKitFont.h3) + label.textColor = .white + label.textAlignment = .center + label.numberOfLines = 2 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let emailTextField: AuthTextField = { + let textField = AuthTextField() + textField.placeholder = L("tourism_email") + textField.keyboardType = .emailAddress + textField.autocapitalizationType = .none + textField.translatesAutoresizingMaskIntoConstraints = false + return textField + }() + + private lazy var sendButton: AppButton = { + let button = AppButton( + label: L("send"), + isPrimary: true, + icon: nil, + target: self, + action: #selector(sendButtonTapped) + ) + return button + }() + + private lazy var cancelButton: AppButton = { + let button = AppButton( + label: L("cancel"), + isPrimary: false, + icon: nil, + target: self, + action: #selector(cancelButtonTapped) + ) + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + } + + private func setupViews() { + view.addSubview(backgroundImageView) + view.addSubview(backButton) + view.addSubview(containerView) + + containerView.addSubview(blurView) + containerView.addSubview(titleLabel) + containerView.addSubview(emailTextField) + containerView.addSubview(sendButton) + containerView.addSubview(cancelButton) + + NSLayoutConstraint.activate([ + // Background Image + backgroundImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + backgroundImageView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + // Back Button + backButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), + backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + + // Container View + containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -120), + + // Blur View + blurView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + blurView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + blurView.topAnchor.constraint(equalTo: containerView.topAnchor), + blurView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + + // Title Label + titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 32), + titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 32), + titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -32), + + // Email Text Field + emailTextField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 32), + emailTextField.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + emailTextField.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + + // Send Button + sendButton.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 32), + sendButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + sendButton.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.43), + sendButton.heightAnchor.constraint(equalToConstant: 44), + + // Cancell Button + cancelButton.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 32), + cancelButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + cancelButton.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.43), + cancelButton.heightAnchor.constraint(equalToConstant: 44), + + containerView.bottomAnchor.constraint(equalTo: sendButton.bottomAnchor, constant: 32) + ]) + + backButton.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside) + } + + // MARK: - buttons listeners + @objc private func backButtonTapped() { + self.navigationController?.popViewController(animated: false) + } + + @objc private func sendButtonTapped() { + sendButton.isLoading = true + authRepository.sendEmailForPasswordReset(email: emailTextField.text ?? "") + .sink(receiveCompletion: { [weak self] completion in + switch completion { + case .finished: + self?.showToast(message: L("we_sent_you_password_reset_email")) + self?.sendButton.isLoading = false + case .failure(let error): + self?.showError(message: error.errorDescription) + } + }, receiveValue: { _ in } + ) + .store(in: &cancellables) + } + + @objc private func cancelButtonTapped() { + self.navigationController?.popViewController(animated: false) + } + + // MARK: - other functions + private func showError(message: String) { + sendButton.isLoading = false + showAlert(title: L("error"), message: message) + } +}