diff --git a/mbw/src/main/assets/token-logos/usdt_logo.png b/mbw/src/main/assets/token-logos/usdt_logo.png new file mode 100644 index 0000000000..bc0fb0473f Binary files /dev/null and b/mbw/src/main/assets/token-logos/usdt_logo.png differ diff --git a/mbw/src/main/java/com/mycelium/bequant/remote/model/User.kt b/mbw/src/main/java/com/mycelium/bequant/remote/model/User.kt deleted file mode 100644 index 8ffa0b02a3..0000000000 --- a/mbw/src/main/java/com/mycelium/bequant/remote/model/User.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.mycelium.bequant.remote.model - -data class User( - val status: Status = Status.REGULAR, -) { - enum class Status { - VIP, - REGULAR; - - fun isVIP() = this == VIP - } -} diff --git a/mbw/src/main/java/com/mycelium/bequant/remote/model/UserStatus.kt b/mbw/src/main/java/com/mycelium/bequant/remote/model/UserStatus.kt new file mode 100644 index 0000000000..40efb7968a --- /dev/null +++ b/mbw/src/main/java/com/mycelium/bequant/remote/model/UserStatus.kt @@ -0,0 +1,16 @@ +package com.mycelium.bequant.remote.model + +enum class UserStatus { + VIP, + REGULAR; + + fun isVIP() = this == VIP + + companion object { + fun fromName(name: String?) = when (name) { + VIP.name -> VIP + REGULAR.name -> REGULAR + else -> null + } + } +} diff --git a/mbw/src/main/java/com/mycelium/bequant/remote/repositories/Api.kt b/mbw/src/main/java/com/mycelium/bequant/remote/repositories/Api.kt index 31a7e57866..4669cfb928 100644 --- a/mbw/src/main/java/com/mycelium/bequant/remote/repositories/Api.kt +++ b/mbw/src/main/java/com/mycelium/bequant/remote/repositories/Api.kt @@ -1,6 +1,5 @@ package com.mycelium.bequant.remote.repositories -import com.mycelium.wallet.external.changelly2.remote.UserRepository object Api { val accountRepository by lazy { AccountApiRepository() } diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/modern/ModernMain.kt b/mbw/src/main/java/com/mycelium/wallet/activity/modern/ModernMain.kt index 719814df49..d9f3188b15 100644 --- a/mbw/src/main/java/com/mycelium/wallet/activity/modern/ModernMain.kt +++ b/mbw/src/main/java/com/mycelium/wallet/activity/modern/ModernMain.kt @@ -52,7 +52,7 @@ import com.mycelium.wallet.event.* import com.mycelium.wallet.external.changelly.ChangellyConstants import com.mycelium.wallet.external.changelly2.ExchangeFragment import com.mycelium.wallet.external.changelly2.HistoryFragment -import com.mycelium.wallet.external.changelly2.remote.UserRepository +import com.mycelium.wallet.external.changelly2.remote.Api import com.mycelium.wallet.external.mediaflow.NewsConstants import com.mycelium.wallet.fio.FioRequestNotificator import com.mycelium.wallet.modularisation.ModularisationVersionHelper @@ -65,6 +65,7 @@ import com.mycelium.wapi.wallet.manager.State import com.squareup.otto.Subscribe import info.guardianproject.netcipher.proxy.OrbotHelper import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import java.util.* import java.util.concurrent.TimeUnit @@ -76,7 +77,7 @@ class ModernMain : AppCompatActivity(), BackHandler { private var mNewsTab: TabLayout.Tab? = null private var mAccountsTab: TabLayout.Tab? = null private var mTransactionsTab: TabLayout.Tab? = null -// private var mVipTab: TabLayout.Tab? = null + private var mVipTab: TabLayout.Tab? = null private var mRecommendationsTab: TabLayout.Tab? = null private var mFioRequestsTab: TabLayout.Tab? = null private var refreshItem: MenuItem? = null @@ -92,7 +93,7 @@ class ModernMain : AppCompatActivity(), BackHandler { lateinit var binding: ModernMainBinding - private val userRepository = UserRepository() + private val userRepository = Api.statusRepository public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -124,8 +125,8 @@ class ModernMain : AppCompatActivity(), BackHandler { mTabsAdapter!!.addTab(mBalanceTab!!, BalanceMasterFragment::class.java, null, TAB_BALANCE) mTransactionsTab = binding.pagerTabs.newTab().setText(getString(R.string.tab_transactions)) mTabsAdapter!!.addTab(mTransactionsTab!!, TransactionHistoryFragment::class.java, null, TAB_HISTORY) -// mVipTab = binding.pagerTabs.newTab().setText(getString(R.string.tab_vip)) -// mTabsAdapter!!.addTab(mVipTab!!, VipFragment::class.java, null, TAB_VIP) + mVipTab = binding.pagerTabs.newTab().setText(getString(R.string.tab_vip)) + mTabsAdapter!!.addTab(mVipTab!!, VipFragment::class.java, null, TAB_VIP) if (getPartnersLocalized()?.isActive() == true) { mRecommendationsTab = @@ -167,15 +168,14 @@ class ModernMain : AppCompatActivity(), BackHandler { lifecycleScope.launchWhenResumed { ChangeLog.showIfNewVersion(this@ModernMain, supportFragmentManager) } -// lifecycleScope.launchWhenStarted { -// userRepository.identify() -// userRepository.userFlow.collect { user -> -// val icon = -// if (user.status.isVIP()) R.drawable.action_bar_logo_vip -// else R.drawable.action_bar_logo -// supportActionBar?.setIcon(icon) -// } -// } + lifecycleScope.launch { + userRepository.statusFlow.collect { status -> + val icon = + if (status.isVIP()) R.drawable.action_bar_logo_vip + else R.drawable.action_bar_logo + supportActionBar?.setIcon(icon) + } + } } fun selectTab(tabTag: String) { diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipFragment.kt b/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipFragment.kt index 34e710921e..6d31991f93 100644 --- a/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipFragment.kt +++ b/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipFragment.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment @@ -49,7 +50,7 @@ class VipFragment : Fragment() { binding.vipProgress.isVisible = state.progress updateButtons(state) handleError(state.error) - handleSuccess(state.success) + handleSuccess(state.isVip) } } @@ -70,39 +71,59 @@ class VipFragment : Fragment() { private fun updateButtons(state: VipViewModel.State) { binding.vipApplyButton.apply { - isEnabled = state.text.isNotEmpty() && !state.progress && !state.error + isEnabled = state.text.isNotEmpty() && !state.progress && state.error == null text = if (state.progress) "" else getString(R.string.apply_vip_code) setOnClickListener { viewModel.applyCode() } } } - private fun handleError(error: Boolean) = binding.apply { - if (error) { - errorText.isVisible = true - vipCodeInput.setBackgroundResource(R.drawable.bg_input_text_filled_error) - } else { - errorText.isVisible = false - vipCodeInput.setBackgroundResource(R.drawable.bg_input_text_filled) + private fun handleError(error: VipViewModel.ErrorType?) = binding.apply { + when (error) { + null -> { + errorText.isVisible = false + vipCodeInput.setBackgroundResource(R.drawable.bg_input_text_filled) + } + + VipViewModel.ErrorType.BAD_REQUEST -> { + errorText.isVisible = true + vipCodeInput.setBackgroundResource(R.drawable.bg_input_text_filled_error) + } + + else -> { + hideKeyBoard() + showViperUnexpectedErrorDialog() + } } } private fun handleSuccess(success: Boolean) { - if (!success) return binding.apply { - vipApplyButton.isVisible = false - vipInputGroup.isVisible = false - vipSuccessGroup.isVisible = true - vipTitle.setText(R.string.vip_title_success) - vipCodeInput.apply { - hint = null - text = null - isFocusable = false - clearFocus() + vipApplyButton.isVisible = !success + vipInputGroup.isVisible = !success + vipSuccessGroup.isVisible = success + vipTitle.setText(if (success) R.string.vip_title_success else R.string.vip_title) + if (success) { + vipCodeInput.apply { + hint = null + text = null + clearFocus() + } + hideKeyBoard() + } else { + vipCodeInput.hint = getString(R.string.vip_code_hint) } - hideKeyBoard() } } + private fun showViperUnexpectedErrorDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(getString(R.string.vip_unexpected_alert_title)) + .setMessage(getString(R.string.vip_unexpected_alert_message)) + .setPositiveButton(R.string.button_ok, null) + .setOnDismissListener { viewModel.resetState() } + .show() + } + private fun hideKeyBoard() { val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as? InputMethodManager imm?.hideSoftInputFromWindow(requireView().windowToken, 0) diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipViewModel.kt b/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipViewModel.kt index 50f3c1c4c1..3fe28483b8 100644 --- a/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipViewModel.kt +++ b/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipViewModel.kt @@ -7,16 +7,17 @@ import com.mycelium.wallet.update import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import retrofit2.HttpException class VipViewModel : ViewModel() { - private val userRepository = Api.userRepository + private val userRepository = Api.statusRepository data class State( - val success: Boolean = false, - val error: Boolean = false, + val isVip: Boolean = false, + val error: ErrorType? = null, val progress: Boolean = true, val text: String = "", ) @@ -26,47 +27,42 @@ class VipViewModel : ViewModel() { init { viewModelScope.launch { - val initialUser = userRepository.userFlow.first() - _stateFlow.update { state -> - state.copy( - progress = false, - success = initialUser.status.isVIP(), - ) + userRepository.statusFlow.collect { status -> + val isVip = status.isVIP() + _stateFlow.update { state -> state.copy(progress = false, isVip = isVip) } } } } fun updateVipText(text: String) { - _stateFlow.update { state -> state.copy(text = text, success = false, error = false) } + _stateFlow.update { s -> s.copy(text = text, error = null) } } - private val exceptionHandler = CoroutineExceptionHandler { _, _ -> - _stateFlow.update { state -> - state.copy( - progress = false, - success = false, - error = true, - ) + private val exceptionHandler = CoroutineExceptionHandler { _, e -> + var errorType = ErrorType.UNEXPECTED + if (e is HttpException) { + if (e.code() == 404 || e.code() == 409 || e.code() == 401) { + errorType = ErrorType.BAD_REQUEST + } } + _stateFlow.update { s -> s.copy(progress = false, error = errorType, isVip = false) } } fun applyCode() { viewModelScope.launch(exceptionHandler) { - _stateFlow.update { state -> - state.copy( - progress = true, - success = false, - error = false, - ) - } + _stateFlow.update { s -> s.copy(progress = true, error = null, isVip = false) } val status = userRepository.applyVIPCode(_stateFlow.value.text) - _stateFlow.update { state -> - state.copy( - progress = false, - success = status.isVIP(), - error = !status.isVIP(), - ) - } + val error = if (status.isVIP()) null else ErrorType.BAD_REQUEST + _stateFlow.update { s -> s.copy(progress = false, error = error) } } } + + fun resetState() { + _stateFlow.update { s -> s.copy(progress = false, error = null, isVip = false) } + } + + enum class ErrorType { + UNEXPECTED, + BAD_REQUEST, + } } \ No newline at end of file diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyAPIService.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyAPIService.kt index a253ae1488..8cb1e2bacc 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyAPIService.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyAPIService.kt @@ -1,7 +1,12 @@ package com.mycelium.wallet.external.changelly -import com.mycelium.wallet.external.changelly.model.* -import org.jetbrains.annotations.TestOnly +import com.mycelium.wallet.external.changelly.model.ChangellyCurrency +import com.mycelium.wallet.external.changelly.model.ChangellyGetExchangeAmountResponse +import com.mycelium.wallet.external.changelly.model.ChangellyListResponse +import com.mycelium.wallet.external.changelly.model.ChangellyResponse +import com.mycelium.wallet.external.changelly.model.ChangellyTransaction +import com.mycelium.wallet.external.changelly.model.ChangellyTransactionOffer +import com.mycelium.wallet.external.changelly.model.FixRate import retrofit2.Call import retrofit2.Response import retrofit2.http.POST @@ -56,7 +61,17 @@ interface ChangellyAPIService { @Query("from") from: String, @Query("to") to: String, @Query("amountFrom") amount: BigDecimal = BigDecimal.ONE, - ): Response> + ): Response> + + @Deprecated( + "To get the fixed rate, you need to use getFixRateForAmount, but the transaction amount must be within limits", + ReplaceWith("getFixRateForAmount") + ) + @POST("getFixRate") + suspend fun getFixRate( + @Query("from") from: String, + @Query("to") to: String, + ): Response> @POST("createFixTransaction") suspend fun createFixTransaction( @@ -66,24 +81,17 @@ interface ChangellyAPIService { @Query("address") address: String, @Query("rateId") rateId: String, @Query("refundAddress") refundAddress: String, - ): Response> + ): ChangellyResponse @POST("getTransactions") suspend fun getTransaction( @Query("id") id: String - ): Response>> + ): ChangellyResponse> @POST("getTransactions") suspend fun getTransactions( @Query("id") id: List, - ): Response>> - - - @TestOnly - @POST("getFixRate") - suspend fun getFixRate(@Query("from") from: String, - @Query("to") to: String): Response> - + ): ChangellyResponse> companion object { const val BCH = "BCH" diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyOfferActivity.java b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyOfferActivity.java index 51c15810e1..3fa726d1b9 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyOfferActivity.java +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyOfferActivity.java @@ -156,7 +156,7 @@ private void createOffer() { amount = getIntent().getDoubleExtra(ChangellyAPIService.AMOUNT, 0); currency = getIntent().getStringExtra(ChangellyAPIService.FROM); receivingAddress = getIntent().getStringExtra(ChangellyAPIService.DESTADDRESS); - ChangellyRetrofitFactory.INSTANCE.getApi() + ChangellyRetrofitFactory.INSTANCE.getChangellyApi() .createTransaction(currency, BTC, amount, receivingAddress) .enqueue(new GetOfferCallback(amount)); progressDialog = new ProgressDialog(this); diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyRetrofitFactory.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyRetrofitFactory.kt index bda9ca63b6..e15262eda7 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyRetrofitFactory.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyRetrofitFactory.kt @@ -7,18 +7,20 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext object ChangellyRetrofitFactory { - private const val BASE_URL = "https://changelly-viper.mycelium.com/v2/" - private const val CHANGELLEY_BASE_URL ="https://api.changelly.com/" + private const val VIPER_BASE_URL = "https://changelly-viper.mycelium.com/v2/" + private const val CHANGELLY_BASE_URL = "https://api.changelly.com/v2/" - private val userKeyPair = UserKeysManager.userSignKeys + private val userKeyPair by lazy { UserKeysManager.userSignKeys } - private fun getHttpClient(): OkHttpClient { + private fun getViperHttpClient(): OkHttpClient { val sslContext = SSLContext.getInstance("TLSv1.3") sslContext.init(null, null, null) return OkHttpClient.Builder().apply { + connectTimeout(3, TimeUnit.SECONDS) // sslSocketFactory uses system defaults X509TrustManager, so deprecation suppressed // referring to sslSocketFactory(SSLSocketFactory, X509TrustManager) docs: /** @@ -27,21 +29,35 @@ object ChangellyRetrofitFactory { * if the implementations are decorated. */ @Suppress("DEPRECATION") sslSocketFactory(sslContext.socketFactory) - addInterceptor(ChangellyHeaderInterceptor()) -// addInterceptor(ChangellyInterceptor()) -// addInterceptor(DigitalSignatureInterceptor(userKeyPair)) + addInterceptor(ChangellyInterceptor()) + addInterceptor(DigitalSignatureInterceptor(userKeyPair)) if (!BuildConfig.DEBUG) return@apply addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) }.build() } + private fun getChangellyHttpClient(): OkHttpClient { + return OkHttpClient.Builder().apply { + addInterceptor(ChangellyInterceptor()) + if (!BuildConfig.DEBUG) return@apply + addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) + }.build() + } - val api: ChangellyAPIService = + val viperApi: ChangellyAPIService by lazy { Retrofit.Builder() - .baseUrl(CHANGELLEY_BASE_URL) + .baseUrl(VIPER_BASE_URL) .addConverterFactory(GsonConverterFactory.create()) - .client(getHttpClient()) + .client(getViperHttpClient()) .build() .create(ChangellyAPIService::class.java) -} + } + val changellyApi: ChangellyAPIService = + Retrofit.Builder() + .baseUrl(CHANGELLY_BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(getChangellyHttpClient()) + .build() + .create(ChangellyAPIService::class.java) +} diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyGetExchangeAmountResponse.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyGetExchangeAmountResponse.kt index c5449b54de..d87086c8e5 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyGetExchangeAmountResponse.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyGetExchangeAmountResponse.kt @@ -18,8 +18,8 @@ data class ChangellyGetExchangeAmountResponse( ) { val receiveAmount: Double get() { - val fee = networkFee.toDoubleOrNull() ?: return .0 +// val fee = networkFee.toDoubleOrNull() ?: return .0 val to = amountTo.toDoubleOrNull() ?: return .0 - return to - fee + return to// - fee } } \ No newline at end of file diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransaction.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransaction.kt index 000c725ac0..e09e605620 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransaction.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransaction.kt @@ -3,19 +3,16 @@ package com.mycelium.wallet.external.changelly.model import java.io.Serializable import java.math.BigDecimal - -class ChangellyTransaction(val id: String, - val status: String, - val moneySent: String, - val amountExpectedFrom: BigDecimal? = null, - val amountExpectedTo: BigDecimal? = null, - val networkFee: BigDecimal? = null, - val currencyFrom: String, - val moneyReceived: String, - val currencyTo: String, - val payoutAddress:String, - val createdAt: Long -) : Serializable { - - fun getExpectedAmount(): BigDecimal? = amountExpectedTo?.minus(networkFee ?: BigDecimal.ZERO) -} \ No newline at end of file +class ChangellyTransaction( + val id: String, + val status: String, + val moneySent: String, + val amountExpectedFrom: BigDecimal? = null, + val amountExpectedTo: BigDecimal? = null, + val networkFee: BigDecimal? = null, + val currencyFrom: String, + val moneyReceived: String, + val currencyTo: String, + val payoutAddress: String, + val createdAt: Long, +) : Serializable diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRate.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRate.kt index 792f2579df..dbace016de 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRate.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRate.kt @@ -11,6 +11,7 @@ data class FixRate( val maxTo: BigDecimal, val minFrom: BigDecimal, val minTo: BigDecimal, - val amountFrom: BigDecimal, - val amountTo: BigDecimal, + val amountFrom: BigDecimal?, + val amountTo: BigDecimal?, + val networkFee: BigDecimal?, ) diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRateForAmount.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRateForAmount.kt deleted file mode 100644 index 2db4651db4..0000000000 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRateForAmount.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.mycelium.wallet.external.changelly.model - -import java.math.BigDecimal - - -data class FixRateForAmount(val id: String, - val result: BigDecimal, - val networkFee: BigDecimal, - val from: String, - val to: String, - val amountFrom: BigDecimal, - val amountTo: BigDecimal, - val max: BigDecimal, - val maxFrom:BigDecimal, - val maxTo:BigDecimal, - val min:BigDecimal, - val minFrom:BigDecimal, - val minTo:BigDecimal) { - - fun getExpectedValue() = result - networkFee -} - diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeFragment.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeFragment.kt index 6e4aa10465..8c021ea835 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeFragment.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeFragment.kt @@ -22,7 +22,10 @@ import com.bumptech.glide.load.resource.bitmap.CircleCrop import com.bumptech.glide.request.RequestOptions import com.mrd.bitlib.model.BitcoinAddress import com.mycelium.view.RingDrawable -import com.mycelium.wallet.* +import com.mycelium.wallet.BuildConfig +import com.mycelium.wallet.MbwManager +import com.mycelium.wallet.R +import com.mycelium.wallet.Utils import com.mycelium.wallet.activity.modern.ModernMain import com.mycelium.wallet.activity.modern.event.BackHandler import com.mycelium.wallet.activity.modern.event.BackListener @@ -36,14 +39,20 @@ import com.mycelium.wallet.activity.util.toStringWithUnit import com.mycelium.wallet.activity.view.ValueKeyboard import com.mycelium.wallet.activity.view.loader import com.mycelium.wallet.databinding.FragmentChangelly2ExchangeBinding -import com.mycelium.wallet.event.* +import com.mycelium.wallet.event.ExchangeRatesRefreshed +import com.mycelium.wallet.event.ExchangeSourceChanged +import com.mycelium.wallet.event.PageSelectedEvent +import com.mycelium.wallet.event.SelectedAccountChanged +import com.mycelium.wallet.event.SelectedCurrencyChanged +import com.mycelium.wallet.event.TransactionBroadcasted import com.mycelium.wallet.external.changelly.model.ChangellyResponse import com.mycelium.wallet.external.changelly.model.ChangellyTransactionOffer -import com.mycelium.wallet.external.changelly.model.FixRate -import com.mycelium.wallet.external.changelly.model.FixRateForAmount import com.mycelium.wallet.external.changelly2.remote.Changelly2Repository +import com.mycelium.wallet.external.changelly2.remote.ViperStatusException +import com.mycelium.wallet.external.changelly2.remote.ViperUnexpectedException import com.mycelium.wallet.external.changelly2.viewmodel.ExchangeViewModel import com.mycelium.wallet.external.partner.openLink +import com.mycelium.wallet.startCoroutineTimer import com.mycelium.wapi.wallet.AesKeyCipher import com.mycelium.wapi.wallet.BroadcastResultType import com.mycelium.wapi.wallet.Transaction @@ -59,6 +68,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import retrofit2.HttpException import java.math.BigDecimal import java.math.RoundingMode import java.util.concurrent.TimeUnit @@ -183,7 +193,7 @@ class ExchangeFragment : Fragment(), BackListener { val exchangeInfoResult = viewModel.exchangeInfo.value?.result if (friendlyDigits == null || exchangeInfoResult == null) N_A else amount.toBigDecimal().setScale(friendlyDigits, RoundingMode.HALF_UP) - ?.div(viewModel.exchangeInfo.value!!.getExpectedValue()) + ?.div(exchangeInfoResult) ?.stripTrailingZeros() ?.toPlainString() ?: N_A } catch (e: NumberFormatException) { @@ -260,51 +270,7 @@ class ExchangeFragment : Fragment(), BackListener { } } binding?.exchangeButton?.setOnClickListener { - loader(true) - Changelly2Repository.createFixTransaction(lifecycleScope, - viewModel.exchangeInfo.value?.id!!, - Util.trimTestnetSymbolDecoration(viewModel.fromCurrency.value?.symbol!!), - Util.trimTestnetSymbolDecoration(viewModel.toCurrency.value?.symbol!!), - viewModel.sellValue.value!!, - viewModel.toAddress.value!!, - viewModel.fromAddress.value!!, - { result -> - if (result?.result != null) { - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Default) { - val unsignedTx = prepareTx( - if (BuildConfig.FLAVOR == "btctestnet") - viewModel.fromAddress.value!! - else - result.result!!.payinAddress!!, - result.result!!.amountExpectedFrom.toPlainString()) - if(unsignedTx != null) { - launch(Dispatchers.Main) { - loader(false) - acceptDialog(unsignedTx, result) { - sendTx(result.result!!.id!!, unsignedTx) - } - } - } - } - } else { - loader(false) - AlertDialog.Builder(requireContext()) - .setMessage(if (result?.error?.message?.startsWith("rateId was expired") == true) - getString(R.string.changelly_error_rate_expired) - else result?.error?.message) - .setPositiveButton(R.string.button_ok, null) - .setOnDismissListener { updateAmount() } - .show() - } - }, - { _, msg -> - loader(false) - AlertDialog.Builder(requireContext()) - .setMessage(msg) - .setPositiveButton(R.string.button_ok, null) - .setOnDismissListener { updateAmount() } - .show() - }) + createFixTransaction() } viewModel.fromCurrency.observe(viewLifecycleOwner) { coin -> binding?.sellLayout?.coinIcon?.let { @@ -356,15 +322,98 @@ class ExchangeFragment : Fragment(), BackListener { } } + private fun createFixTransaction(changellyOnly: Boolean = false){ + loader(true) + lifecycleScope.launch { + try { + val response = Changelly2Repository.createFixTransaction( + viewModel.exchangeInfo.value?.id!!, + Util.trimTestnetSymbolDecoration(viewModel.fromCurrency.value?.symbol!!), + Util.trimTestnetSymbolDecoration(viewModel.toCurrency.value?.symbol!!), + viewModel.sellValue.value!!, + viewModel.toAddress.value!!, + viewModel.fromAddress.value!!, + changellyOnly, + ) + val result = response.result + if (result != null) { + withContext(Dispatchers.Default) { + val addressTo = + if (BuildConfig.FLAVOR == "btctestnet") viewModel.fromAddress.value!! + else result.payinAddress!! + val amount = result.amountExpectedFrom.toPlainString() + val unsignedTx = prepareTx(addressTo, amount) + withContext(Dispatchers.Main) { + loader(false) + if (unsignedTx != null) { + acceptDialog(unsignedTx, response) { + sendTx(result.id!!, unsignedTx) + } + } + } + } + } else { + loader(false) + showErrorNotificationDialog(response.error?.message) + } + } catch (e: Exception) { + loader(false) + when (e) { + is HttpException -> showErrorNotificationDialog(e.message()) + is ViperStatusException -> showViperErrorDialog( + getString(R.string.vip_exchange_expired_title), + getString(R.string.vip_exchange_status_expired_alert_message), + getString(R.string.changelly2_proceed), + getString(R.string.cancel_transaction) + ) + is ViperUnexpectedException -> showViperErrorDialog( + getString(R.string.vip_exchange_unexpected_alert_title), + getString(R.string.vip_exchange_unexpected_alert_message), + getString(R.string.changelly2_proceed), + getString(R.string.vip_alert_cancel) + ) + else -> showErrorNotificationDialog(e.message) + } + } + } + } + private fun showErrorNotificationDialog(message: String?) { + val localizedMessage = if (message?.startsWith("rateId was expired") == true) { + getString(R.string.changelly_error_rate_expired) + } else { + message ?: "Something went wrong." + } + AlertDialog.Builder(requireContext()) + .setMessage(localizedMessage) + .setPositiveButton(R.string.button_ok, null) + .setOnDismissListener { updateAmount() } + .show() + } + + private fun showViperErrorDialog(title: String, message: String, positive:String, negative:String) { + AlertDialog.Builder(requireContext()) + .setTitle(title) + .setMessage(message) + .setPositiveButton(positive) { _, _ -> + updateAmount() + createFixTransaction(true) + } + .setNegativeButton(negative, null) + .show() + } + private fun computeBuyValue() { val amount = viewModel.sellValue.value - viewModel.buyValue.value = if (amount?.isNotEmpty() == true - && viewModel.exchangeInfo.value?.result != null) { + val info = viewModel.exchangeInfo.value + val rate = info?.result + viewModel.buyValue.value = if (amount?.isNotEmpty() == true && rate != null) { try { - (amount.toBigDecimal() * viewModel.exchangeInfo.value?.getExpectedValue()!!) - .setScale(viewModel.toCurrency.value?.friendlyDigits!!, RoundingMode.HALF_UP) - .stripTrailingZeros() - .toPlainString() + val result = amount.toBigDecimal() * rate + if (result <= BigDecimal.ZERO) null + else result + .setScale(viewModel.toCurrency.value?.friendlyDigits!!, RoundingMode.HALF_UP) + .stripTrailingZeros() + .toPlainString() } catch (e: NumberFormatException) { "N/A" } @@ -439,7 +488,12 @@ class ExchangeFragment : Fragment(), BackListener { { result -> val data = result?.result?.firstOrNull() if (data != null) { - viewModel.exchangeInfo.value = data + val info = viewModel.exchangeInfo.value + viewModel.exchangeInfo.value = if (info == null) data else data.copy( + amountFrom = info.amountFrom, + amountTo = info.amountTo, + networkFee = info.networkFee, + ) viewModel.errorRemote.value = "" } else { viewModel.errorRemote.value = result?.error?.message ?: "" @@ -485,8 +539,7 @@ class ExchangeFragment : Fragment(), BackListener { fromAmount, { result -> result?.result?.firstOrNull()?.let { - val info = viewModel.exchangeInfo.value - viewModel.exchangeInfo.postValue(it) + viewModel.exchangeInfo.value = it viewModel.errorRemote.value = "" } ?: run { viewModel.errorRemote.value = result?.error?.message ?: "" diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeResultFragment.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeResultFragment.kt index 7ca6b36b3f..7583d32f13 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeResultFragment.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeResultFragment.kt @@ -3,7 +3,12 @@ package com.mycelium.wallet.external.changelly2 import android.app.AlertDialog import android.graphics.Color import android.os.Bundle -import android.view.* +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import androidx.core.view.forEach import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels @@ -21,8 +26,10 @@ import com.mycelium.wallet.startCoroutineTimer import com.mycelium.wapi.wallet.AddressUtils import com.mycelium.wapi.wallet.TransactionSummary import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import java.text.DateFormat -import java.util.* +import java.util.Date +import java.util.UUID import java.util.concurrent.TimeUnit @@ -82,31 +89,31 @@ class ExchangeResultFragment : DialogFragment() { private fun update(txId: String) { loader(true) - Changelly2Repository.getTransaction(lifecycleScope, txId, - { response -> - response?.result?.first()?.let { result -> - binding?.toolbar?.title = result.getReadableStatus("exchange") - viewModel.setTransaction(result) - } ?: let { - AlertDialog.Builder(requireContext()) - .setMessage(response?.error?.message) - .setPositiveButton(R.string.button_ok) { _, _ -> - dismissAllowingStateLoss() - } - .show() - } - }, - { _, msg -> - AlertDialog.Builder(requireContext()) - .setMessage(msg) - .setPositiveButton(R.string.button_ok) { _, _ -> - dismissAllowingStateLoss() - } - .show() - }, - { - loader(false) - }) + lifecycleScope.launch { + try { + val transaction = Changelly2Repository.getTransaction(txId) + val result = transaction.result?.firstOrNull { it.id == txId } + if (transaction.error != null || result == null) { + showErrorDialog(transaction.error?.message) + return@launch + } + binding?.toolbar?.title = result.getReadableStatus("exchange") + viewModel.setTransaction(result) + } catch (e: Exception) { + showErrorDialog(e.message) + } finally { + loader(false) + } + } + } + + private fun showErrorDialog(message: String?) { + AlertDialog.Builder(requireContext()) + .setMessage(message) + .setPositiveButton(R.string.button_ok) { _, _ -> + dismissAllowingStateLoss() + } + .show() } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/HistoryFragment.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/HistoryFragment.kt index 463c71de81..db1557abf9 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/HistoryFragment.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/HistoryFragment.kt @@ -2,11 +2,18 @@ package com.mycelium.wallet.external.changelly2 import android.content.Context import android.os.Bundle -import android.view.* +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.LinearLayout import androidx.core.view.forEach import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope +import com.mycelium.wallet.Constants.TAG import com.mycelium.wallet.R import com.mycelium.wallet.activity.view.DividerItemDecoration import com.mycelium.wallet.activity.view.loader @@ -16,12 +23,15 @@ import com.mycelium.wallet.external.adapter.TxItem import com.mycelium.wallet.external.changelly2.remote.Changelly2Repository import com.mycelium.wallet.external.changelly2.remote.fixedCurrencyFrom import com.mycelium.wallet.external.changelly2.remote.fixedCurrencyTo +import kotlinx.coroutines.launch import java.text.DateFormat -import java.util.* +import java.util.Date +import java.util.UUID class HistoryFragment : DialogFragment() { + private val historyDateFormat = DateFormat.getDateInstance(DateFormat.LONG) var binding: FragmentChangelly2HistoryBinding? = null val pref by lazy { requireContext().getSharedPreferences(ExchangeFragment.PREF_FILE, Context.MODE_PRIVATE) } val adapter = TxHistoryAdapter() @@ -29,7 +39,7 @@ class HistoryFragment : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NORMAL, R.style.Dialog_Changelly) - setHasOptionsMenu(true) + setHasOptionsMenu(false) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = @@ -64,27 +74,31 @@ class HistoryFragment : DialogFragment() { val txIds = (pref.getStringSet(ExchangeFragment.KEY_HISTORY, null) ?: setOf()).toList() .filterNotNull() .filterNot { it.isEmpty() } - if (txIds.isNotEmpty()) { - loader(true) - Changelly2Repository.getTransactions(lifecycleScope, txIds, - { - it?.result?.let { - adapter.submitList(it.map { - TxItem(it.id, - it.amountExpectedFrom.toString(), it.getExpectedAmount().toString(), - it.fixedCurrencyFrom(), it.fixedCurrencyTo(), - DateFormat.getDateInstance(DateFormat.LONG).format(Date(it.createdAt * 1000L)), - it.getReadableStatus()) - }) - } - }, - { _, _ -> + if (txIds.isEmpty()) return + loader(true) + lifecycleScope.launch { + try { + val result = Changelly2Repository.getTransactions(txIds).result ?: return@launch + adapter.submitList(result.sortedByDescending { + it.createdAt + }.map { + TxItem( + it.id, + it.amountExpectedFrom.toString(), + it.amountExpectedTo.toString(), + it.fixedCurrencyFrom(), + it.fixedCurrencyTo(), + historyDateFormat.format(Date(it.createdAt / 1000L)), + it.getReadableStatus() + ) + }) - }, - { - updateEmpty() - loader(false) - }) + } catch (e: Exception) { + Log.e(TAG, "${e.message}"); + } finally { + updateEmpty() + loader(false) + } } } diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Api.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Api.kt index 1dc5abb078..b3d3e75ba5 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Api.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Api.kt @@ -1,7 +1,6 @@ package com.mycelium.wallet.external.changelly2.remote -import com.mycelium.wallet.external.changelly2.remote.UserRepository object Api { - val userRepository by lazy { UserRepository() } + val statusRepository by lazy { StatusRepository() } } \ No newline at end of file diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Changelly2Repository.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Changelly2Repository.kt index e5273546fe..a591fbda90 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Changelly2Repository.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Changelly2Repository.kt @@ -1,16 +1,25 @@ package com.mycelium.wallet.external.changelly2.remote -import androidx.lifecycle.LifecycleCoroutineScope import com.mycelium.bequant.remote.doRequest import com.mycelium.wallet.external.changelly.ChangellyRetrofitFactory -import com.mycelium.wallet.external.changelly.model.* +import com.mycelium.wallet.external.changelly.model.ChangellyCurrency +import com.mycelium.wallet.external.changelly.model.ChangellyListResponse +import com.mycelium.wallet.external.changelly.model.ChangellyResponse +import com.mycelium.wallet.external.changelly.model.ChangellyTransaction +import com.mycelium.wallet.external.changelly.model.ChangellyTransactionOffer +import com.mycelium.wallet.external.changelly.model.Error +import com.mycelium.wallet.external.changelly.model.FixRate import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext +import retrofit2.HttpException import java.math.BigDecimal object Changelly2Repository { - private val api = ChangellyRetrofitFactory.api - val userRepository by lazy { UserRepository() } - + private val userRepository by lazy { Api.statusRepository } + private val viperApi by lazy { ChangellyRetrofitFactory.viperApi } + private val changellyApi = ChangellyRetrofitFactory.changellyApi fun supportCurrenciesFull( scope: CoroutineScope, @@ -19,7 +28,7 @@ object Changelly2Repository { finally: (() -> Unit)? = null ) { doRequest(scope, { - api.getCurrenciesFull() + changellyApi.getCurrenciesFull() }, success, error, finally) } @@ -28,69 +37,105 @@ object Changelly2Repository { from: String, to: String, amount: BigDecimal, - success: (ChangellyListResponse?) -> Unit, + success: (ChangellyListResponse?) -> Unit, error: (Int, String) -> Unit, finally: (() -> Unit)? = null ) = doRequest(scope, { + val isVip = userRepository.statusFlow.value.isVIP() + val api = if (isVip) viperApi else changellyApi api.getFixRateForAmount(exportSymbol(from), exportSymbol(to), amount) }, success, error, finally) - fun fixRate(scope: CoroutineScope, - from: String, - to: String, - success: (ChangellyListResponse?) -> Unit, - error: (Int, String) -> Unit, - finally: (() -> Unit)? = null) = - doRequest(scope, { - api.getFixRateForAmount(exportSymbol(from), exportSymbol(to)) - }, success, error, finally) - - fun createFixTransaction( + fun fixRate( scope: CoroutineScope, + from: String, + to: String, + success: (ChangellyListResponse?) -> Unit, + error: (Int, String) -> Unit, + finally: (() -> Unit)? = null + ) = + doRequest(scope, { + changellyApi.getFixRate(exportSymbol(from), exportSymbol(to)) + }, success, error, finally) + + suspend fun createFixTransaction( rateId: String, from: String, to: String, amount: String, addressTo: String, refundAddress: String, - success: (ChangellyResponse?) -> Unit, - error: (Int, String) -> Unit, - finally: (() -> Unit)? = null - ) { - doRequest(scope, { - api.createFixTransaction( - exportSymbol(from), - exportSymbol(to), + changellyOnly: Boolean, + ): ChangellyResponse { + val isVip = userRepository.statusFlow.value.isVIP() + val fromSymbol = exportSymbol(from) + val toSymbol = exportSymbol(to) + if (!isVip || changellyOnly) { + return changellyApi.createFixTransaction( + fromSymbol, + toSymbol, amount, addressTo, rateId, - refundAddress + refundAddress, ) - }, success, error, finally) + } + try { + return viperApi.createFixTransaction( + fromSymbol, + toSymbol, + amount, + addressTo, + rateId, + refundAddress, + ) + } catch (e: Exception) { + // Http exception with 401 unauthorized code means that user isn't vip anymore + if (e is HttpException && e.code() == 401) { + userRepository.dropStatus() + throw ViperStatusException(e) + } + throw ViperUnexpectedException(e) + } } - fun getTransaction( - scope: CoroutineScope, - id: String, - success: (ChangellyResponse>?) -> Unit, - error: (Int, String) -> Unit, - finally: (() -> Unit)? = null - ) { - doRequest(scope, { - api.getTransaction(id) - }, success, error, finally) + suspend fun getTransaction(id: String): ChangellyResponse> { + val isVip = userRepository.statusFlow.value.isVIP() + val changellyTransactions = changellyApi.getTransaction(id) + if (!isVip) return changellyTransactions + if (changellyTransactions.result?.any { it.id == id } == true) return changellyTransactions + return try { + viperApi.getTransaction(id) + } catch (e: HttpException) { + ChangellyResponse(null, Error(e.code(), e.message())) + } catch (e: Exception) { + ChangellyResponse(null, Error(500, e.message ?: "")) + } } - fun getTransactions( - scope: LifecycleCoroutineScope, ids: List, - success: (ChangellyResponse>?) -> Unit, - error: (Int, String) -> Unit, - finally: (() -> Unit)? = null - ) { - doRequest(scope, { - api.getTransactions(ids) - }, success, error, finally) + suspend fun getTransactions(ids: List): ChangellyResponse> { + val isVip = userRepository.statusFlow.value.isVIP() + if (!isVip) return changellyApi.getTransactions(ids) + val changellyTransactionsDeferred = withContext(Dispatchers.IO) { + async { changellyApi.getTransactions(ids) } + } + val viperTransactionsDeferred = withContext(Dispatchers.IO) { + async { + try { + viperApi.getTransactions(ids) + } catch (e: HttpException) { + ChangellyResponse(null, Error(e.code(), e.message())) + } catch (e: Exception) { + ChangellyResponse(null, Error(500, e.message ?: "")) + } + } + } + val changellyTransactions = changellyTransactionsDeferred.await() + val viperTransactions = viperTransactionsDeferred.await() + val changellyResult = changellyTransactions.result ?: emptyList() + val viperResult = viperTransactions.result ?: emptyList() + return ChangellyResponse(changellyResult + viperResult) } } @@ -106,4 +151,7 @@ private fun importSymbol(currency: String) = private fun exportSymbol(currency: String) = if (currency.equals("USDT", true)) "USDT20".toLowerCase() - else currency.toLowerCase() \ No newline at end of file + else currency.toLowerCase() + +class ViperUnexpectedException(e: Exception) : Exception(e) +class ViperStatusException(e: Exception) : Exception(e) \ No newline at end of file diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/StatusRepository.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/StatusRepository.kt new file mode 100644 index 0000000000..3284132dbe --- /dev/null +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/StatusRepository.kt @@ -0,0 +1,61 @@ +package com.mycelium.wallet.external.changelly2.remote + +import android.app.Activity +import androidx.core.content.edit +import com.mycelium.bequant.remote.model.UserStatus +import com.mycelium.wallet.WalletApplication +import com.mycelium.wallet.external.vip.VipRetrofitFactory +import com.mycelium.wallet.external.vip.model.ActivateVipRequest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class StatusRepository { + private val vipApi by lazy { VipRetrofitFactory().createApi() } + private val preference = WalletApplication.getInstance() + .getSharedPreferences(PREFERENCES_VIP_FILE, Activity.MODE_PRIVATE) + + private fun getLocalStatus() = UserStatus.fromName(preference.getString(VIP_STATUS_KEY, null)) + + private val _statusFlow = MutableStateFlow(UserStatus.REGULAR) + val statusFlow = _statusFlow.asStateFlow() + + init { + val localStatus = getLocalStatus() + if (localStatus != null) { + _statusFlow.value = localStatus + } else { + GlobalScope.launch(Dispatchers.IO) { + try { + val checkResult = vipApi.check() + // if user is VIP than response contains his code else response contains empty string + val isVIP = checkResult.vipCode.isNotEmpty() + val status = if (isVIP) UserStatus.VIP else UserStatus.REGULAR + preference.edit { putString(VIP_STATUS_KEY, status.name) } + _statusFlow.value = status + } catch (_: Exception) { + } + } + } + } + + suspend fun applyVIPCode(code: String): UserStatus { + val response = vipApi.activate(ActivateVipRequest(code)) + val status = if (response.done) UserStatus.VIP else UserStatus.REGULAR + _statusFlow.value = status + preference.edit { putString(VIP_STATUS_KEY, status.name) } + return status + } + + fun dropStatus() { + preference.edit { putString(VIP_STATUS_KEY, UserStatus.REGULAR.name) } + _statusFlow.value = UserStatus.REGULAR + } + + private companion object { + const val PREFERENCES_VIP_FILE = "VIP_PREFERENCES" + const val VIP_STATUS_KEY = "VIP_STATUS" + } +} diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/UserRepository.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/UserRepository.kt deleted file mode 100644 index be48deb006..0000000000 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/UserRepository.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mycelium.wallet.external.changelly2.remote - -import com.mycelium.bequant.remote.model.User -import com.mycelium.wallet.external.vip.VipRetrofitFactory -import com.mycelium.wallet.external.vip.model.ActivateVipRequest -import com.mycelium.wallet.update -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.withContext - -class UserRepository { - private val _userFlow = MutableStateFlow(null) - private val vipApi = VipRetrofitFactory().createApi() - val userFlow = _userFlow.filterNotNull() - - suspend fun identify() { - try { - val checkResult = withContext(Dispatchers.IO) { vipApi.check() } - // if user is VIP than response contains his code else response contains empty string - val isVIP = checkResult.vipCode.isNotEmpty() - val status = if (isVIP) User.Status.VIP else User.Status.REGULAR - _userFlow.update { user -> user?.copy(status = status) ?: User(status) } - } catch (_: Exception) { - _userFlow.value = User() - } - } - - suspend fun applyVIPCode(code: String): User.Status { - val response = withContext(Dispatchers.IO) { vipApi.activate(ActivateVipRequest(code)) } - val status = if (response.done) User.Status.VIP else User.Status.REGULAR - _userFlow.update { user -> user?.copy(status = status) ?: User(status) } - return status - } -} diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeResultViewModel.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeResultViewModel.kt index ac11567e13..ced68aa85b 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeResultViewModel.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeResultViewModel.kt @@ -41,10 +41,10 @@ class ExchangeResultViewModel : ViewModel() { fun setTransaction(result: ChangellyTransaction) { txId.value = result.id spendValue.value = "${result.amountExpectedFrom} ${result.currencyFrom.toUpperCase()}" - getValue.value = "${result.getExpectedAmount()} ${result.currencyTo.toUpperCase()}" - date.value = DateFormat.getDateInstance(DateFormat.LONG).format(Date(result.createdAt * 1000L)) + getValue.value = "${result.amountExpectedTo} ${result.currencyTo.toUpperCase()}" + date.value = DateFormat.getDateInstance(DateFormat.LONG).format(Date(result.createdAt / 1000L)) spendValueFiat.value = getFiatValue(result.amountExpectedFrom, result.currencyFrom) - getValueFiat.value = getFiatValue(result.getExpectedAmount(), result.currencyTo) + getValueFiat.value = getFiatValue(result.amountExpectedTo, result.currencyTo) isExchangeComplete.value = result.status == "finished" trackInProgress.value = result.status !in LINK_TEXT_LIST trackLinkText.value = diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeViewModel.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeViewModel.kt index 2a0f17202a..447317aad2 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeViewModel.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeViewModel.kt @@ -14,7 +14,6 @@ import com.mycelium.wallet.WalletApplication import com.mycelium.wallet.activity.util.toStringFriendlyWithUnit import com.mycelium.wallet.activity.util.toStringWithUnit import com.mycelium.wallet.external.changelly.model.FixRate -import com.mycelium.wallet.external.changelly.model.FixRateForAmount import com.mycelium.wapi.wallet.Address import com.mycelium.wapi.wallet.Transaction import com.mycelium.wapi.wallet.Util @@ -35,7 +34,7 @@ class ExchangeViewModel(application: Application) : AndroidViewModel(application val mbwManager = MbwManager.getInstance(WalletApplication.getInstance()) var currencies = setOf("BTC", "ETH") val fromAccount = MutableLiveData>() - val exchangeInfo = MutableLiveData() + val exchangeInfo = MutableLiveData() val sellValue = object : MutableLiveData() { override fun setValue(value: String?) { if (this.value != value) { @@ -59,7 +58,7 @@ class ExchangeViewModel(application: Application) : AndroidViewModel(application } } } - val swapEnableDelay = MutableLiveData(false) + val swapEnableDelay = MutableLiveData(false) val swapEnabled = MediatorLiveData().apply { value = false fun update() { @@ -152,7 +151,7 @@ class ExchangeViewModel(application: Application) : AndroidViewModel(application } val exchangeRateToValue = Transformations.map(exchangeInfo) { - it.getExpectedValue().toPlainString() + it.result.toPlainString() } val exchangeRateToCurrency = Transformations.map(exchangeInfo) { diff --git a/mbw/src/main/java/com/mycelium/wallet/external/vip/VipRetrofitFactory.kt b/mbw/src/main/java/com/mycelium/wallet/external/vip/VipRetrofitFactory.kt index 03c22d8046..34577c1313 100644 --- a/mbw/src/main/java/com/mycelium/wallet/external/vip/VipRetrofitFactory.kt +++ b/mbw/src/main/java/com/mycelium/wallet/external/vip/VipRetrofitFactory.kt @@ -7,6 +7,7 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext class VipRetrofitFactory { @@ -21,6 +22,7 @@ class VipRetrofitFactory { sslContext.init(null, null, null) return OkHttpClient.Builder() .apply { + connectTimeout(3, TimeUnit.SECONDS) // sslSocketFactory uses system defaults X509TrustManager, so deprecation suppressed // referring to sslSocketFactory(SSLSocketFactory, X509TrustManager) docs: /** diff --git a/mbw/src/main/res/values-ru/strings.xml b/mbw/src/main/res/values-ru/strings.xml index 17f1d9e46c..29a071086a 100644 --- a/mbw/src/main/res/values-ru/strings.xml +++ b/mbw/src/main/res/values-ru/strings.xml @@ -975,6 +975,7 @@ Транзакция отправлена Проблемы с соединением. Убедитесь, что вы онлайн. Сервис предоставляется сторонним поставщиком API. Нажимая на кнопку Exchange, вы принимаете Условия использования. + Продолжить курс устарел Восстановить Обменный курс устарел, пожалуйста, повторите попытку. @@ -996,4 +997,6 @@ Получите VIP-опыт VIP Всё готово! + Ваш SWAP обмен недоступен + Ваш VIP-статус истек, поэтому теперь применяются обычные (не VIP) тарифы. \ No newline at end of file diff --git a/mbw/src/main/res/values/strings.xml b/mbw/src/main/res/values/strings.xml index ebc3573e78..76ccc5d49f 100644 --- a/mbw/src/main/res/values/strings.xml +++ b/mbw/src/main/res/values/strings.xml @@ -1765,6 +1765,7 @@ All new bitcoins, mined by the pool, are proportionally distributed among RMC bi Service is available through third-party API provider. By clicking Exchange you accept the Terms of Use. Select account: Select account to receive funds: + Proceed Exchange History EXCHANGE No operations history @@ -1869,5 +1870,13 @@ All new bitcoins, mined by the pool, are proportionally distributed among RMC bi Get the VIP experience VIP You are all set! + Ooops, something went wrong + Vip SWAP is not available + Your VIP status has expired, so regular (non-VIP) rates now apply. + VIP service is currently not available due to technical reasons. Try again later or proceed without VIP benefits. + VIP service is currently unavailable + Please try again later + Yes, proceed + Cancel Add address