diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3d4cb0e2..172a0a87 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 34 - versionCode = 202504251 - versionName = "v4.4.7" + versionCode = 202505121 + versionName = "v4.5.0" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ffbd7a0e..dd68f40a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -96,11 +96,6 @@ android:exported="true" android:launchMode="singleTask" android:screenOrientation="portrait" /> - = listOf( + FeeOption( + type = ECONOMY, + feePerKb = economy, + labelStringId = R.string.network_fee_options_low + ), + FeeOption( + type = REGULAR, + feePerKb = regular, + labelStringId = R.string.network_fee_options_medium + ), + FeeOption( + type = LUXURY, + feePerKb = luxury, + labelStringId = R.string.network_fee_options_top + ), +) + +fun FeeOption.getFiat(currencyEntity: CurrencyEntity): Float { + val satoshisPerLtc = 100_000_000.0 + val feeInLtc = feePerKb / satoshisPerLtc + return (feeInLtc * currencyEntity.rate).toFloat() +} + +@SuppressLint("DefaultLocale") +fun FeeOption.getFiatFormatted(currencyEntity: CurrencyEntity): String { + val fiatValue = getFiat(currencyEntity) + val formatted = String.format("%.3f", fiatValue) + return "${currencyEntity.symbol}$formatted" +} diff --git a/app/src/main/java/com/brainwallet/data/model/MoonpayCurrencyLimit.kt b/app/src/main/java/com/brainwallet/data/model/MoonpayCurrencyLimit.kt new file mode 100644 index 00000000..d8344438 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/model/MoonpayCurrencyLimit.kt @@ -0,0 +1,24 @@ +package com.brainwallet.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MoonpayCurrencyLimit( + @SerialName("data") val data: Data = Data() +) { + @Serializable + data class Data( + @SerialName("paymentMethod") val paymentMethod: String = "", + @SerialName("quoteCurrency") val quoteCurrency: CurrencyLimit = CurrencyLimit(), + @SerialName("baseCurrency") val baseCurrency: CurrencyLimit = CurrencyLimit(), + @SerialName("areFeesIncluded") val areFeesIncluded: Boolean = false + ) + + @Serializable + data class CurrencyLimit( + val code: String = "usd", + @SerialName("minBuyAmount") val min: Float = 21f, + @SerialName("maxBuyAmount") val max: Float = 29849f + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/model/QuickFiatAmountOption.kt b/app/src/main/java/com/brainwallet/data/model/QuickFiatAmountOption.kt new file mode 100644 index 00000000..20dc3368 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/model/QuickFiatAmountOption.kt @@ -0,0 +1,11 @@ +package com.brainwallet.data.model + + +data class QuickFiatAmountOption( + val symbol: String = "custom", + val value: Float = -1f +) + +fun QuickFiatAmountOption.isCustom(): Boolean = symbol == "custom" && value == -1f + +fun QuickFiatAmountOption.getFormattedText(): String = "${symbol}${value.toInt()}" \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt index 31c05e30..365bbc0d 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -1,9 +1,15 @@ package com.brainwallet.data.repository import android.content.Context +import android.content.SharedPreferences +import androidx.core.net.toUri +import com.brainwallet.BuildConfig import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Fee +import com.brainwallet.data.model.MoonpayCurrencyLimit import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.data.source.fetchWithCache +import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource @@ -13,10 +19,17 @@ interface LtcRepository { suspend fun fetchFeePerKb(): Fee + suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit + + suspend fun fetchBuyQuote(params: Map): GetMoonpayBuyQuoteResponse + + suspend fun fetchMoonpaySignedUrl(params: Map): String + class Impl( private val context: Context, private val remoteApiSource: RemoteApiSource, - private val currencyDataSource: CurrencyDataSource + private val currencyDataSource: CurrencyDataSource, + private val sharedPreferences: SharedPreferences, ) : LtcRepository { //todo: make it offline first here later, currently just using CurrencyDataSource.getAllCurrencies @@ -42,18 +55,50 @@ interface LtcRepository { } override suspend fun fetchFeePerKb(): Fee { - return runCatching { - val fee = remoteApiSource.getFeePerKb() + return sharedPreferences.fetchWithCache( + key = PREF_KEY_NETWORK_FEE_PER_KB, + cachedAtKey = PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, + cacheTimeMs = 6 * 60 * 60 * 1000, + fetchData = { + remoteApiSource.getFeePerKb() + }, + defaultValue = Fee.Default + ) + } - //todo: cache + override suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit { + return sharedPreferences.fetchWithCache( + key = "${PREF_KEY_BUY_LIMITS_PREFIX}${baseCurrencyCode.lowercase()}", + cachedAtKey = "${PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT}${baseCurrencyCode.lowercase()}", + cacheTimeMs = 5 * 60 * 1000, //5 minutes + fetchData = { + remoteApiSource.getMoonpayCurrencyLimit(baseCurrencyCode) + } + ) + } + + override suspend fun fetchBuyQuote(params: Map): GetMoonpayBuyQuoteResponse = + remoteApiSource.getBuyQuote(params) - return fee - }.getOrElse { Fee.Default } + override suspend fun fetchMoonpaySignedUrl(params: Map): String { + return remoteApiSource.getMoonpaySignedUrl(params) + .signedUrl.toUri() + .buildUpon() + .apply { + if (BuildConfig.DEBUG) { + authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com + } + } + .build() + .toString() } } companion object { - + const val PREF_KEY_NETWORK_FEE_PER_KB = "network_fee_per_kb" + const val PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT = "${PREF_KEY_NETWORK_FEE_PER_KB}_cached_at" + const val PREF_KEY_BUY_LIMITS_PREFIX = "buy_limits:" //e.g. buy_limits:usd + const val PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT = "buy_limits_cached_at:" } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt b/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt new file mode 100644 index 00000000..d86d312d --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt @@ -0,0 +1,45 @@ +package com.brainwallet.data.source + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.brainwallet.di.json +import kotlinx.serialization.encodeToString + +/** + * Generic function to handle caching data in shared preferences + * @param key The key to store the data under + * @param cachedAtKey The key to store the timestamp under + * @param cacheTimeMs How long the cache should be valid in milliseconds + * @param fetchData Suspending function to fetch new data + * @return The data, either from cache or freshly fetched + */ +suspend inline fun SharedPreferences.fetchWithCache( + key: String, + cachedAtKey: String, + cacheTimeMs: Long, + crossinline fetchData: suspend () -> T, + defaultValue: T? = null +): T { + val lastUpdateTime = getLong(cachedAtKey, 0) + val currentTime = System.currentTimeMillis() + val cached = getString(key, null) + ?.let { runCatching { json.decodeFromString(it) }.getOrNull() } + + // Return cached value if it exists and is not expired + if (cached != null && (currentTime - lastUpdateTime) < cacheTimeMs) { + return cached + } + + return runCatching { + // Fetch fresh data + val result = fetchData() + // Save to cache + edit { + putString(key, json.encodeToString(result)) + putLong(cachedAtKey, currentTime) + } + result + }.getOrElse { + cached ?: (defaultValue ?: throw it) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt b/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt index 60c865e2..4e0a1c02 100644 --- a/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt +++ b/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt @@ -2,7 +2,12 @@ package com.brainwallet.data.source import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Fee +import com.brainwallet.data.model.MoonpayCurrencyLimit +import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse +import com.brainwallet.data.source.response.GetMoonpaySignUrlResponse import retrofit2.http.GET +import retrofit2.http.Query +import retrofit2.http.QueryMap //TODO interface RemoteApiSource { @@ -13,6 +18,19 @@ interface RemoteApiSource { @GET("v1/fee-per-kb") suspend fun getFeePerKb(): Fee -// https://prod.apigsltd.net/moonpay/buy?address=ltc1qjnsg3p9rt4r4vy7ncgvrywdykl0zwhkhcp8ue0&code=USD&idate=1742331930290&uid=ec51fa950b271ff3 -// suspend fun getMoonPayBuy() + @GET("v1/moonpay/ltc-to-fiat-limits") + suspend fun getMoonpayCurrencyLimit( + @Query("baseCurrencyCode") baseCurrencyCode: String + ): MoonpayCurrencyLimit + + @GET("v1/moonpay/sign-url") + suspend fun getMoonpaySignedUrl( + @QueryMap params: Map + ): GetMoonpaySignUrlResponse + + @GET("v1/moonpay/buy-quote") + suspend fun getBuyQuote( + @QueryMap params: Map + ): GetMoonpayBuyQuoteResponse + } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/source/response/GetMoonpayBuyQuoteResponse.kt b/app/src/main/java/com/brainwallet/data/source/response/GetMoonpayBuyQuoteResponse.kt new file mode 100644 index 00000000..848732a9 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/source/response/GetMoonpayBuyQuoteResponse.kt @@ -0,0 +1,17 @@ +package com.brainwallet.data.source.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetMoonpayBuyQuoteResponse( + @SerialName("data") val data: Data = Data() + +) { + @Serializable + data class Data( + val totalAmount: Float = 0f, + val baseCurrencyCode: String = "usd", + val quoteCurrencyAmount: Float = 0f, + ) +} diff --git a/app/src/main/java/com/brainwallet/data/source/response/GetMoonpaySignUrlResponse.kt b/app/src/main/java/com/brainwallet/data/source/response/GetMoonpaySignUrlResponse.kt new file mode 100644 index 00000000..51e316cc --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/source/response/GetMoonpaySignUrlResponse.kt @@ -0,0 +1,8 @@ +package com.brainwallet.data.source.response + +import kotlinx.serialization.Serializable + +@Serializable +data class GetMoonpaySignUrlResponse( + val signedUrl: String +) diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index 4dce30f1..e1bb915f 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -10,7 +10,9 @@ import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.tools.util.BRConstants +import com.brainwallet.ui.screens.buylitecoin.BuyLitecoinViewModel import com.brainwallet.ui.screens.home.SettingsViewModel +import com.brainwallet.ui.screens.home.receive.ReceiveDialogViewModel import com.brainwallet.ui.screens.inputwords.InputWordsViewModel import com.brainwallet.ui.screens.ready.ReadyViewModel import com.brainwallet.ui.screens.setpasscode.SetPasscodeViewModel @@ -28,6 +30,8 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidApplication +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module @@ -58,7 +62,7 @@ val dataModule = module { single { CurrencyDataSource.getInstance(get()) } single { provideSharedPreferences(context = androidApplication()) } single { SettingRepository.Impl(get(), get()) } - single { LtcRepository.Impl(get(), get(), get()) } + single { LtcRepository.Impl(get(), get(), get(), get()) } } val viewModelModule = module { @@ -70,6 +74,8 @@ val viewModelModule = module { viewModel { UnLockViewModel() } viewModel { YourSeedProveItViewModel() } viewModel { YourSeedWordsViewModel() } + viewModel { ReceiveDialogViewModel(get(), get()) } + viewModel { BuyLitecoinViewModel(get(), get()) } } val appModule = module { @@ -94,27 +100,6 @@ private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() .addHeader("Accept-Language", "en") chain.proceed(requestBuilder.build()) } - .addInterceptor { chain -> - val request = chain.request() - runCatching { - val result = chain.proceed(request) - if (result.isSuccessful.not()) { - throw HttpException( - retrofit2.Response.error( - result.code, - result.body ?: result.peekBody(Long.MAX_VALUE) - ) - ) - } - result - }.getOrElse { - //retry using dev host - val newRequest = request.newBuilder() - .url("${BRConstants.LEGACY_BW_API_DEV_HOST}/api${request.url.encodedPath}") //legacy dev api need prefix path /api - .build() - chain.proceed(newRequest) - } - } .addInterceptor(HttpLoggingInterceptor().apply { setLevel( when { @@ -139,4 +124,10 @@ internal fun provideRetrofit( .build() internal inline fun provideApi(retrofit: Retrofit): T = - retrofit.create(T::class.java) \ No newline at end of file + retrofit.create(T::class.java) + +inline fun getKoinInstance(): T { + return object : KoinComponent { + val value: T by inject() + }.value +} diff --git a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt index b50bef17..d38eaeb9 100644 --- a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt +++ b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt @@ -1,11 +1,22 @@ package com.brainwallet.navigation import android.app.Activity +import android.app.ProgressDialog import android.content.Context import android.content.Intent +import android.widget.Toast +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri +import com.brainwallet.BuildConfig import com.brainwallet.R +import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.di.getKoinInstance import com.brainwallet.presenter.activities.BreadActivity import com.brainwallet.ui.BrainwalletActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber @@ -55,4 +66,54 @@ object LegacyNavigation { context.startActivity(it) } + @JvmStatic + fun showMoonPayWidget( + context: Context, + params: Map = mapOf(), + isDarkMode: Boolean = true, + ) { + val remoteApiSource: RemoteApiSource = getKoinInstance() + + val progressDialog = ProgressDialog(context).apply { + setMessage(context.getString(R.string.loading)) + setCancelable(false) + show() + } + + CoroutineScope(Dispatchers.Main).launch { + try { + val result = withContext(Dispatchers.IO) { + remoteApiSource.getMoonpaySignedUrl( + params = params.toMutableMap().apply { + put("defaultCurrencyCode", "ltc") + put("currencyCode", "ltc") + put("themeId", "main-v1.0.0") + put("theme", if (isDarkMode) "dark" else "light") + } + ) + } + + val widgetUri = result.signedUrl.toUri().buildUpon() + .apply { + if (BuildConfig.DEBUG) { + authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com + } + } + .build() + val intent = CustomTabsIntent.Builder() + .setColorScheme(if (isDarkMode) CustomTabsIntent.COLOR_SCHEME_DARK else CustomTabsIntent.COLOR_SCHEME_LIGHT) + .build() + intent.launchUrl(context, widgetUri) + } catch (e: Exception) { + Toast.makeText( + context, + "Failed to load: ${e.message}, please try again later", + Toast.LENGTH_LONG + ).show() + } finally { + progressDialog.dismiss() + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/navigation/MainNav.kt b/app/src/main/java/com/brainwallet/navigation/MainNav.kt index f6e2e091..ec2a8a06 100644 --- a/app/src/main/java/com/brainwallet/navigation/MainNav.kt +++ b/app/src/main/java/com/brainwallet/navigation/MainNav.kt @@ -7,6 +7,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute +import com.brainwallet.ui.screens.buylitecoin.BuyLitecoinScreen import com.brainwallet.ui.screens.inputwords.InputWordsScreen import com.brainwallet.ui.screens.ready.ReadyScreen import com.brainwallet.ui.screens.setpasscode.SetPasscodeScreen @@ -113,6 +114,10 @@ fun NavGraphBuilder.mainNavGraph( UnLockScreen(onNavigate = onNavigate, isUpdatePin = route.isUpdatePin) } + composable { navBackStackEntry -> + BuyLitecoinScreen(onNavigate = onNavigate) + } + //todo add more composable screens } diff --git a/app/src/main/java/com/brainwallet/navigation/Route.kt b/app/src/main/java/com/brainwallet/navigation/Route.kt index 2e0e71a8..16f16dcf 100644 --- a/app/src/main/java/com/brainwallet/navigation/Route.kt +++ b/app/src/main/java/com/brainwallet/navigation/Route.kt @@ -45,4 +45,7 @@ sealed class Route : JavaSerializable { @Serializable data class UnLock(val isUpdatePin: Boolean = false) : Route() + @Serializable + object BuyLitecoin : Route() + } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java index aec322e1..b210395c 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java @@ -25,6 +25,7 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.browser.customtabs.CustomTabsIntent; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.app.ActivityCompat; @@ -61,6 +62,8 @@ import com.brainwallet.ui.BrainwalletActivity; import com.brainwallet.ui.screens.home.SettingsViewModel; import com.brainwallet.ui.screens.home.composable.HomeSettingDrawerComposeView; +import com.brainwallet.ui.screens.home.receive.ReceiveDialogFragment; +import com.brainwallet.ui.screens.home.receive.ReceiveDialogKt; import com.brainwallet.util.PermissionUtil; import com.brainwallet.wallet.BRPeerManager; import com.brainwallet.wallet.BRWalletManager; @@ -252,16 +255,22 @@ public boolean handleNavigationItemSelected(int menuItemId) { mSelectedBottomNavItem = 0; } else if (menuItemId == R.id.nav_receive) { if (BRAnimator.isClickAllowed()) { - BRAnimator.showReceiveFragment(BreadActivity.this, true); - } - mSelectedBottomNavItem = 0; - } - else if (menuItemId == R.id.nav_buy) { - if (BRAnimator.isClickAllowed()) { - BRAnimator.showMoonpayFragment(BreadActivity.this); +// BRAnimator.showReceiveFragment(BreadActivity.this, true); + //todo + ReceiveDialogFragment.show(getSupportFragmentManager()); } mSelectedBottomNavItem = 0; } +// else if (menuItemId == R.id.nav_buy) { +// if (BRAnimator.isClickAllowed()) { +//// BRAnimator.showMoonpayFragment(BreadActivity.this); +// } +// +// +// +// +// mSelectedBottomNavItem = 0; +// } return true; } diff --git a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt index 1f4af687..9b47690e 100644 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt +++ b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt @@ -11,9 +11,12 @@ import android.view.ViewGroup import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.view.animation.OvershootInterpolator import android.view.inputmethod.EditorInfo -import android.widget.* -import androidx.annotation.ColorRes -import androidx.annotation.StringRes +import android.widget.Button +import android.widget.EditText +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.fragment.app.Fragment @@ -22,38 +25,53 @@ import androidx.transition.Transition import androidx.transition.TransitionManager import com.brainwallet.R import com.brainwallet.presenter.customviews.BRKeyboard -import com.brainwallet.presenter.customviews.BRLinearLayoutWithCaret import com.brainwallet.presenter.entities.ServiceItems import com.brainwallet.presenter.entities.TransactionItem import com.brainwallet.tools.animation.BRAnimator import com.brainwallet.tools.animation.BRDialog import com.brainwallet.tools.animation.SlideDetector import com.brainwallet.tools.animation.SpringAnimator -import com.brainwallet.tools.manager.* +import com.brainwallet.tools.manager.AnalyticsManager +import com.brainwallet.tools.manager.BRClipboardManager +import com.brainwallet.tools.manager.BRSharedPrefs +import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.security.BRSender import com.brainwallet.tools.security.BitcoinUrlHandler import com.brainwallet.tools.threads.BRExecutor -import com.brainwallet.tools.util.* +import com.brainwallet.tools.util.BRConstants +import com.brainwallet.tools.util.BRCurrency +import com.brainwallet.tools.util.BRExchange +import com.brainwallet.tools.util.Utils import com.brainwallet.wallet.BRWalletManager -import com.google.common.math.Quantiles.scale import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode import java.util.regex.Pattern +//TODO: make sure remove unused after refactor network fee move to HomeSettingDrawerSheet class FragmentSend : Fragment() { - private lateinit var signalLayout: LinearLayout; private lateinit var keyboardLayout: LinearLayout - private lateinit var scanButton: Button; private lateinit var pasteButton: Button; private lateinit var sendButton: Button; private lateinit var isoCurrencySymbolButton: Button - private lateinit var commentEdit: EditText; private lateinit var addressEdit: EditText;private lateinit var amountEdit: EditText - private lateinit var isoCurrencySymbolText: TextView; private lateinit var balanceText: TextView; private lateinit var feeText: TextView; private lateinit var feeDescription: TextView; private lateinit var warningText: TextView - private var amountLabelOn = true; private var ignoreCleanup = false; private var feeButtonsShown = false - private lateinit var edit: ImageView + private lateinit var signalLayout: LinearLayout; + private lateinit var keyboardLayout: LinearLayout + private lateinit var scanButton: Button; + private lateinit var pasteButton: Button; + private lateinit var sendButton: Button; + private lateinit var isoCurrencySymbolButton: Button + private lateinit var commentEdit: EditText; + private lateinit var addressEdit: EditText; + private lateinit var amountEdit: EditText + private lateinit var isoCurrencySymbolText: TextView; + private lateinit var balanceText: TextView; + private lateinit var feeText: TextView; + private lateinit var feeDescription: TextView; + private lateinit var warningText: TextView + private var amountLabelOn = true; + private var ignoreCleanup = false; + private var feeButtonsShown = false private var currentBalance: Long = 0 private var keyboardIndex = 0 private lateinit var keyboard: BRKeyboard private lateinit var closeButton: ImageButton private lateinit var amountLayout: ConstraintLayout - private lateinit var feeLayout: BRLinearLayoutWithCaret private var selectedIsoCurrencySymbol: String? = null private lateinit var backgroundLayout: ScrollView private lateinit var amountBuilder: StringBuilder @@ -78,16 +96,17 @@ class FragmentSend : Fragment() { amountEdit = rootView.findViewById(R.id.amount_edit) as EditText balanceText = rootView.findViewById(R.id.balance_text) as TextView feeText = rootView.findViewById(R.id.fee_text) as TextView - edit = rootView.findViewById(R.id.edit) as ImageView isoCurrencySymbolButton = rootView.findViewById(R.id.iso_button) as Button keyboardLayout = rootView.findViewById(R.id.keyboard_layout) as LinearLayout amountLayout = rootView.findViewById(R.id.amount_layout) as ConstraintLayout - feeLayout = rootView.findViewById(R.id.fee_buttons_layout) as BRLinearLayoutWithCaret - feeDescription = rootView.findViewById(R.id.fee_description) as TextView - warningText = rootView.findViewById(R.id.warning_text) as TextView +// feeLayout = rootView.findViewById(R.id.fee_buttons_layout) as BRLinearLayoutWithCaret +// feeDescription = rootView.findViewById(R.id.fee_description) as TextView +// warningText = rootView.findViewById(R.id.warning_text) as TextView closeButton = rootView.findViewById(R.id.close_button) as ImageButton selectedIsoCurrencySymbol = - if (BRSharedPrefs.getPreferredLTC(context)) "LTC" else BRSharedPrefs.getIsoSymbol(context) + if (BRSharedPrefs.getPreferredLTC(context)) "LTC" else BRSharedPrefs.getIsoSymbol( + context + ) amountBuilder = StringBuilder(0) setListeners() @@ -102,18 +121,20 @@ class FragmentSend : Fragment() { signalLayout.setOnTouchListener(SlideDetector(signalLayout) { animateClose() }) AnalyticsManager.logCustomEvent(BRConstants._20191105_VSC) - setupFeesSelector(rootView) - showFeeSelectionButtons(feeButtonsShown) - edit.setOnClickListener { - feeButtonsShown = !feeButtonsShown - showFeeSelectionButtons(feeButtonsShown) - } +// setupFeesSelector(rootView) +// showFeeSelectionButtons(feeButtonsShown) +// edit.setOnClickListener { +// feeButtonsShown = !feeButtonsShown +// showFeeSelectionButtons(feeButtonsShown) +// } keyboardIndex = signalLayout.indexOfChild(keyboardLayout) // TODO: all views are using the layout of this button. Views should be refactored without it // Hiding until layouts are built. showKeyboard(false) signalLayout.layoutTransition = BRAnimator.getDefaultTransition() - + + updateText() + return rootView } @@ -121,62 +142,62 @@ class FragmentSend : Fragment() { super.onActivityCreated(savedInstanceState) } - private fun setupFeesSelector(rootView: View) { - val feesSegment = rootView.findViewById(R.id.fees_segment) - feesSegment.setOnCheckedChangeListener { _, checkedTypeId -> onFeeTypeSelected(checkedTypeId) } - onFeeTypeSelected(R.id.regular_fee_but) - } - - private fun onFeeTypeSelected(checkedTypeId: Int) { - val feeManager = FeeManager.getInstance() - when (checkedTypeId) { - R.id.regular_fee_but -> { - feeManager.setFeeType(FeeManager.REGULAR) - BRWalletManager.getInstance().setFeePerKb(feeManager.currentFees.regular) - setFeeInformation(R.string.FeeSelector_regularTime, 0, 0, View.GONE) - } - R.id.economy_fee_but -> { - feeManager.setFeeType(FeeManager.ECONOMY) - BRWalletManager.getInstance().setFeePerKb(feeManager.currentFees.economy) - setFeeInformation( - R.string.FeeSelector_economyTime, - R.string.FeeSelector_economyWarning, - R.color.chili, - View.VISIBLE, - ) - } - R.id.luxury_fee_but -> { - feeManager.setFeeType(FeeManager.LUXURY) - BRWalletManager.getInstance().setFeePerKb(feeManager.currentFees.luxury) - setFeeInformation( - R.string.FeeSelector_luxuryTime, - R.string.FeeSelector_luxuryMessage, - R.color.cheddar, - View.VISIBLE, - ) - } - else -> { - } - } - updateText() - } - - private fun setFeeInformation( - @StringRes deliveryTime: Int, - @StringRes warningStringId: Int, - @ColorRes warningColorId: Int, - visibility: Int, - ) { - feeDescription.text = - getString(R.string.FeeSelector_estimatedDeliver, getString(deliveryTime)) - if (warningStringId != 0) { - warningText.setText(warningStringId) - } - if (warningColorId != 0) { - warningText.setTextColor(resources.getColor(warningColorId, null)) - } - warningText.visibility = visibility - } +// private fun setupFeesSelector(rootView: View) { +// val feesSegment = rootView.findViewById(R.id.fees_segment) +// feesSegment.setOnCheckedChangeListener { _, checkedTypeId -> onFeeTypeSelected(checkedTypeId) } +// onFeeTypeSelected(R.id.regular_fee_but) +// } + +// private fun onFeeTypeSelected(checkedTypeId: Int) { +// val feeManager = FeeManager.getInstance() +// when (checkedTypeId) { +// R.id.regular_fee_but -> { +// feeManager.setFeeType(FeeManager.REGULAR) +// BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.regular) +// setFeeInformation(R.string.FeeSelector_regularTime, 0, 0, View.GONE) +// } +// R.id.economy_fee_but -> { +// feeManager.setFeeType(FeeManager.ECONOMY) +// BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.economy) +// setFeeInformation( +// R.string.FeeSelector_economyTime, +// R.string.FeeSelector_economyWarning, +// R.color.chili, +// View.VISIBLE, +// ) +// } +// R.id.luxury_fee_but -> { +// feeManager.setFeeType(FeeManager.LUXURY) +// BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.luxury) +// setFeeInformation( +// R.string.FeeSelector_luxuryTime, +// R.string.FeeSelector_luxuryMessage, +// R.color.cheddar, +// View.VISIBLE, +// ) +// } +// else -> { +// } +// } +// updateText() +// } + +// private fun setFeeInformation( +// @StringRes deliveryTime: Int, +// @StringRes warningStringId: Int, +// @ColorRes warningColorId: Int, +// visibility: Int, +// ) { +// feeDescription.text = +// getString(R.string.FeeSelector_estimatedDeliver, getString(deliveryTime)) +// if (warningStringId != 0) { +// warningText.setText(warningStringId) +// } +// if (warningColorId != 0) { +// warningText.setTextColor(resources.getColor(warningColorId, null)) +// } +// warningText.visibility = visibility +// } private fun setListeners() { amountEdit.setOnClickListener { @@ -187,8 +208,9 @@ class FragmentSend : Fragment() { amountEdit.textSize = 24f balanceText.visibility = View.VISIBLE feeText.visibility = View.VISIBLE - edit.visibility = View.VISIBLE - isoCurrencySymbolText.text = BRCurrency.getSymbolByIso(activity, selectedIsoCurrencySymbol) +// edit.visibility = View.VISIBLE + isoCurrencySymbolText.text = + BRCurrency.getSymbolByIso(activity, selectedIsoCurrencySymbol) isoCurrencySymbolText.textSize = 28f val scaleX = amountEdit.scaleX amountEdit.scaleX = 0f @@ -242,7 +264,13 @@ class FragmentSend : Fragment() { ConstraintSet.TOP, px4, ) - set.connect(isoCurrencySymbolText.id, ConstraintSet.BOTTOM, -1, ConstraintSet.TOP, -1) + set.connect( + isoCurrencySymbolText.id, + ConstraintSet.BOTTOM, + -1, + ConstraintSet.TOP, + -1 + ) set.applyTo(amountLayout) } } @@ -328,7 +356,11 @@ class FragmentSend : Fragment() { ) isoCurrencySymbolButton.setOnClickListener { selectedIsoCurrencySymbol = - if (selectedIsoCurrencySymbol.equals(BRSharedPrefs.getIsoSymbol(context), ignoreCase = true)) { + if (selectedIsoCurrencySymbol.equals( + BRSharedPrefs.getIsoSymbol(context), + ignoreCase = true + ) + ) { "LTC" } else { BRSharedPrefs.getIsoSymbol(context) @@ -372,7 +404,8 @@ class FragmentSend : Fragment() { if (allFilled) { BRSender.getInstance().sendTransaction( context, - TransactionItem(sendAddress, + TransactionItem( + sendAddress, Utils.fetchServiceItem(context, ServiceItems.WALLETOPS), null, litoshiAmount.toLong(), @@ -490,9 +523,11 @@ class FragmentSend : Fragment() { key.isEmpty() -> { handleDeleteClick() } + Character.isDigit(key[0]) -> { handleDigitClick(key.substring(0, 1).toInt()) } + key[0] == '.' -> { handleSeparatorClick() } @@ -537,7 +572,7 @@ class FragmentSend : Fragment() { private fun updateText() { if (activity == null) return var tempDoubleAmountValue = 0.0 - if (amountBuilder.toString() != "" && amountBuilder.toString() != "." ) { + if (amountBuilder.toString() != "" && amountBuilder.toString() != ".") { tempDoubleAmountValue = amountBuilder.toString().toDouble() } val scaleValue = 4 @@ -547,42 +582,68 @@ class FragmentSend : Fragment() { val selectedISOSymbol = selectedIsoCurrencySymbol val currencySymbol = BRCurrency.getSymbolByIso(activity, selectedIsoCurrencySymbol) if (!amountLabelOn) isoCurrencySymbolText.text = currencySymbol - isoCurrencySymbolButton.text = String.format("%s(%s)", - BRCurrency.getCurrencyName(activity, selectedIsoCurrencySymbol), - currencySymbol) + isoCurrencySymbolButton.text = String.format( + "%s(%s)", + BRCurrency.getCurrencyName(activity, selectedIsoCurrencySymbol), + currencySymbol + ) // Balance depending on ISOSymbol currentBalance = BRWalletManager.getInstance().getBalance(activity) - val balanceForISOSymbol = BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(currentBalance)) - val formattedBalance = BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, balanceForISOSymbol) + val balanceForISOSymbol = BRExchange.getAmountFromLitoshis( + activity, + selectedISOSymbol, + BigDecimal(currentBalance) + ) + val formattedBalance = + BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, balanceForISOSymbol) // Current amount depending on ISOSymbol val currentAmountInLitoshis = if (selectedIsoCurrencySymbol.equals("LTC", ignoreCase = true)) { BRExchange.convertltcsToLitoshis(tempDoubleAmountValue).toLong() } else { - BRExchange.getLitoshisFromAmount(activity,selectedIsoCurrencySymbol,BigDecimal(tempDoubleAmountValue)).toLong() + BRExchange.getLitoshisFromAmount( + activity, + selectedIsoCurrencySymbol, + BigDecimal(tempDoubleAmountValue) + ).toLong() } - Timber.d("timber: updateText: currentAmountInLitoshis %d",currentAmountInLitoshis) + Timber.d("timber: updateText: currentAmountInLitoshis %d", currentAmountInLitoshis) // Network Fee depending on ISOSymbol - var networkFee = if(currentAmountInLitoshis > 0) { BRWalletManager.getInstance().feeForTransactionAmount(currentAmountInLitoshis) } - else { 0 } //Amount is zero so network fee is also zero + var networkFee = if (currentAmountInLitoshis > 0) { + BRWalletManager.getInstance().feeForTransactionAmount(currentAmountInLitoshis) + } else { + 0 + } //Amount is zero so network fee is also zero val networkFeeForISOSymbol = - BRExchange.getAmountFromLitoshis(activity,selectedISOSymbol, BigDecimal(networkFee)).setScale(scaleValue, RoundingMode.HALF_UP) - val formattedNetworkFee = BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, networkFeeForISOSymbol) + BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(networkFee)) + .setScale(scaleValue, RoundingMode.HALF_UP) + val formattedNetworkFee = BRCurrency.getFormattedCurrencyString( + activity, + selectedISOSymbol, + networkFeeForISOSymbol + ) // Service Fee depending on ISOSymbol var serviceFee = Utils.tieredOpsFee(activity, currentAmountInLitoshis) val serviceFeeForISOSymbol = - BRExchange.getAmountFromLitoshis(activity,selectedISOSymbol,BigDecimal(serviceFee)).setScale(scaleValue, RoundingMode.HALF_UP) - val formattedServiceFee = BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, serviceFeeForISOSymbol) + BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(serviceFee)) + .setScale(scaleValue, RoundingMode.HALF_UP) + val formattedServiceFee = BRCurrency.getFormattedCurrencyString( + activity, + selectedISOSymbol, + serviceFeeForISOSymbol + ) // Total Fees depending on ISOSymbol val totalFees = networkFee + serviceFee val totalFeeForISOSymbol = - BRExchange.getAmountFromLitoshis( activity,selectedISOSymbol,BigDecimal(totalFees)).setScale(scaleValue, RoundingMode.HALF_UP) - val formattedTotalFees = BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, totalFeeForISOSymbol) + BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(totalFees)) + .setScale(scaleValue, RoundingMode.HALF_UP) + val formattedTotalFees = + BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, totalFeeForISOSymbol) // Update UI with alert red when over balance if (BigDecimal(currentAmountInLitoshis).toDouble() > currentBalance.toDouble()) { @@ -590,8 +651,7 @@ class FragmentSend : Fragment() { feeText.setTextColor(requireContext().getColor(R.color.chili)) amountEdit.setTextColor(requireContext().getColor(R.color.chili)) if (!amountLabelOn) isoCurrencySymbolText.setTextColor(requireContext().getColor(R.color.chili)) - } - else { + } else { balanceText.setTextColor(requireContext().getColor(R.color.cheddar)) feeText.setTextColor(requireContext().getColor(R.color.cheddar)) amountEdit.setTextColor(requireContext().getColor(R.color.cheddar)) @@ -599,12 +659,14 @@ class FragmentSend : Fragment() { } balanceText.text = getString(R.string.Send_balance, formattedBalance) - feeText.text = String.format("(%s + %s): %s + %s = %s", + feeText.text = String.format( + "(%s + %s): %s + %s = %s", getString(R.string.Network_feeLabel), getString(R.string.Fees_Service), formattedNetworkFee, formattedServiceFee, - formattedTotalFees) + formattedTotalFees + ) amountLayout.requestLayout() } @@ -627,13 +689,13 @@ class FragmentSend : Fragment() { } } - private fun showFeeSelectionButtons(b: Boolean) { - if (!b) { - signalLayout.removeView(feeLayout) - } else { - signalLayout.addView(feeLayout, signalLayout.indexOfChild(amountLayout) + 1) - } - } +// private fun showFeeSelectionButtons(b: Boolean) { +// if (!b) { +// signalLayout.removeView(feeLayout) +// } else { +// signalLayout.addView(feeLayout, signalLayout.indexOfChild(amountLayout) + 1) +// } +// } private fun setAmount() { val tmpAmount = amountBuilder.toString() @@ -648,7 +710,7 @@ class FragmentSend : Fragment() { newAmount.append(",") } } - + amountEdit.setText(newAmount.toString()) } @@ -680,7 +742,8 @@ class FragmentSend : Fragment() { private fun loadMetaData() { ignoreCleanup = false if (!Utils.isNullOrEmpty(savedMemo)) commentEdit.setText(savedMemo) - if (!Utils.isNullOrEmpty(savedIsoCurrencySymbol)) selectedIsoCurrencySymbol = savedIsoCurrencySymbol + if (!Utils.isNullOrEmpty(savedIsoCurrencySymbol)) selectedIsoCurrencySymbol = + savedIsoCurrencySymbol if (!Utils.isNullOrEmpty(savedAmount)) { amountBuilder = StringBuilder(savedAmount!!) Handler().postDelayed({ @@ -709,7 +772,6 @@ class FragmentSend : Fragment() { } - ///DEV WIP // val approximateNetworkFee = BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, feeForISOSymbol) diff --git a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSignal.java b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSignal.java index 4c136b41..5757a798 100644 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSignal.java +++ b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSignal.java @@ -31,7 +31,7 @@ public class FragmentSignal extends DialogFragment { private final Runnable popBackStackRunnable = new Runnable() { @Override public void run() { - dismiss(); + dismissAllowingStateLoss(); handler.postDelayed(completionRunnable, 300); } }; diff --git a/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java b/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java index 963a56af..f9457baa 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java @@ -5,34 +5,23 @@ import androidx.annotation.StringDef; import com.brainwallet.data.repository.LtcRepository; -import com.brainwallet.presenter.entities.ServiceItems; import com.brainwallet.tools.threads.BRExecutor; -import com.brainwallet.tools.util.Utils; import com.brainwallet.data.model.Fee; -import com.platform.APIClient; import org.koin.java.KoinJavaComponent; -import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import okhttp3.Request; -import okhttp3.Response; -import timber.log.Timber; //we are still using this, maybe in the future will deprecate? +@Deprecated public final class FeeManager { private static final FeeManager instance; private String feeType; - public Fee currentFees; + public Fee currentFeeOptions; public static FeeManager getInstance() { return instance; @@ -44,8 +33,8 @@ public static FeeManager getInstance() { } private void initWithDefaultValues() { - currentFees = Fee.getDefault(); - feeType = REGULAR; + currentFeeOptions = Fee.getDefault(); + feeType = LUXURY; } private FeeManager() { @@ -56,37 +45,22 @@ public void setFeeType(@FeeType String feeType) { } public void resetFeeType() { - this.feeType = REGULAR; + this.feeType = LUXURY; } - public boolean isRegularFee() { - return feeType.equals(REGULAR); + public boolean isLuxuryFee() { + return feeType.equals(LUXURY); } - public static final String LUXURY = "luxury"; - public static final String REGULAR = "regular"; - public static final String ECONOMY = "economy"; + public static final String LUXURY = "luxury";//top + public static final String REGULAR = "regular";//medium + public static final String ECONOMY = "economy";//low public void setFees(long luxuryFee, long regularFee, long economyFee) { - // TODO: to be implemented when feePerKB API will be ready - currentFees = new Fee(luxuryFee, regularFee, economyFee, System.currentTimeMillis()); + currentFeeOptions = new Fee(luxuryFee, regularFee, economyFee); } public static void updateFeePerKb(Context app) { - -// String jsonString = "{'fee_per_kb': 10000, 'fee_per_kb_economy': 2500, 'fee_per_kb_luxury': 66746}"; -// try { -// JSONObject obj = new JSONObject(jsonString); -// // TODO: Refactor when mobile-api v0.4.0 is in prod -// long regularFee = obj.optLong("fee_per_kb"); -// long economyFee = obj.optLong("fee_per_kb_economy"); -// long luxuryFee = obj.optLong("fee_per_kb_luxury"); -// FeeManager.getInstance().setFees(luxuryFee, regularFee, economyFee); -// BRSharedPrefs.putFeeTime(app, System.currentTimeMillis()); //store the time of the last successful fee fetch -// } catch (JSONException e) { -// Timber.e(new IllegalArgumentException("updateFeePerKb: FAILED: " + jsonString, e)); -// } - LtcRepository ltcRepository = KoinJavaComponent.get(LtcRepository.class); BRExecutor.getInstance().executeSuspend( (coroutineScope, continuation) -> ltcRepository.fetchFeePerKb(continuation) @@ -98,43 +72,6 @@ public static void updateFeePerKb(Context app) { }); } - // createGETRequestURL - // Creates the params and headers to make a GET Request - @Deprecated - private static String createGETRequestURL(Context app, String myURL) { - Request request = new Request.Builder() - .url(myURL) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .header("User-agent", Utils.getAgentString(app, "android/HttpURLConnection")) - .header("BW-client-code", Utils.fetchServiceItem(app, ServiceItems.CLIENTCODE)) - .get().build(); - String response = null; - Response resp = APIClient.getInstance(app).sendRequest(request, false, 0); - - try { - if (resp == null) { - Timber.i("timber: urlGET: %s resp is null", myURL); - return null; - } - response = resp.body().string(); - String strDate = resp.header("date"); - if (strDate == null) { - Timber.i("timber: urlGET: strDate is null!"); - return response; - } - SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - Date date = formatter.parse(strDate); - long timeStamp = date.getTime(); - BRSharedPrefs.putSecureTime(app, timeStamp); - } catch (ParseException | IOException e) { - Timber.e(e); - } finally { - if (resp != null) resp.close(); - } - return response; - } - @Retention(RetentionPolicy.SOURCE) @StringDef({LUXURY, REGULAR, ECONOMY}) public @interface FeeType { diff --git a/app/src/main/java/com/brainwallet/tools/qrcode/QRUtils.java b/app/src/main/java/com/brainwallet/tools/qrcode/QRUtils.java index 5728657c..a0e9fd5f 100644 --- a/app/src/main/java/com/brainwallet/tools/qrcode/QRUtils.java +++ b/app/src/main/java/com/brainwallet/tools/qrcode/QRUtils.java @@ -83,6 +83,21 @@ public static boolean generateQR(Context ctx, String bitcoinURL, ImageView qrcod return true; } + public static Bitmap generateQR(Context ctx, String litecoinUrl) { + if (litecoinUrl == null || litecoinUrl.isEmpty()) return null; + WindowManager manager = (WindowManager) ctx.getSystemService(Activity.WINDOW_SERVICE); + Display display = manager.getDefaultDisplay(); + Point point = new Point(); + display.getSize(point); + int width = point.x; + int height = point.y; + int smallerDimension = Math.min(width, height); + smallerDimension = (int) (smallerDimension * 0.45f); + Bitmap bitmap = null; + bitmap = QRUtils.encodeAsBitmap(litecoinUrl, smallerDimension); + return bitmap; + } + private static String guessAppropriateEncoding(CharSequence contents) { // Very crude at the moment for (int i = 0; i < contents.length(); i++) { diff --git a/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java b/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java index 40ad36b1..7c15df6d 100644 --- a/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java +++ b/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java @@ -16,6 +16,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import timber.log.Timber; @@ -31,6 +32,27 @@ public class CurrencyDataSource implements BRDataSourceInterface { BRSQLiteHelper.CURRENCY_RATE }; + /// Set the most popular fiats + /// Hack: Database needs a symbol column. This is injected here. + private final HashMap codeSymbolsMap = new HashMap() {{ + put("USD","$"); + put("EUR","€"); + put("GBP","£"); + put("SGD","$"); + put("CAD","$"); + put("AUD","$"); + put("RUB","₽"); + put("KRW","₩"); + put("MXN","$"); + put("SAR","﷼"); + put("UAH","₴"); + put("NGN","₦"); + put("JPY","¥"); + put("CNY","¥"); + put("IDR","Rp"); + put("TRY","₺"); + }}; + private static CurrencyDataSource instance; public static CurrencyDataSource getInstance(Context context) { @@ -71,26 +93,6 @@ public List getAllCurrencies(Boolean shouldBeFiltered) { List currencies = new ArrayList<>(); - /// Set the most popular fiats - /// Hack: Database needs a symbol column. This is injected here. - HashMap codeSymbolsMap = new HashMap(); - codeSymbolsMap.put("USD","$"); - codeSymbolsMap.put("EUR","€"); - codeSymbolsMap.put("GBP","£"); - codeSymbolsMap.put("SGD","$"); - codeSymbolsMap.put("CAD","$"); - codeSymbolsMap.put("AUD","$"); - codeSymbolsMap.put("RUB","₽"); - codeSymbolsMap.put("KRW","₩"); - codeSymbolsMap.put("MXN","$"); - codeSymbolsMap.put("SAR","﷼"); - codeSymbolsMap.put("UAH","₴"); - codeSymbolsMap.put("NGN","₦"); - codeSymbolsMap.put("JPY","¥"); - codeSymbolsMap.put("CNY","¥"); - codeSymbolsMap.put("IDR","Rp"); - codeSymbolsMap.put("TRY","₺"); - /// Set the most popular fiats List filteredFiatCodes = Arrays.asList("USD","EUR","GBP", "SGD","CAD","AUD","RUB","KRW","MXN","SAR","UAH","NGN","JPY","CNY","IDR","TRY"); @@ -125,7 +127,7 @@ public List getAllCurrencies(Boolean shouldBeFiltered) { List completeCurrencyEntities = new ArrayList<>(); for(int i=0;i : ViewModel() { protected fun handleError(t: Throwable) { val errorMessage = t.message ?: "Oops, something went wrong" //todo more error handler + + if (t is retrofit2.HttpException) { + val message = t.response()?.errorBody()?.string()?.let { + runCatching { + json.decodeFromString(it)["message"]?.toString() + }.getOrNull() + } ?: "Oops, something went wrong" + + sendUiEffect( + UiEffect.ShowMessage( + type = UiEffect.ShowMessage.Type.Error, + message = message + ) + ) + return + } + sendUiEffect( UiEffect.ShowMessage( type = UiEffect.ShowMessage.Type.Error, diff --git a/app/src/main/java/com/brainwallet/ui/composable/Foundation.kt b/app/src/main/java/com/brainwallet/ui/composable/Foundation.kt index 2fabe5ae..f72137f5 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/Foundation.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/Foundation.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -18,6 +19,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.dp import com.brainwallet.ui.theme.BrainwalletTheme @@ -75,17 +77,21 @@ fun BrainwalletBottomSheet( fun BrainwalletButton( onClick: () -> Unit, modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = CircleShape, + colors: ButtonColors = ButtonDefaults.buttonColors( + containerColor = BrainwalletTheme.colors.surface, + contentColor = BrainwalletTheme.colors.content + ), content: @Composable RowScope.() -> Unit ) { Button( onClick = onClick, - shape = CircleShape, - colors = ButtonDefaults.buttonColors( - containerColor = BrainwalletTheme.colors.surface, - contentColor = BrainwalletTheme.colors.content - ), + enabled = enabled, + shape = shape, + colors = colors, modifier = modifier - .border(1.dp, BrainwalletTheme.colors.border, CircleShape) + .border(1.dp, BrainwalletTheme.colors.border, shape) .height(50.dp), content = content ) diff --git a/app/src/main/java/com/brainwallet/ui/composable/LargeButton.kt b/app/src/main/java/com/brainwallet/ui/composable/LargeButton.kt index a6f63965..48721758 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/LargeButton.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/LargeButton.kt @@ -10,16 +10,14 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledTonalButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.unit.dp -import com.brainwallet.ui.theme.BrainwalletColors import com.brainwallet.ui.theme.BrainwalletTheme @Composable fun LargeButton( modifier: Modifier = Modifier, + enabled: Boolean = true, onClick: () -> Unit, colors: ButtonColors = ButtonDefaults.buttonColors( containerColor = BrainwalletTheme.colors.background, @@ -32,6 +30,7 @@ fun LargeButton( modifier = modifier .fillMaxWidth() .height(56.dp), + enabled = enabled, onClick = onClick, colors = colors, shape = shape, diff --git a/app/src/main/java/com/brainwallet/ui/composable/LoadingDialog.kt b/app/src/main/java/com/brainwallet/ui/composable/LoadingDialog.kt new file mode 100644 index 00000000..8c6270ad --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/LoadingDialog.kt @@ -0,0 +1,55 @@ +package com.brainwallet.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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 com.brainwallet.R +import com.brainwallet.ui.theme.BrainwalletTheme + +@Composable +fun LoadingDialog( + text: String = stringResource(R.string.loading), + onDismissRequest: () -> Unit = {}, +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + dismissOnClickOutside = false, + dismissOnBackPress = false + ) + ) { + Card( + shape = BrainwalletTheme.shapes.medium, + modifier = Modifier + .fillMaxWidth(0.8f) + .wrapContentHeight(), + ) { + Column( + modifier = Modifier + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = text, + style = BrainwalletTheme.typography.bodyLarge + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyButton.kt b/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyButton.kt new file mode 100644 index 00000000..e3513194 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyButton.kt @@ -0,0 +1,66 @@ +package com.brainwallet.ui.composable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.brainwallet.R +import com.brainwallet.ui.theme.BrainwalletTheme +import com.brainwallet.ui.theme.lavender +import com.brainwallet.ui.theme.nearBlack + +@Composable +fun MoonpayBuyButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: () -> Unit, +) { + Button( + modifier = modifier, + shape = BrainwalletTheme.shapes.large, + colors = ButtonDefaults.buttonColors( + containerColor = lavender, + contentColor = nearBlack + ), + enabled = enabled, + onClick = onClick, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.buy_ltc).uppercase(), + style = BrainwalletTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold) + ) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.powered_by_moonpay).uppercase(), + style = BrainwalletTheme.typography.labelSmall.copy(fontSize = 8.sp) + ) + Image( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_moonpay_logo), + contentDescription = "moonpay" + ) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyWidget.kt b/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyWidget.kt new file mode 100644 index 00000000..006e45bf --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyWidget.kt @@ -0,0 +1,56 @@ +package com.brainwallet.ui.composable + + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.viewinterop.AndroidView + +//TODO: wip here + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun MoonpayBuyWidget( + modifier: Modifier = Modifier, + signedUrl: String = "https://buy.moonpay.com", +) { + AndroidView( + modifier = modifier.clip(MaterialTheme.shapes.large), + factory = { ctx -> + WebView(ctx).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + setBackgroundColor(0) + + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.useWideViewPort = true + + webViewClient = object : WebViewClient() { + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + } + } + + } + }, + update = { + it.loadUrl(signedUrl) + } + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/composable/WheelPicker.kt b/app/src/main/java/com/brainwallet/ui/composable/WheelPicker.kt new file mode 100644 index 00000000..27e9ce40 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/WheelPicker.kt @@ -0,0 +1,634 @@ +package com.brainwallet.ui.composable + +import android.util.Log +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.calculateTargetValue +import androidx.compose.animation.core.exponentialDecay +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.math.absoluteValue + +interface WheelPickerContentScope { + val state: WheelPickerState +} + +interface WheelPickerDisplayScope : WheelPickerContentScope { + @Composable + fun Content(index: Int) +} + +@Composable +fun VerticalWheelPicker( + modifier: Modifier = Modifier, + count: Int, + state: WheelPickerState = rememberWheelPickerState(), + key: ((index: Int) -> Any)? = null, + itemHeight: Dp = 35.dp, + unfocusedCount: Int = 2, + userScrollEnabled: Boolean = true, + reverseLayout: Boolean = false, + debug: Boolean = false, + focus: @Composable () -> Unit = { WheelPickerFocusVertical() }, + display: @Composable WheelPickerDisplayScope.(index: Int) -> Unit = { + DefaultWheelPickerDisplay( + it + ) + }, + content: @Composable WheelPickerContentScope.(index: Int) -> Unit, +) { + WheelPicker( + modifier = modifier, + isVertical = true, + count = count, + state = state, + key = key, + itemSize = itemHeight, + unfocusedCount = unfocusedCount, + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + debug = debug, + focus = focus, + display = display, + content = content, + ) +} + + +@Composable +private fun WheelPicker( + modifier: Modifier, + isVertical: Boolean, + count: Int, + state: WheelPickerState, + key: ((index: Int) -> Any)?, + itemSize: Dp, + unfocusedCount: Int, + userScrollEnabled: Boolean, + reverseLayout: Boolean, + debug: Boolean, + focus: @Composable () -> Unit, + display: @Composable WheelPickerDisplayScope.(index: Int) -> Unit, + content: @Composable WheelPickerContentScope.(index: Int) -> Unit, +) { + require(count >= 0) { "Require count >= 0" } + require(unfocusedCount >= 0) { "Require unfocusedCount >= 0" } + require(itemSize > 0.dp) { "Require itemSize > 0.dp" } + + SafeBox( + modifier = modifier, + isVertical = isVertical, + itemSize = itemSize, + unfocusedCount = unfocusedCount, + ) { safeUnfocusedCount -> + InternalWheelPicker( + isVertical = isVertical, + count = count, + state = state, + key = key, + itemSize = itemSize, + unfocusedCount = safeUnfocusedCount, + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + debug = debug, + focus = focus, + display = display, + content = content, + ) + + if (debug && unfocusedCount != safeUnfocusedCount) { + LaunchedEffect(unfocusedCount, safeUnfocusedCount) { + logMsg(true) { "unfocusedCount $unfocusedCount -> $safeUnfocusedCount" } + } + } + } +} + +@Composable +private fun InternalWheelPicker( + isVertical: Boolean, + count: Int, + state: WheelPickerState, + key: ((index: Int) -> Any)?, + itemSize: Dp, + unfocusedCount: Int, + userScrollEnabled: Boolean, + reverseLayout: Boolean, + debug: Boolean, + focus: @Composable () -> Unit, + display: @Composable WheelPickerDisplayScope.(index: Int) -> Unit, + content: @Composable WheelPickerContentScope.(index: Int) -> Unit, +) { + state.debug = debug + LaunchedEffect(state, count) { + state.updateCount(count) + } + + val nestedScrollConnection = remember(state) { + WheelPickerNestedScrollConnection(state) + }.apply { + this.isVertical = isVertical + this.itemSizePx = with(LocalDensity.current) { itemSize.roundToPx() } + this.reverseLayout = reverseLayout + } + + val totalSize = remember(itemSize, unfocusedCount) { + itemSize * (unfocusedCount * 2 + 1) + } + + val displayScope = remember(state) { + WheelPickerDisplayScopeImpl(state) + }.apply { + this.content = content + } + + Box( + modifier = Modifier + .nestedScroll(nestedScrollConnection) + .graphicsLayer { + this.alpha = if (state.isReady) 1f else 0f + } + .run { + if (totalSize > 0.dp) { + if (isVertical) { + height(totalSize).widthIn(40.dp) + } else { + width(totalSize).heightIn(40.dp) + } + } else { + this + } + }, + contentAlignment = Alignment.Center, + ) { + + val lazyListScope: LazyListScope.() -> Unit = { + + repeat(unfocusedCount) { + item(contentType = "placeholder") { + ItemSizeBox( + isVertical = isVertical, + itemSize = itemSize, + ) + } + } + + items( + count = count, + key = key, + ) { index -> + ItemSizeBox( + isVertical = isVertical, + itemSize = itemSize, + ) { + displayScope.display(index) + } + } + + repeat(unfocusedCount) { + item(contentType = "placeholder") { + ItemSizeBox( + isVertical = isVertical, + itemSize = itemSize, + ) + } + } + } + + if (isVertical) { + LazyColumn( + state = state.lazyListState, + horizontalAlignment = Alignment.CenterHorizontally, + reverseLayout = reverseLayout, + userScrollEnabled = userScrollEnabled && state.isReady, + modifier = Modifier.matchParentSize(), + content = lazyListScope, + ) + } else { + LazyRow( + state = state.lazyListState, + verticalAlignment = Alignment.CenterVertically, + reverseLayout = reverseLayout, + userScrollEnabled = userScrollEnabled && state.isReady, + modifier = Modifier.matchParentSize(), + content = lazyListScope, + ) + } + + ItemSizeBox( + modifier = Modifier.align(Alignment.Center), + isVertical = isVertical, + itemSize = itemSize, + ) { + focus() + } + } +} + +@Composable +private fun SafeBox( + modifier: Modifier = Modifier, + isVertical: Boolean, + itemSize: Dp, + unfocusedCount: Int, + content: @Composable (safeUnfocusedCount: Int) -> Unit, +) { + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + val maxSize = if (isVertical) maxHeight else maxWidth + val result = remember(maxSize, itemSize, unfocusedCount) { + val totalSize = itemSize * (unfocusedCount * 2 + 1) + if (totalSize <= maxSize) { + unfocusedCount + } else { + (((maxSize - itemSize) / 2f) / itemSize).toInt().coerceAtLeast(0) + } + } + content(result) + } +} + +@Composable +private fun ItemSizeBox( + modifier: Modifier = Modifier, + isVertical: Boolean, + itemSize: Dp, + content: @Composable () -> Unit = { }, +) { + Box( + modifier + .run { + if (isVertical) { + height(itemSize) + } else { + width(itemSize) + } + }, + contentAlignment = Alignment.Center, + ) { + content() + } +} + +private class WheelPickerNestedScrollConnection( + private val state: WheelPickerState, +) : NestedScrollConnection { + var isVertical: Boolean = true + var itemSizePx: Int = 0 + var reverseLayout: Boolean = false + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + state.synchronizeCurrentIndexSnapshot() + return super.onPostScroll(consumed, available, source) + } + + override suspend fun onPreFling(available: Velocity): Velocity { + val currentIndex = state.synchronizeCurrentIndexSnapshot() + return if (currentIndex >= 0) { + available.flingItemCount( + isVertical = isVertical, + itemSize = itemSizePx, + decay = exponentialDecay(2f), + reverseLayout = reverseLayout, + ).let { flingItemCount -> + if (flingItemCount == 0) { + state.animateScrollToIndex(currentIndex) + } else { + state.animateScrollToIndex(currentIndex - flingItemCount) + } + } + available + } else { + super.onPreFling(available) + } + } +} + +private fun Velocity.flingItemCount( + isVertical: Boolean, + itemSize: Int, + decay: DecayAnimationSpec, + reverseLayout: Boolean, +): Int { + if (itemSize <= 0) return 0 + val velocity = if (isVertical) y else x + val targetValue = decay.calculateTargetValue(0f, velocity) + val flingItemCount = (targetValue / itemSize).toInt() + return if (reverseLayout) -flingItemCount else flingItemCount +} + +private class WheelPickerDisplayScopeImpl( + override val state: WheelPickerState, +) : WheelPickerDisplayScope { + + var content: @Composable WheelPickerContentScope.(index: Int) -> Unit by mutableStateOf({}) + + @Composable + override fun Content(index: Int) { + content(index) + } +} + +internal inline fun logMsg(debug: Boolean, block: () -> String) { + if (debug) { + Log.i("WheelPicker", block()) + } +} + +//default +/** + * The default implementation of focus view in vertical. + */ +@Composable +fun WheelPickerFocusVertical( + modifier: Modifier = Modifier, + dividerSize: Dp = 1.dp, + dividerColor: Color = DefaultDividerColor, +) { + Box( + modifier = modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .background(dividerColor) + .height(dividerSize) + .fillMaxWidth() + .align(Alignment.TopCenter), + ) + Box( + modifier = Modifier + .background(dividerColor) + .height(dividerSize) + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) + } +} + +/** + * Default divider color. + */ +private val DefaultDividerColor: Color + @Composable + get() { + val color = if (isSystemInDarkTheme()) Color.White else Color.Black + return color.copy(alpha = 0.2f) + } + +/** + * Default display. + */ +@Composable +fun WheelPickerDisplayScope.DefaultWheelPickerDisplay( + index: Int, +) { + val focused = index == state.currentIndexSnapshot + val animateScale by animateFloatAsState( + targetValue = if (focused) 1.0f else 0.8f, + label = "Wheel picker item scale", + ) + Box( + modifier = Modifier.graphicsLayer { + this.alpha = if (focused) 1.0f else 0.3f + this.scaleX = animateScale + this.scaleY = animateScale + } + ) { + Content(index) + } +} + +//state +@Composable +fun rememberWheelPickerState( + initialIndex: Int = 0, +): WheelPickerState { + return rememberSaveable(saver = WheelPickerState.Saver) { + WheelPickerState( + initialIndex = initialIndex, + ) + } +} + +@Composable +fun WheelPickerState.CurrentIndex( + block: suspend (Int) -> Unit, +) { + val blockUpdated by rememberUpdatedState(block) + LaunchedEffect(this) { + snapshotFlow { currentIndex } + .collect { blockUpdated(it) } + } +} + +class WheelPickerState internal constructor( + initialIndex: Int, +) { + internal var debug = false + internal val lazyListState = LazyListState() + internal var isReady by mutableStateOf(false) + + private var _count = 0 + private var _currentIndex by mutableIntStateOf(-1) + private var _currentIndexSnapshot by mutableIntStateOf(-1) + + private var _pendingIndex: Int? = initialIndex.coerceAtLeast(0) + private var _pendingIndexContinuation: CancellableContinuation? = null + set(value) { + field = value + if (value == null) _pendingIndex = null + } + + /** + * Index of picker when it is idle, -1 means that there is no data. + * + * Note that this property is observable and if you use it in the composable function + * it will be recomposed on every change. + */ + val currentIndex: Int get() = _currentIndex + + /** + * Index of picker when it is idle or drag but not fling, -1 means that there is no data. + * + * Note that this property is observable and if you use it in the composable function + * it will be recomposed on every change. + */ + val currentIndexSnapshot: Int get() = _currentIndexSnapshot + + /** + * [LazyListState.interactionSource] + */ + val interactionSource: InteractionSource get() = lazyListState.interactionSource + + /** + * [LazyListState.isScrollInProgress] + */ + val isScrollInProgress: Boolean get() = lazyListState.isScrollInProgress + + suspend fun animateScrollToIndex(index: Int) { + logMsg(debug) { "animateScrollToIndex index:$index count:$_count" } + @Suppress("NAME_SHADOWING") + val index = index.coerceAtLeast(0) + lazyListState.animateScrollToItem(index) + synchronizeCurrentIndex() + } + + suspend fun scrollToIndex(index: Int) { + logMsg(debug) { "scrollToIndex index:$index count:$_count" } + @Suppress("NAME_SHADOWING") + val index = index.coerceAtLeast(0) + + // Always cancel last continuation. + _pendingIndexContinuation?.let { + logMsg(debug) { "cancelAwaitIndex" } + _pendingIndexContinuation = null + it.cancel() + } + + awaitIndex(index) + + lazyListState.scrollToItem(index) + synchronizeCurrentIndex() + } + + private suspend fun awaitIndex(index: Int) { + if (_count > 0) return + logMsg(debug) { "awaitIndex:$index start" } + suspendCancellableCoroutine { cont -> + _pendingIndex = index + _pendingIndexContinuation = cont + cont.invokeOnCancellation { + logMsg(debug) { "awaitIndex:$index canceled" } + _pendingIndexContinuation = null + } + } + logMsg(debug) { "awaitIndex:$index finish" } + } + + internal suspend fun updateCount(count: Int) { + logMsg(debug) { "updateCount count:$count currentIndex:$_currentIndex" } + + // Update count + _count = count + + val maxIndex = count - 1 + if (maxIndex < _currentIndex) { + if (count > 0) { + scrollToIndex(maxIndex) + } else { + synchronizeCurrentIndex() + } + } + + if (count > 0) { + val pendingIndex = _pendingIndex + if (pendingIndex != null) { + logMsg(debug) { "Found pendingIndex:$pendingIndex pendingIndexContinuation:$_pendingIndexContinuation" } + val continuation = _pendingIndexContinuation + _pendingIndexContinuation = null + + if (continuation?.isActive == true) { + logMsg(debug) { "resume pendingIndexContinuation" } + continuation.resume(Unit) + } else { + scrollToIndex(pendingIndex) + } + } else { + if (_currentIndex < 0) { + synchronizeCurrentIndex() + } + } + } + + isReady = count > 0 + } + + private fun synchronizeCurrentIndex() { + val index = synchronizeCurrentIndexSnapshot() + if (_currentIndex != index) { + logMsg(debug) { "setCurrentIndex:$index" } + _currentIndex = index + _currentIndexSnapshot = index + } + } + + internal fun synchronizeCurrentIndexSnapshot(): Int { + return (mostStartItemInfo()?.index ?: -1).also { + _currentIndexSnapshot = it + } + } + + /** + * The item closest to the viewport start. + */ + private fun mostStartItemInfo(): LazyListItemInfo? { + if (_count <= 0) return null + + val layoutInfo = lazyListState.layoutInfo + val listInfo = layoutInfo.visibleItemsInfo + + if (listInfo.isEmpty()) return null + if (listInfo.size == 1) return listInfo.first() + + val firstItem = listInfo.first() + val firstOffsetDelta = (firstItem.offset - layoutInfo.viewportStartOffset).absoluteValue + return if (firstOffsetDelta < firstItem.size / 2) { + firstItem + } else { + listInfo[1] + } + } + + companion object { + val Saver = listSaver( + save = { listOf(it.currentIndex) }, + restore = { WheelPickerState(initialIndex = it[0]) }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinEvent.kt new file mode 100644 index 00000000..31e528fe --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinEvent.kt @@ -0,0 +1,11 @@ +package com.brainwallet.ui.screens.buylitecoin + +import android.content.Context + +sealed class BuyLitecoinEvent { + data class OnLoad(val context: Context) : BuyLitecoinEvent() + data class OnFiatAmountChange( + val fiatAmount: Float, + val needFetch: Boolean = true + ) : BuyLitecoinEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt new file mode 100644 index 00000000..97260328 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt @@ -0,0 +1,167 @@ +package com.brainwallet.ui.screens.buylitecoin + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.brainwallet.R +import com.brainwallet.navigation.LegacyNavigation +import com.brainwallet.navigation.OnNavigate +import com.brainwallet.navigation.UiEffect +import com.brainwallet.ui.composable.BrainwalletScaffold +import com.brainwallet.ui.composable.BrainwalletTopAppBar +import com.brainwallet.ui.composable.LargeButton +import com.brainwallet.ui.composable.LoadingDialog +import com.brainwallet.ui.screens.home.receive.ReceiveDialogEvent +import com.brainwallet.ui.theme.BrainwalletTheme +import org.koin.compose.koinInject + +//TODO: wip +@Composable +fun BuyLitecoinScreen( + onNavigate: OnNavigate, + viewModel: BuyLitecoinViewModel = koinInject() +) { + val state by viewModel.state.collectAsState() + val loadingState by viewModel.loadingState.collectAsState() + val appSetting by viewModel.appSetting.collectAsState() + val context = LocalContext.current + + LaunchedEffect(Unit) { + viewModel.onEvent(BuyLitecoinEvent.OnLoad(context)) + viewModel.uiEffect.collect { effect -> + when (effect) { + is UiEffect.ShowMessage -> + Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() + + else -> Unit + } + } + } + + BrainwalletScaffold( + topBar = { + BrainwalletTopAppBar( + navigationIcon = { + IconButton( + onClick = { onNavigate.invoke(UiEffect.Navigate.Back()) }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back), + ) + } + } + ) + } + ) { paddingValues -> + + Box( + modifier = Modifier + .padding(paddingValues) + .padding(16.dp) + .fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Text( + text = stringResource(R.string.buy_litecoin_title), + style = MaterialTheme.typography.titleLarge, + ) + + OutlinedTextField( + enabled = loadingState.visible.not(), + prefix = { + Text( + text = appSetting.currency.symbol, + style = BrainwalletTheme.typography.titleLarge.copy(color = BrainwalletTheme.colors.content) + ) + }, + textStyle = BrainwalletTheme.typography.titleLarge.copy(color = BrainwalletTheme.colors.content), + value = "${if (state.fiatAmount < 1) "" else state.fiatAmount.toInt()}", + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { input -> + val amount = input.toFloatOrNull() ?: 0f + viewModel.onEvent(BuyLitecoinEvent.OnFiatAmountChange(amount, false)) + }, + shape = BrainwalletTheme.shapes.extraLarge, + isError = state.isValid().not(), + supportingText = { + state.errorFiatAmountStringId?.let { + Text(stringResource(it, state.fiatAmount)) + } + } + ) + + Text( + text = state.getLtcAmountFormatted(loadingState.visible), + style = MaterialTheme.typography.titleLarge.copy( + color = BrainwalletTheme.colors.content.copy( + 0.7f + ) + ), + ) + + Text( + text = stringResource(R.string.buy_litecoin_desc), + style = MaterialTheme.typography.titleMedium + ) + + Text( + text = state.address, + style = MaterialTheme.typography.titleMedium.copy( + color = BrainwalletTheme.colors.content.copy( + 0.7f + ) + ) + ) + + } + + + LargeButton( + modifier = Modifier.align(Alignment.BottomCenter), + enabled = loadingState.visible.not(), + onClick = { + LegacyNavigation.showMoonPayWidget( + context = context, + params = mapOf( + "baseCurrencyCode" to appSetting.currency.code, + "baseCurrencyAmount" to state.fiatAmount.toString(), + "language" to appSetting.languageCode, + "walletAddress" to state.address + ) + ) + } + ) { + Text(stringResource(R.string.buy_litecoin_button_moonpay)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinState.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinState.kt new file mode 100644 index 00000000..628d6f0c --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinState.kt @@ -0,0 +1,19 @@ +package com.brainwallet.ui.screens.buylitecoin + +import com.brainwallet.data.model.MoonpayCurrencyLimit +import timber.log.Timber + +data class BuyLitecoinState( + val moonpayCurrencyLimit: MoonpayCurrencyLimit = MoonpayCurrencyLimit(), + val fiatAmount: Float = moonpayCurrencyLimit.data.baseCurrency.min, + val ltcAmount: Float = 0f, + val address: String = "", + val errorFiatAmountStringId: Int? = null +) + +fun BuyLitecoinState.isValid(): Boolean = errorFiatAmountStringId == null + +fun BuyLitecoinState.getLtcAmountFormatted(isLoading: Boolean): String = + (if (isLoading || ltcAmount < 0f) "x.xxxŁ" else "%.3fŁ".format(ltcAmount)).also { + Timber.d("TImber: ltcamount $ltcAmount") + } diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt new file mode 100644 index 00000000..5e6e6316 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt @@ -0,0 +1,128 @@ +package com.brainwallet.ui.screens.buylitecoin + +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.brainwallet.R +import com.brainwallet.data.model.AppSetting +import com.brainwallet.data.repository.LtcRepository +import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.tools.manager.BRSharedPrefs +import com.brainwallet.ui.BrainwalletViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class BuyLitecoinViewModel( + private val settingRepository: SettingRepository, + private val ltcRepository: LtcRepository +) : BrainwalletViewModel() { + + private val _state = MutableStateFlow(BuyLitecoinState()) + val state: StateFlow = _state.asStateFlow() + + val appSetting = settingRepository.settings + .distinctUntilChanged() + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + AppSetting() + ) + + init { + viewModelScope.launch { + state.map { it.fiatAmount } + .debounce(1000) + .distinctUntilChanged() + .filter { + val (_, min, max) = state.value.moonpayCurrencyLimit.data.baseCurrency + it in min..max + } + .collect { + onEvent(BuyLitecoinEvent.OnFiatAmountChange(it)) + } + } + + } + + override fun onEvent(event: BuyLitecoinEvent) { + when (event) { + is BuyLitecoinEvent.OnLoad -> viewModelScope.launch { + delay(500) + _state.update { it.copy(address = BRSharedPrefs.getReceiveAddress(event.context)) } + try { + onLoading(true) + + _state.getAndUpdate { + val limitResult = ltcRepository.fetchLimits( + baseCurrencyCode = appSetting.value.currency.code + ) + + it.copy( + moonpayCurrencyLimit = limitResult, + fiatAmount = limitResult.data.baseCurrency.min, + ) + } + } catch (e: Exception) { + handleError(e) + } finally { + onLoading(false) + } + + } + + is BuyLitecoinEvent.OnFiatAmountChange -> viewModelScope.launch { + //do validation + val (_, min, max) = state.value.moonpayCurrencyLimit.data.baseCurrency + val errorStringId = when { + event.fiatAmount < min -> R.string.buy_litecoin_fiat_amount_validation_min + event.fiatAmount > max -> R.string.buy_litecoin_fiat_amount_validation_max + else -> null + } + _state.update { + it.copy( + errorFiatAmountStringId = errorStringId, + fiatAmount = event.fiatAmount + ) + } + + if (event.needFetch.not()) { + return@launch + } + + try { + onLoading(true) + + _state.update { + val result = ltcRepository.fetchBuyQuote( + mapOf( + "currencyCode" to "ltc", + "baseCurrencyCode" to appSetting.value.currency.code, + "baseCurrencyAmount" to event.fiatAmount.toString(), + ) + ) + + it.copy( + ltcAmount = result.data.quoteCurrencyAmount, + ) + } + + } catch (e: Exception) { + handleError(e) + } finally { + onLoading(false) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsState.kt b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsState.kt index 9f924c3c..60a209bd 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsState.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsState.kt @@ -1,6 +1,10 @@ package com.brainwallet.ui.screens.home + import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.Fee +import com.brainwallet.data.model.FeeOption import com.brainwallet.data.model.Language +import com.brainwallet.data.model.toFeeOptions data class SettingsState( val darkMode: Boolean = true, @@ -15,4 +19,5 @@ data class SettingsState( val fiatSelectorBottomSheetVisible: Boolean = false, val shareAnalyticsDataEnabled: Boolean = false, val lastSyncMetadata: String? = null, + val currentFeeOptions: List = Fee.Default.toFeeOptions(), ) \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt index e9515ce8..8859d395 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt @@ -5,6 +5,8 @@ import androidx.core.os.LocaleListCompat import androidx.lifecycle.viewModelScope import com.brainwallet.data.model.AppSetting import com.brainwallet.data.model.Language +import com.brainwallet.data.model.toFeeOptions +import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.repository.SettingRepository import com.brainwallet.ui.BrainwalletViewModel import com.brainwallet.util.EventBus @@ -21,7 +23,8 @@ import kotlinx.coroutines.launch class SettingsViewModel( - private val settingRepository: SettingRepository + private val settingRepository: SettingRepository, + private val ltcRepository: LtcRepository ) : BrainwalletViewModel() { private val _state = MutableStateFlow(SettingsState()) @@ -50,7 +53,8 @@ class SettingsViewModel( _state.update { it.copy( shareAnalyticsDataEnabled = event.shareAnalyticsDataEnabled, - lastSyncMetadata = event.lastSyncMetadata + lastSyncMetadata = event.lastSyncMetadata, + currentFeeOptions = ltcRepository.fetchFeePerKb().toFeeOptions() ) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/buy/BuyScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/home/buy/BuyScreen.kt deleted file mode 100644 index 2e974ad2..00000000 --- a/app/src/main/java/com/brainwallet/ui/screens/home/buy/BuyScreen.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.brainwallet.ui.screens.home.buy - -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable - -//TODO: wip -@Composable -fun BuyScreen() { - Box {} -} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt b/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt index 42b370a8..5c0c6a9c 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt @@ -60,7 +60,7 @@ fun HomeSettingDrawerSheet( LaunchedEffect(Unit) { viewModel.onEvent(SettingsEvent.OnLoad( shareAnalyticsDataEnabled = BRSharedPrefs.getShareData(context), //currently just load analytics share data here - lastSyncMetadata = BRSharedPrefs.getSyncMetadata(context) //currently just load sync metadata here + lastSyncMetadata = BRSharedPrefs.getSyncMetadata(context), //currently just load sync metadata here )) } @@ -134,6 +134,8 @@ fun HomeSettingDrawerSheet( modifier = Modifier .fillMaxSize() .wrapContentHeight(), + selectedCurrency = state.selectedCurrency, + feeOptions = state.currentFeeOptions, onEvent = { viewModel.onEvent(it) } diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LitecoinBlockchainDetail.kt b/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LitecoinBlockchainDetail.kt index 339584cd..138ab31c 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LitecoinBlockchainDetail.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LitecoinBlockchainDetail.kt @@ -1,26 +1,50 @@ package com.brainwallet.ui.screens.home.composable.settingsrows +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.brainwallet.R +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.Fee +import com.brainwallet.data.model.FeeOption +import com.brainwallet.data.model.getFiatFormatted +import com.brainwallet.data.model.toFeeOptions +import com.brainwallet.tools.manager.FeeManager import com.brainwallet.ui.screens.home.SettingsEvent +import com.brainwallet.ui.theme.BrainwalletTheme +import com.brainwallet.wallet.BRWalletManager //TODO @Composable fun LitecoinBlockchainDetail( modifier: Modifier = Modifier, + selectedCurrency: CurrencyEntity, + feeOptions: List, onEvent: (SettingsEvent) -> Unit, ) { + var feeOptionsState by remember { mutableIntStateOf(2) } //2 -> index of top, since we have [low,medium,top] + /// Layout values val contentHeight = 60 val horizontalPadding = 14 @@ -35,19 +59,89 @@ fun LitecoinBlockchainDetail( ) { Row( - modifier = Modifier - .height(contentHeight.dp), + modifier = Modifier.height(contentHeight.dp), verticalAlignment = Alignment.CenterVertically ) { - Text(stringResource(R.string.settings_blockchain_litecoin_description)) - Spacer(modifier = Modifier.weight(1f)) - Button(onClick = { - onEvent.invoke(SettingsEvent.OnBlockchainSyncClick) - }) { + Text( + text = stringResource(R.string.settings_blockchain_litecoin_description), + modifier = Modifier.weight(1.3f) + ) + Button( + modifier = Modifier.weight(.7f), + onClick = { + onEvent.invoke(SettingsEvent.OnBlockchainSyncClick) + } + ) { Text(stringResource(R.string.settings_blockchain_litecoin_button)) } } + HorizontalDivider(color = BrainwalletTheme.colors.content) + + NetworkFeeSelector( + selectedCurrency = selectedCurrency, + feeOptions = feeOptions, + selectedIndex = feeOptionsState + ) { newSelectedIndex -> + feeOptionsState = newSelectedIndex + + //just update inside BRWalletManager.setFeePerKb + BRWalletManager.getInstance().setFeePerKb(feeOptions[newSelectedIndex].feePerKb) + } + + } + } +} + +@Composable +private fun NetworkFeeSelector( + selectedCurrency: CurrencyEntity, + feeOptions: List, + selectedIndex: Int, + onSelectedChange: (Int) -> Unit +) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.network_fee_options_desc), + fontSize = 12.sp, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + feeOptions.forEachIndexed { index, feeOption -> + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "${feeOption.feePerKb}", + fontWeight = FontWeight.Bold + ) + Text( + text = feeOptions[index].getFiatFormatted(selectedCurrency), //fiat? + fontSize = 12.sp + ) + } + } + } + + SingleChoiceSegmentedButtonRow { + feeOptions.forEachIndexed { index, feeOption -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = feeOptions.size, + baseShape = MaterialTheme.shapes.extraLarge + ), + onClick = { onSelectedChange.invoke(index) }, + selected = index == selectedIndex, + label = { Text(stringResource(feeOption.labelStringId)) } + ) + } } + + } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt new file mode 100644 index 00000000..291015e2 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt @@ -0,0 +1,452 @@ +@file:OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) + +package com.brainwallet.ui.screens.home.receive + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.AssistChip +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.OutlinedIconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.brainwallet.R +import com.brainwallet.data.model.getFormattedText +import com.brainwallet.data.model.isCustom +import com.brainwallet.navigation.LegacyNavigation +import com.brainwallet.navigation.UiEffect +import com.brainwallet.ui.composable.MoonpayBuyButton +import com.brainwallet.ui.composable.VerticalWheelPicker +import com.brainwallet.ui.composable.WheelPickerFocusVertical +import com.brainwallet.ui.composable.rememberWheelPickerState +import com.brainwallet.ui.theme.BrainwalletAppTheme +import com.brainwallet.ui.theme.BrainwalletTheme +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import org.koin.android.ext.android.inject +import org.koin.compose.koinInject +import timber.log.Timber + +@Composable +fun ReceiveDialog( + onDismissRequest: () -> Unit, + viewModel: ReceiveDialogViewModel = koinInject() +) { + val state by viewModel.state.collectAsState() + val loadingState by viewModel.loadingState.collectAsState() + val appSetting by viewModel.appSetting.collectAsState() + val context = LocalContext.current + val wheelPickerFiatCurrencyState = rememberWheelPickerState(0) + + LaunchedEffect(Unit) { + viewModel.onEvent(ReceiveDialogEvent.OnLoad(context)) + viewModel.uiEffect.collect { effect -> + when (effect) { + is UiEffect.ShowMessage -> Toast.makeText( + context, + effect.message, + Toast.LENGTH_SHORT + ).show() + + else -> Unit + } + } + } + + LaunchedEffect(Unit) { + delay(500) + wheelPickerFiatCurrencyState.scrollToIndex(state.getSelectedFiatCurrencyIndex()) + } + + LaunchedEffect(wheelPickerFiatCurrencyState) { + snapshotFlow { wheelPickerFiatCurrencyState.currentIndex } + .filter { it > -1 } + .distinctUntilChanged() + .debounce(700) + .collect { + Timber.i("wheelPickerFiatCurrencyState: currentIndex $it") + + viewModel.onEvent(ReceiveDialogEvent.OnFiatCurrencyChange(state.fiatCurrencies[it])) + } + } + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = BrainwalletTheme.colors.content //invert surface + ), + expandedHeight = 56.dp, + title = { + Text( + text = stringResource(R.string.bottom_nav_item_buy_receive_title).uppercase(), + style = BrainwalletTheme.typography.titleSmall + ) + }, + navigationIcon = { + if (state.moonpayWidgetVisible()) { + IconButton(onClick = { + viewModel.onEvent(ReceiveDialogEvent.OnSignedUrlClear) + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + } + }, + actions = { + IconButton(onClick = onDismissRequest) { + Icon( + Icons.Default.Close, + contentDescription = stringResource(R.string.AccessibilityLabels_close), + tint = BrainwalletTheme.colors.surface + ) + } + } + ) + + //moonpay widget + //todo: revisit this later +// AnimatedVisibility(visible = state.moonpayWidgetVisible()) { +// state.moonpayBuySignedUrl?.let { signedUrl -> +// MoonpayBuyWidget( +// modifier = Modifier.height(500.dp), +// signedUrl = signedUrl +// ) +// } +// } + + + //buy / receive +// AnimatedVisibility(visible = state.moonpayWidgetVisible().not()) { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + state.qrBitmap?.asImageBitmap()?.let { imageBitmap -> + Image( + modifier = Modifier + .weight(1f), + bitmap = imageBitmap, + contentDescription = "address" + ) + } ?: Box( + modifier = Modifier + .weight(1f) + .height(180.dp) + .background(Color.Gray) + ) + + Column( + modifier = Modifier + .weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = state.address, + style = BrainwalletTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Bold, + color = BrainwalletTheme.colors.surface + ), + maxLines = 4, + overflow = TextOverflow.Ellipsis + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(R.string.new_address).uppercase(), + style = BrainwalletTheme.typography.bodySmall.copy( + color = BrainwalletTheme.colors.surface, + ), + modifier = Modifier.weight(1f) + ) + OutlinedIconButton( + modifier = Modifier.size(32.dp), + onClick = { + viewModel.onEvent(ReceiveDialogEvent.OnCopyClick(context)) + Toast.makeText( + context, + R.string.Receive_copied, + Toast.LENGTH_SHORT + ) + .show() + }, + colors = IconButtonDefaults.outlinedIconButtonColors( + containerColor = BrainwalletTheme.colors.content.copy(alpha = 0.5f) + ), + ) { + Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.ic_copy), + contentDescription = stringResource(R.string.URLHandling_copy), + tint = BrainwalletTheme.colors.surface + ) + } + } + } + } + + HorizontalDivider() + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + VerticalWheelPicker( + modifier = Modifier.weight(1f), + focus = { + WheelPickerFocusVertical( + dividerColor = BrainwalletTheme.colors.surface.copy( + alpha = 0.5f + ) + ) + }, + unfocusedCount = 1, + count = state.fiatCurrencies.size, + state = wheelPickerFiatCurrencyState, + ) { index -> + Text( + text = state.fiatCurrencies[index].code, + fontWeight = FontWeight.Bold, + color = BrainwalletTheme.colors.surface + ) + } + + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = state.getLtcAmountFormatted(loadingState.visible), + style = BrainwalletTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + color = BrainwalletTheme.colors.surface + ) + ) + Text( + text = state.getRatesUpdatedAtFormatted(), + style = BrainwalletTheme.typography.bodySmall.copy( + color = BrainwalletTheme.colors.surface + ) + ) + } + } + + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(items = state.getQuickFiatAmountOptions()) { index, quickFiatAmountOption -> + AssistChip( + enabled = loadingState.visible.not(), + onClick = { + viewModel.onEvent( + ReceiveDialogEvent.OnFiatAmountOptionIndexChange( + index, + quickFiatAmountOption + ) + ) + }, + label = { + Text( + text = if (quickFiatAmountOption.isCustom()) + stringResource(R.string.custom) + else quickFiatAmountOption.getFormattedText() + ) + }, + leadingIcon = { + if (index == state.selectedQuickFiatAmountOptionIndex) { + Icon(Icons.Default.Check, contentDescription = null) + } + } + ) + } + } + + + AnimatedVisibility(visible = state.isQuickFiatAmountOptionCustom()) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + prefix = { + Text( + text = state.selectedFiatCurrency.symbol, + style = BrainwalletTheme.typography.bodyMedium.copy(color = BrainwalletTheme.colors.surface) + ) + }, + trailingIcon = { + IconButton(onClick = { + viewModel.onEvent(ReceiveDialogEvent.OnFiatAmountChange(state.fiatAmount)) + }) { + Icon(Icons.Default.Done, contentDescription = null) + } + }, + textStyle = BrainwalletTheme.typography.bodyMedium.copy(color = BrainwalletTheme.colors.surface), + value = "${if (state.fiatAmount < 1) "" else state.fiatAmount}", + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { input -> + val amount = input.toFloatOrNull() ?: 0f + viewModel.onEvent(ReceiveDialogEvent.OnFiatAmountChange(amount, false)) + }, + shape = BrainwalletTheme.shapes.large, + isError = state.errorFiatAmountStringId != null, + supportingText = { + state.errorFiatAmountStringId?.let { + Text(stringResource(it, state.fiatAmount)) + } + } + ) + } + + MoonpayBuyButton( + modifier = Modifier.fillMaxWidth(), + enabled = loadingState.visible.not(), + onClick = { + + //todo: revisit this later + //viewModel.onEvent(ReceiveDialogEvent.OnMoonpayButtonClick) + + LegacyNavigation.showMoonPayWidget( + context = context, + params = mapOf( + "baseCurrencyCode" to state.selectedFiatCurrency.code, + "baseCurrencyAmount" to state.fiatAmount.toString(), + "language" to appSetting.languageCode, + "walletAddress" to state.address, + ), + isDarkMode = appSetting.isDarkMode + ) + onDismissRequest.invoke() + }, + ) + +// } + } + + + } +} + +/** + * describe [ReceiveDialogFragment] for backward compat, + * since we are still using [com.brainwallet.presenter.activities.BreadActivity] + */ +class ReceiveDialogFragment : DialogFragment() { + + private val viewModel: ReceiveDialogViewModel by inject() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setContent { + val appSetting by viewModel.appSetting.collectAsState() + /** + * we need this theme inside this fragment, + * because we are still using fragment to display ReceiveDialog composable + * pls check BreadActivity.handleNavigationItemSelected + */ + BrainwalletAppTheme(appSetting = appSetting) { + Box( + modifier = Modifier + .padding(12.dp) + .background( + BrainwalletTheme.colors.content, + shape = BrainwalletTheme.shapes.large + ) + .border( + width = 1.dp, + color = BrainwalletTheme.colors.surface, + shape = BrainwalletTheme.shapes.large + ) + .padding(12.dp), + ) { + ReceiveDialog( + viewModel = viewModel, + onDismissRequest = { dismiss() } + ) + } + } + } + } + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) + isCancelable = false + } + + companion object { + @JvmStatic + fun show(manager: FragmentManager) { + ReceiveDialogFragment().show(manager, "ReceiveDialogFragment") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogEvent.kt new file mode 100644 index 00000000..f1968756 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogEvent.kt @@ -0,0 +1,21 @@ +package com.brainwallet.ui.screens.home.receive + +import android.content.Context +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.QuickFiatAmountOption + +sealed class ReceiveDialogEvent { + data class OnLoad(val context: Context) : ReceiveDialogEvent() + data class OnCopyClick(val context: Context) : ReceiveDialogEvent() + data class OnFiatAmountChange(val fiatAmount: Float, val needFetch: Boolean = true) : + ReceiveDialogEvent() + + data class OnFiatCurrencyChange(val fiatCurrency: CurrencyEntity) : ReceiveDialogEvent() + data class OnFiatAmountOptionIndexChange( + val index: Int, + val quickFiatAmountOption: QuickFiatAmountOption + ) : ReceiveDialogEvent() + + object OnMoonpayButtonClick : ReceiveDialogEvent() + object OnSignedUrlClear : ReceiveDialogEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogState.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogState.kt new file mode 100644 index 00000000..745d172a --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogState.kt @@ -0,0 +1,61 @@ +package com.brainwallet.ui.screens.home.receive + +import android.graphics.Bitmap +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.MoonpayCurrencyLimit +import com.brainwallet.data.model.QuickFiatAmountOption +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + + +data class ReceiveDialogState( + val address: String = "", + val qrBitmap: Bitmap? = null, + val fiatCurrencies: List = listOf(), + val selectedFiatCurrency: CurrencyEntity = CurrencyEntity( + "USD", + "US Dollar", + -1f, + "$" + ), // default from shared prefs then the user can override using picker wheel at [ReceiveDialog] + val moonpayCurrencyLimit: MoonpayCurrencyLimit = MoonpayCurrencyLimit(), + val fiatAmount: Float = 0f, + val ltcAmount: Float = 0f, + val ratesUpdatedAt: Long = System.currentTimeMillis(), + val selectedQuickFiatAmountOptionIndex: Int = 1, //default is 10X, other [min, 10x, max, custom] + val errorFiatAmountStringId: Int? = null, + val moonpayBuySignedUrl: String? = null, +) + +fun ReceiveDialogState.getSelectedFiatCurrencyIndex(): Int = fiatCurrencies + .indexOfFirst { it.code.lowercase() == selectedFiatCurrency.code.lowercase() } + +fun ReceiveDialogState.getDefaultFiatAmount(): Float { + val (_, min, max) = moonpayCurrencyLimit.data.baseCurrency + return min * 10 //default is 10X +} + +fun ReceiveDialogState.getRatesUpdatedAtFormatted(): String { + val date = Date(ratesUpdatedAt) + val format = SimpleDateFormat("d MMM yyyy HH:mm:ss", Locale.ENGLISH) + return format.format(date).uppercase() +} + +fun ReceiveDialogState.getLtcAmountFormatted(isLoading: Boolean): String = + (if (isLoading || ltcAmount < 0f) "x.xxxŁ" else "%.3fŁ".format(ltcAmount)).also { + Timber.d("TImber: ltcamount $ltcAmount") + } + +fun ReceiveDialogState.getQuickFiatAmountOptions(): List { + val selectedFiatSymbol = selectedFiatCurrency.symbol + val (_, min, max) = moonpayCurrencyLimit.data.baseCurrency + return listOf(min, min * 10, max) + .map { QuickFiatAmountOption(symbol = selectedFiatSymbol, it) } + QuickFiatAmountOption() +} + +fun ReceiveDialogState.isQuickFiatAmountOptionCustom(): Boolean = + selectedQuickFiatAmountOptionIndex == 3 //3 will be custom + +fun ReceiveDialogState.moonpayWidgetVisible(): Boolean = moonpayBuySignedUrl != null \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt new file mode 100644 index 00000000..80ab1984 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt @@ -0,0 +1,179 @@ +package com.brainwallet.ui.screens.home.receive + +import androidx.lifecycle.viewModelScope +import com.brainwallet.R +import com.brainwallet.data.model.AppSetting +import com.brainwallet.data.model.isCustom +import com.brainwallet.data.repository.LtcRepository +import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.tools.manager.BRClipboardManager +import com.brainwallet.tools.manager.BRSharedPrefs +import com.brainwallet.tools.qrcode.QRUtils +import com.brainwallet.tools.sqlite.CurrencyDataSource +import com.brainwallet.ui.BrainwalletViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.launch + +//todo: wip +class ReceiveDialogViewModel( + private val settingRepository: SettingRepository, + private val ltcRepository: LtcRepository, +) : BrainwalletViewModel() { + + private val _state = MutableStateFlow(ReceiveDialogState()) + val state: StateFlow = _state.asStateFlow() + + val appSetting = settingRepository.settings + .distinctUntilChanged() + .onEach { setting -> + onEvent(ReceiveDialogEvent.OnFiatCurrencyChange(setting.currency)) + } + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + AppSetting() + ) + + override fun onEvent(event: ReceiveDialogEvent) { + when (event) { + is ReceiveDialogEvent.OnLoad -> _state.update { + val address = BRSharedPrefs.getReceiveAddress(event.context) + it.copy( + address = address, + qrBitmap = QRUtils.generateQR(event.context, "litecoin:${address}"), + fiatCurrencies = CurrencyDataSource.getInstance(event.context) + .getAllCurrencies(true), + ) + } + + is ReceiveDialogEvent.OnCopyClick -> BRClipboardManager.putClipboard( + event.context, + state.value.address + ) + + is ReceiveDialogEvent.OnFiatAmountChange -> viewModelScope.launch { + //do validation + val (_, min, max) = state.value.moonpayCurrencyLimit.data.baseCurrency + val errorStringId = when { + event.fiatAmount < min -> R.string.buy_litecoin_fiat_amount_validation_min + event.fiatAmount > max -> R.string.buy_litecoin_fiat_amount_validation_max + else -> null + } + _state.update { + it.copy( + errorFiatAmountStringId = errorStringId, + fiatAmount = event.fiatAmount + ) + } + + if (event.needFetch.not()) { + return@launch + } + + try { + onLoading(true) + + _state.update { + val result = ltcRepository.fetchBuyQuote( + mapOf( + "currencyCode" to "ltc", + "baseCurrencyCode" to it.selectedFiatCurrency.code, + "baseCurrencyAmount" to event.fiatAmount.toString(), + ) + ) + + it.copy( + ltcAmount = result.data.quoteCurrencyAmount, + ) + } + + } catch (e: Exception) { + handleError(e) + } finally { + onLoading(false) + } + + } + + is ReceiveDialogEvent.OnFiatCurrencyChange -> viewModelScope.launch { + try { + onLoading(true) + val currencyLimit = ltcRepository.fetchLimits(event.fiatCurrency.code) + + _state.updateAndGet { + it.copy( + selectedFiatCurrency = event.fiatCurrency, + moonpayCurrencyLimit = currencyLimit, + selectedQuickFiatAmountOptionIndex = 1, //default to 10X + fiatAmount = it.getDefaultFiatAmount(), + ) + }.let { + onEvent( + ReceiveDialogEvent.OnFiatAmountOptionIndexChange( + index = it.selectedQuickFiatAmountOptionIndex, + quickFiatAmountOption = it.getQuickFiatAmountOptions()[it.selectedQuickFiatAmountOptionIndex] + ) + ) + } + + } catch (e: Exception) { + handleError(e) + } finally { + onLoading(false) + } + } + + is ReceiveDialogEvent.OnFiatAmountOptionIndexChange -> _state.updateAndGet { + it.copy( + selectedQuickFiatAmountOptionIndex = event.index, + fiatAmount = if (event.quickFiatAmountOption.isCustom()) it.fiatAmount + else event.quickFiatAmountOption.value + ) + }.let { + if (event.quickFiatAmountOption.isCustom().not()) { + onEvent(ReceiveDialogEvent.OnFiatAmountChange(it.fiatAmount)) + } + } + + ReceiveDialogEvent.OnMoonpayButtonClick -> viewModelScope.launch { + try { + onLoading(true) + + val currentState = state.value + val signedUrl = ltcRepository.fetchMoonpaySignedUrl( + mapOf( + "baseCurrencyCode" to currentState.selectedFiatCurrency.code, + "baseCurrencyAmount" to currentState.fiatAmount.toString(), + "language" to appSetting.value.languageCode, + "walletAddress" to currentState.address, + "defaultCurrencyCode" to "ltc", + "currencyCode" to "ltc", + "themeId" to "main-v1.0.0", + "theme" to if (appSetting.value.isDarkMode) "dark" else "light" + ) + ) + + _state.update { it.copy(moonpayBuySignedUrl = signedUrl) } + + } catch (e: Exception) { + handleError(e) + } finally { + + onLoading(false) + + } + + } + + ReceiveDialogEvent.OnSignedUrlClear -> _state.update { it.copy(moonpayBuySignedUrl = null) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/topup/TopUpScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/topup/TopUpScreen.kt index ddc14ded..22f5722d 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/topup/TopUpScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/topup/TopUpScreen.kt @@ -3,10 +3,6 @@ package com.brainwallet.ui.screens.topup -import android.graphics.Bitmap -import android.view.ViewGroup -import android.webkit.WebView -import android.webkit.WebViewClient import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -15,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForward @@ -25,12 +20,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate @@ -39,27 +28,25 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.viewmodel.compose.viewModel import com.brainwallet.R import com.brainwallet.navigation.OnNavigate +import com.brainwallet.navigation.Route import com.brainwallet.navigation.UiEffect import com.brainwallet.tools.manager.AnalyticsManager import com.brainwallet.tools.util.BRConstants import com.brainwallet.ui.composable.BorderedLargeButton import com.brainwallet.ui.composable.BrainwalletScaffold import com.brainwallet.ui.composable.BrainwalletTopAppBar -import com.brainwallet.ui.composable.MediumTextButton import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItEvent import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItViewModel -import kotlinx.coroutines.delay @Composable fun TopUpScreen( onNavigate: OnNavigate, viewModel: YourSeedProveItViewModel = viewModel() ) { - val state by viewModel.state.collectAsState() + val context = LocalContext.current /// Layout values @@ -69,13 +56,6 @@ fun TopUpScreen( val spacerHeight = 90 val skipButtonWidth = 100 - var shouldShowWebView by remember { mutableStateOf(false) } - var backEnabled by remember { mutableStateOf(false) } - var shouldSkipBeVisible by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - } - BrainwalletScaffold( topBar = { BrainwalletTopAppBar( @@ -93,7 +73,6 @@ fun TopUpScreen( } ) { paddingValues -> - Spacer(modifier = Modifier.height(spacerHeight.dp)) Column( modifier = Modifier @@ -103,129 +82,54 @@ fun TopUpScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), ) { - - if (!shouldShowWebView) { + Spacer(modifier = Modifier.weight(1f)) + Row { + Icon( + Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = stringResource(R.string.down_left_arrow), + modifier = Modifier + .rotate(45f) + .graphicsLayer( + scaleX = 2f, + scaleY = 2f + ) + ) Spacer(modifier = Modifier.weight(1f)) - Row { - Icon( - Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = stringResource(R.string.down_left_arrow), - modifier = Modifier - .rotate(45f) - .graphicsLayer( - scaleX = 2f, - scaleY = 2f - ) - ) - Spacer(modifier = Modifier.weight(1f)) - } + } - Text( - text = stringResource(R.string.top_up_title), - style = MaterialTheme.typography.displaySmall.copy(textAlign = TextAlign.Left), - modifier = Modifier.fillMaxWidth() - ) + Text( + text = stringResource(R.string.top_up_title), + style = MaterialTheme.typography.displaySmall.copy(textAlign = TextAlign.Left), + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = stringResource(R.string.top_up_detail_1), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxWidth() + ) + BorderedLargeButton( + onClick = { onNavigate.invoke(UiEffect.Navigate(destinationRoute = Route.BuyLitecoin)) }, + modifier = Modifier.fillMaxWidth() + ) { Text( - text = stringResource(R.string.top_up_detail_1), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.fillMaxWidth() + text = stringResource(R.string.top_up_button_1), + style = MaterialTheme.typography.bodyLarge ) } - if (shouldShowWebView) { - AndroidView( - factory = { - WebView(it).apply { - setInitialScale(99) - setLayerType(ViewGroup.LAYER_TYPE_SOFTWARE, null) - settings.apply { - javaScriptEnabled = true - useWideViewPort = true - loadWithOverviewMode = true - builtInZoomControls = true - domStorageEnabled = true - allowContentAccess = true - } - webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { - backEnabled = view.canGoBack() - } - } - webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - shouldSkipBeVisible = true - } - } - - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT) - - } - }, - update = { - it.loadUrl(BRConstants.MOBILE_MP_LINK) - }, - modifier = Modifier - .height(600.dp) - .weight(1f) + BorderedLargeButton( + onClick = { + viewModel.onEvent(YourSeedProveItEvent.OnGameAndSync) + AnalyticsManager.logCustomEvent(BRConstants._20250303_DSTU) + }, + modifier = Modifier.fillMaxWidth() + + ) { + Text( + text = stringResource(R.string.top_up_button_2), + style = MaterialTheme.typography.bodyMedium ) - Row( - modifier = Modifier - .fillMaxWidth() - .height(buttonRowHeight.dp), - - ){ - Spacer(modifier = Modifier.weight(1f)) - if(shouldSkipBeVisible) { - MediumTextButton( - onClick = { - viewModel.onEvent(YourSeedProveItEvent.OnGameAndSync) - AnalyticsManager.logCustomEvent(BRConstants._20250303_DSTU) - }, - modifier = Modifier - .width(skipButtonWidth.dp) - .padding(vertical = horizontalVerticalSpacing.dp), - - ) { - - Text( - text = stringResource(R.string.top_up_button_3), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Right - ) - } - } - } - - } - if (!shouldShowWebView) { - BorderedLargeButton( - onClick = { - shouldShowWebView = true - }, - modifier = Modifier.fillMaxWidth() - - ) { - Text( - text = stringResource(R.string.top_up_button_1), - style = MaterialTheme.typography.bodyLarge - ) - } - BorderedLargeButton( - onClick = { - viewModel.onEvent(YourSeedProveItEvent.OnGameAndSync) - AnalyticsManager.logCustomEvent(BRConstants._20250303_DSTU) - }, - modifier = Modifier.fillMaxWidth() - - ) { - Text( - text = stringResource(R.string.top_up_button_2), - style = MaterialTheme.typography.bodyMedium - ) - } - } Spacer(modifier = Modifier.weight(0.05f)) diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt index 6f4d19c4..998ddb00 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt @@ -18,8 +18,8 @@ class YourSeedProveItViewModel : BrainwalletViewModel() { when (event) { is YourSeedProveItEvent.OnLoad -> _state.update { it.copy( - correctSeedWords = event.seedWords.mapIndexed { index, word -> - index to SeedWordItem(expected = word) + correctSeedWords = event.seedWords.mapIndexed { index, word -> + index to SeedWordItem(expected = word) }.toMap(), shuffledSeedWords = event.seedWords.shuffled() ) diff --git a/app/src/main/java/com/brainwallet/ui/theme/Theme.kt b/app/src/main/java/com/brainwallet/ui/theme/Theme.kt index 819eb43e..36afabbe 100644 --- a/app/src/main/java/com/brainwallet/ui/theme/Theme.kt +++ b/app/src/main/java/com/brainwallet/ui/theme/Theme.kt @@ -1,6 +1,7 @@ package com.brainwallet.ui.theme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -84,6 +85,9 @@ object BrainwalletTheme { val typography: Typography @Composable @ReadOnlyComposable get() = MaterialTheme.typography + val shapes: Shapes + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes + //todo: add typography, shape? for the design system } diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index 7a37d9cd..507a5125 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -514,9 +514,9 @@ public void initWallet(final Context ctx) { String firstAddress = BRWalletManager.getFirstAddress(pubkeyEncoded); BRSharedPrefs.putFirstAddress(ctx, firstAddress); FeeManager feeManager = FeeManager.getInstance(); - if (feeManager.isRegularFee()) { - feeManager.updateFeePerKb(ctx); - BRWalletManager.getInstance().setFeePerKb(feeManager.currentFees.regular); + if (feeManager.isLuxuryFee()) { + FeeManager.updateFeePerKb(ctx); + BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.luxury); } } diff --git a/app/src/main/res/drawable/ic_copy.xml b/app/src/main/res/drawable/ic_copy.xml new file mode 100644 index 00000000..6a0293ec --- /dev/null +++ b/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml new file mode 100644 index 00000000..33f6260c --- /dev/null +++ b/app/src/main/res/drawable/ic_import.xml @@ -0,0 +1,19 @@ + + + + diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml index c60707bd..742ea48f 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -216,105 +216,15 @@ android:textSize="18sp" app:buttonType="2" app:isBreadButton="true" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.0" /> - - - - - - - - - - - - - - - - - - - - + - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu_us.xml b/app/src/main/res/menu/bottom_nav_menu_us.xml index 2d26a165..187ac237 100644 --- a/app/src/main/res/menu/bottom_nav_menu_us.xml +++ b/app/src/main/res/menu/bottom_nav_menu_us.xml @@ -1,27 +1,28 @@ + + - + android:title="@string/bottom_nav_item_buy_receive_title" /> - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ecbc81d5..cf9095d7 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -435,4 +435,19 @@ مشاركة بيانات التحليلات تحديث الرقم السري عرض + رسوم الشبكة (لكل كيلو بايت): القيمة الأعلى تعني اكتمال المعاملة في وقت أقرب + قمة + واسطة + قليل + مزامنة البيانات التعريفية: + شراء لايتكوين + مدعوم من مونباي + تحميل... + المبلغ أقل من الحد الأدنى (%1$f) + يتجاوز المبلغ الحد الأقصى (%1$f) + شراء لايتكوين + يتم إيداعها في عنوان LTC الخاص بك: + الشراء باستخدام Moonpay + العنوان الجديد + شراء / تلقي diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4d0e68ac..a07b7400 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -796,4 +796,19 @@ Analyse-Daten teilen PIN aktualisieren Anzeigen + Netzwerkgebühr (pro KB): Ein höherer Wert bedeutet, dass die Transaktion früher abgeschlossen wird + Spitze + Medium + Niedrig + Metadaten synchronisieren: + Kaufen Sie LTC + Unterstützt von Moonpay + Laden... + Betrag liegt unter dem Mindestlimit (%1$f) + Betrag überschreitet Höchstgrenze (%1$f) + Litecoin kaufen + Bitte überweisen Sie es an Ihre LTC-Adresse: + Kaufen Sie mit Moonpay + Neue Adresse + Kaufen / Empfangen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9f12f07d..1a918843 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -169,9 +169,9 @@ Ignorar - SEGURIDAD DEL DISPOSITIVO COMPROMETIDA\nCualquier aplicación \"jailbreak\" puede acceder a los datos del llavero de Loaf y robar tus Litecoins. Borra esta cartera de inmediato y restáurala en un dispositivo seguro. + SEGURIDAD DEL DISPOSITIVO COMPROMETIDA\nCualquier aplicación "jailbreak" puede acceder a los datos del llavero de Loaf y robar tus Litecoins. Borra esta cartera de inmediato y restáurala en un dispositivo seguro. - SEGURIDAD DEL DISPOSITIVO COMPROMETIDA\nCualquier aplicación \"jailbreak\" puede acceder a los datos del llavero de Loaf y robar tus Litecoins. Usa Loaf únicamente en un dispositivo sin jailbreak. + SEGURIDAD DEL DISPOSITIVO COMPROMETIDA\nCualquier aplicación "jailbreak" puede acceder a los datos del llavero de Loaf y robar tus Litecoins. Usa Loaf únicamente en un dispositivo sin jailbreak. AVISO @@ -795,4 +795,19 @@ Compartir datos analíticos Actualizar PIN Mostrar + Tarifa de red (por kb): un valor más alto significa que la transacción se completa antes + Arriba + Medio + Bajo + Sincronizar metadatos: + Comprar LTC + Desarrollado por Moonpay + Cargando... + El monto está por debajo del límite mínimo (%1$f) + El importe supera el límite máximo (%1$f) + Comprar Litecoin + Se depositará en su dirección LTC: + Compra con Moonpay + Nueva dirección + Comprar / Recibir diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 93928b67..b0e92d08 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -796,4 +796,19 @@ Partager les données analytiques Mettre à jour le code PIN Afficher + Frais de réseau (par Ko): une valeur plus élevée signifie que la transaction se termine plus tôt + Haut + Moyen + Faible + Synchroniser les métadonnées : + Acheter des SLD + Propulsé par Moonpay + Chargement... + Le montant est inférieur à la limite minimale (%1$f) + Le montant dépasse la limite maximale (%1$f) + Acheter du Litecoin + Soyez déposé à votre adresse LTC : + Acheter avec Moonpay + Nouvelle adresse + Acheter / Recevoir diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 8a0158a5..3c263e70 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -434,4 +434,19 @@ विश्लेषण डेटा साझा करें पिन अपडेट करें दिखाएं + नेटवर्क शुल्क (प्रति केबी): उच्च मूल्य का मतलब है कि लेनदेन जल्दी पूरा हो जाएगा + शीर्ष + मध्यम + कम + मेटाडेटा सिंक करें: + एलटीसी खरीदें + मूनपे द्वारा संचालित + लोड हो रहा है... + राशि न्यूनतम सीमा से कम है (%1$f) + राशि अधिकतम सीमा से अधिक है (%1$f) + लाइटकॉइन खरीदें + अपने एलटीसी पते पर जमा करें: + मूनपे से खरीदें + नया पता + खरीदें/प्राप्त करें diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 7899cb2d..371a07fa 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -798,4 +798,19 @@ Bagikan data analitik Perbarui PIN Tampilkan + Biaya Jaringan (per kb): Nilai yang lebih tinggi berarti transaksi selesai lebih cepat + Atas + Sedang + Rendah + Sinkronkan metadata: + Beli LTC + Didukung oleh Moonpay + Memuat... + Jumlahnya di bawah batas minimum (%1$f) + Jumlah melebihi batas maksimum (%1$f) + Beli Litecoin + Disetorkan ke Alamat LTC Anda: + Beli dengan Moonpay + Alamat Baru + Beli / Terima diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6641145b..0246634a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -796,4 +796,19 @@ Condividere i dati analitici Aggiorna PIN Mostra + Tariffa di rete (per kb): un valore più elevato significa che la transazione viene completata prima + Superiore + Medio + Basso + Sincronizza i metadati: + Acquista LTC + Alimentato da Moonpay + Caricamento... + L\'importo è inferiore al limite minimo (%1$f) + L\'importo supera il limite massimo (%1$f) + Acquista Litecoin + Essere depositato al tuo indirizzo LTC: + Acquista con Moonpay + Nuovo indirizzo + Acquista/Ricevi diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 523cd7f2..9523482b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -796,4 +796,19 @@ 分析データを共有 PIN を更新 表示 + ネットワーク料金 (kb あたり): 値が高いほど、トランザクションが早く完了することを意味します + トップ + 中くらい + 低い + メタデータを同期する: + LTCを購入する + ムーンペイ提供 + 読み込み中... + 金額が下限値 (%1$f) を下回っています + 量が上限を超えています (%1$f) + ライトコインを購入する + LTCアドレスに入金してください: + Moonpay で購入する + 新しい住所 + 買う/受け取る diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3deda254..0f409eb0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -796,4 +796,19 @@ 분석 데이터 공유 PIN 업데이트 표시 + 네트워크 수수료(kb당): 값이 높을수록 거래가 더 빨리 완료됨을 의미합니다. + 맨 위 + 중간 + 낮은 + 메타데이터 동기화: + 라이트코인 구매 + 문페이 제공 + 로드 중... + 금액이 최소 한도(%1$f)보다 낮습니다. + 금액이 최대 한도(%1$f)를 초과했습니다. + 라이트코인 구매 + LTC 주소로 입금하세요: + 문페이로 구매하세요 + 새 주소 + 구매 / 받기 diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 48a2dc0b..535904d3 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -795,4 +795,19 @@ Compartilhar dados analíticos Atualizar PIN Mostrar + Taxa de rede (por kb): valor mais alto significa que a transação é concluída mais cedo + Principal + Médio + Baixo + Sincronizar metadados: + Comprar LTC + Desenvolvido por Moonpay + Carregando... + O valor está abaixo do limite mínimo (%1$f) + O valor excede o limite máximo (%1$f) + Comprar Litecoin + Seja depositado em seu endereço LTC: + Compre com Moonpay + Novo endereço + Comprar / Receber diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 22df2082..5434b353 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -796,4 +796,19 @@ Делиться аналитическими данными Обновить PIN Показать + Сетевая плата (за КБ): более высокое значение означает, что транзакция завершится раньше. + Вершина + Середина + Низкий + Синхронизировать метаданные: + Купить LTC + При поддержке Moonpay + Загрузка... + Сумма ниже минимального лимита (%1$f) + Сумма превышает максимальный лимит (%1$f) + Купить Лайткоин + Депонируйте на свой адрес LTC: + Купить с помощью Moonpay + Новый адрес + Купить/получить diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 12d62935..eef96cac 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -434,4 +434,19 @@ Dela analysdata Uppdatera PIN Visa + Nätverksavgift (per kb): Högre värde innebär att transaktionen slutförs tidigare + Bästa + Medium + Låg + Synkronisera metadata: + Köp LTC + Drivs av Moonpay + Belastning... + Beloppet är under minimigränsen (%1$f) + Beloppet överskrider maxgränsen (%1$f) + Köp Litecoin + Sätt in till din LTC-adress: + Köp med Moonpay + Ny adress + Köp/ta emot diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d1e3c107..b67506d4 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -819,4 +819,20 @@ Analitik verileri paylaş PIN güncelle Göster + Ağ Ücreti (kb başına): Daha yüksek değer, işlemin daha erken tamamlanacağı anlamına gelir + Tepe + Orta + Düşük + + Meta verileri senkronize et: + LTC satın al + Moonpay tarafından desteklenmektedir + Yükleniyor... + Tutar minimum sınırın altında (%1$f) + Tutar maksimum sınırı aşıyor (%1$f) + Litecoin satın al + LTC Adresinize yatırın: + Moonpay ile satın alın + Yeni Adres + Satın Al / Al diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 3f7aca4a..f8899d68 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -802,4 +802,20 @@ Ділитися аналітичними даними Оновити PIN Показати + Плата за мережу (за кб): вищий показник означає, що транзакція завершується швидше + Топ + Середній + Низький + + Синхронізувати метадані: + Купити LTC + На основі Moonpay + Завантаження... + Сума нижче мінімального ліміту (%1$f) + Сума перевищує максимальний ліміт (%1$f) + Купуйте Litecoin + Зробіть депозит на свою адресу LTC: + Купуйте за допомогою Moonpay + Нова адреса + Купити / Отримати diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b25903f6..763288ac 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -796,4 +796,19 @@ 分享分析数据 更新 PIN 显示 + 网络费用(每 kb):值越高意味着交易完成得越快 + 顶部 + 中等的 + 低的 + 同步元数据: + 购买莱特币 + 由 月付 提供支持 + 加载中... + 金额低于最低限额 (%1$f) + 金额超过最大限制 (%1$f) + 购买莱特币 + 请存入您的 LTC 地址: + 使用 Moonpay 购买 + 新地址 + 购买/接收 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 13e6b9cc..fe1abc56 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -796,4 +796,19 @@ 分享分析數據 更新 PIN 顯示 + 網路費用(每 kb):數值越高代表交易完成越快 + 頂部 + 中等的 + 低的 + 同步元資料: + 購買萊特幣 + 由 月付 提供支持 + 載入中... + 金額低於最低限額 (%1$f) + 金額超過最大限制 (%1$f) + 購買萊特幣 + 請存入您的 LTC 地址: + 使用 Moonpay 購買 + 新地址 + 購買/接收 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b2f4e52..f7c7deb0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,7 +74,7 @@ receive - send + Send Settings @@ -839,5 +839,20 @@ Share analytics data Update PIN Show + Buy LTC + Powered by Moonpay + Loading... + Network Fee (per kb): Higher value mean the transaction completes sooner + Top + Medium + Low + Amount is below minimum limit (%f) + Amount exceeds maximum limit (%f) + Buy Litecoin + Do be deposited to your LTC Address: + Buy with Moonpay + New Address + Buy / Receive + Custom