From eda0f532bd4094d1010e1ce64eadd25e91110d55 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Wed, 7 May 2025 10:42:41 +0100 Subject: [PATCH 1/7] version bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 80cc7aec..28d3f833 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 = 202505021 - versionName = "v4.4.7" + versionCode = 202505071 + versionName = "v4.5.0" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") From 1c26de9406b06118c016251e30c35b2d1ad1b6c8 Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Mon, 12 May 2025 18:59:29 +0700 Subject: [PATCH 2/7] feat: new UI for receive and topup flow (moonpay integration) (#72) * feat: [WIP][UI] new UI for receive and topup flow * feat: [WIP][UI] implement copy address and some refactor * feat: replace webview using CustomTabsIntent from androidx.browser * feat: replace webview using CustomTabsIntent from androidx.browser * feat: add PickerWheel * chore: rename param at QRUtils.generateQR * chore: revert default startDestination at BrainwalletActivity * feat: wip moonpay integration * feat: wip moonpay integration * chore: new UI for ReceiveDialog * chore: parse error from response API at BrainwalletViewModel * chore: add /moonpay/buy-quote and remove dev base url * chore: make LegacyNavigation.showMoonPayWidget receive params for the widget url * chore: add GetMoonpayBuyQuoteResponse * chore: wip moonpay integration to call buy-quote * fix: fix wheel picker for IDR at ReceiveDialog * fix: fix invalid signature * feat: moonpay integration at onboarding flow at BuyLitecoinScreen also fix some stuffs * chore: replace base url for moonpay buy integration when debug * chore: introduce LocalCacheSource.kt and cache fetch limits * chore: adjustment receive dialog UI - debounce fiat currency to 2s - increment in 10 - remove decimal places - default value to 10% of range * chore: reorder bottom navigation item and add new strings * chore: add new translations * chore: refactor UI for buy and receive dialog * chore: cleanup ReceiveDialogViewModel * chore: using MoonpayBuyWidget * chore: adjustment moonpaywidget * chore: for now moonpay widget using CustomTabsIntent --- .../data/model/MoonpayCurrencyLimit.kt | 24 + .../data/model/QuickFiatAmountOption.kt | 11 + .../data/repository/LtcRepository.kt | 65 +- .../data/source/LocalCacheSource.kt | 45 ++ .../data/source/RemoteApiSource.kt | 22 +- .../response/GetMoonpayBuyQuoteResponse.kt | 17 + .../response/GetMoonpaySignUrlResponse.kt | 8 + .../main/java/com/brainwallet/di/Module.kt | 36 +- .../navigation/LegacyNavigation.kt | 61 ++ .../com/brainwallet/navigation/MainNav.kt | 5 + .../java/com/brainwallet/navigation/Route.kt | 3 + .../presenter/activities/BreadActivity.java | 23 +- .../com/brainwallet/tools/qrcode/QRUtils.java | 15 + .../tools/sqlite/CurrencyDataSource.java | 51 +- .../brainwallet/ui/BrainwalletViewModel.kt | 19 + .../brainwallet/ui/composable/Foundation.kt | 18 +- .../brainwallet/ui/composable/LargeButton.kt | 5 +- .../ui/composable/LoadingDialog.kt | 55 ++ .../ui/composable/MoonpayBuyButton.kt | 66 ++ .../ui/composable/MoonpayBuyWidget.kt | 56 ++ .../brainwallet/ui/composable/WheelPicker.kt | 634 ++++++++++++++++++ .../screens/buylitecoin/BuyLitecoinEvent.kt | 11 + .../screens/buylitecoin/BuyLitecoinScreen.kt | 167 +++++ .../screens/buylitecoin/BuyLitecoinState.kt | 19 + .../buylitecoin/BuyLitecoinViewModel.kt | 128 ++++ .../ui/screens/home/buy/BuyScreen.kt | 10 - .../ui/screens/home/receive/ReceiveDialog.kt | 452 +++++++++++++ .../home/receive/ReceiveDialogEvent.kt | 21 + .../home/receive/ReceiveDialogState.kt | 61 ++ .../home/receive/ReceiveDialogViewModel.kt | 179 +++++ .../ui/screens/topup/TopUpScreen.kt | 182 ++--- .../YourSeedProveItViewModel.kt | 4 +- .../java/com/brainwallet/ui/theme/Theme.kt | 4 + app/src/main/res/drawable/ic_copy.xml | 9 + app/src/main/res/drawable/ic_import.xml | 19 + app/src/main/res/menu/bottom_nav_menu.xml | 10 +- app/src/main/res/menu/bottom_nav_menu_us.xml | 23 +- app/src/main/res/values-ar/strings.xml | 11 + app/src/main/res/values-de/strings.xml | 11 + app/src/main/res/values-es/strings.xml | 11 + app/src/main/res/values-fr/strings.xml | 11 + app/src/main/res/values-hi/strings.xml | 11 + app/src/main/res/values-in/strings.xml | 11 + app/src/main/res/values-it/strings.xml | 11 + app/src/main/res/values-ja/strings.xml | 11 + app/src/main/res/values-ko/strings.xml | 11 + app/src/main/res/values-pt/strings.xml | 11 + app/src/main/res/values-ru/strings.xml | 11 + app/src/main/res/values-sv/strings.xml | 11 + app/src/main/res/values-tr/strings.xml | 12 + app/src/main/res/values-uk/strings.xml | 12 + app/src/main/res/values-zh-rCN/strings.xml | 11 + app/src/main/res/values-zh-rTW/strings.xml | 11 + app/src/main/res/values/strings.xml | 14 +- 54 files changed, 2479 insertions(+), 251 deletions(-) create mode 100644 app/src/main/java/com/brainwallet/data/model/MoonpayCurrencyLimit.kt create mode 100644 app/src/main/java/com/brainwallet/data/model/QuickFiatAmountOption.kt create mode 100644 app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt create mode 100644 app/src/main/java/com/brainwallet/data/source/response/GetMoonpayBuyQuoteResponse.kt create mode 100644 app/src/main/java/com/brainwallet/data/source/response/GetMoonpaySignUrlResponse.kt create mode 100644 app/src/main/java/com/brainwallet/ui/composable/LoadingDialog.kt create mode 100644 app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyButton.kt create mode 100644 app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyWidget.kt create mode 100644 app/src/main/java/com/brainwallet/ui/composable/WheelPicker.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinEvent.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinState.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt delete mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/buy/BuyScreen.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogEvent.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogState.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt create mode 100644 app/src/main/res/drawable/ic_copy.xml create mode 100644 app/src/main/res/drawable/ic_import.xml 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 38b325fd..365bbc0d 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -2,21 +2,29 @@ package com.brainwallet.data.repository import android.content.Context import android.content.SharedPreferences -import androidx.core.content.edit +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.di.json +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 -import kotlinx.serialization.encodeToString interface LtcRepository { suspend fun fetchRates(): List 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, @@ -47,27 +55,42 @@ interface LtcRepository { } override suspend fun fetchFeePerKb(): Fee { - val lastUpdateTime = sharedPreferences.getLong(PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, 0) - val currentTime = System.currentTimeMillis() - val cachedFee = sharedPreferences.getString(PREF_KEY_NETWORK_FEE_PER_KB, null) - ?.let { json.decodeFromString(it) } + 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 + ) + } - return runCatching { - // Check if cache exists and is less than 6 hours old - if (cachedFee != null && (currentTime - lastUpdateTime) < 6 * 60 * 60 * 1000) { - return cachedFee + 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) } + ) + } - val fee = remoteApiSource.getFeePerKb() - sharedPreferences.edit { - putString(PREF_KEY_NETWORK_FEE_PER_KB, json.encodeToString(fee)) - putLong(PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, currentTime) - } + override suspend fun fetchBuyQuote(params: Map): GetMoonpayBuyQuoteResponse = + remoteApiSource.getBuyQuote(params) - return fee - }.getOrElse { - cachedFee ?: 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() } } @@ -75,5 +98,7 @@ interface LtcRepository { 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 8dcfb909..b95bbeec 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 @@ -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,28 +100,6 @@ private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() .addHeader("Accept-Language", "en") chain.proceed(requestBuilder.build()) } - .addInterceptor { chain -> - val request = chain.request() - runCatching { - chain.proceed(request).use { response -> - if (response.isSuccessful.not()) { - throw HttpException( - retrofit2.Response.error( - response.code, - response.body ?: response.peekBody(Long.MAX_VALUE) - ) - ) - } - response - } - }.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 { @@ -140,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 +} \ No newline at end of file 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/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/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/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/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/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index 0d1f4a90..dc0df45e 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -19,10 +19,10 @@ android:icon="@drawable/ic_nav_receive" android:title="@string/Button.receive" /> - + + + + + \ 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 8a3ee8f0..cf9095d7 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -439,4 +439,15 @@ قمة واسطة قليل + مزامنة البيانات التعريفية: + شراء لايتكوين + مدعوم من مونباي + تحميل... + المبلغ أقل من الحد الأدنى (%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 6d272928..a07b7400 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -800,4 +800,15 @@ 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 24bf3fc0..1a918843 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -799,4 +799,15 @@ 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 1571dd93..b0e92d08 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -800,4 +800,15 @@ 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 a03d849c..3c263e70 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -438,4 +438,15 @@ शीर्ष मध्यम कम + मेटाडेटा सिंक करें: + एलटीसी खरीदें + मूनपे द्वारा संचालित + लोड हो रहा है... + राशि न्यूनतम सीमा से कम है (%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 d3bb43e0..371a07fa 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -802,4 +802,15 @@ 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 592ebba1..0246634a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -800,4 +800,15 @@ 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 c6ffb20c..9523482b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -800,4 +800,15 @@ トップ 中くらい 低い + メタデータを同期する: + 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 8ec4b384..0f409eb0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -800,4 +800,15 @@ 맨 위 중간 낮은 + 메타데이터 동기화: + 라이트코인 구매 + 문페이 제공 + 로드 중... + 금액이 최소 한도(%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 27b64935..535904d3 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -799,4 +799,15 @@ 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 4df74670..5434b353 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -800,4 +800,15 @@ Вершина Середина Низкий + Синхронизировать метаданные: + Купить 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 78b503fd..eef96cac 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -438,4 +438,15 @@ 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 a526c60c..b67506d4 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -823,4 +823,16 @@ 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 b5d20546..f8899d68 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -806,4 +806,16 @@ Топ Середній Низький + + Синхронізувати метадані: + Купити 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 53cd941f..763288ac 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -800,4 +800,15 @@ 顶部 中等的 低的 + 同步元数据: + 购买莱特币 + 由 月付 提供支持 + 加载中... + 金额低于最低限额 (%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 69514c55..fe1abc56 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -800,4 +800,15 @@ 頂部 中等的 低的 + 同步元資料: + 購買萊特幣 + 由 月付 提供支持 + 載入中... + 金額低於最低限額 (%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 d107536c..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,8 +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 From 8a2b043972dc133d789158e052858b95521fbe64 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Thu, 22 May 2025 14:55:09 +0700 Subject: [PATCH 3/7] chore: set allowSpend to false when recommend rescan click --- .../main/java/com/brainwallet/tools/manager/PromptManager.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/brainwallet/tools/manager/PromptManager.java b/app/src/main/java/com/brainwallet/tools/manager/PromptManager.java index e6c9a029..a716d7f7 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/PromptManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/PromptManager.java @@ -9,6 +9,7 @@ import android.content.Intent; import android.view.View; +import com.brainwallet.presenter.activities.settings.SyncBlockchainActivity; import com.brainwallet.tools.security.BRKeyStore; import com.brainwallet.tools.threads.BRExecutor; import com.brainwallet.R; @@ -88,6 +89,7 @@ public void run() { BRSharedPrefs.putStartHeight(app, 0); BRPeerManager.getInstance().rescan(); BRSharedPrefs.putScanRecommended(app, false); + BRSharedPrefs.putAllowSpend(app, false); } }); } From 630c5ccf8378357af2ad265dcdb08b09cc40cba8 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Wed, 21 May 2025 12:18:36 +0700 Subject: [PATCH 4/7] fix: add delete transaction data from local database --- app/src/main/java/com/brainwallet/wallet/BRWalletManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index 507a5125..46d8f782 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -440,6 +440,7 @@ public static void onTxDeleted(String hash, int notifyUser, final int recommendR final Context ctx = BrainwalletApp.getBreadContext(); if (ctx != null) { BRSharedPrefs.putScanRecommended(ctx, true); + TransactionDataSource.getInstance(ctx).deleteTxByHash(hash); } else { Timber.i("timber: onTxDeleted: Failed! ctx is null"); } From cbb47b81a6fc3e6dc80374b6e126c25697d7678a Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Sun, 18 May 2025 09:39:39 +0100 Subject: [PATCH 5/7] Removed chatty event --- app/src/main/java/com/brainwallet/tools/util/BRConstants.java | 2 -- .../test/java/com/brainwallet/tools/database/DatabaseTests.kt | 1 - app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt | 2 -- 3 files changed, 5 deletions(-) diff --git a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java index 7c989533..868e2f96 100644 --- a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java +++ b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java @@ -129,7 +129,6 @@ private BRConstants() { public static final String _20201118_DTGS = "did_tap_get_support"; public static final String _20200217_DUWP = "did_unlock_with_pin"; public static final String _20200217_DUWB = "did_unlock_with_biometrics"; - public static final String _20200301_DUDFPK = "did_use_default_fee_per_kb"; public static final String _20201121_SIL = "started_IFPS_lookup"; public static final String _20201121_DRIA = "did_resolve_IPFS_address"; public static final String _20201121_FRIA = "failed_resolve_IPFS_address"; @@ -170,7 +169,6 @@ private BRConstants() { _20201118_DTGS, _20200217_DUWP, _20200217_DUWB, - _20200301_DUDFPK, _20201121_SIL, _20201121_DRIA, _20201121_FRIA, diff --git a/app/src/test/java/com/brainwallet/tools/database/DatabaseTests.kt b/app/src/test/java/com/brainwallet/tools/database/DatabaseTests.kt index 9a3a7d4f..3b8821d0 100644 --- a/app/src/test/java/com/brainwallet/tools/database/DatabaseTests.kt +++ b/app/src/test/java/com/brainwallet/tools/database/DatabaseTests.kt @@ -68,7 +68,6 @@ class DatabaseTests { // Assert.assertSame(BRConstants._20201118_DTGS,"did_tap_get_support"); // Assert.assertSame(BRConstants._20200217_DUWP,"did_unlock_with_pin"); // Assert.assertSame(BRConstants._20200217_DUWB,"did_unlock_with_biometrics"); -// Assert.assertSame(BRConstants._20200301_DUDFPK,"did_use_default_fee_per_kb"); // Assert.assertSame(BRConstants._20201121_SIL,"started_IFPS_lookup"); // Assert.assertSame(BRConstants._20201121_DRIA,"did_resolve_IPFS_address"); // Assert.assertSame(BRConstants._20201121_FRIA,"failed_resolve_IPFS_address"); diff --git a/app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt b/app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt index 82eabe93..5779b8df 100644 --- a/app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt +++ b/app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt @@ -43,7 +43,6 @@ class BRConstantsTest { assertSame(BRConstants._20201118_DTGS,"did_tap_get_support"); assertSame(BRConstants._20200217_DUWP,"did_unlock_with_pin"); assertSame(BRConstants._20200217_DUWB,"did_unlock_with_biometrics"); - assertSame(BRConstants._20200301_DUDFPK,"did_use_default_fee_per_kb"); assertSame(BRConstants._20201121_SIL,"started_IFPS_lookup"); assertSame(BRConstants._20201121_DRIA,"did_resolve_IPFS_address"); assertSame(BRConstants._20201121_FRIA,"failed_resolve_IPFS_address"); @@ -128,7 +127,6 @@ class BRConstantsTest { // Assert.assertSame(BRConstants._20201118_DTGS,"did_tap_get_support"); // Assert.assertSame(BRConstants._20200217_DUWP,"did_unlock_with_pin"); // Assert.assertSame(BRConstants._20200217_DUWB,"did_unlock_with_biometrics"); -// Assert.assertSame(BRConstants._20200301_DUDFPK,"did_use_default_fee_per_kb"); // Assert.assertSame(BRConstants._20201121_SIL,"started_IFPS_lookup"); // Assert.assertSame(BRConstants._20201121_DRIA,"did_resolve_IPFS_address"); // Assert.assertSame(BRConstants._20201121_FRIA,"failed_resolve_IPFS_address"); From 8f7c86a75948be7f7787473e05accad45875cf26 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Sat, 17 May 2025 03:33:51 +0700 Subject: [PATCH 6/7] chore: add analytics at BRWalletManager.publishCallback --- .../java/com/brainwallet/tools/util/BRConstants.java | 4 +++- .../java/com/brainwallet/wallet/BRWalletManager.java | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java index 868e2f96..bd8f810b 100644 --- a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java +++ b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java @@ -136,6 +136,7 @@ private BRConstants() { public static final String _20230407_DCS = "did_complete_sync"; public static final String _20250303_DSTU = "did_skip_top_up"; + public static final String _20250517_WCINFO = "wallet_callback_info"; ///Dev: These events not yet used public static final String _20200207_DTHB = "did_tap_header_balance"; @@ -185,7 +186,8 @@ private BRConstants() { _20241006_UCR, _HOME_OPEN, _20250222_PAC, - _20250303_DSTU + _20250303_DSTU, + _20250517_WCINFO }) public @interface Event { } diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index 46d8f782..abc5b2f7 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -9,6 +9,7 @@ import android.media.MediaPlayer; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.os.Bundle; import android.os.Handler; import android.os.NetworkOnMainThreadException; import android.os.SystemClock; @@ -334,6 +335,14 @@ public void onClick(DialogInterface dialog, int which) { */ public static void publishCallback(final String message, final int error, byte[] txHash) { Timber.d("timber: publishCallback: " + message + ", err:" + error + ", txHash: " + Arrays.toString(txHash)); + + Bundle params = new Bundle(); + params.putString("function", "BRWalletManager.publishCallback"); + params.putString("message", message); + params.putInt("error", error); + params.putString("txHash", Arrays.toString(txHash)); + AnalyticsManager.logCustomEventWithParams(BRConstants._20250517_WCINFO, params); + final Context app = BrainwalletApp.getBreadContext(); BRExecutor.getInstance().forMainThreadTasks().execute(new Runnable() { @Override From a7bb8b62c0e17525abb5af47d53046f2225ef385 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Sun, 25 May 2025 15:44:47 +0700 Subject: [PATCH 7/7] chore: make sure calculation and static fee same as iOS, add setting for selected fee type --- .../java/com/brainwallet/data/model/Fee.kt | 27 ++++++++---- .../data/repository/LtcRepository.kt | 18 ++++---- .../data/repository/SettingRepository.kt | 17 ++++++++ .../presenter/fragments/FragmentSend.kt | 32 ++++++++------- .../brainwallet/tools/manager/FeeManager.java | 27 +++++++++--- .../brainwallet/tools/util/BRExchange.java | 12 ++++-- .../com/brainwallet/tools/util/Utils.java | 41 ++++++++----------- .../ui/screens/home/SettingsEvent.kt | 2 + .../ui/screens/home/SettingsState.kt | 2 + .../ui/screens/home/SettingsViewModel.kt | 28 ++++++++++++- .../home/composable/HomeSettingDrawerSheet.kt | 1 + .../settingsrows/LitecoinBlockchainDetail.kt | 18 ++------ .../brainwallet/wallet/BRWalletManager.java | 7 +--- 13 files changed, 145 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/brainwallet/data/model/Fee.kt b/app/src/main/java/com/brainwallet/data/model/Fee.kt index 0ea99667..b1d086ae 100644 --- a/app/src/main/java/com/brainwallet/data/model/Fee.kt +++ b/app/src/main/java/com/brainwallet/data/model/Fee.kt @@ -8,8 +8,6 @@ import com.brainwallet.tools.manager.FeeManager.LUXURY import com.brainwallet.tools.manager.FeeManager.REGULAR import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlin.math.ceil -import kotlin.math.round @Serializable data class Fee( @@ -22,23 +20,31 @@ data class Fee( @JvmField @SerialName("fee_per_kb_economy") var economy: Long, + var timestamp: Long ) { companion object { - //from legacy - // this is the default that matches the mobile-api if the server is unavailable - private const val defaultEconomyFeePerKB: Long = - 2500L // From legacy minimum. default min is 1000 as Litecoin Core version v0.17.1 + /** + * Default value for economy fee rate per kilobyte. + * Used as a fallback when fee rate cannot be determined dynamically. + * + * Previous value: 2500L (2.5 satoshis per byte). From legacy minimum. default min is 1000 as Litecoin Core version v0.17.1 + * Updated economy to 8000L (8 satoshis per byte) on 2023-11-16 (same as iOS) + */ + private const val defaultEconomyFeePerKB: Long = 8000L private const val defaultRegularFeePerKB: Long = 25000L private const val defaultLuxuryFeePerKB: Long = 66746L private const val defaultTimestamp: Long = 1583015199122L -// {"fee_per_kb":5289,"fee_per_kb_economy":2645,"fee_per_kb_luxury":10578} - + /** + * currently we are using this static [Default] for our fee + * maybe we need to update core if we need dynamic fee? + */ @JvmStatic val Default = Fee( defaultLuxuryFeePerKB, defaultRegularFeePerKB, defaultEconomyFeePerKB, + defaultTimestamp ) } } @@ -80,4 +86,9 @@ fun FeeOption.getFiatFormatted(currencyEntity: CurrencyEntity): String { val fiatValue = getFiat(currencyEntity) val formatted = String.format("%.3f", fiatValue) return "${currencyEntity.symbol}$formatted" +} + +fun List.getSelectedIndex(selectedFeeType: String): Int { + return indexOfFirst { it.type == selectedFeeType }.takeIf { it >= 0 } + ?: 2 //2 -> index of top, since we have [low,medium,top] } \ 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 365bbc0d..962602f7 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -54,17 +54,13 @@ interface LtcRepository { } - override suspend fun fetchFeePerKb(): Fee { - 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 - ) - } + /** + * for now we just using [Fee.Default] + * will move to [RemoteApiSource.getFeePerKb] after fix the calculation when we do send + * + * maybe need updaete core if we need to use dynamic fee? + */ + override suspend fun fetchFeePerKb(): Fee = Fee.Default //using static fee override suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit { return sharedPreferences.fetchWithCache( diff --git a/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt b/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt index 2e4b3388..3f217918 100644 --- a/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt @@ -5,6 +5,7 @@ import androidx.core.content.edit import com.brainwallet.data.model.AppSetting import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Language +import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +37,10 @@ interface SettingRepository { fun toggleDarkMode(isDarkMode: Boolean) + fun putSelectedFeeType(feeType: String) + + fun getSelectedFeeType(): String + class Impl( private val sharedPreferences: SharedPreferences, private val currencyDataSource: CurrencyDataSource @@ -77,6 +82,17 @@ interface SettingRepository { _state.update { it.copy(isDarkMode = isDarkMode) } } + override fun putSelectedFeeType(feeType: String) { + sharedPreferences.edit { + putString(KEY_SELECTED_FEE_TYPE, feeType) + } + } + + override fun getSelectedFeeType(): String = + sharedPreferences.getString(KEY_SELECTED_FEE_TYPE, FeeManager.REGULAR) + ?: FeeManager.REGULAR + + private fun load(): AppSetting { return AppSetting( isDarkMode = sharedPreferences.getBoolean(KEY_IS_DARK_MODE, true), @@ -100,5 +116,6 @@ interface SettingRepository { const val KEY_IS_DARK_MODE = "is_dark_mode" const val KEY_LANGUAGE_CODE = "language_code" const val KEY_FIAT_CURRENCY_CODE = "fiat_currency_code" + const val KEY_SELECTED_FEE_TYPE = "selected_fee_type" } } \ No newline at end of file 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 9b47690e..83d08dcb 100644 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt +++ b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt @@ -135,6 +135,9 @@ class FragmentSend : Fragment() { updateText() + //update fee + BRWalletManager.getInstance().setFeePerKb(FeeManager.getInstance().currentFeeValue) + return rootView } @@ -496,7 +499,6 @@ class FragmentSend : Fragment() { override fun onStop() { super.onStop() - FeeManager.getInstance().resetFeeType() } override fun onResume() { @@ -611,9 +613,22 @@ class FragmentSend : Fragment() { } Timber.d("timber: updateText: currentAmountInLitoshis %d", currentAmountInLitoshis) + // 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 + ) + + val totalAmountToCalculateFees = currentAmountInLitoshis + serviceFee + // Network Fee depending on ISOSymbol - var networkFee = if (currentAmountInLitoshis > 0) { - BRWalletManager.getInstance().feeForTransactionAmount(currentAmountInLitoshis) + var networkFee = if (totalAmountToCalculateFees > 0) { + BRWalletManager.getInstance().feeForTransactionAmount(totalAmountToCalculateFees) } else { 0 } //Amount is zero so network fee is also zero @@ -626,17 +641,6 @@ class FragmentSend : Fragment() { 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 - ) - // Total Fees depending on ISOSymbol val totalFees = networkFee + serviceFee val totalFeeForISOSymbol = 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 f9457baa..bf927976 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java @@ -4,9 +4,10 @@ import androidx.annotation.StringDef; +import com.brainwallet.data.model.Fee; import com.brainwallet.data.repository.LtcRepository; +import com.brainwallet.data.repository.SettingRepository; import com.brainwallet.tools.threads.BRExecutor; -import com.brainwallet.data.model.Fee; import org.koin.java.KoinJavaComponent; @@ -14,7 +15,6 @@ import java.lang.annotation.RetentionPolicy; //we are still using this, maybe in the future will deprecate? -@Deprecated public final class FeeManager { @@ -56,8 +56,24 @@ public boolean isLuxuryFee() { public static final String REGULAR = "regular";//medium public static final String ECONOMY = "economy";//low - public void setFees(long luxuryFee, long regularFee, long economyFee) { - currentFeeOptions = new Fee(luxuryFee, regularFee, economyFee); + public void setFees(Fee fee) { + currentFeeOptions = fee; + } + + public long getCurrentFeeValue() { + SettingRepository settingRepository = KoinJavaComponent.get(SettingRepository.class); + String feeType = settingRepository.getSelectedFeeType(); + + switch (feeType) { + case LUXURY: + return currentFeeOptions.luxury; + case REGULAR: + return currentFeeOptions.regular; + case ECONOMY: + return currentFeeOptions.economy; + default: + return currentFeeOptions.regular; // Default to regular fee + } } public static void updateFeePerKb(Context app) { @@ -66,8 +82,7 @@ public static void updateFeePerKb(Context app) { (coroutineScope, continuation) -> ltcRepository.fetchFeePerKb(continuation) ).whenComplete((fee, throwable) -> { - //legacy logic - FeeManager.getInstance().setFees(fee.luxury, fee.regular, fee.economy); + FeeManager.getInstance().setFees(fee); BRSharedPrefs.putFeeTime(app, System.currentTimeMillis()); //store the time of the last successful fee fetch }); } diff --git a/app/src/main/java/com/brainwallet/tools/util/BRExchange.java b/app/src/main/java/com/brainwallet/tools/util/BRExchange.java index 52b6df6b..c199b115 100644 --- a/app/src/main/java/com/brainwallet/tools/util/BRExchange.java +++ b/app/src/main/java/com/brainwallet/tools/util/BRExchange.java @@ -102,11 +102,17 @@ public static BigDecimal getLitoshisFromAmount(Context app, String iso, BigDecim if (iso.equalsIgnoreCase("LTC")) { result = BRExchange.getLitoshisForLitecoin(app, amount); } else { - //multiply by 100 because core function localAmount accepts the smallest amount e.g. cents CurrencyEntity ent = CurrencyDataSource.getInstance(app).getCurrencyByIso(iso); if (ent == null) return new BigDecimal(0); - BigDecimal rate = new BigDecimal(ent.rate).multiply(new BigDecimal(100)); - result = new BigDecimal(BRWalletManager.getInstance().bitcoinAmount(amount.multiply(new BigDecimal(100)).longValue(), rate.doubleValue())); + + // Get the exchange rate + BigDecimal rate = new BigDecimal(ent.rate); + + result = amount.divide(rate, 8, BRConstants.ROUNDING_MODE) + .multiply(new BigDecimal("100000000")); + + // Round to a whole number of litoshis + result = result.setScale(0, BRConstants.ROUNDING_MODE); } return result; } diff --git a/app/src/main/java/com/brainwallet/tools/util/Utils.java b/app/src/main/java/com/brainwallet/tools/util/Utils.java index 8e7e6062..fb03d0c3 100644 --- a/app/src/main/java/com/brainwallet/tools/util/Utils.java +++ b/app/src/main/java/com/brainwallet/tools/util/Utils.java @@ -267,38 +267,29 @@ else if (name == ServiceItems.CLIENTCODE) { } /// Description: 1715876807 public static long tieredOpsFee(Context app, long sendAmount) { - - double doubleRate = 83.000; - double sendAmountDouble = new Double(String.valueOf(sendAmount)); - String usIso = Currency.getInstance(new Locale("en", "US")).getCurrencyCode(); - CurrencyEntity currency = CurrencyDataSource.getInstance(app).getCurrencyByIso(usIso); - if (currency != null) { - doubleRate = currency.rate; + if (sendAmount < 1_398_000) { + return 69900; } - double usdInLTC = sendAmountDouble * doubleRate / 100_000_000.0; - usdInLTC = Math.floor(usdInLTC * 100) / 100; - - if (isBetween(usdInLTC, 0.00, 20.00)) { - double lowRate = usdInLTC * 0.01; - return (long) ((lowRate / doubleRate) * 100_000_000.0); + else if (sendAmount < 6_991_000) { + return 111_910; + } + else if (sendAmount < 27_965_000) { + return 279_700; } - else if (isBetween(usdInLTC, 20.00, 50.00)) { - return (long) ((0.30 / doubleRate) * 100_000_000.0); + else if (sendAmount < 139_820_000) { + return 699_540; } - else if (isBetween(usdInLTC, 50.00, 100.00)) { - return (long) ((1.00 / doubleRate) * 100_000_000.0); - } - else if (isBetween(usdInLTC, 100.00, 500.00)) { - return (long) ((2.00 / doubleRate) * 100_000_000.0); + else if (sendAmount < 279_653_600) { + return 1_049_300; } - else if (isBetween(usdInLTC, 500.00, 1000.00)) { - return (long) ((2.50 / doubleRate) * 100_000_000.0); + else if (sendAmount < 699_220_000) { + return 1_398_800; } - else if ( usdInLTC > 1000.00) { - return (long) ((3.00 / doubleRate) * 100_000_000.0); + else if (sendAmount < 1_398_440_000) { + return 2_797_600; } else { - return (long) ((3.00 / doubleRate) * 100_000_000.0); + return 2_797_600; } } private static boolean isBetween(double x, double lower, double upper) { diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt index 48e0750a..61a8ced4 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt @@ -8,6 +8,7 @@ sealed class SettingsEvent { val shareAnalyticsDataEnabled: Boolean = false, val lastSyncMetadata: String? = null, ) : SettingsEvent() + object OnSecurityUpdatePinClick : SettingsEvent() object OnSecuritySeedPhraseClick : SettingsEvent() object OnSecurityShareAnalyticsDataClick : SettingsEvent() @@ -20,4 +21,5 @@ sealed class SettingsEvent { object OnFiatSelectorDismiss : SettingsEvent() data class OnFiatChange(val currency: CurrencyEntity) : SettingsEvent() object OnBlockchainSyncClick : SettingsEvent() + data class OnFeeTypeChange(val feeType: String) : SettingsEvent() } \ 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 60a209bd..d0336bff 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 @@ -5,6 +5,7 @@ import com.brainwallet.data.model.Fee import com.brainwallet.data.model.FeeOption import com.brainwallet.data.model.Language import com.brainwallet.data.model.toFeeOptions +import com.brainwallet.tools.manager.FeeManager data class SettingsState( val darkMode: Boolean = true, @@ -20,4 +21,5 @@ data class SettingsState( val shareAnalyticsDataEnabled: Boolean = false, val lastSyncMetadata: String? = null, val currentFeeOptions: List = Fee.Default.toFeeOptions(), + val selectedFeeType: String = FeeManager.LUXURY ) \ 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 8859d395..738af4be 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 @@ -8,8 +8,10 @@ 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.tools.manager.FeeManager import com.brainwallet.ui.BrainwalletViewModel import com.brainwallet.util.EventBus +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -47,6 +49,25 @@ class SettingsViewModel( AppSetting() ) + init { + viewModelScope.launch { + while (true) { + /** + * need update fee options every 4s, since we are fetching every 4s + * pls check at + * - [CurrencyUpdateWorker] + * - [LtcRepository.fetchRates] + * - [LtcRepository.fetchFeePerKb] + */ + + _state.update { + it.copy(currentFeeOptions = FeeManager.getInstance().currentFeeOptions.toFeeOptions()) + } + delay(4000) + } + } + } + override fun onEvent(event: SettingsEvent) { when (event) { is SettingsEvent.OnLoad -> viewModelScope.launch { @@ -54,7 +75,7 @@ class SettingsViewModel( it.copy( shareAnalyticsDataEnabled = event.shareAnalyticsDataEnabled, lastSyncMetadata = event.lastSyncMetadata, - currentFeeOptions = ltcRepository.fetchFeePerKb().toFeeOptions() + selectedFeeType = settingRepository.getSelectedFeeType() ) } } @@ -143,6 +164,11 @@ class SettingsViewModel( EventBus.emit(EventBus.Event.Message(LEGACY_EFFECT_ON_SHARE_ANALYTICS_DATA_TOGGLE)) } + + is SettingsEvent.OnFeeTypeChange -> _state.update { + settingRepository.putSelectedFeeType(event.feeType) + it.copy(selectedFeeType = event.feeType) + } } } 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 1daa4b33..b3559014 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 @@ -135,6 +135,7 @@ fun HomeSettingDrawerSheet( .fillMaxSize() .wrapContentHeight(), selectedCurrency = state.selectedCurrency, + selectedFeeType = state.selectedFeeType, 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 138ab31c..f964e418 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 @@ -14,10 +14,6 @@ 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 @@ -26,24 +22,21 @@ 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.data.model.getSelectedIndex 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, + selectedFeeType: String, 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 @@ -81,12 +74,9 @@ fun LitecoinBlockchainDetail( NetworkFeeSelector( selectedCurrency = selectedCurrency, feeOptions = feeOptions, - selectedIndex = feeOptionsState + selectedIndex = feeOptions.getSelectedIndex(selectedFeeType) ) { newSelectedIndex -> - feeOptionsState = newSelectedIndex - - //just update inside BRWalletManager.setFeePerKb - BRWalletManager.getInstance().setFeePerKb(feeOptions[newSelectedIndex].feePerKb) + onEvent.invoke(SettingsEvent.OnFeeTypeChange(feeOptions[newSelectedIndex].type)) } } diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index abc5b2f7..59aaed33 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -523,11 +523,8 @@ public void initWallet(final Context ctx) { m.createWallet(transactionsCount, pubkeyEncoded); String firstAddress = BRWalletManager.getFirstAddress(pubkeyEncoded); BRSharedPrefs.putFirstAddress(ctx, firstAddress); - FeeManager feeManager = FeeManager.getInstance(); - if (feeManager.isLuxuryFee()) { - FeeManager.updateFeePerKb(ctx); - BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.luxury); - } + //set fee here + BRWalletManager.getInstance().setFeePerKb(FeeManager.getInstance().getCurrentFeeValue()); } if (!pm.isCreated()) {