move all ui-related classes and files to ui module

This commit is contained in:
2024-07-15 18:31:58 +03:00
parent 9a1bce5707
commit ee7499f117
171 changed files with 405 additions and 1354 deletions
@@ -32,37 +32,6 @@ fun <Success : Any, Error : Any, SuccessDomain : Any, ErrorDomain : Any>
}
}
fun <Success : Any, Error : Any, SuccessDomain : Any, ErrorDomain : Any>
ApiResult<Success, Error>.mapOAuthResult(
successMapper: (Success) -> SuccessDomain,
errorMapper: (Error?) -> ErrorDomain?
): ApiResult<SuccessDomain, ErrorDomain> {
if (BuildConfig.DEBUG) printStackTraceIfAny()
return when (this) {
is ApiResult.Success -> {
ApiResult.success(successMapper(value))
}
is ApiResult.Failure.NetworkFailure -> {
ApiResult.networkFailure(error)
}
is ApiResult.Failure.UnknownFailure -> {
ApiResult.unknownFailure(error)
}
is ApiResult.Failure.HttpFailure -> {
ApiResult.httpFailure(code, errorMapper(error))
}
is ApiResult.Failure.ApiFailure -> {
ApiResult.apiFailure(errorMapper(error))
}
}
}
fun <Success : ApiResponse<*>, SuccessDomain : Any, ErrorDomain : Any>
ApiResult<Success, RestApiError>.mapApiResult(
successMapper: (Success) -> SuccessDomain,
@@ -86,30 +55,18 @@ fun <Success : ApiResponse<*>, SuccessDomain : Any, ErrorDomain : Any>
}
}
fun <R : Any> ApiResult<R, RestApiError>.mapDefault(): ApiResult<R, RestApiErrorDomain> =
mapResult(
successMapper = { response -> response },
errorMapper = { error -> error?.toDomain() }
)
fun <T : Any, R : ApiResponse<T>> ApiResult<R, RestApiError>.mapApiDefault(): ApiResult<T, RestApiErrorDomain> =
mapResult(
successMapper = { response -> response.requireResponse() },
errorMapper = { error -> error?.toDomain() }
)
//@OptIn(ExperimentalContracts::class)
//inline fun <R : Any, E : Any, C> OAuthResponse<R, E>.fold(
// onSuccess: (value: R) -> C,
// onFailure: (failure: E) -> C,
//): C {
// contract {
// callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE)
// callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
// }
// return when (this) {
// is OAuthResponse.Success -> onSuccess(response)
// is OAuthResponse.Error -> onFailure(error)
// }
//}
fun <Success : Any, Error : Any> ApiResult<Success, Error>.isSuccess(): Boolean =
this is ApiResult.Success
fun <Success : Any, Error : Any> ApiResult<Success, Error>.printStackTraceIfAny() {
val throwable = when (this) {
is ApiResult.Failure.NetworkFailure -> error
@@ -118,9 +75,3 @@ fun <Success : Any, Error : Any> ApiResult<Success, Error>.printStackTraceIfAny(
}
throwable?.printStackTrace()
}
fun ApiResult.Failure.HttpFailure<*>?.tryCastToRestErrorDomain() =
this?.error as? RestApiErrorDomain
fun ApiResult.Failure.ApiFailure<*>?.tryCastToRestErrorDomain() =
this?.error as? RestApiErrorDomain
@@ -1,156 +0,0 @@
package com.meloda.app.fast.network
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// TODO: 09/05/2024, Danil Nikolaev: reimplement as sealed class
@JsonClass(generateAdapter = true)
open class OAuthError(
@Json(name = "error") open val error: String,
@Json(name = "error_description") open val errorDescription: String?,
@Json(name = "error_type") open val errorType: String?
)
@JsonClass(generateAdapter = true)
data class ValidationRequiredError(
@Json(name = "error") override val error: String, // "need_validation"
@Json(name = "error_description") override val errorDescription: String, // "sms sent, use code param" if sms method; "use app code" if 2fa app
@Json(name = "validation_type") val validationType: String, // 2fa_app, 2sa_sms
@Json(name = "validation_sid") val validationSid: String,
@Json(name = "phone_mask") val phoneMask: String, // "+7 *** *** ** 50"
@Json(name = "redirect_uri") val redirectUri: String,
@Json(name = "validation_resend") val validationResend: String?, // Приходит, если для отправки кода нужно вызвать метод auth.validatePhone
@Json(name = "cant_get_code_open_restore") val restoreIfCannotGetCode: Boolean?
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = null
)
@JsonClass(generateAdapter = true)
data class CaptchaRequiredError(
@Json(name = "error") override val error: String, // "need_captcha"
@Json(name = "captcha_sid") val captchaSid: String,
@Json(name = "captcha_img") val captchaImage: String,
@Json(name = "captcha_ts") val captchaTs: Double?,
@Json(name = "captcha_ratio") val captchaRatio: Double?,
@Json(name = "captcha_track") val captchaTrack: String?,
@Json(name = "is_refresh_enabled") val isRefreshEnabled: Boolean?,
@Json(name = "is_sound_captcha_available") val isSoundCaptchaAvailable: Boolean?
) : OAuthError(
error = error,
errorDescription = null,
errorType = null
)
@JsonClass(generateAdapter = true)
data class UserBannedError(
@Json(name = "error") override val error: String, // need_validation
@Json(name = "error_description") override val errorDescription: String, // user has been banned
@Json(name = "ban_info") val banInfo: BanInfo
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = null
) {
@JsonClass(generateAdapter = true)
data class BanInfo(
@Json(name = "member_name") val memberName: String,
@Json(name = "message") val message: String,
@Json(name = "access_token") val accessToken: String,
@Json(name = "restore_url") val restoreUrl: String
)
}
@JsonClass(generateAdapter = true)
data class InvalidCredentialsError(
@Json(name = "error") override val error: String, // "invalid_client"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "username_or_password_is_incorrect"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
@JsonClass(generateAdapter = true)
data class WrongValidationCodeError(
@Json(name = "error") override val error: String, // "invalid_request"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "wrong_otp"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
@JsonClass(generateAdapter = true)
data class WrongValidationCodeFormatError(
@Json(name = "error") override val error: String, // "invalid_request"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "otp_format_is_incorrect"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
@JsonClass(generateAdapter = true)
data class TooManyTriesError(
@Json(name = "error") override val error: String, // "9;Flood control"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "password_bruteforce_attempt"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
is ValidationRequiredError -> {
OAuthErrorDomain.ValidationRequiredError(
description = errorDescription,
validationType = ValidationType.parse(validationType),
validationSid = validationSid,
phoneMask = phoneMask,
redirectUri = redirectUri,
validationResend = validationResend,
restoreIfCannotGetCode = restoreIfCannotGetCode
)
}
is CaptchaRequiredError -> {
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = captchaSid,
captchaImageUrl = captchaImage
)
}
is UserBannedError -> {
OAuthErrorDomain.UserBannedError(
memberName = banInfo.memberName,
message = banInfo.message,
accessToken = banInfo.accessToken,
restoreUrl = banInfo.restoreUrl
)
}
is InvalidCredentialsError -> {
OAuthErrorDomain.InvalidCredentialsError
}
is WrongValidationCodeError -> {
OAuthErrorDomain.WrongValidationCode
}
is WrongValidationCodeFormatError -> {
OAuthErrorDomain.WrongValidationCodeFormat
}
is TooManyTriesError -> {
OAuthErrorDomain.TooManyTriesError
}
else -> null
}
@@ -2,6 +2,8 @@ package com.meloda.app.fast.network
sealed class OAuthErrorDomain {
data object UnknownError : OAuthErrorDomain()
data class ValidationRequiredError(
val description: String,
val validationType: ValidationType,
@@ -28,6 +30,4 @@ sealed class OAuthErrorDomain {
data object WrongValidationCode : OAuthErrorDomain()
data object WrongValidationCodeFormat : OAuthErrorDomain()
data object TooManyTriesError: OAuthErrorDomain()
data object UnknownError : OAuthErrorDomain()
}
@@ -1,8 +1,6 @@
package com.meloda.app.fast.network
sealed interface OAuthResponse<out R : Any, out E : Any> {
data class Success<out R : Any>(val response: R) : OAuthResponse<R, Nothing>
data class Error<out E : Any>(val error: E?) : OAuthResponse<Nothing, E>
}
@@ -1,6 +1,6 @@
package com.meloda.app.fast.network
import android.util.Log
import com.meloda.app.fast.model.api.responses.AuthDirectErrorOnlyResponse
import com.squareup.moshi.Moshi
import okhttp3.Request
import okio.Timeout
@@ -28,10 +28,10 @@ class OAuthResultCallFactory(private val moshi: Moshi) : CallAdapter.Factory() {
if (getRawType(callInnerType) == OAuthResponse::class.java) {
if (callInnerType is ParameterizedType) {
val resultInnerType = getParameterUpperBound(0, callInnerType)
return ResultCallAdapter<Any, OAuthError>(resultInnerType, moshi)
return ResultCallAdapter<Any>(resultInnerType, moshi)
}
return ResultCallAdapter<Nothing, Nothing>(Nothing::class.java, moshi)
return ResultCallAdapter<Nothing>(Nothing::class.java, moshi)
}
}
}
@@ -60,36 +60,36 @@ internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : C
abstract fun cloneImpl(): Call<Out>
}
private class ResultCallAdapter<R : Any, E : OAuthError>(
private class ResultCallAdapter<R : Any>(
private val type: Type,
private val moshi: Moshi
) : CallAdapter<R, Call<OAuthResponse<R, E>>> {
) : CallAdapter<R, Call<OAuthResponse<R, AuthDirectErrorOnlyResponse>>> {
override fun responseType() = type
override fun adapt(call: Call<R>): Call<OAuthResponse<R, E>> = ResultCall(call, moshi)
override fun adapt(call: Call<R>): Call<OAuthResponse<R, AuthDirectErrorOnlyResponse>> =
ResultCall(call, moshi)
}
internal class ResultCall<R : Any, E : OAuthError>(
internal class ResultCall<R : Any>(
proxy: Call<R>,
private val moshi: Moshi
) : CallDelegate<R, OAuthResponse<R, E>>(proxy) {
) : CallDelegate<R, OAuthResponse<R, AuthDirectErrorOnlyResponse>>(proxy) {
override fun enqueueImpl(callback: Callback<OAuthResponse<R, E>>) {
override fun enqueueImpl(callback: Callback<OAuthResponse<R, AuthDirectErrorOnlyResponse>>) {
proxy.enqueue(ResultCallback(this, callback, moshi))
}
override fun cloneImpl(): ResultCall<R, E> {
override fun cloneImpl(): ResultCall<R> {
return ResultCall(proxy.clone(), moshi)
}
private class ResultCallback<R : Any, E : OAuthError>(
private val proxy: ResultCall<R, E>,
private val callback: Callback<OAuthResponse<R, E>>,
private class ResultCallback<R : Any>(
private val proxy: ResultCall<R>,
private val callback: Callback<OAuthResponse<R, AuthDirectErrorOnlyResponse>>,
private val moshi: Moshi
) : Callback<R> {
@Suppress("UNCHECKED_CAST")
override fun onResponse(call: Call<R>, response: Response<R>) {
when {
response.isSuccessful -> {
@@ -106,92 +106,19 @@ internal class ResultCall<R : Any, E : OAuthError>(
else -> {
val errorBodyString = response.errorBody()?.string()
val baseError: OAuthError = moshi.adapter(OAuthError::class.java)
val baseError = moshi.adapter(AuthDirectErrorOnlyResponse::class.java)
.fromJson(errorBodyString.orEmpty()) ?: return
val error: OAuthError? = when (baseError.error) {
"9;Flood control" -> {
moshi.adapter(TooManyTriesError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"invalid_client" -> {
moshi.adapter(InvalidCredentialsError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"need_captcha" -> {
moshi.adapter(CaptchaRequiredError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"invalid_request" -> {
when (val type = baseError.errorType) {
"wrong_otp" -> {
moshi.adapter(WrongValidationCodeError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"otp_format_is_incorrect" -> {
moshi.adapter(WrongValidationCodeFormatError::class.java)
.fromJson(errorBodyString.orEmpty())
}
else -> {
Log.d(
"ResultCallback",
"onResponse: invalid_request; error_type: $type"
)
error("Unknown type: $type")
}
}
}
"need_validation" -> {
when (val description = baseError.errorDescription) {
"user has been banned" -> {
moshi.adapter(UserBannedError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"sms sent, use code param",
"use app code" -> {
moshi.adapter(ValidationRequiredError::class.java)
.fromJson(errorBodyString.orEmpty())
}
else -> {
Log.d(
"ResultCallback",
"onResponse: need_validation; description: $description"
)
error("Unknown description: $description")
}
}
}
else -> null
}
error?.let {
callback.onResponse(
proxy,
Response.success(OAuthResponse.Error(error) as OAuthResponse<R, E>)
)
}
callback.onResponse(
proxy,
Response.success(OAuthResponse.Error(baseError) as OAuthResponse<R, AuthDirectErrorOnlyResponse>)
)
}
}
}
override fun onFailure(call: Call<R>, error: Throwable) {
val b = error
// TODO: 12/04/2024, Danil Nikolaev: handle
// callback.onResponse(
// proxy,
// Response.success(OAuthAnswer.Error((throwable = error)))
// )
throw error
}
}
@@ -1,9 +1,10 @@
package com.meloda.app.fast.network;
enum class ValidationType(val value: String) {
APP("2fa_app"), SMS("2fa_sms");
APP("2fa_app"),
SMS("2fa_sms");
companion object {
fun parse(value: String): ValidationType = entries.first { it.value == value }
}
}
companion object {
fun parse(value: String): ValidationType = entries.first { it.value == value }
}
}
@@ -0,0 +1,52 @@
package com.meloda.app.fast.network
enum class VkErrorCode(val code: Int) {
UNKNOWN_ERROR(1),
APP_DISABLED(2),
UNKNOWN_METHOD(3),
INVALID_SIGNATURE(4),
USER_AUTHORIZATION_FAILED(5),
TOO_MANY_REQUESTS(6),
NO_RIGHTS(7),
BAD_REQUEST(8),
TOO_MANY_SIMILAR_ACTIONS(9),
INTERNAL_SERVER_ERROR(10),
IN_TEST_MODE(11),
EXECUTE_CODE_COMPILE_ERROR(12),
EXECUTE_CODE_RUNTIME_ERROR(13),
CAPTCHA_NEEDED(14),
ACCESS_DENIED(15),
REQUIRES_REQUESTS_OVER_HTTPS(16),
VALIDATION_REQUIRED(17),
USER_BANNED_OR_DELETED(18),
ACTION_PROHIBITED(20),
ACTION_ALLOWED_ONLY_FOR_STANDALONE(21),
METHOD_OFF(23),
CONFIRMATION_REQUIRED(24),
PARAMETER_IS_NOT_SPECIFIED(100),
INCORRECT_APP_ID(101),
OUT_OF_LIMITS(103),
INCORRECT_USER_ID(113),
INCORRECT_TIMESTAMP(150),
ACCESS_TO_ALBUM_DENIED(200),
ACCESS_TO_AUDIO_DENIED(201),
ACCESS_TO_GROUP_DENIED(203),
ALBUM_IS_FULL(300),
ACTION_DENIED(500),
PERMISSION_DENIED(600),
CANNOT_SEND_MESSAGE_BLACK_LIST(900),
CANNOT_SEND_MESSAGE_GROUP(901),
INVALID_DOC_ID(1150),
INVALID_DOC_TITLE(1152),
ACCESS_TO_DOC_DENIED(1153),
SOME_AUTH_ERROR(104),
ACCESS_TOKEN_EXPIRED(1117);
companion object {
fun parse(code: Int): VkErrorCode = entries.firstOrNull { it.code == code }
?: throw IllegalArgumentException("Unknown error with value: $code")
}
}
@@ -1,62 +0,0 @@
package com.meloda.app.fast.network
@Suppress("unused")
object VkErrorCodes {
const val UnknownError = 1
const val AppDisabled = 2
const val UnknownMethod = 3
const val InvalidSignature = 4
const val UserAuthorizationFailed = 5
const val TooManyRequests = 6
const val NoRights = 7
const val BadRequest = 8
const val TooManySimilarActions = 9
const val InternalServerError = 10
const val InTestMode = 11
const val ExecuteCodeCompileError = 12
const val ExecuteCodeRuntimeError = 13
const val CaptchaNeeded = 14
const val AccessDenied = 15
const val RequiresRequestsOverHttps = 16
const val ValidationRequired = 17
const val UserBannedOrDeleted = 18
const val ActionProhibited = 20
const val ActionAllowedOnlyForStandalone = 21
const val MethodOff = 23
const val ConfirmationRequired = 24
const val ParameterIsNotSpecified = 100
const val IncorrectAppId = 101
const val OutOfLimits = 103
const val IncorrectUserId = 113
const val IncorrectTimestamp = 150
const val AccessToAlbumDenied = 200
const val AccessToAudioDenied = 201
const val AccessToGroupDenied = 203
const val AlbumIsFull = 300
const val ActionDenied = 500
const val PermissionDenied = 600
const val CannotSendMessageBlackList = 900
const val CannotSendMessageGroup = 901
const val InvalidDocId = 1150
const val InvalidDocTitle = 1152
const val AccessToDocDenied = 1153
const val AccessTokenExpired = 1117
}
object VkOAuthErrors {
const val UNKNOWN = "unknown_error"
const val NEED_VALIDATION = "need_validation"
const val NEED_CAPTCHA = "need_captcha"
const val INVALID_CLIENT = "invalid_client"
const val INVALID_REQUEST = "invalid_request"
const val FLOOD_CONTROL = "9;Flood control"
}
object VkErrorTypes {
const val WRONG_OTP_FORMAT = "otp_format_is_incorrect"
const val WRONG_OTP = "wrong_otp"
const val PASSWORD_BRUTEFORCE_ATTEMPT = "password_bruteforce_attempt"
}
@@ -0,0 +1,16 @@
package com.meloda.app.fast.network
enum class VkOAuthError(val value: String) {
UNKNOWN("unknown_error"),
NEED_VALIDATION("need_validation"),
NEED_CAPTCHA("need_captcha"),
INVALID_CLIENT("invalid_client"),
INVALID_REQUEST("invalid_request"),
FLOOD_CONTROL("9;Flood control");
companion object {
fun parse(value: String): VkOAuthError = entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown error with value: $value")
}
}
@@ -0,0 +1,12 @@
package com.meloda.app.fast.network;
enum class VkOAuthErrorType(val value: String) {
WRONG_OTP_FORMAT("otp_format_is_incorrect"),
WRONG_OTP("wrong_otp"),
PASSWORD_BRUTEFORCE_ATTEMPT("password_bruteforce_attempt");
companion object {
fun parse(value: String): VkOAuthErrorType = entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown error type with value: $value")
}
}
@@ -1,14 +1,23 @@
package com.meloda.app.fast.network.service.auth
import com.meloda.app.fast.model.api.responses.SendSmsResponse
import com.meloda.app.fast.model.api.responses.ValidateLoginResponse
import com.meloda.app.fast.model.api.responses.ValidatePhoneResponse
import com.meloda.app.fast.network.ApiResponse
import com.meloda.app.fast.network.RestApiError
import com.slack.eithernet.ApiResult
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.QueryMap
interface AuthService {
@GET(AuthUrls.SEND_SMS)
suspend fun sendSms(@Query("sid") validationSid: String): ApiResult<ApiResponse<SendSmsResponse>, RestApiError>
@GET(AuthUrls.VALIDATE_PHONE)
suspend fun validatePhone(
@Query("sid") validationSid: String
): ApiResult<ApiResponse<ValidatePhoneResponse>, RestApiError>
@GET(AuthUrls.VALIDATE_LOGIN)
suspend fun validateLogin(
@QueryMap param: Map<String, String>
): ApiResult<ApiResponse<ValidateLoginResponse>, RestApiError>
}
@@ -3,6 +3,8 @@ package com.meloda.app.fast.network.service.auth
import com.meloda.app.fast.common.AppConstants
object AuthUrls {
private const val URL = AppConstants.URL_API
const val SEND_SMS = "${AppConstants.URL_API}/auth.validatePhone"
const val VALIDATE_PHONE = "$URL/auth.validatePhone"
const val VALIDATE_LOGIN = "$URL/auth.validateLogin"
}
@@ -1,6 +1,7 @@
package com.meloda.app.fast.network.service.oauth
import com.meloda.app.fast.model.api.responses.AuthDirectResponse
import com.meloda.app.fast.model.api.responses.GetAnonymousTokenResponse
import com.slack.eithernet.ApiResult
import com.slack.eithernet.DecodeErrorBody
import retrofit2.http.GET
@@ -11,6 +12,12 @@ interface OAuthService {
@DecodeErrorBody
@GET(OAuthUrls.DIRECT_AUTH)
suspend fun auth(
@QueryMap param: Map<String, String?>
@QueryMap param: Map<String, String>
): ApiResult<AuthDirectResponse, AuthDirectResponse>
@DecodeErrorBody
@GET(OAuthUrls.GET_ANONYMOUS_TOKEN)
suspend fun getAnonymousToken(
@QueryMap param: Map<String, String>
): ApiResult<GetAnonymousTokenResponse, GetAnonymousTokenResponse>
}
@@ -3,6 +3,8 @@ package com.meloda.app.fast.network.service.oauth
import com.meloda.app.fast.common.AppConstants
object OAuthUrls {
private const val URL = AppConstants.URL_OAUTH
const val DIRECT_AUTH = "${AppConstants.URL_OAUTH}/token"
const val DIRECT_AUTH = "$URL/token"
const val GET_ANONYMOUS_TOKEN = "$URL/get_anonym_token"
}