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