From ba8ea45b3465947eb7e33c9d41f9e90771f7386d Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 31 Mar 2025 17:57:14 +0100 Subject: [PATCH 01/44] Revert "Updated the APIManager" This reverts commit 4e252ac29cfb6c3ab26ae5ea4a8f6de154311f2e. # Conflicts: # app/src/main/java/com/brainwallet/tools/manager/APIManager.kt --- app/build.gradle.kts | 1 - .../data/model/CurrencyEntity.java | 6 +- .../main/java/com/brainwallet/di/Module.kt | 4 +- .../presenter/activities/util/BRActivity.java | 4 +- .../tools/manager/BRApiManager.java | 317 +++++++++--------- .../currency/BackupRateFetchTests.kt | 6 +- .../java/com/brainwallet/data/BaseURLTests.kt | 19 +- .../tools/util/ProdAPIManagerTests.kt | 26 +- 8 files changed, 185 insertions(+), 198 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d20ad2c8..7922396b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -188,7 +188,6 @@ dependencies { } implementation("androidx.webkit:webkit:1.9.0") - implementation("com.squareup.moshi:moshi-kotlin:1.15.2") implementation(libs.androidx.core) implementation(libs.androidx.appcompat) implementation(libs.androidx.legacy.support) diff --git a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java index 319ed216..f0c31051 100644 --- a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java +++ b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java @@ -5,13 +5,13 @@ public class CurrencyEntity implements Serializable { //Change this after modifying the class - private static final long serialVersionUID = 7526472295622777000L; + private static final long serialVersionUID = 7526472295622776147L; public static final String TAG = CurrencyEntity.class.getName(); public String code; - public String name = ""; + public String name; public float rate; - public String symbol = ""; + public String symbol; public CurrencyEntity(String code, String name, float rate, String symbol) { this.code = code; diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index 05db629b..75a2cf5f 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -5,7 +5,7 @@ import android.content.SharedPreferences import com.brainwallet.BuildConfig import com.brainwallet.data.repository.SettingRepository import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.tools.manager.APIManager +import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.ui.screens.home.SettingsViewModel import com.brainwallet.ui.screens.inputwords.InputWordsViewModel @@ -32,7 +32,7 @@ val dataModule = module { it.initialize() } } - single { APIManager(get()) } + single { BRApiManager(get()) } single { CurrencyDataSource.getInstance(get()) } single { provideSharedPreferences(context = androidApplication()) } single { SettingRepository.Impl(get(), get()) } diff --git a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java index bd118415..6f62d97c 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java @@ -16,7 +16,7 @@ import com.brainwallet.presenter.activities.intro.RecoverActivity; import com.brainwallet.presenter.activities.intro.WriteDownActivity; import com.brainwallet.tools.animation.BRAnimator; -import com.brainwallet.tools.manager.APIManager; +import com.brainwallet.tools.manager.BRApiManager; import com.brainwallet.tools.manager.InternetManager; import com.brainwallet.tools.security.AuthManager; import com.brainwallet.tools.security.BRKeyStore; @@ -148,7 +148,7 @@ public static void init(Activity app) { if (!(app instanceof RecoverActivity || app instanceof WriteDownActivity)) { - APIManager apiManager = KoinJavaComponent.get(APIManager.class); + BRApiManager apiManager = KoinJavaComponent.get(BRApiManager.class); apiManager.startTimer(app); } diff --git a/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java b/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java index e298b178..9b7a81de 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java @@ -30,7 +30,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; -import java.util.Optional; import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -38,164 +37,158 @@ import okhttp3.Request; import okhttp3.Response; import timber.log.Timber; -// -//public class BRApiManager { -// private Timer timer; -// -// private TimerTask timerTask; -// -// private Handler handler; -// -// private RemoteConfigSource remoteConfigSource; -// -// public BRApiManager(RemoteConfigSource remoteConfigSource) { -// this.remoteConfigSource = remoteConfigSource; -// this.handler = new Handler(); -// } -// -// private Set getCurrencies(Activity context) { -// Set set = new LinkedHashSet<>(); -// try { -// JSONArray arr = fetchRates(context); -// FeeManager.updateFeePerKb(context); -// if (arr != null) { -// String selectedISO = BRSharedPrefs.getIsoSymbol(context); -// int length = arr.length(); -// for (int i = 0; i < length; i++) { -// CurrencyEntity tempCurrencyEntity = new CurrencyEntity(); -// try { -// JSONObject tmpJSONObj = (JSONObject) arr.get(i); -// tempCurrencyEntity.name = "no_currency_name"; -// if (tmpJSONObj.getString("name").toString() != null) { -// tempCurrencyEntity.name = tmpJSONObj.getString("name"); -// } -// tempCurrencyEntity.code = tmpJSONObj.getString("code"); -// tempCurrencyEntity.rate = (float) tmpJSONObj.getDouble("n"); -// tempCurrencyEntity.symbol = new String(""); -// if (tempCurrencyEntity.code.equalsIgnoreCase(selectedISO)) { -// BRSharedPrefs.putIso(context, tempCurrencyEntity.code); -// BRSharedPrefs.putCurrencyListPosition(context, i - 1); -// } -// set.add(tempCurrencyEntity); -// Timber.d(":::timber: CE: %s",tempCurrencyEntity.code); -// -// } catch (JSONException e) { -// ///Timber.e(e); -// // Timber.d(":::timber: ERROR: %s",e); -// } -// } -// } else { -// Timber.d("timber: getCurrencies: failed to get currencies"); -// } -// } catch (Exception e) { -// Timber.e(e); -// } -// List tempList = new ArrayList<>(set); -// Collections.reverse(tempList); -// return new LinkedHashSet<>(set); -// } -// -// -// private void initializeTimerTask(final Context context) { -// timerTask = new TimerTask() { -// public void run() { -// //use a handler to run a toast that shows the current timestamp -// handler.post(new Runnable() { -// public void run() { -// BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(new Runnable() { -// @Override -// public void run() { -// Set tmp = getCurrencies((Activity) context); -// CurrencyDataSource.getInstance(context).putCurrencies(tmp); -// } -// }); -// } -// }); -// } -// }; -// } -// -// public void startTimer(Context context) { -// //set a new Timer -// if (timer != null) return; -// timer = new Timer(); -// -// //initialize the TimerTask's job -// initializeTimerTask(context); -// -// //schedule the timer, after the first 0ms the TimerTask will run every 60000ms -// timer.schedule(timerTask, 0, 4000); -// } -// -// public JSONArray fetchRates(Activity activity) { -// String jsonString = createGETRequestURL(activity, BW_API_PROD_HOST + "/api/v1/rates"); -// JSONArray jsonArray = null; -// if (jsonString == null) return null; -// try { -// jsonArray = new JSONArray(jsonString); -// // DEV Uncomment to view values -// // Timber.d("timber: baseUrlProd: %s", getBaseUrlProd()); -// // Timber.d("timber: JSON %s",jsonArray.toString()); -// } catch (JSONException ex) { -// Timber.e(ex); -// } -// if (jsonArray != null && !BuildConfig.DEBUG) { -// AnalyticsManager.logCustomEvent(_20250222_PAC); -// } -// return jsonArray == null ? backupFetchRates(activity) : jsonArray; -// } -// -// public JSONArray backupFetchRates(Activity activity) { -// String jsonString = createGETRequestURL(activity, BW_API_DEV_HOST + "/api/v1/rates"); -// -// JSONArray jsonArray = null; -// if (jsonString == null) return null; -// try { -// jsonArray = new JSONArray(jsonString); -// -// } catch (JSONException e) { -// Timber.e(e); -// } -// if (jsonArray != null && !BuildConfig.DEBUG) { -// AnalyticsManager.logCustomEvent(_20230113_BAC); -// } -// -// return jsonArray; -// } -// -// // createGETRequestURL -// // Creates the params and headers to make a GET Request -// private 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")) -// .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; -// } -// ///Set timestamp to prefs -// long timeStamp = new Date().getTime(); -// BRSharedPrefs.putSecureTime(app, timeStamp); -// -// assert resp.body() != null; -// response = resp.body().string(); -// -// } catch (IOException e) { -// Timber.e(e); -// } finally { -// if (resp != null) resp.close(); -// } -// return response; -// } -// -// public String getBaseUrlProd() { -// return BW_API_PROD_HOST; -// } -//} + +public class BRApiManager { + private Timer timer; + + private TimerTask timerTask; + + private Handler handler; + + private RemoteConfigSource remoteConfigSource; + + public BRApiManager(RemoteConfigSource remoteConfigSource) { + this.remoteConfigSource = remoteConfigSource; + this.handler = new Handler(); + } + + private Set getCurrencies(Activity context) { + Set set = new LinkedHashSet<>(); + try { + JSONArray arr = fetchRates(context); + FeeManager.updateFeePerKb(context); + if (arr != null) { + String selectedISO = BRSharedPrefs.getIsoSymbol(context); + int length = arr.length(); + for (int i = 0; i < length; i++) { + CurrencyEntity tempCurrencyEntity = new CurrencyEntity(); + try { + JSONObject tmpJSONObj = (JSONObject) arr.get(i); + tempCurrencyEntity.name = tmpJSONObj.getString("name"); + tempCurrencyEntity.code = tmpJSONObj.getString("code"); + tempCurrencyEntity.rate = (float) tmpJSONObj.getDouble("n"); + tempCurrencyEntity.symbol = new String(""); + if (tempCurrencyEntity.code.equalsIgnoreCase(selectedISO)) { + BRSharedPrefs.putIso(context, tempCurrencyEntity.code); + BRSharedPrefs.putCurrencyListPosition(context, i - 1); + } + set.add(tempCurrencyEntity); + } catch (JSONException e) { + Timber.e(e); + } + } + } else { + Timber.d("timber: getCurrencies: failed to get currencies"); + } + } catch (Exception e) { + Timber.e(e); + } + List tempList = new ArrayList<>(set); + Collections.reverse(tempList); + return new LinkedHashSet<>(set); + } + + + private void initializeTimerTask(final Context context) { + timerTask = new TimerTask() { + public void run() { + //use a handler to run a toast that shows the current timestamp + handler.post(new Runnable() { + public void run() { + BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(new Runnable() { + @Override + public void run() { + Set tmp = getCurrencies((Activity) context); + CurrencyDataSource.getInstance(context).putCurrencies(tmp); + } + }); + } + }); + } + }; + } + + public void startTimer(Context context) { + //set a new Timer + if (timer != null) return; + timer = new Timer(); + + //initialize the TimerTask's job + initializeTimerTask(context); + + //schedule the timer, after the first 0ms the TimerTask will run every 60000ms + timer.schedule(timerTask, 0, 4000); + } + + public JSONArray fetchRates(Activity activity) { + String jsonString = createGETRequestURL(activity, BW_API_PROD_HOST + "/api/v1/rates"); + JSONArray jsonArray = null; + if (jsonString == null) return null; + try { + jsonArray = new JSONArray(jsonString); + // DEV Uncomment to view values + // Timber.d("timber: baseUrlProd: %s", getBaseUrlProd()); + // Timber.d("timber: JSON %s",jsonArray.toString()); + } catch (JSONException ex) { + Timber.e(ex); + } + if (jsonArray != null && !BuildConfig.DEBUG) { + AnalyticsManager.logCustomEvent(_20250222_PAC); + } + return jsonArray == null ? backupFetchRates(activity) : jsonArray; + } + + public JSONArray backupFetchRates(Activity activity) { + String jsonString = createGETRequestURL(activity, BW_API_DEV_HOST + "/api/v1/rates"); + + JSONArray jsonArray = null; + if (jsonString == null) return null; + try { + jsonArray = new JSONArray(jsonString); + + } catch (JSONException e) { + Timber.e(e); + } + if (jsonArray != null && !BuildConfig.DEBUG) { + AnalyticsManager.logCustomEvent(_20230113_BAC); + } + + return jsonArray; + } + + // createGETRequestURL + // Creates the params and headers to make a GET Request + private 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")) + .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; + } + ///Set timestamp to prefs + long timeStamp = new Date().getTime(); + BRSharedPrefs.putSecureTime(app, timeStamp); + + assert resp.body() != null; + response = resp.body().string(); + + } catch (IOException e) { + Timber.e(e); + } finally { + if (resp != null) resp.close(); + } + return response; + } + + public String getBaseUrlProd() { + return BW_API_PROD_HOST; + } +} diff --git a/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt b/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt index 4932a7fb..ba5c19ec 100644 --- a/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt +++ b/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.presenter.activities.util.ActivityUTILS -import com.brainwallet.tools.manager.APIManager +import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.util.BRConstants import com.brainwallet.tools.util.Utils import com.platform.APIClient @@ -22,11 +22,11 @@ import org.junit.Test class BackupRateFetchTests { private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: APIManager + private lateinit var apiManager: BRApiManager @Before fun setUp() { - apiManager = spyk(APIManager(remoteConfigSource), recordPrivateCalls = true) + apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) } @Test diff --git a/app/src/test/java/com/brainwallet/data/BaseURLTests.kt b/app/src/test/java/com/brainwallet/data/BaseURLTests.kt index fa603bf2..d0163299 100644 --- a/app/src/test/java/com/brainwallet/data/BaseURLTests.kt +++ b/app/src/test/java/com/brainwallet/data/BaseURLTests.kt @@ -1,7 +1,7 @@ package com.brainwallet.data import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.tools.manager.APIManager +import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.util.BRConstants import io.mockk.every import io.mockk.mockk @@ -13,25 +13,20 @@ import org.junit.Test class BaseURLTests { private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: APIManager + private lateinit var apiManager: BRApiManager @Before fun setUp() { - apiManager = spyk(APIManager(remoteConfigSource), recordPrivateCalls = true) + apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) } @Test - fun `invoke getPRODBaseURL with KEY_API_BASEURL_PROD_NEW_ENABLED true, then should return new getPRODBaseURL`() { + fun `invoke getBaseUrlProd with KEY_API_BASEURL_PROD_NEW_ENABLED true, then should return new baseUrlProd`() { every { remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_PROD_NEW_ENABLED) } returns true - val actual = apiManager.getPRODBaseURL() - assertEquals(BRConstants.BW_API_PROD_HOST, actual) - } - @Test - fun `invoke getDEVBaseURL with KEY_API_BASEURL_DEV_NEW_ENABLED true, then should return new getDEVBaseURL`() { - every { remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_DEV_NEW_ENABLED) } returns true - val actual = apiManager.getDEVBaseURL() - assertEquals(BRConstants.BW_API_DEV_HOST, actual) + val actual = apiManager.baseUrlProd + + assertEquals(BRConstants.BW_API_PROD_HOST, actual) } } diff --git a/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt b/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt index 75af20f7..2386d1f0 100644 --- a/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt +++ b/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.presenter.activities.util.ActivityUTILS -import com.brainwallet.tools.manager.APIManager +import com.brainwallet.tools.manager.BRApiManager import com.platform.APIClient import io.mockk.every import io.mockk.mockk @@ -22,11 +22,11 @@ import org.junit.Test class ProdAPIManagerTests { private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: APIManager + private lateinit var apiManager: BRApiManager @Before fun setUp() { - apiManager = spyk(APIManager(remoteConfigSource), recordPrivateCalls = true) + apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) } @Test @@ -85,17 +85,17 @@ class ProdAPIManagerTests { .body(responseString.toResponseBody()) .build() - val currencyEntityList = apiManager.fetchRates(activity) - val jsonUSD = currencyEntityList?.get(154) - val jsonEUR = currencyEntityList?.get(49) - val jsonGBP = currencyEntityList?.get(52) + val result = apiManager.fetchRates(activity) + val jsonUSD = result.getJSONObject(154) + val jsonEUR = result.getJSONObject(49) + val jsonGBP = result.getJSONObject(52) - assertEquals("USD", jsonUSD?.code) - assertEquals("US Dollar", jsonUSD?.name) - assertEquals("EUR", jsonEUR?.code) - assertEquals("Euro", jsonEUR?.name) - assertEquals("GBP", jsonGBP?.code) - assertEquals("British Pound Sterling", jsonGBP?.name) + assertEquals("USD", jsonUSD.optString("code")) + assertEquals("US Dollar", jsonUSD.optString("name")) + assertEquals("EUR", jsonEUR.optString("code")) + assertEquals("Euro", jsonEUR.optString("name")) + assertEquals("GBP", jsonGBP.optString("code")) + assertEquals("British Pound Sterling", jsonGBP.optString("name")) ///DEV: Very flaky test not enough time for the response verifyAll { From 638ce3a3b76b7114b60ef6dc8f398dc26b8e2b25 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 31 Mar 2025 19:09:11 +0100 Subject: [PATCH 02/44] Reverted the Kotlin APIManager. --- app/build.gradle.kts | 4 +- .../brainwallet/tools/manager/APIManager.kt | 119 ------------------ 2 files changed, 2 insertions(+), 121 deletions(-) delete mode 100644 app/src/main/java/com/brainwallet/tools/manager/APIManager.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7922396b..7d06d2e7 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 = 202503281 - versionName = "v4.4.1" + versionCode = 202503312 + versionName = "v4.4.3" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") diff --git a/app/src/main/java/com/brainwallet/tools/manager/APIManager.kt b/app/src/main/java/com/brainwallet/tools/manager/APIManager.kt deleted file mode 100644 index fe3b2072..00000000 --- a/app/src/main/java/com/brainwallet/tools/manager/APIManager.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.brainwallet.tools.manager - -import android.app.Activity -import android.content.Context -import com.brainwallet.data.model.CurrencyEntity -import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.presenter.entities.ServiceItems -import com.brainwallet.tools.sqlite.CurrencyDataSource -import com.brainwallet.tools.util.BRConstants.BW_API_DEV_HOST -import com.brainwallet.tools.util.BRConstants.BW_API_PROD_HOST -import com.brainwallet.tools.util.Utils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import com.squareup.moshi.Moshi -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Types -import timber.log.Timber -import java.io.IOException -import java.util.* -import kotlin.collections.LinkedHashSet - -class APIManager (private val remoteConfigSource: RemoteConfigSource) { - fun getPRODBaseURL(): String = BW_API_PROD_HOST - fun getDEVBaseURL(): String = BW_API_DEV_HOST - - private var timer: Timer? = null - private var pollPeriod : Long = 5000 - - private val client = OkHttpClient() - private val moshi: Moshi = Moshi.Builder().build() - private val type = Types.newParameterizedType(List::class.java, CurrencyEntity::class.java) - private val jsonAdapter: JsonAdapter> = moshi.adapter(type) - - fun getCurrencies(context: Activity): Set { - val set = LinkedHashSet() - try { - val arr = fetchRates(context) - FeeManager.updateFeePerKb(context) - arr?.let { currencyList -> - val selectedISO = BRSharedPrefs.getIsoSymbol(context) - currencyList.forEachIndexed { i, tempCurrencyEntity -> - if (tempCurrencyEntity.code.equals(selectedISO, ignoreCase = true)) { - BRSharedPrefs.putIso(context, tempCurrencyEntity.code) - BRSharedPrefs.putCurrencyListPosition(context, i - 1) - } - set.add(tempCurrencyEntity) - } - } ?: Timber.d("timber: getCurrencies: failed to get currencies") - } catch (e: Exception) { - Timber.e(e) - } - return set.reversed().toSet() - } - - private fun initializeTimerTask(context: Context) { - timer = Timer().apply { - schedule(object : TimerTask() { - override fun run() { - CoroutineScope(Dispatchers.IO).launch { - val tmp = getCurrencies(context as Activity) - withContext(Dispatchers.Main) { - CurrencyDataSource.getInstance(context).putCurrencies(tmp) - } - } - } - }, 0, pollPeriod) - } - } - - fun startTimer(context: Context) { - if (timer != null) return - initializeTimerTask(context) - } - - fun fetchRates(activity: Activity): List? { - val jsonString = createGETRequestURL(activity, "$BW_API_PROD_HOST/api/v1/rates") - return parseJsonArray(jsonString) ?: backupFetchRates(activity) - } - - private fun backupFetchRates(activity: Activity): List? { - val jsonString = createGETRequestURL(activity, "$BW_API_DEV_HOST/api/v1/rates") - return parseJsonArray(jsonString) - } - - private fun parseJsonArray(jsonString: String?): List? { - return try { - jsonString?.let { jsonAdapter.fromJson(it) } - } catch (e: IOException) { - Timber.e(e) - null - } - } - - private fun createGETRequestURL(app: Context, url: String): String? { - val request = Request.Builder() - .url(url) - .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() - - return try { - client.newCall(request).execute().use { response -> - response.body?.string().also { - BRSharedPrefs.putSecureTime(app, System.currentTimeMillis()) - } - } - } catch (e: IOException) { - Timber.e(e) - null - } - } -} From 5344a96a6a0c56895945e7e8d0005eee30dbfd39 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 31 Mar 2025 19:43:55 +0100 Subject: [PATCH 03/44] Add version and code to welcome screen --- .../ui/screens/welcome/WelcomeScreen.kt | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt index 370b9574..4472eb38 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt @@ -41,6 +41,7 @@ import com.brainwallet.R import com.brainwallet.navigation.OnNavigate import com.brainwallet.navigation.Route import com.brainwallet.navigation.UiEffect +import com.brainwallet.tools.util.BRConstants import com.brainwallet.ui.composable.BorderedLargeButton import com.brainwallet.ui.composable.BrainwalletButton import com.brainwallet.ui.composable.DarkModeToggleButton @@ -75,7 +76,7 @@ fun WelcomeScreen( val halfLeadTrailPadding = leadTrailPadding / 2 val doubleLeadTrailPadding = leadTrailPadding * 2 val rowPadding = 8 - val activeRowHeight = 70 + val activeRowHeight = 58 val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.welcomeemoji20250212)) val progress by animateLottieCompositionAsState( @@ -91,7 +92,7 @@ fun WelcomeScreen( verticalArrangement = Arrangement.SpaceBetween ) { - Spacer(modifier = Modifier.weight(0.4f)) + Spacer(modifier = Modifier.weight(0.2f)) Image( painterResource(R.drawable.brainwallet_logotype_white), @@ -125,15 +126,16 @@ fun WelcomeScreen( modifier = Modifier .fillMaxWidth() .height(activeRowHeight.dp) - .padding(horizontal = halfLeadTrailPadding.dp) + .padding(horizontal = leadTrailPadding.dp) .padding(vertical = rowPadding.dp), horizontalArrangement = Arrangement.SpaceEvenly ) { - Spacer(modifier = Modifier.weight(0.1f)) - + BrainwalletButton( - modifier = Modifier.weight(0.9f), + modifier = Modifier + .weight(1f) + .fillMaxWidth(), onClick = { viewModel.onEvent(WelcomeEvent.OnLanguageSelectorButtonClick) } @@ -145,7 +147,7 @@ fun WelcomeScreen( ) } - Spacer(modifier = Modifier.weight(0.2f)) + Spacer(modifier = Modifier.weight(0.1f)) DarkModeToggleButton( modifier = Modifier @@ -157,10 +159,12 @@ fun WelcomeScreen( } ) - Spacer(modifier = Modifier.weight(0.2f)) + Spacer(modifier = Modifier.weight(0.1f)) BrainwalletButton( - modifier = Modifier.weight(0.9f), + modifier = Modifier + .weight(1f) + .fillMaxWidth(), onClick = { viewModel.onEvent(WelcomeEvent.OnFiatButtonClick) } ) { Text( @@ -170,8 +174,6 @@ fun WelcomeScreen( ) } - Spacer(modifier = Modifier.weight(0.1f)) - } // Ready Button BorderedLargeButton( @@ -180,7 +182,7 @@ fun WelcomeScreen( }, shape = RoundedCornerShape(50), modifier = Modifier - .padding(horizontal = halfLeadTrailPadding.dp) + .padding(horizontal = leadTrailPadding.dp) .padding(vertical = rowPadding.dp) .height(activeRowHeight.dp) @@ -199,7 +201,7 @@ fun WelcomeScreen( }, shape = RoundedCornerShape(50), modifier = Modifier - .padding(horizontal = halfLeadTrailPadding.dp) + .padding(horizontal = leadTrailPadding.dp) .padding(vertical = rowPadding.dp) .height(activeRowHeight.dp) .clip(RoundedCornerShape(50)) @@ -211,6 +213,12 @@ fun WelcomeScreen( ) } + Text( modifier = Modifier + .padding(vertical = 8.dp), + text = BRConstants.APP_VERSION_NAME_CODE, + fontSize = 13.sp, + color = BrainwalletTheme.colors.content + ) Spacer(modifier = Modifier.weight(0.5f)) } From ebc3bb9a47dbb8d1e0fe9af6ddb9ee14e959a1cf Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 31 Mar 2025 19:48:18 +0100 Subject: [PATCH 04/44] reset padding --- .../com/brainwallet/ui/screens/welcome/WelcomeScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt index 4472eb38..f4d8ba3a 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt @@ -76,6 +76,7 @@ fun WelcomeScreen( val halfLeadTrailPadding = leadTrailPadding / 2 val doubleLeadTrailPadding = leadTrailPadding * 2 val rowPadding = 8 + val versionPadding = 12 val activeRowHeight = 58 val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.welcomeemoji20250212)) @@ -131,7 +132,7 @@ fun WelcomeScreen( horizontalArrangement = Arrangement.SpaceEvenly ) { - + BrainwalletButton( modifier = Modifier .weight(1f) @@ -214,12 +215,11 @@ fun WelcomeScreen( } Text( modifier = Modifier - .padding(vertical = 8.dp), + .padding(vertical = versionPadding.dp), text = BRConstants.APP_VERSION_NAME_CODE, fontSize = 13.sp, color = BrainwalletTheme.colors.content ) - Spacer(modifier = Modifier.weight(0.5f)) } //language selector From c770f01549d7d1847a6672bbb97b247fd7ab1a40 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 31 Mar 2025 17:57:14 +0100 Subject: [PATCH 05/44] Revert "Updated the APIManager" This reverts commit 4e252ac29cfb6c3ab26ae5ea4a8f6de154311f2e. # Conflicts: # app/src/main/java/com/brainwallet/tools/manager/APIManager.kt --- app/build.gradle.kts | 1 - .../data/model/CurrencyEntity.java | 6 +- .../main/java/com/brainwallet/di/Module.kt | 4 +- .../presenter/activities/util/BRActivity.java | 4 +- .../tools/manager/BRApiManager.java | 317 +++++++++--------- .../currency/BackupRateFetchTests.kt | 6 +- .../java/com/brainwallet/data/BaseURLTests.kt | 19 +- .../tools/util/ProdAPIManagerTests.kt | 26 +- 8 files changed, 185 insertions(+), 198 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d20ad2c8..7922396b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -188,7 +188,6 @@ dependencies { } implementation("androidx.webkit:webkit:1.9.0") - implementation("com.squareup.moshi:moshi-kotlin:1.15.2") implementation(libs.androidx.core) implementation(libs.androidx.appcompat) implementation(libs.androidx.legacy.support) diff --git a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java index 319ed216..f0c31051 100644 --- a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java +++ b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java @@ -5,13 +5,13 @@ public class CurrencyEntity implements Serializable { //Change this after modifying the class - private static final long serialVersionUID = 7526472295622777000L; + private static final long serialVersionUID = 7526472295622776147L; public static final String TAG = CurrencyEntity.class.getName(); public String code; - public String name = ""; + public String name; public float rate; - public String symbol = ""; + public String symbol; public CurrencyEntity(String code, String name, float rate, String symbol) { this.code = code; diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index 05db629b..75a2cf5f 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -5,7 +5,7 @@ import android.content.SharedPreferences import com.brainwallet.BuildConfig import com.brainwallet.data.repository.SettingRepository import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.tools.manager.APIManager +import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.ui.screens.home.SettingsViewModel import com.brainwallet.ui.screens.inputwords.InputWordsViewModel @@ -32,7 +32,7 @@ val dataModule = module { it.initialize() } } - single { APIManager(get()) } + single { BRApiManager(get()) } single { CurrencyDataSource.getInstance(get()) } single { provideSharedPreferences(context = androidApplication()) } single { SettingRepository.Impl(get(), get()) } diff --git a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java index bd118415..6f62d97c 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java @@ -16,7 +16,7 @@ import com.brainwallet.presenter.activities.intro.RecoverActivity; import com.brainwallet.presenter.activities.intro.WriteDownActivity; import com.brainwallet.tools.animation.BRAnimator; -import com.brainwallet.tools.manager.APIManager; +import com.brainwallet.tools.manager.BRApiManager; import com.brainwallet.tools.manager.InternetManager; import com.brainwallet.tools.security.AuthManager; import com.brainwallet.tools.security.BRKeyStore; @@ -148,7 +148,7 @@ public static void init(Activity app) { if (!(app instanceof RecoverActivity || app instanceof WriteDownActivity)) { - APIManager apiManager = KoinJavaComponent.get(APIManager.class); + BRApiManager apiManager = KoinJavaComponent.get(BRApiManager.class); apiManager.startTimer(app); } diff --git a/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java b/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java index e298b178..9b7a81de 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java @@ -30,7 +30,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; -import java.util.Optional; import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -38,164 +37,158 @@ import okhttp3.Request; import okhttp3.Response; import timber.log.Timber; -// -//public class BRApiManager { -// private Timer timer; -// -// private TimerTask timerTask; -// -// private Handler handler; -// -// private RemoteConfigSource remoteConfigSource; -// -// public BRApiManager(RemoteConfigSource remoteConfigSource) { -// this.remoteConfigSource = remoteConfigSource; -// this.handler = new Handler(); -// } -// -// private Set getCurrencies(Activity context) { -// Set set = new LinkedHashSet<>(); -// try { -// JSONArray arr = fetchRates(context); -// FeeManager.updateFeePerKb(context); -// if (arr != null) { -// String selectedISO = BRSharedPrefs.getIsoSymbol(context); -// int length = arr.length(); -// for (int i = 0; i < length; i++) { -// CurrencyEntity tempCurrencyEntity = new CurrencyEntity(); -// try { -// JSONObject tmpJSONObj = (JSONObject) arr.get(i); -// tempCurrencyEntity.name = "no_currency_name"; -// if (tmpJSONObj.getString("name").toString() != null) { -// tempCurrencyEntity.name = tmpJSONObj.getString("name"); -// } -// tempCurrencyEntity.code = tmpJSONObj.getString("code"); -// tempCurrencyEntity.rate = (float) tmpJSONObj.getDouble("n"); -// tempCurrencyEntity.symbol = new String(""); -// if (tempCurrencyEntity.code.equalsIgnoreCase(selectedISO)) { -// BRSharedPrefs.putIso(context, tempCurrencyEntity.code); -// BRSharedPrefs.putCurrencyListPosition(context, i - 1); -// } -// set.add(tempCurrencyEntity); -// Timber.d(":::timber: CE: %s",tempCurrencyEntity.code); -// -// } catch (JSONException e) { -// ///Timber.e(e); -// // Timber.d(":::timber: ERROR: %s",e); -// } -// } -// } else { -// Timber.d("timber: getCurrencies: failed to get currencies"); -// } -// } catch (Exception e) { -// Timber.e(e); -// } -// List tempList = new ArrayList<>(set); -// Collections.reverse(tempList); -// return new LinkedHashSet<>(set); -// } -// -// -// private void initializeTimerTask(final Context context) { -// timerTask = new TimerTask() { -// public void run() { -// //use a handler to run a toast that shows the current timestamp -// handler.post(new Runnable() { -// public void run() { -// BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(new Runnable() { -// @Override -// public void run() { -// Set tmp = getCurrencies((Activity) context); -// CurrencyDataSource.getInstance(context).putCurrencies(tmp); -// } -// }); -// } -// }); -// } -// }; -// } -// -// public void startTimer(Context context) { -// //set a new Timer -// if (timer != null) return; -// timer = new Timer(); -// -// //initialize the TimerTask's job -// initializeTimerTask(context); -// -// //schedule the timer, after the first 0ms the TimerTask will run every 60000ms -// timer.schedule(timerTask, 0, 4000); -// } -// -// public JSONArray fetchRates(Activity activity) { -// String jsonString = createGETRequestURL(activity, BW_API_PROD_HOST + "/api/v1/rates"); -// JSONArray jsonArray = null; -// if (jsonString == null) return null; -// try { -// jsonArray = new JSONArray(jsonString); -// // DEV Uncomment to view values -// // Timber.d("timber: baseUrlProd: %s", getBaseUrlProd()); -// // Timber.d("timber: JSON %s",jsonArray.toString()); -// } catch (JSONException ex) { -// Timber.e(ex); -// } -// if (jsonArray != null && !BuildConfig.DEBUG) { -// AnalyticsManager.logCustomEvent(_20250222_PAC); -// } -// return jsonArray == null ? backupFetchRates(activity) : jsonArray; -// } -// -// public JSONArray backupFetchRates(Activity activity) { -// String jsonString = createGETRequestURL(activity, BW_API_DEV_HOST + "/api/v1/rates"); -// -// JSONArray jsonArray = null; -// if (jsonString == null) return null; -// try { -// jsonArray = new JSONArray(jsonString); -// -// } catch (JSONException e) { -// Timber.e(e); -// } -// if (jsonArray != null && !BuildConfig.DEBUG) { -// AnalyticsManager.logCustomEvent(_20230113_BAC); -// } -// -// return jsonArray; -// } -// -// // createGETRequestURL -// // Creates the params and headers to make a GET Request -// private 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")) -// .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; -// } -// ///Set timestamp to prefs -// long timeStamp = new Date().getTime(); -// BRSharedPrefs.putSecureTime(app, timeStamp); -// -// assert resp.body() != null; -// response = resp.body().string(); -// -// } catch (IOException e) { -// Timber.e(e); -// } finally { -// if (resp != null) resp.close(); -// } -// return response; -// } -// -// public String getBaseUrlProd() { -// return BW_API_PROD_HOST; -// } -//} + +public class BRApiManager { + private Timer timer; + + private TimerTask timerTask; + + private Handler handler; + + private RemoteConfigSource remoteConfigSource; + + public BRApiManager(RemoteConfigSource remoteConfigSource) { + this.remoteConfigSource = remoteConfigSource; + this.handler = new Handler(); + } + + private Set getCurrencies(Activity context) { + Set set = new LinkedHashSet<>(); + try { + JSONArray arr = fetchRates(context); + FeeManager.updateFeePerKb(context); + if (arr != null) { + String selectedISO = BRSharedPrefs.getIsoSymbol(context); + int length = arr.length(); + for (int i = 0; i < length; i++) { + CurrencyEntity tempCurrencyEntity = new CurrencyEntity(); + try { + JSONObject tmpJSONObj = (JSONObject) arr.get(i); + tempCurrencyEntity.name = tmpJSONObj.getString("name"); + tempCurrencyEntity.code = tmpJSONObj.getString("code"); + tempCurrencyEntity.rate = (float) tmpJSONObj.getDouble("n"); + tempCurrencyEntity.symbol = new String(""); + if (tempCurrencyEntity.code.equalsIgnoreCase(selectedISO)) { + BRSharedPrefs.putIso(context, tempCurrencyEntity.code); + BRSharedPrefs.putCurrencyListPosition(context, i - 1); + } + set.add(tempCurrencyEntity); + } catch (JSONException e) { + Timber.e(e); + } + } + } else { + Timber.d("timber: getCurrencies: failed to get currencies"); + } + } catch (Exception e) { + Timber.e(e); + } + List tempList = new ArrayList<>(set); + Collections.reverse(tempList); + return new LinkedHashSet<>(set); + } + + + private void initializeTimerTask(final Context context) { + timerTask = new TimerTask() { + public void run() { + //use a handler to run a toast that shows the current timestamp + handler.post(new Runnable() { + public void run() { + BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(new Runnable() { + @Override + public void run() { + Set tmp = getCurrencies((Activity) context); + CurrencyDataSource.getInstance(context).putCurrencies(tmp); + } + }); + } + }); + } + }; + } + + public void startTimer(Context context) { + //set a new Timer + if (timer != null) return; + timer = new Timer(); + + //initialize the TimerTask's job + initializeTimerTask(context); + + //schedule the timer, after the first 0ms the TimerTask will run every 60000ms + timer.schedule(timerTask, 0, 4000); + } + + public JSONArray fetchRates(Activity activity) { + String jsonString = createGETRequestURL(activity, BW_API_PROD_HOST + "/api/v1/rates"); + JSONArray jsonArray = null; + if (jsonString == null) return null; + try { + jsonArray = new JSONArray(jsonString); + // DEV Uncomment to view values + // Timber.d("timber: baseUrlProd: %s", getBaseUrlProd()); + // Timber.d("timber: JSON %s",jsonArray.toString()); + } catch (JSONException ex) { + Timber.e(ex); + } + if (jsonArray != null && !BuildConfig.DEBUG) { + AnalyticsManager.logCustomEvent(_20250222_PAC); + } + return jsonArray == null ? backupFetchRates(activity) : jsonArray; + } + + public JSONArray backupFetchRates(Activity activity) { + String jsonString = createGETRequestURL(activity, BW_API_DEV_HOST + "/api/v1/rates"); + + JSONArray jsonArray = null; + if (jsonString == null) return null; + try { + jsonArray = new JSONArray(jsonString); + + } catch (JSONException e) { + Timber.e(e); + } + if (jsonArray != null && !BuildConfig.DEBUG) { + AnalyticsManager.logCustomEvent(_20230113_BAC); + } + + return jsonArray; + } + + // createGETRequestURL + // Creates the params and headers to make a GET Request + private 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")) + .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; + } + ///Set timestamp to prefs + long timeStamp = new Date().getTime(); + BRSharedPrefs.putSecureTime(app, timeStamp); + + assert resp.body() != null; + response = resp.body().string(); + + } catch (IOException e) { + Timber.e(e); + } finally { + if (resp != null) resp.close(); + } + return response; + } + + public String getBaseUrlProd() { + return BW_API_PROD_HOST; + } +} diff --git a/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt b/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt index 4932a7fb..ba5c19ec 100644 --- a/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt +++ b/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.presenter.activities.util.ActivityUTILS -import com.brainwallet.tools.manager.APIManager +import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.util.BRConstants import com.brainwallet.tools.util.Utils import com.platform.APIClient @@ -22,11 +22,11 @@ import org.junit.Test class BackupRateFetchTests { private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: APIManager + private lateinit var apiManager: BRApiManager @Before fun setUp() { - apiManager = spyk(APIManager(remoteConfigSource), recordPrivateCalls = true) + apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) } @Test diff --git a/app/src/test/java/com/brainwallet/data/BaseURLTests.kt b/app/src/test/java/com/brainwallet/data/BaseURLTests.kt index fa603bf2..d0163299 100644 --- a/app/src/test/java/com/brainwallet/data/BaseURLTests.kt +++ b/app/src/test/java/com/brainwallet/data/BaseURLTests.kt @@ -1,7 +1,7 @@ package com.brainwallet.data import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.tools.manager.APIManager +import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.util.BRConstants import io.mockk.every import io.mockk.mockk @@ -13,25 +13,20 @@ import org.junit.Test class BaseURLTests { private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: APIManager + private lateinit var apiManager: BRApiManager @Before fun setUp() { - apiManager = spyk(APIManager(remoteConfigSource), recordPrivateCalls = true) + apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) } @Test - fun `invoke getPRODBaseURL with KEY_API_BASEURL_PROD_NEW_ENABLED true, then should return new getPRODBaseURL`() { + fun `invoke getBaseUrlProd with KEY_API_BASEURL_PROD_NEW_ENABLED true, then should return new baseUrlProd`() { every { remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_PROD_NEW_ENABLED) } returns true - val actual = apiManager.getPRODBaseURL() - assertEquals(BRConstants.BW_API_PROD_HOST, actual) - } - @Test - fun `invoke getDEVBaseURL with KEY_API_BASEURL_DEV_NEW_ENABLED true, then should return new getDEVBaseURL`() { - every { remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_DEV_NEW_ENABLED) } returns true - val actual = apiManager.getDEVBaseURL() - assertEquals(BRConstants.BW_API_DEV_HOST, actual) + val actual = apiManager.baseUrlProd + + assertEquals(BRConstants.BW_API_PROD_HOST, actual) } } diff --git a/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt b/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt index 75af20f7..2386d1f0 100644 --- a/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt +++ b/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.presenter.activities.util.ActivityUTILS -import com.brainwallet.tools.manager.APIManager +import com.brainwallet.tools.manager.BRApiManager import com.platform.APIClient import io.mockk.every import io.mockk.mockk @@ -22,11 +22,11 @@ import org.junit.Test class ProdAPIManagerTests { private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: APIManager + private lateinit var apiManager: BRApiManager @Before fun setUp() { - apiManager = spyk(APIManager(remoteConfigSource), recordPrivateCalls = true) + apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) } @Test @@ -85,17 +85,17 @@ class ProdAPIManagerTests { .body(responseString.toResponseBody()) .build() - val currencyEntityList = apiManager.fetchRates(activity) - val jsonUSD = currencyEntityList?.get(154) - val jsonEUR = currencyEntityList?.get(49) - val jsonGBP = currencyEntityList?.get(52) + val result = apiManager.fetchRates(activity) + val jsonUSD = result.getJSONObject(154) + val jsonEUR = result.getJSONObject(49) + val jsonGBP = result.getJSONObject(52) - assertEquals("USD", jsonUSD?.code) - assertEquals("US Dollar", jsonUSD?.name) - assertEquals("EUR", jsonEUR?.code) - assertEquals("Euro", jsonEUR?.name) - assertEquals("GBP", jsonGBP?.code) - assertEquals("British Pound Sterling", jsonGBP?.name) + assertEquals("USD", jsonUSD.optString("code")) + assertEquals("US Dollar", jsonUSD.optString("name")) + assertEquals("EUR", jsonEUR.optString("code")) + assertEquals("Euro", jsonEUR.optString("name")) + assertEquals("GBP", jsonGBP.optString("code")) + assertEquals("British Pound Sterling", jsonGBP.optString("name")) ///DEV: Very flaky test not enough time for the response verifyAll { From f8c1bd50ca6d4031a27060dad4c337a0660c36ea Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 31 Mar 2025 19:09:11 +0100 Subject: [PATCH 06/44] Reverted the Kotlin APIManager. --- app/build.gradle.kts | 4 +- .../brainwallet/tools/manager/APIManager.kt | 119 ------------------ 2 files changed, 2 insertions(+), 121 deletions(-) delete mode 100644 app/src/main/java/com/brainwallet/tools/manager/APIManager.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7922396b..7d06d2e7 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 = 202503281 - versionName = "v4.4.1" + versionCode = 202503312 + versionName = "v4.4.3" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") diff --git a/app/src/main/java/com/brainwallet/tools/manager/APIManager.kt b/app/src/main/java/com/brainwallet/tools/manager/APIManager.kt deleted file mode 100644 index fe3b2072..00000000 --- a/app/src/main/java/com/brainwallet/tools/manager/APIManager.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.brainwallet.tools.manager - -import android.app.Activity -import android.content.Context -import com.brainwallet.data.model.CurrencyEntity -import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.presenter.entities.ServiceItems -import com.brainwallet.tools.sqlite.CurrencyDataSource -import com.brainwallet.tools.util.BRConstants.BW_API_DEV_HOST -import com.brainwallet.tools.util.BRConstants.BW_API_PROD_HOST -import com.brainwallet.tools.util.Utils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import com.squareup.moshi.Moshi -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Types -import timber.log.Timber -import java.io.IOException -import java.util.* -import kotlin.collections.LinkedHashSet - -class APIManager (private val remoteConfigSource: RemoteConfigSource) { - fun getPRODBaseURL(): String = BW_API_PROD_HOST - fun getDEVBaseURL(): String = BW_API_DEV_HOST - - private var timer: Timer? = null - private var pollPeriod : Long = 5000 - - private val client = OkHttpClient() - private val moshi: Moshi = Moshi.Builder().build() - private val type = Types.newParameterizedType(List::class.java, CurrencyEntity::class.java) - private val jsonAdapter: JsonAdapter> = moshi.adapter(type) - - fun getCurrencies(context: Activity): Set { - val set = LinkedHashSet() - try { - val arr = fetchRates(context) - FeeManager.updateFeePerKb(context) - arr?.let { currencyList -> - val selectedISO = BRSharedPrefs.getIsoSymbol(context) - currencyList.forEachIndexed { i, tempCurrencyEntity -> - if (tempCurrencyEntity.code.equals(selectedISO, ignoreCase = true)) { - BRSharedPrefs.putIso(context, tempCurrencyEntity.code) - BRSharedPrefs.putCurrencyListPosition(context, i - 1) - } - set.add(tempCurrencyEntity) - } - } ?: Timber.d("timber: getCurrencies: failed to get currencies") - } catch (e: Exception) { - Timber.e(e) - } - return set.reversed().toSet() - } - - private fun initializeTimerTask(context: Context) { - timer = Timer().apply { - schedule(object : TimerTask() { - override fun run() { - CoroutineScope(Dispatchers.IO).launch { - val tmp = getCurrencies(context as Activity) - withContext(Dispatchers.Main) { - CurrencyDataSource.getInstance(context).putCurrencies(tmp) - } - } - } - }, 0, pollPeriod) - } - } - - fun startTimer(context: Context) { - if (timer != null) return - initializeTimerTask(context) - } - - fun fetchRates(activity: Activity): List? { - val jsonString = createGETRequestURL(activity, "$BW_API_PROD_HOST/api/v1/rates") - return parseJsonArray(jsonString) ?: backupFetchRates(activity) - } - - private fun backupFetchRates(activity: Activity): List? { - val jsonString = createGETRequestURL(activity, "$BW_API_DEV_HOST/api/v1/rates") - return parseJsonArray(jsonString) - } - - private fun parseJsonArray(jsonString: String?): List? { - return try { - jsonString?.let { jsonAdapter.fromJson(it) } - } catch (e: IOException) { - Timber.e(e) - null - } - } - - private fun createGETRequestURL(app: Context, url: String): String? { - val request = Request.Builder() - .url(url) - .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() - - return try { - client.newCall(request).execute().use { response -> - response.body?.string().also { - BRSharedPrefs.putSecureTime(app, System.currentTimeMillis()) - } - } - } catch (e: IOException) { - Timber.e(e) - null - } - } -} From ff58b57c1d2150cf791bd4a3da4fb6ec6a8ec67a Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Sat, 5 Apr 2025 00:19:01 +0700 Subject: [PATCH 07/44] fix: fix crash when FragmentSignal dismissed --- .../com/brainwallet/presenter/fragments/FragmentSignal.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 05bbed1e..bf936591 100644 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSignal.java +++ b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSignal.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import com.brainwallet.R; @@ -20,7 +21,7 @@ import timber.log.Timber; -public class FragmentSignal extends Fragment { +public class FragmentSignal extends DialogFragment { public static final String TITLE = "title"; public static final String ICON_DESCRIPTION = "iconDescription"; public static final String RES_ID = "resId"; @@ -34,7 +35,7 @@ public class FragmentSignal extends Fragment { @Override public void run() { if (isAdded()) { - getParentFragmentManager().popBackStack(); + dismiss(); handler.postDelayed(completionRunnable, 300); } } From d64ea930d45d0adeabf2a97d4d7f7070ba15d62e Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Mon, 7 Apr 2025 10:39:48 +0700 Subject: [PATCH 08/44] fix: fix sync after wipe --- .../com/brainwallet/tools/manager/SyncManager.java | 2 +- .../ui/screens/inputwords/InputWordsViewModel.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/brainwallet/tools/manager/SyncManager.java b/app/src/main/java/com/brainwallet/tools/manager/SyncManager.java index 924c6fc6..42e195f5 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/SyncManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/SyncManager.java @@ -22,7 +22,7 @@ public class SyncManager { private static SyncManager instance; private static final long SYNC_PERIOD = TimeUnit.HOURS.toMillis(24); private static SyncProgressTask syncTask; - public boolean running; + public volatile boolean running; public static SyncManager getInstance() { if (instance == null) instance = new SyncManager(); diff --git a/app/src/main/java/com/brainwallet/ui/screens/inputwords/InputWordsViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/inputwords/InputWordsViewModel.kt index bb2243dc..7da2b003 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/inputwords/InputWordsViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/inputwords/InputWordsViewModel.kt @@ -94,12 +94,12 @@ class InputWordsViewModel : BrainwalletViewModel() { return } - BRWalletManager.getInstance().run { - wipeWalletButKeystore(event.context) - wipeKeyStore(event.context) - PostAuth.getInstance().setPhraseForKeyStore(cleanPhrase) - BRSharedPrefs.putAllowSpend(event.context, false) - } + BRWalletManager.getInstance().wipeAll(event.context) + + BRSharedPrefs.putAllowSpend(event.context, false) + BRSharedPrefs.putStartHeight(event.context, 0) + + PostAuth.getInstance().setPhraseForKeyStore(cleanPhrase) viewModelScope.launch { EventBus.emit(EventBus.Event.Message(EFFECT_LEGACY_RECOVER_WALLET_AUTH)) From 9a8a20f6b636543858d5592897d61b9b86f28762 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Tue, 8 Apr 2025 05:49:27 +0700 Subject: [PATCH 09/44] fix: fix wrong lifecycle to trigger callback at FragmentSignal --- .../presenter/fragments/FragmentSignal.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) 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 bf936591..4c136b41 100644 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSignal.java +++ b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSignal.java @@ -14,13 +14,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; import com.brainwallet.R; import com.brainwallet.presenter.interfaces.BROnSignalCompletion; -import timber.log.Timber; - public class FragmentSignal extends DialogFragment { public static final String TITLE = "title"; public static final String ICON_DESCRIPTION = "iconDescription"; @@ -34,10 +31,8 @@ public class FragmentSignal extends DialogFragment { private final Runnable popBackStackRunnable = new Runnable() { @Override public void run() { - if (isAdded()) { - dismiss(); - handler.postDelayed(completionRunnable, 300); - } + dismiss(); + handler.postDelayed(completionRunnable, 300); } }; @@ -88,8 +83,8 @@ public void run() { }; @Override - public void onDestroyView() { - super.onDestroyView(); + public void onDestroy() { + super.onDestroy(); // Remove callbacks to prevent execution after the fragment is destroyed handler.removeCallbacks(popBackStackRunnable); handler.removeCallbacks(completionRunnable); From 832fa36a00f21e511c4cdfddfb89ccd7c8f010d9 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Tue, 8 Apr 2025 09:28:37 +0100 Subject: [PATCH 10/44] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From b294cd602138cf934863c6cecdd266ed691cc54a Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Tue, 8 Apr 2025 20:52:44 +0100 Subject: [PATCH 11/44] updated core changes per @andhikayuana --- app/src/main/jni/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/jni/core b/app/src/main/jni/core index 5b9905a7..86649c2d 160000 --- a/app/src/main/jni/core +++ b/app/src/main/jni/core @@ -1 +1 @@ -Subproject commit 5b9905a72205a461e0a9935dce22f516d59e752e +Subproject commit 86649c2dd89d4c1e13fb58de4aa07567f3ac11bd From 089995ada123903b9848d0924e868558fe10c8de Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Fri, 11 Apr 2025 13:17:03 +0700 Subject: [PATCH 12/44] Rename .java to .kt --- .../data/model/{CurrencyEntity.java => CurrencyEntity.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/brainwallet/data/model/{CurrencyEntity.java => CurrencyEntity.kt} (100%) diff --git a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt similarity index 100% rename from app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java rename to app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt From 7e85987bf446994eb9a648d85517d730898bf99c Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Fri, 11 Apr 2025 13:17:07 +0700 Subject: [PATCH 13/44] chore: refactor BRApiManager & APIClient --- app/build.gradle.kts | 4 +- .../brainwallet/data/model/CurrencyEntity.kt | 61 ++++-- .../data/repository/LtcRepository.kt | 40 ++++ .../data/source/RemoteApiSource.kt | 17 ++ .../main/java/com/brainwallet/di/Module.kt | 76 ++++++- .../presenter/activities/util/BRActivity.java | 11 +- .../tools/manager/BRApiManager.java | 194 ------------------ .../brainwallet/tools/manager/FeeManager.java | 1 + .../worker/CurrencyUpdateWorker.kt | 32 +++ app/src/main/java/com/platform/APIClient.java | 1 + .../currency/BackupRateFetchTests.kt | 140 ++++++------- .../java/com/brainwallet/data/BaseURLTests.kt | 34 +-- .../tools/util/ProdAPIManagerTests.kt | 170 +++++++-------- gradle/libs.versions.toml | 13 +- 14 files changed, 390 insertions(+), 404 deletions(-) create mode 100644 app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt create mode 100644 app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt delete mode 100644 app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java create mode 100644 app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d06d2e7..b8f91239 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -216,7 +216,9 @@ dependencies { implementation(platform(libs.koin.bom)) implementation(libs.bundles.koin) - implementation(libs.squareup.okhttp) + implementation(platform(libs.squareup.okhttp.bom)) + implementation(libs.bundles.squareup.okhttp) + implementation(libs.bundles.squareup.retrofit) implementation(libs.jakewarthon.timber) implementation(libs.commons.io) implementation(libs.bundles.eclipse.jetty) diff --git a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt index f0c31051..1ccba70c 100644 --- a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt +++ b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt @@ -1,25 +1,42 @@ -package com.brainwallet.data.model; +package com.brainwallet.data.model -import java.io.Serializable; +import kotlinx.serialization.SerialName +import java.io.Serializable -public class CurrencyEntity implements Serializable { - - //Change this after modifying the class - private static final long serialVersionUID = 7526472295622776147L; - - public static final String TAG = CurrencyEntity.class.getName(); - public String code; - public String name; - public float rate; - public String symbol; - - public CurrencyEntity(String code, String name, float rate, String symbol) { - this.code = code; - this.name = name; - this.rate = rate; - this.symbol = symbol; - } - - public CurrencyEntity() { - } +@kotlinx.serialization.Serializable +data class CurrencyEntity( + @JvmField + var code: String ="", + @JvmField + var name: String = "", + @JvmField + @SerialName("n") + var rate: Float = 0F, + @JvmField + var symbol: String = "" +) : Serializable { +// @JvmField +// var code: String? = null +// @JvmField +// var name: String? = null +// @JvmField +// var rate: Float = 0f +// @JvmField +// var symbol: String? = null +// +// constructor(code: String?, name: String?, rate: Float, symbol: String?) { +// this.code = code +// this.name = name +// this.rate = rate +// this.symbol = symbol +// } +// +// constructor() +// +// companion object { +// //Change this after modifying the class +// private const val serialVersionUID = 7526472295622776147L +// +// val TAG: String = CurrencyEntity::class.java.name +// } } diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt new file mode 100644 index 00000000..02358b34 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -0,0 +1,40 @@ +package com.brainwallet.data.repository + +import android.content.Context +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.tools.manager.BRSharedPrefs +import com.brainwallet.tools.manager.FeeManager +import com.brainwallet.tools.sqlite.CurrencyDataSource + +interface LtcRepository { + suspend fun fetchRates(): List + //todo + + class Impl( + private val context: Context, + private val remoteApiSource: RemoteApiSource, + private val currencyDataSource: CurrencyDataSource + ) : LtcRepository { + + //todo: make it offline first here later, currently just using CurrencyDataSource.getAllCurrencies + override suspend fun fetchRates(): List { + val rates = remoteApiSource.getRates() + + //legacy logic + FeeManager.updateFeePerKb(context) + val selectedISO = BRSharedPrefs.getIsoSymbol(context) + rates.forEachIndexed { index, currencyEntity -> + if (currencyEntity.code.equals(selectedISO, ignoreCase = true)) { + BRSharedPrefs.putIso(context, currencyEntity.code) + BRSharedPrefs.putCurrencyListPosition(context, index - 1) + } + } + + //save to local + currencyDataSource.putCurrencies(rates) + return rates + } + + } +} \ 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 new file mode 100644 index 00000000..0ce3bdc8 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt @@ -0,0 +1,17 @@ +package com.brainwallet.data.source + +import com.brainwallet.data.model.CurrencyEntity +import retrofit2.http.GET + +//TODO +interface RemoteApiSource { + + @GET("api/v1/rates") + suspend fun getRates(): List + + @GET("v1/fee-per-kb") + suspend fun getFeePerKb() + +// https://prod.apigsltd.net/moonpay/buy?address=ltc1qjnsg3p9rt4r4vy7ncgvrywdykl0zwhkhcp8ue0&code=USD&idate=1742331930290&uid=ec51fa950b271ff3 +// suspend fun getMoonPayBuy() +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index 75a2cf5f..2da6b071 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -3,10 +3,12 @@ package com.brainwallet.di import android.content.Context import android.content.SharedPreferences import com.brainwallet.BuildConfig +import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.sqlite.CurrencyDataSource +import com.brainwallet.tools.util.BRConstants import com.brainwallet.ui.screens.home.SettingsViewModel import com.brainwallet.ui.screens.inputwords.InputWordsViewModel import com.brainwallet.ui.screens.ready.ReadyViewModel @@ -17,25 +19,43 @@ import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItViewModel import com.brainwallet.ui.screens.yourseedwords.YourSeedWordsViewModel import com.brainwallet.util.cryptography.KeyStoreKeyGenerator import com.brainwallet.util.cryptography.KeyStoreManager +import com.brainwallet.worker.CurrencyUpdateWorker import com.google.firebase.ktx.Firebase import com.google.firebase.remoteconfig.ktx.remoteConfig +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidApplication import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory //todo module using koin as di framework here +val json = Json { + ignoreUnknownKeys = true + explicitNulls = false + prettyPrint = true +} + val dataModule = module { + factory { provideOkHttpClient() } + single { provideRetrofit(get(), BRConstants.BW_API_PROD_HOST) } + + single { provideApi(get()) } + single { RemoteConfigSource.FirebaseImpl(Firebase.remoteConfig).also { it.initialize() } } - single { BRApiManager(get()) } single { CurrencyDataSource.getInstance(get()) } single { provideSharedPreferences(context = androidApplication()) } single { SettingRepository.Impl(get(), get()) } + single { LtcRepository.Impl(get(), get(), get()) } } val viewModelModule = module { @@ -51,6 +71,7 @@ val viewModelModule = module { val appModule = module { single { KeyStoreManager(get(), KeyStoreKeyGenerator.Impl()) } + single { CurrencyUpdateWorker(get()) } } private fun provideSharedPreferences( @@ -58,4 +79,53 @@ private fun provideSharedPreferences( name: String = "${BuildConfig.APPLICATION_ID}.prefs" ): SharedPreferences { return context.getSharedPreferences(name, Context.MODE_PRIVATE) -} \ No newline at end of file +} + +private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() + .addInterceptor { chain -> + val requestBuilder = chain.request() + .newBuilder() + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .addHeader("X-Litecoin-Testnet", "false") + .addHeader("Accept-Language", "en") +// .addHeader("User-agent",) + chain.proceed(requestBuilder.build()) + } + .addInterceptor { chain -> + val request = chain.request() + runCatching { + chain.proceed(request) + }.getOrElse { + //retry using dev host + val newRequest = request.newBuilder() + .url(BRConstants.BW_API_DEV_HOST + request.url.encodedPath) + .build() + chain.proceed(newRequest) + } + } + .addInterceptor(HttpLoggingInterceptor().apply { + setLevel( + when { + BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY + else -> HttpLoggingInterceptor.Level.NONE + } + ) + }) + .build() + +internal fun provideRetrofit( + okHttpClient: OkHttpClient, + baseUrl: String = BRConstants.BW_API_PROD_HOST +): Retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory( + json.asConverterFactory( + "application/json; charset=UTF8".toMediaType() + ) + ) + .build() + +internal fun provideApi(retrofit: Retrofit): RemoteApiSource = + retrofit.create(RemoteApiSource::class.java) \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java index 6f62d97c..df68dc39 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java @@ -16,7 +16,6 @@ import com.brainwallet.presenter.activities.intro.RecoverActivity; import com.brainwallet.presenter.activities.intro.WriteDownActivity; import com.brainwallet.tools.animation.BRAnimator; -import com.brainwallet.tools.manager.BRApiManager; import com.brainwallet.tools.manager.InternetManager; import com.brainwallet.tools.security.AuthManager; import com.brainwallet.tools.security.BRKeyStore; @@ -25,6 +24,7 @@ import com.brainwallet.tools.threads.BRExecutor; import com.brainwallet.tools.util.BRConstants; import com.brainwallet.wallet.BRWalletManager; +import com.brainwallet.worker.CurrencyUpdateWorker; import org.koin.java.KoinJavaComponent; @@ -51,11 +51,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } -// @Override -// protected void attachBaseContext(Context newBase) { -// super.attachBaseContext(LocaleHelper.Companion.getInstance().setLocale(newBase)); -// } - @Override protected void onStop() { super.onStop(); @@ -148,8 +143,8 @@ public static void init(Activity app) { if (!(app instanceof RecoverActivity || app instanceof WriteDownActivity)) { - BRApiManager apiManager = KoinJavaComponent.get(BRApiManager.class); - apiManager.startTimer(app); + CurrencyUpdateWorker currencyUpdateWorker = KoinJavaComponent.get(CurrencyUpdateWorker.class); + currencyUpdateWorker.start(); } //show wallet locked if it is diff --git a/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java b/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java deleted file mode 100644 index 9b7a81de..00000000 --- a/app/src/main/java/com/brainwallet/tools/manager/BRApiManager.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.brainwallet.tools.manager; - -import static com.brainwallet.tools.util.BRConstants.BW_API_DEV_HOST; -import static com.brainwallet.tools.util.BRConstants.BW_API_PROD_HOST; -import static com.brainwallet.tools.util.BRConstants._20230113_BAC; -import static com.brainwallet.tools.util.BRConstants._20250222_PAC; - -import android.app.Activity; -import android.content.Context; -import android.os.Handler; - -import com.brainwallet.BuildConfig; -import com.brainwallet.data.model.CurrencyEntity; -import com.brainwallet.tools.sqlite.CurrencyDataSource; -import com.brainwallet.tools.threads.BRExecutor; -import com.brainwallet.tools.util.Utils; -import com.brainwallet.data.source.RemoteConfigSource; -import com.platform.APIClient; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; - -import okhttp3.Request; -import okhttp3.Response; -import timber.log.Timber; - -public class BRApiManager { - private Timer timer; - - private TimerTask timerTask; - - private Handler handler; - - private RemoteConfigSource remoteConfigSource; - - public BRApiManager(RemoteConfigSource remoteConfigSource) { - this.remoteConfigSource = remoteConfigSource; - this.handler = new Handler(); - } - - private Set getCurrencies(Activity context) { - Set set = new LinkedHashSet<>(); - try { - JSONArray arr = fetchRates(context); - FeeManager.updateFeePerKb(context); - if (arr != null) { - String selectedISO = BRSharedPrefs.getIsoSymbol(context); - int length = arr.length(); - for (int i = 0; i < length; i++) { - CurrencyEntity tempCurrencyEntity = new CurrencyEntity(); - try { - JSONObject tmpJSONObj = (JSONObject) arr.get(i); - tempCurrencyEntity.name = tmpJSONObj.getString("name"); - tempCurrencyEntity.code = tmpJSONObj.getString("code"); - tempCurrencyEntity.rate = (float) tmpJSONObj.getDouble("n"); - tempCurrencyEntity.symbol = new String(""); - if (tempCurrencyEntity.code.equalsIgnoreCase(selectedISO)) { - BRSharedPrefs.putIso(context, tempCurrencyEntity.code); - BRSharedPrefs.putCurrencyListPosition(context, i - 1); - } - set.add(tempCurrencyEntity); - } catch (JSONException e) { - Timber.e(e); - } - } - } else { - Timber.d("timber: getCurrencies: failed to get currencies"); - } - } catch (Exception e) { - Timber.e(e); - } - List tempList = new ArrayList<>(set); - Collections.reverse(tempList); - return new LinkedHashSet<>(set); - } - - - private void initializeTimerTask(final Context context) { - timerTask = new TimerTask() { - public void run() { - //use a handler to run a toast that shows the current timestamp - handler.post(new Runnable() { - public void run() { - BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(new Runnable() { - @Override - public void run() { - Set tmp = getCurrencies((Activity) context); - CurrencyDataSource.getInstance(context).putCurrencies(tmp); - } - }); - } - }); - } - }; - } - - public void startTimer(Context context) { - //set a new Timer - if (timer != null) return; - timer = new Timer(); - - //initialize the TimerTask's job - initializeTimerTask(context); - - //schedule the timer, after the first 0ms the TimerTask will run every 60000ms - timer.schedule(timerTask, 0, 4000); - } - - public JSONArray fetchRates(Activity activity) { - String jsonString = createGETRequestURL(activity, BW_API_PROD_HOST + "/api/v1/rates"); - JSONArray jsonArray = null; - if (jsonString == null) return null; - try { - jsonArray = new JSONArray(jsonString); - // DEV Uncomment to view values - // Timber.d("timber: baseUrlProd: %s", getBaseUrlProd()); - // Timber.d("timber: JSON %s",jsonArray.toString()); - } catch (JSONException ex) { - Timber.e(ex); - } - if (jsonArray != null && !BuildConfig.DEBUG) { - AnalyticsManager.logCustomEvent(_20250222_PAC); - } - return jsonArray == null ? backupFetchRates(activity) : jsonArray; - } - - public JSONArray backupFetchRates(Activity activity) { - String jsonString = createGETRequestURL(activity, BW_API_DEV_HOST + "/api/v1/rates"); - - JSONArray jsonArray = null; - if (jsonString == null) return null; - try { - jsonArray = new JSONArray(jsonString); - - } catch (JSONException e) { - Timber.e(e); - } - if (jsonArray != null && !BuildConfig.DEBUG) { - AnalyticsManager.logCustomEvent(_20230113_BAC); - } - - return jsonArray; - } - - // createGETRequestURL - // Creates the params and headers to make a GET Request - private 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")) - .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; - } - ///Set timestamp to prefs - long timeStamp = new Date().getTime(); - BRSharedPrefs.putSecureTime(app, timeStamp); - - assert resp.body() != null; - response = resp.body().string(); - - } catch (IOException e) { - Timber.e(e); - } finally { - if (resp != null) resp.close(); - } - return response; - } - - public String getBaseUrlProd() { - return BW_API_PROD_HOST; - } -} 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 3ca79a7b..91e12a9e 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java @@ -95,6 +95,7 @@ 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) diff --git a/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt b/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt new file mode 100644 index 00000000..739da912 --- /dev/null +++ b/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt @@ -0,0 +1,32 @@ +package com.brainwallet.worker + +import com.brainwallet.data.repository.LtcRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +class CurrencyUpdateWorker( + private val ltcRepository: LtcRepository +) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var job: Job? = null + + fun start() { + if (job?.isActive == true && job != null) { + job?.cancel() + } + + job = scope.launch(Dispatchers.IO) { + while (isActive) { + ltcRepository.fetchRates() + delay(4000L) //4secs + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/platform/APIClient.java b/app/src/main/java/com/platform/APIClient.java index da333b08..d55592cf 100644 --- a/app/src/main/java/com/platform/APIClient.java +++ b/app/src/main/java/com/platform/APIClient.java @@ -35,6 +35,7 @@ import static com.brainwallet.tools.util.BRCompressor.gZipExtract; +@Deprecated public class APIClient { // proto is the transport protocol to use for talking to the API (either http or https) diff --git a/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt b/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt index ba5c19ec..9e997f57 100644 --- a/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt +++ b/app/src/test/java/com/brainwallet/currency/BackupRateFetchTests.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Context import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.presenter.activities.util.ActivityUTILS -import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.util.BRConstants import com.brainwallet.tools.util.Utils import com.platform.APIClient @@ -20,74 +19,75 @@ import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Before import org.junit.Test +//TODO: need update this test after refactor class BackupRateFetchTests { - private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: BRApiManager - - @Before - fun setUp() { - apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) - } - - @Test - fun `invoke backupFetchRates, should return success with parsed JSONArray`() { - val activity: Activity = mockk(relaxed = true) - val responseString = """ - [ - { - "code" : "CZK", - "price" : "Kč3065.541255", - "name" : "Czech Republic Koruna", - "n" : 3065.541255 - }, - { - "code" : "KRW", - "price" : "₩183797.935875", - "name" : "South Korean Won", - "n" : 183797.935875 - }, - { - "code" : "BYN", - "price" : "Br418.909259625", - "n" : 418.909259625 - }, - { - "code" : "CNY", - "price" : "CN¥927.7974375", - "name" : "Chinese Yuan", - "n" : 927.7974375 - } - ] - """.trimIndent() - mockkStatic(ActivityUTILS::class) - mockkObject(APIClient.getInstance(activity)) - every { - remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_PROD_NEW_ENABLED) - } returns false - every { - apiManager invoke "createGETRequestURL" withArguments (listOf( - activity as Context, - BRConstants.BW_API_DEV_HOST - )) - } returns responseString - every { ActivityUTILS.isMainThread() } returns false - every { APIClient.getInstance(activity).getCurrentLocale(activity) } returns "en" - - val request = Request.Builder() - .url(BRConstants.BW_API_DEV_HOST) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .header("User-agent", Utils.getAgentString(activity, "android/HttpURLConnection")) - .get().build() - every { - APIClient.getInstance(activity).sendRequest(request, false, 0) - } returns Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("OK") - .body(responseString.toResponseBody()) - .build() - - } +// private val remoteConfigSource: RemoteConfigSource = mockk() +// private lateinit var apiManager: BRApiManager +// +// @Before +// fun setUp() { +// apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) +// } +// +// @Test +// fun `invoke backupFetchRates, should return success with parsed JSONArray`() { +// val activity: Activity = mockk(relaxed = true) +// val responseString = """ +// [ +// { +// "code" : "CZK", +// "price" : "Kč3065.541255", +// "name" : "Czech Republic Koruna", +// "n" : 3065.541255 +// }, +// { +// "code" : "KRW", +// "price" : "₩183797.935875", +// "name" : "South Korean Won", +// "n" : 183797.935875 +// }, +// { +// "code" : "BYN", +// "price" : "Br418.909259625", +// "n" : 418.909259625 +// }, +// { +// "code" : "CNY", +// "price" : "CN¥927.7974375", +// "name" : "Chinese Yuan", +// "n" : 927.7974375 +// } +// ] +// """.trimIndent() +// mockkStatic(ActivityUTILS::class) +// mockkObject(APIClient.getInstance(activity)) +// every { +// remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_PROD_NEW_ENABLED) +// } returns false +// every { +// apiManager invoke "createGETRequestURL" withArguments (listOf( +// activity as Context, +// BRConstants.BW_API_DEV_HOST +// )) +// } returns responseString +// every { ActivityUTILS.isMainThread() } returns false +// every { APIClient.getInstance(activity).getCurrentLocale(activity) } returns "en" +// +// val request = Request.Builder() +// .url(BRConstants.BW_API_DEV_HOST) +// .header("Content-Type", "application/json") +// .header("Accept", "application/json") +// .header("User-agent", Utils.getAgentString(activity, "android/HttpURLConnection")) +// .get().build() +// every { +// APIClient.getInstance(activity).sendRequest(request, false, 0) +// } returns Response.Builder() +// .request(request) +// .protocol(Protocol.HTTP_1_1) +// .code(200) +// .message("OK") +// .body(responseString.toResponseBody()) +// .build() +// +// } } \ No newline at end of file diff --git a/app/src/test/java/com/brainwallet/data/BaseURLTests.kt b/app/src/test/java/com/brainwallet/data/BaseURLTests.kt index d0163299..f0423f90 100644 --- a/app/src/test/java/com/brainwallet/data/BaseURLTests.kt +++ b/app/src/test/java/com/brainwallet/data/BaseURLTests.kt @@ -1,7 +1,6 @@ package com.brainwallet.data import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.tools.manager.BRApiManager import com.brainwallet.tools.util.BRConstants import io.mockk.every import io.mockk.mockk @@ -10,24 +9,25 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +//TODO: need update this test after refactor class BaseURLTests { - private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: BRApiManager - - @Before - fun setUp() { - apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) - } - - @Test - fun `invoke getBaseUrlProd with KEY_API_BASEURL_PROD_NEW_ENABLED true, then should return new baseUrlProd`() { - every { remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_PROD_NEW_ENABLED) } returns true - - val actual = apiManager.baseUrlProd - - assertEquals(BRConstants.BW_API_PROD_HOST, actual) - } +// private val remoteConfigSource: RemoteConfigSource = mockk() +// private lateinit var apiManager: BRApiManager +// +// @Before +// fun setUp() { +// apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) +// } +// +// @Test +// fun `invoke getBaseUrlProd with KEY_API_BASEURL_PROD_NEW_ENABLED true, then should return new baseUrlProd`() { +// every { remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_PROD_NEW_ENABLED) } returns true +// +// val actual = apiManager.baseUrlProd +// +// assertEquals(BRConstants.BW_API_PROD_HOST, actual) +// } } diff --git a/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt b/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt index 2386d1f0..1dfee111 100644 --- a/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt +++ b/app/src/test/java/com/brainwallet/tools/util/ProdAPIManagerTests.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Context import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.presenter.activities.util.ActivityUTILS -import com.brainwallet.tools.manager.BRApiManager import com.platform.APIClient import io.mockk.every import io.mockk.mockk @@ -20,89 +19,90 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +//TODO: need update this test after refactor class ProdAPIManagerTests { - private val remoteConfigSource: RemoteConfigSource = mockk() - private lateinit var apiManager: BRApiManager - - @Before - fun setUp() { - apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) - } - - @Test - fun `invoke fetchRates, should return success with parsed JSONArray`() { - val activity: Activity = mockk(relaxed = true) - - val responseString = """ - [ - { - "code": "USD", - "n": 416.81128312406213, - "price": "USD416.811283124062145364", - "name": "US Dollar" - }, - { - "code": "EUR", - "n": 7841.21263788453, - "price": "Af7841.212637884529266812", - "name": "Euro" - }, - { - "code": "GBP", - "n": 10592.359754930994, - "price": "ALL10592.359754930995026136", - "name": "British Pound" - } - ] - """.trimIndent() - mockkStatic(ActivityUTILS::class) - mockkObject(APIClient.getInstance(activity)) - every { - remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_PROD_NEW_ENABLED) - } returns false - every { - apiManager invoke "createGETRequestURL" withArguments (listOf( - activity as Context, - BRConstants.BW_API_PROD_HOST - )) - } returns responseString - every { ActivityUTILS.isMainThread() } returns false - every { APIClient.getInstance(activity).getCurrentLocale(activity) } returns "en" - - val request = Request.Builder() - .url(BRConstants.BW_API_PROD_HOST) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .header("User-agent", Utils.getAgentString(activity, "android/HttpURLConnection")) - .get().build() - every { - APIClient.getInstance(activity).sendRequest(request, false, 0) - } returns Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("OK") - .body(responseString.toResponseBody()) - .build() - - val result = apiManager.fetchRates(activity) - val jsonUSD = result.getJSONObject(154) - val jsonEUR = result.getJSONObject(49) - val jsonGBP = result.getJSONObject(52) - - assertEquals("USD", jsonUSD.optString("code")) - assertEquals("US Dollar", jsonUSD.optString("name")) - assertEquals("EUR", jsonEUR.optString("code")) - assertEquals("Euro", jsonEUR.optString("name")) - assertEquals("GBP", jsonGBP.optString("code")) - assertEquals("British Pound Sterling", jsonGBP.optString("name")) - - ///DEV: Very flaky test not enough time for the response - verifyAll { - ActivityUTILS.isMainThread() - APIClient.getInstance(activity).getCurrentLocale(activity) - APIClient.getInstance(activity).sendRequest(any(), any(), any()) - } - - } +// private val remoteConfigSource: RemoteConfigSource = mockk() +// private lateinit var apiManager: BRApiManager +// +// @Before +// fun setUp() { +// apiManager = spyk(BRApiManager(remoteConfigSource), recordPrivateCalls = true) +// } +// +// @Test +// fun `invoke fetchRates, should return success with parsed JSONArray`() { +// val activity: Activity = mockk(relaxed = true) +// +// val responseString = """ +// [ +// { +// "code": "USD", +// "n": 416.81128312406213, +// "price": "USD416.811283124062145364", +// "name": "US Dollar" +// }, +// { +// "code": "EUR", +// "n": 7841.21263788453, +// "price": "Af7841.212637884529266812", +// "name": "Euro" +// }, +// { +// "code": "GBP", +// "n": 10592.359754930994, +// "price": "ALL10592.359754930995026136", +// "name": "British Pound" +// } +// ] +// """.trimIndent() +// mockkStatic(ActivityUTILS::class) +// mockkObject(APIClient.getInstance(activity)) +// every { +// remoteConfigSource.getBoolean(RemoteConfigSource.KEY_API_BASEURL_PROD_NEW_ENABLED) +// } returns false +// every { +// apiManager invoke "createGETRequestURL" withArguments (listOf( +// activity as Context, +// BRConstants.BW_API_PROD_HOST +// )) +// } returns responseString +// every { ActivityUTILS.isMainThread() } returns false +// every { APIClient.getInstance(activity).getCurrentLocale(activity) } returns "en" +// +// val request = Request.Builder() +// .url(BRConstants.BW_API_PROD_HOST) +// .header("Content-Type", "application/json") +// .header("Accept", "application/json") +// .header("User-agent", Utils.getAgentString(activity, "android/HttpURLConnection")) +// .get().build() +// every { +// APIClient.getInstance(activity).sendRequest(request, false, 0) +// } returns Response.Builder() +// .request(request) +// .protocol(Protocol.HTTP_1_1) +// .code(200) +// .message("OK") +// .body(responseString.toResponseBody()) +// .build() +// +// val result = apiManager.fetchRates(activity) +// val jsonUSD = result.getJSONObject(154) +// val jsonEUR = result.getJSONObject(49) +// val jsonGBP = result.getJSONObject(52) +// +// assertEquals("USD", jsonUSD.optString("code")) +// assertEquals("US Dollar", jsonUSD.optString("name")) +// assertEquals("EUR", jsonEUR.optString("code")) +// assertEquals("Euro", jsonEUR.optString("name")) +// assertEquals("GBP", jsonGBP.optString("code")) +// assertEquals("British Pound Sterling", jsonGBP.optString("name")) +// +// ///DEV: Very flaky test not enough time for the response +// verifyAll { +// ActivityUTILS.isMainThread() +// APIClient.getInstance(activity).getCurrentLocale(activity) +// APIClient.getInstance(activity).sendRequest(any(), any(), any()) +// } +// +// } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f4ab271f..90f8dedd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,8 @@ google-zxing = "3.5.2" google-play-asset-delivery = "2.2.2" google-play-feature-delivery = "2.1.0" google-play-review = "2.0.1" -squareup-okhttp = "4.12.0" +squareup-okhttp-bom = "4.12.0" +squareup-retrofit = "2.11.0" firebase-bom = "32.7.1" jakewarthon-timber = "4.7.1" eclipse-jetty = "9.2.19.v20160908" @@ -68,8 +69,6 @@ androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui- androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-work = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.1" } -#google-dagger = { module = "com.google.dagger:dagger", version.ref = "google-dagger" } -#google-dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "google-dagger" } google-zxing = { module = "com.google.zxing:core", version.ref = "google-zxing" } google-material = { module = "com.google.android.material:material", version.ref = "google-material" } google-play-asset-delivery = { module = "com.google.android.play:asset-delivery", version.ref = "google-play-asset-delivery" } @@ -78,7 +77,11 @@ google-play-feature-delivery = { module = "com.google.android.play:feature-deliv google-play-feature-delivery-ktx = { module = "com.google.android.play:feature-delivery-ktx", version.ref = "google-play-feature-delivery" } google-play-review = { module = "com.google.android.play:review", version.ref = "google-play-review" } google-play-review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "google-play-review" } -squareup-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "squareup-okhttp" } +squareup-okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "squareup-okhttp-bom" } +squareup-okhttp = { module = "com.squareup.okhttp3:okhttp" } +squareup-okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" } +squareup-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "squareup-retrofit" } +squareup-retrofit-kotlinx-serialization-json = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "squareup-retrofit" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } firebase-analytics = { module = "com.google.firebase:firebase-analytics" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } @@ -123,6 +126,8 @@ eclipse-jetty = ["eclipse-jetty-webapp", "eclipse-jetty-websocket", "eclipse-jet android-test = ["androidx-test-core", "androidx-test-core-ktx", "androidx-test-rules","androidx-test-espresso-core", "androidx-test-junit-ext","androidx-test-juniext-ext-ktx", "androidx-test-runner", "androidx-test-uiautomator"] androidx-compose-ui-test = ["androidx-compose-ui-test-junit4", "androidx-compose-ui-test-manifest"] koin = ["koin-android", "koin-android-compat", "koin-compose", "koin-compose-viewmodel"] +squareup-retrofit = ["squareup-retrofit", "squareup-retrofit-kotlinx-serialization-json"] +squareup-okhttp = ["squareup-okhttp", "squareup-okhttp-logging-interceptor"] [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 0aa41e77d719adf9524f2ec5a20644722dbc560d Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Mon, 14 Apr 2025 13:25:26 +0700 Subject: [PATCH 14/44] chore: remove unused part at APIClient --- app/src/main/java/com/platform/APIClient.java | 99 +++---------------- 1 file changed, 15 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/platform/APIClient.java b/app/src/main/java/com/platform/APIClient.java index d55592cf..90b22f68 100644 --- a/app/src/main/java/com/platform/APIClient.java +++ b/app/src/main/java/com/platform/APIClient.java @@ -1,24 +1,17 @@ package com.platform; -import android.annotation.TargetApi; +import static com.brainwallet.tools.util.BRCompressor.gZipExtract; + import android.content.Context; -import android.content.pm.ApplicationInfo; import android.net.Uri; -import android.os.Build; import android.os.NetworkOnMainThreadException; import com.brainwallet.BrainwalletApp; -import com.brainwallet.BrainwalletApp; -import com.brainwallet.BuildConfig; import com.brainwallet.presenter.activities.util.ActivityUTILS; -import com.brainwallet.tools.util.BRConstants; import com.brainwallet.tools.util.Utils; -import org.json.JSONException; -import org.json.JSONObject; - import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -30,11 +23,8 @@ import okhttp3.Response; import okhttp3.ResponseBody; import timber.log.Timber; -import com.brainwallet.tools.manager.AnalyticsManager; -import com.brainwallet.tools.util.BRConstants; - -import static com.brainwallet.tools.util.BRCompressor.gZipExtract; +//some part still used e.g. [sendRequest] @Deprecated public class APIClient { @@ -55,12 +45,12 @@ public class APIClient { private static final boolean PRINT_FILES = false; - private SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + private final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - private boolean platformUpdating = false; + private final boolean platformUpdating = false; private AtomicInteger itemsLeftToUpdate = new AtomicInteger(0); - private Context ctx; + private final Context ctx; public static synchronized APIClient getInstance(Context context) { if (ourInstance == null) ourInstance = new APIClient(context); @@ -72,42 +62,14 @@ private APIClient(Context context) { itemsLeftToUpdate = new AtomicInteger(0); } - //returns the fee per kb or 0 if something went wrong - public long feePerKb() { - if (ActivityUTILS.isMainThread()) { - throw new NetworkOnMainThreadException(); - } - Response response = null; - try { - String strUtl = BASE_URL + FEE_PER_KB_URL; - Request request = new Request.Builder().url(strUtl).get().build(); - String body = null; - try { - response = sendRequest(request, false, 0); - body = response.body().string(); - Timber.d("timber: fee per kb %s",body); - } catch (IOException e) { - Timber.e(e); - AnalyticsManager.logCustomEvent(BRConstants._20200111_RNI); - } - JSONObject object = null; - object = new JSONObject(body); - return (long) object.getInt("fee_per_kb"); - } catch (JSONException e) { - Timber.e(e); - } finally { - if (response != null) response.close(); - } - return 0; - } - + // sendRequest still using, e.g. inside RemoteKVStore public Response sendRequest(Request locRequest, boolean needsAuth, int retryCount) { if (retryCount > 1) throw new RuntimeException("sendRequest: Warning retryCount is: " + retryCount); if (ActivityUTILS.isMainThread()) { throw new NetworkOnMainThreadException(); } - String lang = getCurrentLocale(ctx); + String lang = ctx.getResources().getConfiguration().locale.getLanguage(); Request request = locRequest.newBuilder() .header("X-Litecoin-Testnet", "false") .header("Accept-Language", lang) @@ -154,23 +116,15 @@ public Response sendRequest(Request locRequest, boolean needsAuth, int retryCoun Timber.d("timber: sendRequest: the content is gzip, unzipping"); byte[] decompressed = gZipExtract(data); postReqBody = ResponseBody.create(null, decompressed); - try { - if (response.code() != 200) { - Timber.d("timber: sendRequest: (%s)%s, code (%d), mess (%s), body (%s)", request.method(), - request.url(), response.code(), response.message(), new String(decompressed, "utf-8")); - } - } catch (UnsupportedEncodingException e) { - Timber.e(e); + if (response.code() != 200) { + Timber.d("timber: sendRequest: (%s)%s, code (%d), mess (%s), body (%s)", request.method(), + request.url(), response.code(), response.message(), new String(decompressed, StandardCharsets.UTF_8)); } return response.newBuilder().body(postReqBody).build(); } else { - try { - if (response.code() != 200) { - Timber.d("timber: sendRequest: (%s)%s, code (%d), mess (%s), body (%s)", request.method(), - request.url(), response.code(), response.message(), new String(data, "utf-8")); - } - } catch (UnsupportedEncodingException e) { - Timber.e(e); + if (response.code() != 200) { + Timber.d("timber: sendRequest: (%s)%s, code (%d), mess (%s), body (%s)", request.method(), + request.url(), response.code(), response.message(), new String(data, StandardCharsets.UTF_8)); } } @@ -178,27 +132,4 @@ public Response sendRequest(Request locRequest, boolean needsAuth, int retryCoun return response.newBuilder().body(postReqBody).build(); } - - public String buildUrl(String path) { - return BASE_URL + path; - } - - private void itemFinished() { - int items = itemsLeftToUpdate.incrementAndGet(); - if (items >= 4) { - Timber.d("timber: PLATFORM ALL UPDATED: %s", items); - platformUpdating = false; - itemsLeftToUpdate.set(0); - } - } - - @TargetApi(Build.VERSION_CODES.N) - public String getCurrentLocale(Context ctx) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return ctx.getResources().getConfiguration().getLocales().get(0).getLanguage(); - } else { - //noinspection deprecation - return ctx.getResources().getConfiguration().locale.getLanguage(); - } - } } From 0cc29cbcd9ec8ffca56d5c6bb4da98dd8a0882a7 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Mon, 14 Apr 2025 15:41:13 +0700 Subject: [PATCH 15/44] feat: wip new peer discovery --- .../repository/SelectedPeersRepository.kt | 81 +++++++++++++++++++ .../data/source/RemoteConfigSource.kt | 4 +- .../main/java/com/brainwallet/di/Module.kt | 2 + .../com/brainwallet/wallet/BRPeerManager.java | 37 ++++++++- .../brainwallet/wallet/BRWalletManager.java | 2 +- .../main/res/xml/remote_config_defaults.xml | 10 +-- 6 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt diff --git a/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt new file mode 100644 index 00000000..766cfb7d --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt @@ -0,0 +1,81 @@ +package com.brainwallet.data.repository + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.brainwallet.di.json +import kotlinx.serialization.json.jsonObject +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Response +import okio.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +interface SelectedPeersRepository { + + suspend fun fetchSelectedPeers(): Set + + class Impl( + private val okHttpClient: OkHttpClient, + private val sharedPreferences: SharedPreferences, + ) : SelectedPeersRepository { + + private companion object { + const val PREF_KEY_SELECTED_PEERS = "selected_peers" + const val PREF_KEY_SELECTED_PEERS_CACHED_AT = "${PREF_KEY_SELECTED_PEERS}_cached_at" + } + + override suspend fun fetchSelectedPeers(): Set { + val lastUpdateTime = sharedPreferences.getLong(PREF_KEY_SELECTED_PEERS_CACHED_AT, 0) + val currentTime = System.currentTimeMillis() + val cachedPeers = sharedPreferences.getStringSet(PREF_KEY_SELECTED_PEERS, null) + + // Check if cache exists and is less than 6 hours old + if (!cachedPeers.isNullOrEmpty() && (currentTime - lastUpdateTime) < 6 * 60 * 60 * 1000) { + return cachedPeers + } + + val request = okhttp3.Request.Builder() + .url(LITECOIN_NODES_URL) + .build() + + return suspendCoroutine { continuation -> + okHttpClient.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + continuation.resume(emptySet()) //just return empty if failed or need hardcoded? + } + + override fun onResponse(call: Call, response: Response) { + val jsonString = response.body?.string() + + val parsedResult = jsonString?.let { + val jsonElement = json.parseToJsonElement(it) + val dataObject = jsonElement.jsonObject["data"]?.jsonObject + val nodesObject = dataObject?.get("nodes")?.jsonObject + + //TODO: need filter criteria here? + nodesObject?.keys + ?.filter { it.endsWith(":9333") } + ?.map { it.replace(":9333", "") } + ?.toSet() + ?: emptySet() + } ?: emptySet() + + sharedPreferences.edit { + putStringSet(PREF_KEY_SELECTED_PEERS, parsedResult) + putLong(PREF_KEY_SELECTED_PEERS_CACHED_AT, currentTime) + } + + continuation.resume(parsedResult) + } + }) + } + } + + } + + companion object { + const val LITECOIN_NODES_URL = "https://api.blockchair.com/litecoin/nodes" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt b/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt index 058d8dc9..dc5c7ff4 100644 --- a/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt +++ b/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt @@ -13,9 +13,7 @@ interface RemoteConfigSource { companion object { const val KEY_FEATURE_MENU_HIDDEN_EXAMPLE = "feature_menu_hidden_example" - const val KEY_API_BASEURL_PROD_NEW_ENABLED = "key_api_baseurl_prod_new_enabled" - const val KEY_API_BASEURL_DEV_NEW_ENABLED = "key_api_baseurl_dev_new_enabled" - const val KEY_KEYSTORE_MANAGER_ENABLED = "key_keystore_manager_enabled" + const val KEY_FEATURE_SELECTED_PEERS_ENABLED = "feature_selected_peers_enabled" } fun initialize() diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index 2da6b071..e64e071a 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -7,6 +7,7 @@ import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.repository.SettingRepository import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.data.source.RemoteConfigSource +import com.brainwallet.data.repository.SelectedPeersRepository import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.tools.util.BRConstants import com.brainwallet.ui.screens.home.SettingsViewModel @@ -52,6 +53,7 @@ val dataModule = module { it.initialize() } } + single { SelectedPeersRepository.Impl(get(), get()) } single { CurrencyDataSource.getInstance(get()) } single { provideSharedPreferences(context = androidApplication()) } single { SettingRepository.Impl(get(), get()) } diff --git a/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java b/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java index 0cc46e61..a27938b8 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java @@ -1,8 +1,12 @@ package com.brainwallet.wallet; +import static com.brainwallet.data.source.RemoteConfigSource.KEY_FEATURE_SELECTED_PEERS_ENABLED; + import android.content.Context; import com.brainwallet.BrainwalletApp; +import com.brainwallet.data.repository.SelectedPeersRepository; +import com.brainwallet.data.source.RemoteConfigSource; import com.brainwallet.presenter.entities.BlockEntity; import com.brainwallet.presenter.entities.PeerEntity; import com.brainwallet.tools.manager.BRSharedPrefs; @@ -12,10 +16,18 @@ import com.brainwallet.tools.threads.BRExecutor; import com.brainwallet.tools.util.TrustedNode; +import org.koin.java.KoinJavaComponent; + import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.CoroutineScopeKt; +import kotlinx.coroutines.CoroutineStart; +import kotlinx.coroutines.future.FutureKt; import timber.log.Timber; public class BRPeerManager { @@ -164,7 +176,7 @@ public void updateFixedPeer(Context ctx) { } else { Timber.d("timber: updateFixedPeer: succeeded"); } - connect(); + wrapConnectV2(); } public void networkChanged(boolean isOnline) { @@ -172,11 +184,21 @@ public void networkChanged(boolean isOnline) { BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(new Runnable() { @Override public void run() { - BRPeerManager.getInstance().connect(); + wrapConnectV2(); } }); } + //wrap logic enable/disable connect with new flow + public void wrapConnectV2() { + RemoteConfigSource remoteConfigSource = KoinJavaComponent.get(RemoteConfigSource.class); + if (remoteConfigSource.getBoolean(KEY_FEATURE_SELECTED_PEERS_ENABLED)) { + fetchSelectedPeers().whenComplete((strings, throwable) -> connect()); + } else { + connect(); + } + } + public void addStatusUpdateListener(OnTxStatusUpdate listener) { if (statusUpdateListeners.contains(listener)) return; statusUpdateListeners.add(listener); @@ -186,6 +208,17 @@ public void removeListener(OnTxStatusUpdate listener) { statusUpdateListeners.remove(listener); } + public CompletableFuture> fetchSelectedPeers() { + SelectedPeersRepository selectedPeersRepository = KoinJavaComponent.get(SelectedPeersRepository.class); + + return FutureKt.future( + CoroutineScopeKt.CoroutineScope(EmptyCoroutineContext.INSTANCE), + EmptyCoroutineContext.INSTANCE, + CoroutineStart.DEFAULT, + (coroutineScope, continuation) -> selectedPeersRepository.fetchSelectedPeers(continuation) + ); + } + public static void setOnSyncFinished(OnSyncSucceeded listener) { onSyncFinished = listener; } diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index 792fad8e..d5aee0d2 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -547,7 +547,7 @@ public void initWallet(final Context ctx) { BRPeerManager.getInstance().updateFixedPeer(ctx); } - pm.connect(); + pm.wrapConnectV2(); if (BRSharedPrefs.getStartHeight(ctx) == 0) { BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(new Runnable() { @Override diff --git a/app/src/main/res/xml/remote_config_defaults.xml b/app/src/main/res/xml/remote_config_defaults.xml index e9f361c4..6852653a 100644 --- a/app/src/main/res/xml/remote_config_defaults.xml +++ b/app/src/main/res/xml/remote_config_defaults.xml @@ -1,15 +1,7 @@ - key_api_baseurl_prod_new_enabled - false - - - key_api_baseurl_dev_new_enabled - false - - - key_keystore_manager_enabled + feature_selected_peers_enabled true From 54d2d9ef348caf53948423c9da73ccd617504541 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Tue, 15 Apr 2025 04:51:09 +0700 Subject: [PATCH 16/44] feat: implement selected peer ip address from cache (fetched from API) --- .../com/brainwallet/wallet/BRPeerManager.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java b/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java index a27938b8..209814e7 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import kotlin.coroutines.EmptyCoroutineContext; import kotlinx.coroutines.CoroutineScopeKt; @@ -191,14 +192,18 @@ public void run() { //wrap logic enable/disable connect with new flow public void wrapConnectV2() { - RemoteConfigSource remoteConfigSource = KoinJavaComponent.get(RemoteConfigSource.class); - if (remoteConfigSource.getBoolean(KEY_FEATURE_SELECTED_PEERS_ENABLED)) { + if (featureSelectedPeersEnabled()) { fetchSelectedPeers().whenComplete((strings, throwable) -> connect()); } else { connect(); } } + public static boolean featureSelectedPeersEnabled() { + RemoteConfigSource remoteConfigSource = KoinJavaComponent.get(RemoteConfigSource.class); + return remoteConfigSource.getBoolean(KEY_FEATURE_SELECTED_PEERS_ENABLED); + } + public void addStatusUpdateListener(OnTxStatusUpdate listener) { if (statusUpdateListeners.contains(listener)) return; statusUpdateListeners.add(listener); @@ -219,6 +224,14 @@ public CompletableFuture> fetchSelectedPeers() { ); } + public static Set fetchSelectedPeersBlocking() { + try { + return BRPeerManager.getInstance().fetchSelectedPeers().get(); + } catch (ExecutionException | InterruptedException e) { + return java.util.Collections.emptySet(); + } + } + public static void setOnSyncFinished(OnSyncSucceeded listener) { onSyncFinished = listener; } From c6923b11b89de4ee4ed48ec0b4f69aa3f01b0334 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Tue, 15 Apr 2025 04:52:02 +0700 Subject: [PATCH 17/44] feat: implement selected peer ip address from cache (fetched from API) --- app/src/main/jni/transition/PeerManager.c | 106 +++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/app/src/main/jni/transition/PeerManager.c b/app/src/main/jni/transition/PeerManager.c index 48e50f92..4c2ce73e 100644 --- a/app/src/main/jni/transition/PeerManager.c +++ b/app/src/main/jni/transition/PeerManager.c @@ -226,6 +226,109 @@ static int networkIsReachable(void *info) { return (isNetworkOn == JNI_TRUE) ? 1 : 0; } +/** + * communicate with java to check if featureSelectedPeersEnabled is on + */ +static int featureSelectedPeersEnabled(void *info) { + __android_log_print(ANDROID_LOG_DEBUG, "Message from C: ", "featureSelectedPeersEnabled"); + + JNIEnv *env = getEnv(); + jmethodID mid; + jboolean isFeatureSelectedPeersOn; + + if (!env) return 0; + + //call java methods + mid = (*env)->GetStaticMethodID(env, _peerManagerClass, "featureSelectedPeersEnabled", "()Z"); + isFeatureSelectedPeersOn = (*env)->CallStaticBooleanMethod(env, _peerManagerClass, mid); + return (isFeatureSelectedPeersOn == JNI_TRUE) ? 1 : 0; +} + +/** + * obtain selected peers from BRPeerManager.fetchSelectedPeersBlocking + */ +static char **fetchSelectedPeers(void *info) { + __android_log_print(ANDROID_LOG_DEBUG, "Message from C: ", "fetchSelectedPeers"); + + JNIEnv *env = getEnv(); + if (!env) return NULL; + + jclass peerManagerClass = (*env)->FindClass(env, "com/brainwallet/wallet/BRPeerManager"); + if (!peerManagerClass) return NULL; + + jmethodID fetchSelectedPeersBlockingMethod = (*env)->GetStaticMethodID( + env, peerManagerClass, "fetchSelectedPeersBlocking", "()Ljava/util/Set;"); + if (!fetchSelectedPeersBlockingMethod) { + (*env)->DeleteLocalRef(env, peerManagerClass); + return NULL; + } + + jobject set = (*env)->CallStaticObjectMethod(env, peerManagerClass, fetchSelectedPeersBlockingMethod); + (*env)->DeleteLocalRef(env, peerManagerClass); + if (!set) return NULL; + + jclass setClass = (*env)->GetObjectClass(env, set); + jmethodID sizeMethod = (*env)->GetMethodID(env, setClass, "size", "()I"); + jmethodID iteratorMethod = (*env)->GetMethodID(env, setClass, "iterator", "()Ljava/util/Iterator;"); + if (!sizeMethod || !iteratorMethod) { + (*env)->DeleteLocalRef(env, setClass); + (*env)->DeleteLocalRef(env, set); + return NULL; + } + + jint size = (*env)->CallIntMethod(env, set, sizeMethod); + jobject iterator = (*env)->CallObjectMethod(env, set, iteratorMethod); + (*env)->DeleteLocalRef(env, setClass); + + if (!iterator) { + (*env)->DeleteLocalRef(env, set); + return NULL; + } + + jclass iteratorClass = (*env)->GetObjectClass(env, iterator); + jmethodID hasNextMethod = (*env)->GetMethodID(env, iteratorClass, "hasNext", "()Z"); + jmethodID nextMethod = (*env)->GetMethodID(env, iteratorClass, "next", "()Ljava/lang/Object;"); + if (!hasNextMethod || !nextMethod) { + (*env)->DeleteLocalRef(env, iteratorClass); + (*env)->DeleteLocalRef(env, iterator); + (*env)->DeleteLocalRef(env, set); + return NULL; + } + + char **ipAddresses = NULL; + if (size > 0) { + ipAddresses = (char **)calloc(size + 1, sizeof(char *)); // +1 for NULL-termination + if (!ipAddresses) { + (*env)->DeleteLocalRef(env, iteratorClass); + (*env)->DeleteLocalRef(env, iterator); + (*env)->DeleteLocalRef(env, set); + return NULL; + } + } + + size_t index = 0; + while ((*env)->CallBooleanMethod(env, iterator, hasNextMethod)) { + jstring element = (jstring)(*env)->CallObjectMethod(env, iterator, nextMethod); + if (!element) break; + const char *peerStr = (*env)->GetStringUTFChars(env, element, NULL); + if (peerStr) { + ipAddresses[index] = strdup(peerStr); + (*env)->ReleaseStringUTFChars(env, element, peerStr); + index++; + } + (*env)->DeleteLocalRef(env, element); + } + + if (ipAddresses) + ipAddresses[index] = NULL; // NULL-terminate + + (*env)->DeleteLocalRef(env, iteratorClass); + (*env)->DeleteLocalRef(env, iterator); + (*env)->DeleteLocalRef(env, set); + + return ipAddresses; +} + static void threadCleanup(void *info) { if (_jvmPM) (*_jvmPM)->DetachCurrentThread(_jvmPM); @@ -279,9 +382,10 @@ Java_com_brainwallet_wallet_BRPeerManager_create(JNIEnv *env, jobject thiz, _peerManager = BRPeerManagerNew(&BR_CHAIN_PARAMS, _wallet, (uint32_t) earliestKeyTime, _blocks, (size_t) blocksCount, _peers, (size_t) peersCount, (double) fpRate); + BRPeerManagerSetCallbacks(_peerManager, NULL, syncStarted, syncStopped, txStatusUpdate, - saveBlocks, savePeers, networkIsReachable, threadCleanup); + saveBlocks, savePeers, networkIsReachable, threadCleanup, featureSelectedPeersEnabled, fetchSelectedPeers); } if (_peerManager == NULL) { From ef4e6d2744eb29851db792604e22656477bd9bb2 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Wed, 16 Apr 2025 05:30:37 +0700 Subject: [PATCH 18/44] feat: filter out peers with NODE_NETWORK, NODE_BLOOM --- .../data/repository/SelectedPeersRepository.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt index 766cfb7d..008453a0 100644 --- a/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt @@ -9,6 +9,7 @@ import okhttp3.Callback import okhttp3.OkHttpClient import okhttp3.Response import okio.IOException +import timber.log.Timber import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -54,12 +55,19 @@ interface SelectedPeersRepository { val dataObject = jsonElement.jsonObject["data"]?.jsonObject val nodesObject = dataObject?.get("nodes")?.jsonObject - //TODO: need filter criteria here? - nodesObject?.keys - ?.filter { it.endsWith(":9333") } - ?.map { it.replace(":9333", "") } - ?.toSet() + //filter criteria + val requiredServices = 0x01 or 0x04 // NODE_NETWORK | NODE_BLOOM + + nodesObject?.entries + ?.filter { entry -> + val flags = + entry.value.jsonObject["flags"]?.toString()?.toIntOrNull() + flags != null && (flags and requiredServices) == requiredServices + } + ?.map { it.key.replace(":9333", "") } + ?.toSet().also { Timber.d("Total Selected Peers ${it?.size}") } ?: emptySet() + } ?: emptySet() sharedPreferences.edit { From 23369394c110dc5c310efa3d01645b24c2c1ead5 Mon Sep 17 00:00:00 2001 From: andhikayuana Date: Wed, 16 Apr 2025 12:19:12 +0700 Subject: [PATCH 19/44] fix: race condition when clear shared prefs values after wipeAll --- app/src/main/java/com/brainwallet/wallet/BRWalletManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index 792fad8e..42d460d9 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -232,12 +232,12 @@ public void wipeWalletButKeystore(final Context ctx) { @Override public void run() { Timber.d("timber: Running peerManagerFreeEverything"); + BRSharedPrefs.clearAllPrefs(ctx); BRPeerManager.getInstance().peerManagerFreeEverything(); walletFreeEverything(); TransactionDataSource.getInstance(ctx).deleteAllTransactions(); MerkleBlockDataSource.getInstance(ctx).deleteAllBlocks(); PeerDataSource.getInstance(ctx).deleteAllPeers(); - BRSharedPrefs.clearAllPrefs(ctx); } }); } From 391e319fa920ea79bf4ddf00e2f4b4a89b16d241 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Wed, 16 Apr 2025 08:21:49 +0100 Subject: [PATCH 20/44] Updating the core library from the new peer discovery (v4.2.0) --- app/src/main/jni/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/jni/core b/app/src/main/jni/core index 86649c2d..55b43a86 160000 --- a/app/src/main/jni/core +++ b/app/src/main/jni/core @@ -1 +1 @@ -Subproject commit 86649c2dd89d4c1e13fb58de4aa07567f3ac11bd +Subproject commit 55b43a869300dfc82ead7faa87e71626d580b464 From 836b6f097d8953701140c3e231cc66a22e46923f Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Fri, 25 Apr 2025 16:02:14 +0700 Subject: [PATCH 21/44] Chore/revert pre peer discovery (Android) (#69) * chore: update core submodule * chore: resolve conflict * Update build.gradle.kts version and code bump * chore: for now at BRPeerManager.wrapConnectV2 only using connect, since the core using hardcoded peers * fix: fix write down confirm screen (#63) * fix: fix write down confirm screen * fix: fix allow seed word item not unique * chore: cherry picked and adjust from - f2fa8e1e4fb9c7429fa86461b24b4463a7969ecf - 98644c425b2fdae7b8b424708fb1a61ad983a9d8 - 93969278da8f95adc0c9f0f8411c8a27876a4d77 - 7ad4b9853b7d1bf7ddbaa576242d8800615a48fb - 8e57d2a67420565b40b753dec5a533776da4821f * fix: fix crash can't parse response inside LtcRepository.fetchRates * fix: fix crash (failed parse) when SelectedPeersRepository.fetchSelectedPeers got unsuccessful response * code bump * fix: ConcurrentModificationException at BRPeerManager.txStatusUpdate * feat: implement new API * code bump --------- Co-authored-by: Kerry Washington --- app/build.gradle.kts | 4 +- .../brainwallet/data/model/CurrencyEntity.kt | 27 +----- .../java/com/brainwallet/data/model/Fee.kt | 37 +++++++++ .../data/repository/LtcRepository.kt | 47 +++++++---- .../repository/SelectedPeersRepository.kt | 5 ++ .../data/source/RemoteApiSource.kt | 5 +- .../main/java/com/brainwallet/di/Module.kt | 23 +++-- .../brainwallet/presenter/entities/Fee.java | 15 ---- .../tools/manager/BRSharedPrefs.java | 83 ++++++++++++------- .../brainwallet/tools/manager/FeeManager.java | 51 +++++++----- .../tools/manager/SyncManager.java | 51 ++++-------- .../brainwallet/tools/threads/BRExecutor.java | 16 ++++ .../brainwallet/tools/util/BRConstants.java | 4 +- .../com/brainwallet/ui/BrainwalletActivity.kt | 5 -- .../ui/screens/home/SettingsEvent.kt | 5 +- .../ui/screens/home/SettingsState.kt | 1 + .../ui/screens/home/SettingsViewModel.kt | 7 +- .../home/composable/HomeSettingDrawerSheet.kt | 14 +++- .../yourseedproveit/YourSeedProveItEvent.kt | 1 + .../yourseedproveit/YourSeedProveItScreen.kt | 3 +- .../yourseedproveit/YourSeedProveItState.kt | 7 +- .../YourSeedProveItViewModel.kt | 18 ++-- .../yourseedwords/YourSeedWordsEvent.kt | 2 +- .../yourseedwords/YourSeedWordsScreen.kt | 12 ++- .../yourseedwords/YourSeedWordsViewModel.kt | 11 +-- .../com/brainwallet/wallet/BRPeerManager.java | 21 +++-- app/src/main/jni/core | 2 +- app/src/main/jni/transition/PeerManager.c | 2 +- app/src/main/res/values/strings.xml | 1 + 29 files changed, 292 insertions(+), 188 deletions(-) create mode 100644 app/src/main/java/com/brainwallet/data/model/Fee.kt delete mode 100644 app/src/main/java/com/brainwallet/presenter/entities/Fee.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b8f91239..3d4cb0e2 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 = 202503312 - versionName = "v4.4.3" + versionCode = 202504251 + versionName = "v4.4.7" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") diff --git a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt index 1ccba70c..a46406bc 100644 --- a/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt +++ b/app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt @@ -14,29 +14,4 @@ data class CurrencyEntity( var rate: Float = 0F, @JvmField var symbol: String = "" -) : Serializable { -// @JvmField -// var code: String? = null -// @JvmField -// var name: String? = null -// @JvmField -// var rate: Float = 0f -// @JvmField -// var symbol: String? = null -// -// constructor(code: String?, name: String?, rate: Float, symbol: String?) { -// this.code = code -// this.name = name -// this.rate = rate -// this.symbol = symbol -// } -// -// constructor() -// -// companion object { -// //Change this after modifying the class -// private const val serialVersionUID = 7526472295622776147L -// -// val TAG: String = CurrencyEntity::class.java.name -// } -} +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/model/Fee.kt b/app/src/main/java/com/brainwallet/data/model/Fee.kt new file mode 100644 index 00000000..246100a4 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/model/Fee.kt @@ -0,0 +1,37 @@ +package com.brainwallet.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Fee( + @JvmField + @SerialName("fee_per_kb") + var luxury: Long, + @JvmField + @SerialName("fee_per_kb_economy") + var regular: Long, + @JvmField + @SerialName("fee_per_kb_luxury") + var economy: Long, + var timestamp: Long +) { + companion object { + //from legacy + // this is the default that matches the mobile-api if the server is unavailable + private const val defaultEconomyFeePerKB: Long = + 2500L // From legacy minimum. default min is 1000 as Litecoin Core version v0.17.1 + private const val defaultRegularFeePerKB: Long = 25000L + private const val defaultLuxuryFeePerKB: Long = 66746L + private const val defaultTimestamp: Long = 1583015199122L + + @JvmStatic + val Default = Fee( + defaultLuxuryFeePerKB, + defaultRegularFeePerKB, + defaultEconomyFeePerKB, + defaultTimestamp + ) + + } +} 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 02358b34..31c05e30 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -2,6 +2,7 @@ package com.brainwallet.data.repository import android.content.Context import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.Fee import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager @@ -9,7 +10,8 @@ import com.brainwallet.tools.sqlite.CurrencyDataSource interface LtcRepository { suspend fun fetchRates(): List - //todo + + suspend fun fetchFeePerKb(): Fee class Impl( private val context: Context, @@ -19,22 +21,39 @@ interface LtcRepository { //todo: make it offline first here later, currently just using CurrencyDataSource.getAllCurrencies override suspend fun fetchRates(): List { - val rates = remoteApiSource.getRates() - - //legacy logic - FeeManager.updateFeePerKb(context) - val selectedISO = BRSharedPrefs.getIsoSymbol(context) - rates.forEachIndexed { index, currencyEntity -> - if (currencyEntity.code.equals(selectedISO, ignoreCase = true)) { - BRSharedPrefs.putIso(context, currencyEntity.code) - BRSharedPrefs.putCurrencyListPosition(context, index - 1) + return runCatching { + val rates = remoteApiSource.getRates() + + //legacy logic + FeeManager.updateFeePerKb(context) + val selectedISO = BRSharedPrefs.getIsoSymbol(context) + rates.forEachIndexed { index, currencyEntity -> + if (currencyEntity.code.equals(selectedISO, ignoreCase = true)) { + BRSharedPrefs.putIso(context, currencyEntity.code) + BRSharedPrefs.putCurrencyListPosition(context, index - 1) + } } - } - //save to local - currencyDataSource.putCurrencies(rates) - return rates + //save to local + currencyDataSource.putCurrencies(rates) + return rates + }.getOrElse { currencyDataSource.getAllCurrencies(true) } + + } + + override suspend fun fetchFeePerKb(): Fee { + return runCatching { + val fee = remoteApiSource.getFeePerKb() + + //todo: cache + + return fee + }.getOrElse { Fee.Default } } } + + companion object { + + } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt index 008453a0..0275d542 100644 --- a/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/SelectedPeersRepository.kt @@ -50,6 +50,11 @@ interface SelectedPeersRepository { override fun onResponse(call: Call, response: Response) { val jsonString = response.body?.string() + if (response.isSuccessful.not()) { + continuation.resume(cachedPeers ?: emptySet()) + return + } + val parsedResult = jsonString?.let { val jsonElement = json.parseToJsonElement(it) val dataObject = jsonElement.jsonObject["data"]?.jsonObject 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 0ce3bdc8..60c865e2 100644 --- a/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt +++ b/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt @@ -1,16 +1,17 @@ package com.brainwallet.data.source import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.Fee import retrofit2.http.GET //TODO interface RemoteApiSource { - @GET("api/v1/rates") + @GET("v1/rates") suspend fun getRates(): List @GET("v1/fee-per-kb") - suspend fun getFeePerKb() + suspend fun getFeePerKb(): Fee // https://prod.apigsltd.net/moonpay/buy?address=ltc1qjnsg3p9rt4r4vy7ncgvrywdykl0zwhkhcp8ue0&code=USD&idate=1742331930290&uid=ec51fa950b271ff3 // suspend fun getMoonPayBuy() diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index e64e071a..4dce30f1 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -4,10 +4,10 @@ import android.content.Context import android.content.SharedPreferences import com.brainwallet.BuildConfig import com.brainwallet.data.repository.LtcRepository +import com.brainwallet.data.repository.SelectedPeersRepository import com.brainwallet.data.repository.SettingRepository import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.data.repository.SelectedPeersRepository import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.tools.util.BRConstants import com.brainwallet.ui.screens.home.SettingsViewModel @@ -31,6 +31,7 @@ import org.koin.android.ext.koin.androidApplication import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module +import retrofit2.HttpException import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory @@ -46,7 +47,7 @@ val dataModule = module { factory { provideOkHttpClient() } single { provideRetrofit(get(), BRConstants.BW_API_PROD_HOST) } - single { provideApi(get()) } + single { provideApi(get()) } single { RemoteConfigSource.FirebaseImpl(Firebase.remoteConfig).also { @@ -91,17 +92,25 @@ private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() .addHeader("Content-Type", "application/json") .addHeader("X-Litecoin-Testnet", "false") .addHeader("Accept-Language", "en") -// .addHeader("User-agent",) chain.proceed(requestBuilder.build()) } .addInterceptor { chain -> val request = chain.request() runCatching { - chain.proceed(request) + 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.BW_API_DEV_HOST + request.url.encodedPath) + .url("${BRConstants.LEGACY_BW_API_DEV_HOST}/api${request.url.encodedPath}") //legacy dev api need prefix path /api .build() chain.proceed(newRequest) } @@ -129,5 +138,5 @@ internal fun provideRetrofit( ) .build() -internal fun provideApi(retrofit: Retrofit): RemoteApiSource = - retrofit.create(RemoteApiSource::class.java) \ No newline at end of file +internal inline fun provideApi(retrofit: Retrofit): T = + retrofit.create(T::class.java) \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/presenter/entities/Fee.java b/app/src/main/java/com/brainwallet/presenter/entities/Fee.java deleted file mode 100644 index bbe80465..00000000 --- a/app/src/main/java/com/brainwallet/presenter/entities/Fee.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.brainwallet.presenter.entities; - -public class Fee { - public final long luxury; - public final long regular; - public final long economy; - public final long timestamp; - - public Fee(long luxury, long regular, long economy, long timestamp) { - this.luxury = luxury; - this.regular = regular; - this.economy = economy; - this.timestamp = timestamp; - } -} diff --git a/app/src/main/java/com/brainwallet/tools/manager/BRSharedPrefs.java b/app/src/main/java/com/brainwallet/tools/manager/BRSharedPrefs.java index a3edc499..6fd054b9 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/BRSharedPrefs.java +++ b/app/src/main/java/com/brainwallet/tools/manager/BRSharedPrefs.java @@ -3,15 +3,17 @@ import android.content.Context; import android.content.SharedPreferences; -import com.brainwallet.BrainwalletApp; +import android.util.Log; + import com.brainwallet.data.repository.SettingRepository; import com.brainwallet.tools.util.BRConstants; import org.koin.java.KoinJavaComponent; -import org.koin.mp.KoinPlatformTools_jvmKt; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Currency; +import java.util.Date; import java.util.List; import java.util.Locale; @@ -45,16 +47,14 @@ public static String getIsoSymbol(Context context) { try { if (defaultLanguage == "ru") { defIso = Currency.getInstance(new Locale("ru", "RU")).getCurrencyCode(); - } - else if (defaultLanguage == "en") { + } else if (defaultLanguage == "en") { defIso = Currency.getInstance(Locale.US).getCurrencyCode(); - } - else { + } else { defIso = Currency.getInstance(Locale.getDefault()).getCurrencyCode(); } } catch (IllegalArgumentException e) { Timber.e(e); - defIso = Currency.getInstance(Locale.US).getCurrencyCode(); + defIso = Currency.getInstance(Locale.US).getCurrencyCode(); } return settingsToGet.getString(SettingRepository.KEY_FIAT_CURRENCY_CODE, defIso); //using new shared prefs used by setting repository } @@ -74,38 +74,64 @@ public static void notifyIsoChanged(String iso) { } } - ////////////////////////////////////////////////////////////////////////////// - //////////////////// Active Shared Preferences /////////////////////////////// - public static void putLastSyncTimestamp(Context activity, long time) { - SharedPreferences prefs = activity.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); + /// /////////////////////////////////////////////////////////////////////////// + /// ///////////////// Active Shared Preferences /////////////////////////////// + + public static void putStartSyncTimestamp(Context context, long time) { + if (context == null) { + Log.e("BRSharedPrefs", "Context is null in putStartSyncTimestamp!"); + return; + } + SharedPreferences prefs = context.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); - editor.putLong("lastSyncTime", time); + editor.putLong("startSyncTime", time); editor.apply(); } - public static long getLastSyncTimestamp(Context activity) { - SharedPreferences prefs = activity.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); - return prefs.getLong("lastSyncTime", 0L); + + public static long getStartSyncTimestamp(Context context) { + if (context == null) { + Log.e("BRSharedPrefs", "Context is null in getStartSyncTimestamp!"); + } + SharedPreferences startSyncTime = context.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); + return startSyncTime.getLong("startSyncTime", System.currentTimeMillis()); } - public static void putStartSyncTimestamp(Context activity, long time) { - SharedPreferences prefs = activity.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); + + public static void putEndSyncTimestamp(Context context, long time) { + + if (context == null) { + Log.e("BRSharedPrefs", "Context is null in putEndSyncTimestamp!"); + return; + } + + SharedPreferences prefs = context.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); - editor.putLong("startSyncTime", time); + editor.putLong("endSyncTime", time); editor.apply(); } - public static long getStartSyncTimestamp(Context activity) { - SharedPreferences prefs = activity.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); - return prefs.getLong("startSyncTime", 0L); + + public static String getSyncMetadata(Context context) { + SharedPreferences syncMetadata = context.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); + return syncMetadata.getString("syncMetadata", " No Sync Duration metadata"); } - public static void putSyncTimeElapsed(Context activity, long time) { - SharedPreferences prefs = activity.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); + public static void putSyncMetadata(Context activity, long startSyncTime, long endSyncTime) { + SharedPreferences prefs = activity.getSharedPreferences(BRConstants.PREFS_NAME, 0); SharedPreferences.Editor editor = prefs.edit(); - editor.putLong("syncTimeElapsed", time); + + double syncDuration = (double) (endSyncTime - startSyncTime) / 1_000.0 / 60.0; + + SimpleDateFormat sdf = new SimpleDateFormat("MMM dd HH:mm"); + Date startDate = new Date(startSyncTime); + Date endDate = new Date(endSyncTime); + + String formattedMetadata = String.format("Duration: %3.2f mins\nStarted: %d (%s)\nEnded: %d (%s)", syncDuration, startSyncTime, sdf.format(startDate), endSyncTime, sdf.format(endDate)); + editor.putString("syncMetadata", formattedMetadata); editor.apply(); } - public static long getSyncTimeElapsed(Context activity) { - SharedPreferences prefs = activity.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); - return prefs.getLong("syncTimeElapsed", 0L); + + public static long getEndSyncTimestamp(Context context) { + SharedPreferences endSyncTime = context.getSharedPreferences(BRConstants.PREFS_NAME, Context.MODE_PRIVATE); + return endSyncTime.getLong("endSyncTime", System.currentTimeMillis()); } public static boolean getPhraseWroteDown(Context context) { @@ -232,9 +258,10 @@ public static void putUseFingerprint(Context activity, boolean use) { editor.putBoolean("useFingerprint", use); editor.apply(); } + public static int getStartHeight(Context context) { SharedPreferences settingsToGet = context.getSharedPreferences(BRConstants.PREFS_NAME, 0); - return settingsToGet.getInt(BRConstants.START_HEIGHT, 0); + return settingsToGet.getInt(BRConstants.START_HEIGHT, 0); } public static void putStartHeight(Context context, int startHeight) { 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 91e12a9e..963a56af 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java @@ -4,13 +4,14 @@ 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.presenter.entities.Fee; +import com.brainwallet.data.model.Fee; import com.platform.APIClient; -import org.json.JSONException; -import org.json.JSONObject; +import org.koin.java.KoinJavaComponent; import java.io.IOException; import java.lang.annotation.Retention; @@ -24,16 +25,9 @@ import okhttp3.Response; import timber.log.Timber; - +//we are still using this, maybe in the future will deprecate? public final class FeeManager { - // this is the default that matches the mobile-api if the server is unavailable - private static final long defaultEconomyFeePerKB = 2_500L; // From legacy minimum. default min is 1000 as Litecoin Core version v0.17.1 - private static final long defaultRegularFeePerKB = 2_5000L; - private static final long defaultLuxuryFeePerKB = 66_746L; - private static final long defaultTimestamp = 1583015199122L; - - private Fee defaultValues = new Fee(defaultLuxuryFeePerKB, defaultRegularFeePerKB, defaultEconomyFeePerKB, defaultTimestamp); private static final FeeManager instance; @@ -50,7 +44,7 @@ public static FeeManager getInstance() { } private void initWithDefaultValues() { - currentFees = defaultValues; + currentFees = Fee.getDefault(); feeType = REGULAR; } @@ -75,22 +69,33 @@ public boolean isRegularFee() { 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()); } 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); +// 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) + ).whenComplete((fee, throwable) -> { + + //legacy logic + FeeManager.getInstance().setFees(fee.luxury, fee.regular, fee.economy); 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)); - } + }); } // createGETRequestURL diff --git a/app/src/main/java/com/brainwallet/tools/manager/SyncManager.java b/app/src/main/java/com/brainwallet/tools/manager/SyncManager.java index 42e195f5..ace9dcc1 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/SyncManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/SyncManager.java @@ -1,5 +1,7 @@ package com.brainwallet.tools.manager; +import static com.brainwallet.tools.manager.BRSharedPrefs.putSyncMetadata; + import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; @@ -44,8 +46,6 @@ public synchronized void startSyncingProgressThread(Context app) { } syncTask = new SyncProgressTask(); syncTask.start(); - BRSharedPrefs.putStartSyncTimestamp(app, System.currentTimeMillis()); - BRSharedPrefs.putSyncTimeElapsed(app, 0L); updateStartSyncData(app); } catch (IllegalThreadStateException ex) { Timber.e(ex); @@ -54,41 +54,11 @@ public synchronized void startSyncingProgressThread(Context app) { private synchronized void updateStartSyncData(Context app) { final double progress = BRPeerManager.syncProgress(BRSharedPrefs.getStartHeight(app)); - long startSync = BRSharedPrefs.getStartSyncTimestamp(app); - long lastSync = BRSharedPrefs.getLastSyncTimestamp(app); - long elapsed = BRSharedPrefs.getSyncTimeElapsed(app); - - if (elapsed > 0L) { - elapsed = (System.currentTimeMillis() - lastSync) + elapsed; - } - else { - elapsed = 1L; - } - BRSharedPrefs.putLastSyncTimestamp(app, System.currentTimeMillis()); - BRSharedPrefs.putSyncTimeElapsed(app, elapsed); - double minutesValue = ((double) elapsed / 1_000.0 / 60.0); - String minutesString = String.format( "%3.2f mins", minutesValue); - String millisecString = String.format( "%5d msec", elapsed); - Timber.d("timber: ||\nprogress: %s\nThread: %s\nrunning lastSyncingTime: %s\nelapsed: %s | %s", String.format( "%.2f", progress * 100.00),Thread.currentThread().getName(),String.valueOf(BRSharedPrefs.getLastSyncTimestamp(app)), millisecString, minutesString); - } private synchronized void markFinishedSyncData(Context app) { - Timber.d("timber: || markFinish threadname:%s", Thread.currentThread().getName()); + Timber.d("timber: || SYNC ELAPSE markFinish threadname:%s", Thread.currentThread().getName()); final double progress = BRPeerManager.syncProgress(BRSharedPrefs.getStartHeight(app)); - long startSync = BRSharedPrefs.getStartSyncTimestamp(app); - long lastSync = BRSharedPrefs.getLastSyncTimestamp(app); - long elapsed = BRSharedPrefs.getSyncTimeElapsed(app); - double minutesValue = ((double) elapsed / 1_000.0 / 60.0); - String minutesString = String.format( "%3.2f mins", minutesValue); - String millisecString = String.format( "%5d msec", elapsed); - Timber.d("timber: ||\ncompletedprogress: %s\nstartSyncTime: %s\nlastSyncingTime: %s\ntotalTimeelapsed: %s | %s", String.format( "%.2f", progress * 100.00),String.valueOf(startSync),String.valueOf(lastSync), millisecString, minutesString); - - Bundle params = new Bundle(); - params.putDouble("sync_time_elapsed", minutesValue); - params.putLong("sync_start_timestamp", startSync); - params.putLong("sync_last_timestamp", lastSync); - AnalyticsManager.logCustomEventWithParams(BRConstants._20230407_DCS, params); } public synchronized void stopSyncingProgressThread(Context app) { @@ -139,7 +109,10 @@ public void run() { app = BreadActivity.getApp(); progressStatus = 0; running = true; - Timber.d("timber: run: starting: %s", progressStatus); + long runTimeStamp = System.currentTimeMillis(); + Timber.d("timber: run: starting: %s date: %d", progressStatus, runTimeStamp); + ///Set StartSync + BRSharedPrefs.putStartSyncTimestamp(app, runTimeStamp); if (app != null) { final long lastBlockTimeStamp = BRPeerManager.getInstance().getLastBlockTimestamp() * 1000; @@ -162,6 +135,16 @@ public void run() { progressStatus = BRPeerManager.syncProgress(startHeight); if (progressStatus == 1) { running = false; + /// Record sync time + long startTimeStamp = BRSharedPrefs.getStartSyncTimestamp(app); + long endSyncTimeStamp = System.currentTimeMillis(); + BRSharedPrefs.putEndSyncTimestamp(app, endSyncTimeStamp); + + double syncDuration = (double) (endSyncTimeStamp - startTimeStamp) / 1_000.0 / 60.0; + /// only update if the sync duration is longer than 2 mins + if (syncDuration > 2.0) { + putSyncMetadata(app, startTimeStamp, endSyncTimeStamp); + } continue; } final long lastBlockTimeStamp = BRPeerManager.getInstance().getLastBlockTimestamp() * 1000; diff --git a/app/src/main/java/com/brainwallet/tools/threads/BRExecutor.java b/app/src/main/java/com/brainwallet/tools/threads/BRExecutor.java index ec127999..b1b21255 100644 --- a/app/src/main/java/com/brainwallet/tools/threads/BRExecutor.java +++ b/app/src/main/java/com/brainwallet/tools/threads/BRExecutor.java @@ -1,5 +1,6 @@ package com.brainwallet.tools.threads; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionHandler; @@ -7,6 +8,12 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.CoroutineScopeKt; +import kotlinx.coroutines.CoroutineStart; +import kotlinx.coroutines.future.FutureKt; import timber.log.Timber; /* @@ -130,4 +137,13 @@ public Executor forMainThreadTasks() { public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { Timber.d("timber: rejectedExecution: "); } + + public CompletableFuture executeSuspend(kotlin.jvm.functions.Function2, ? extends Object> paramToExec) { + return FutureKt.future( + CoroutineScopeKt.CoroutineScope(EmptyCoroutineContext.INSTANCE), + EmptyCoroutineContext.INSTANCE, + CoroutineStart.DEFAULT, + paramToExec + ); + } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java index 5788f6de..1c4d4a53 100644 --- a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java +++ b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java @@ -106,8 +106,8 @@ private BRConstants() { /** * API Hosts */ - public static final String BW_API_PROD_HOST = "https://prod.apigsltd.net"; - public static final String BW_API_DEV_HOST = "https://dev.apigsltd.net"; + public static final String BW_API_PROD_HOST = "https://api.grunt.ltd"; + public static final String LEGACY_BW_API_DEV_HOST = "https://dev.apigsltd.net"; public static final String BLOCK_EXPLORER_BASE_URL = "https://blockchair.com/litecoin/transaction/"; diff --git a/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt b/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt index 34608ffd..8ef9dfda 100644 --- a/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt +++ b/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt @@ -32,7 +32,6 @@ import com.brainwallet.ui.screens.inputwords.InputWordsViewModel.Companion.LEGAC import com.brainwallet.ui.screens.inputwords.InputWordsViewModel.Companion.LEGACY_DIALOG_WIPE_ALERT import com.brainwallet.ui.screens.inputwords.InputWordsViewModel.Companion.LEGACY_EFFECT_RESET_PIN import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItViewModel.Companion.LEGACY_EFFECT_ON_PAPERKEY_PROVED -import com.brainwallet.ui.screens.yourseedwords.YourSeedWordsViewModel.Companion.LEGACY_EFFECT_ON_SAVED_PAPERKEY import com.brainwallet.ui.theme.BrainwalletAppTheme import com.brainwallet.util.EventBus import com.brainwallet.wallet.BRWalletManager @@ -114,10 +113,6 @@ class BrainwalletActivity : BRActivity() { } } - LEGACY_EFFECT_ON_SAVED_PAPERKEY -> { - PostAuth.getInstance().onPhraseProveAuth(this, false) - } - LEGACY_EFFECT_ON_PAPERKEY_PROVED -> { BRSharedPrefs.putPhraseWroteDown(this@BrainwalletActivity, true) LegacyNavigation.startBreadActivity( diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt index 531305b6..48e0750a 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt @@ -4,7 +4,10 @@ import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Language sealed class SettingsEvent { - data class OnLoad(val shareAnalyticsDataEnabled: Boolean = false) : SettingsEvent() + data class OnLoad( + val shareAnalyticsDataEnabled: Boolean = false, + val lastSyncMetadata: String? = null, + ) : SettingsEvent() object OnSecurityUpdatePinClick : SettingsEvent() object OnSecuritySeedPhraseClick : SettingsEvent() object OnSecurityShareAnalyticsDataClick : SettingsEvent() 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 3efe10a7..9f924c3c 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 @@ -14,4 +14,5 @@ data class SettingsState( val languageSelectorBottomSheetVisible: Boolean = false, val fiatSelectorBottomSheetVisible: Boolean = false, val shareAnalyticsDataEnabled: Boolean = false, + val lastSyncMetadata: String? = null, ) \ 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 7ec71f98..e9515ce8 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 @@ -47,7 +47,12 @@ class SettingsViewModel( override fun onEvent(event: SettingsEvent) { when (event) { is SettingsEvent.OnLoad -> viewModelScope.launch { - _state.update { it.copy(shareAnalyticsDataEnabled = event.shareAnalyticsDataEnabled) } + _state.update { + it.copy( + shareAnalyticsDataEnabled = event.shareAnalyticsDataEnabled, + lastSyncMetadata = event.lastSyncMetadata + ) + } } SettingsEvent.OnToggleDarkMode -> viewModelScope.launch { 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 497770f8..42b370a8 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 @@ -6,6 +6,7 @@ import android.util.AttributeSet import androidx.browser.customtabs.CustomTabsIntent 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.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -57,7 +58,10 @@ fun HomeSettingDrawerSheet( val context = LocalContext.current LaunchedEffect(Unit) { - viewModel.onEvent(SettingsEvent.OnLoad(BRSharedPrefs.getShareData(context))) //currently just load analytics share data here + 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 + )) } /// Layout values @@ -173,6 +177,14 @@ fun HomeSettingDrawerSheet( ) } + item { + SettingRowItem( + modifier = Modifier.height(100.dp), + title = stringResource(R.string.settings_title_sync_metadata), + description = state.lastSyncMetadata ?: "No sync metadata" + ) + } + item { SettingRowItem( title = stringResource(R.string.settings_title_app_version), diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItEvent.kt index d9f530b0..68816d6a 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItEvent.kt @@ -6,6 +6,7 @@ sealed class YourSeedProveItEvent { ) : YourSeedProveItEvent() data class OnDropSeedWordItem( + val index: Int, val expectedWord: String, val actualWord: String ) : YourSeedProveItEvent() diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt index 3e793c6a..171186ab 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt @@ -147,7 +147,7 @@ fun YourSeedProveItScreen( verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), maxItemsInEachRow = maxItemsPerRow ) { - state.correctSeedWords.entries.forEachIndexed { index, (expectedWord, actualWord) -> + state.correctSeedWords.values.forEachIndexed { index, (expectedWord, actualWord) -> val label = if (expectedWord != actualWord && actualWord.isEmpty()) { "${index + 1}" @@ -173,6 +173,7 @@ fun YourSeedProveItScreen( viewModel.onEvent( YourSeedProveItEvent.OnDropSeedWordItem( + index = index, expectedWord = expectedWord, actualWord = text.toString() ) diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItState.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItState.kt index 991763a2..1da5cc92 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItState.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItState.kt @@ -1,7 +1,12 @@ package com.brainwallet.ui.screens.yourseedproveit data class YourSeedProveItState( - val correctSeedWords: Map = mapOf(), + val correctSeedWords: Map = emptyMap(), val shuffledSeedWords: List = emptyList(), val orderCorrected: Boolean = false, ) + +data class SeedWordItem( + val expected: String, + val actual: String = "" +) \ No newline at end of file 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 99af1398..6f4d19c4 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,25 +18,31 @@ class YourSeedProveItViewModel : BrainwalletViewModel() { when (event) { is YourSeedProveItEvent.OnLoad -> _state.update { it.copy( - correctSeedWords = event.seedWords.associateWith { "" }, + correctSeedWords = event.seedWords.mapIndexed { index, word -> + index to SeedWordItem(expected = word) + }.toMap(), shuffledSeedWords = event.seedWords.shuffled() ) } is YourSeedProveItEvent.OnDropSeedWordItem -> _state.update { - val correctSeedWords = it.correctSeedWords.toMutableMap().apply { - this[event.expectedWord] = event.actualWord - } + val correctSeedWords = it.correctSeedWords.map { (index, seedWordItem) -> + if (index == event.index && seedWordItem.expected == event.expectedWord) { + index to seedWordItem.copy(actual = event.actualWord) + } else { + index to seedWordItem + } + }.toMap() it.copy( correctSeedWords = correctSeedWords, - orderCorrected = correctSeedWords.all { (expectedWord, actualWord) -> expectedWord == actualWord } + orderCorrected = correctSeedWords.all { (_, seedWordItem) -> seedWordItem.expected == seedWordItem.actual } ) } YourSeedProveItEvent.OnClear -> _state.update { it.copy( - correctSeedWords = it.correctSeedWords.mapValues { "" } + correctSeedWords = it.correctSeedWords.mapValues { SeedWordItem(expected = it.value.expected) } ) } diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsEvent.kt index 9f9db09b..ba2cbde0 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsEvent.kt @@ -1,5 +1,5 @@ package com.brainwallet.ui.screens.yourseedwords sealed class YourSeedWordsEvent { - object OnSavedItClick : YourSeedWordsEvent() + data class OnSavedItClick(val seedWords: List) : YourSeedWordsEvent() } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt index bd2e5119..c541f588 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt @@ -21,6 +21,7 @@ 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -51,6 +52,15 @@ fun YourSeedWordsScreen( val leadingCopyPadding = 16 val detailLineHeight = 28 + LaunchedEffect(Unit) { + viewModel.uiEffect.collect { effect -> + when (effect) { + is UiEffect.Navigate -> onNavigate.invoke(effect) + else -> Unit + } + } + } + BrainwalletScaffold( topBar = { BrainwalletTopAppBar( @@ -122,7 +132,7 @@ fun YourSeedWordsScreen( LargeButton( onClick = { - viewModel.onEvent(YourSeedWordsEvent.OnSavedItClick) + viewModel.onEvent(YourSeedWordsEvent.OnSavedItClick(seedWords)) }, ) { Text( diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsViewModel.kt index 650a4381..562b8b97 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsViewModel.kt @@ -1,21 +1,18 @@ package com.brainwallet.ui.screens.yourseedwords import androidx.lifecycle.viewModelScope +import com.brainwallet.navigation.Route +import com.brainwallet.navigation.UiEffect import com.brainwallet.ui.BrainwalletViewModel -import com.brainwallet.util.EventBus import kotlinx.coroutines.launch class YourSeedWordsViewModel : BrainwalletViewModel() { override fun onEvent(event: YourSeedWordsEvent) { when (event) { - YourSeedWordsEvent.OnSavedItClick -> viewModelScope.launch { - EventBus.emit(EventBus.Event.Message(LEGACY_EFFECT_ON_SAVED_PAPERKEY)) + is YourSeedWordsEvent.OnSavedItClick -> viewModelScope.launch { + sendUiEffect(UiEffect.Navigate(destinationRoute = Route.YourSeedProveIt(event.seedWords))) } } } - - companion object { - const val LEGACY_EFFECT_ON_SAVED_PAPERKEY = "onSavedPaperKey" - } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java b/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java index 209814e7..6f591402 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRPeerManager.java @@ -72,7 +72,6 @@ public static void syncStarted() { public static void syncSucceeded() { Context ctx = BrainwalletApp.getBreadContext(); if (ctx == null) return; - BRSharedPrefs.putLastSyncTimestamp(ctx, System.currentTimeMillis()); SyncManager.getInstance().updateAlarms(ctx); BRSharedPrefs.putAllowSpend(ctx, true); SyncManager.getInstance().stopSyncingProgressThread(ctx); @@ -99,9 +98,12 @@ public static void syncFailed() { public static void txStatusUpdate() { Timber.d("timber: txStatusUpdate"); - for (OnTxStatusUpdate listener : statusUpdateListeners) { - if (listener != null) listener.onStatusUpdate(); + synchronized (statusUpdateListeners) { + for (OnTxStatusUpdate listener : statusUpdateListeners) { + if (listener != null) listener.onStatusUpdate(); + } } + BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(new Runnable() { @Override public void run() { @@ -192,11 +194,14 @@ public void run() { //wrap logic enable/disable connect with new flow public void wrapConnectV2() { - if (featureSelectedPeersEnabled()) { - fetchSelectedPeers().whenComplete((strings, throwable) -> connect()); - } else { - connect(); - } +// if (featureSelectedPeersEnabled()) { +// fetchSelectedPeers().whenComplete((strings, throwable) -> connect()); +// } else { +// connect(); +// } + //currently we are just using connect(), since the core using hardcoded peers + //https://github.com/gruntsoftware/core/commit/0b7f85feac840c7667338c340c808dfccde4251a + connect(); } public static boolean featureSelectedPeersEnabled() { diff --git a/app/src/main/jni/core b/app/src/main/jni/core index 55b43a86..28d7d31a 160000 --- a/app/src/main/jni/core +++ b/app/src/main/jni/core @@ -1 +1 @@ -Subproject commit 55b43a869300dfc82ead7faa87e71626d580b464 +Subproject commit 28d7d31ae057bdde33b1d19e6b7af3ba07710c89 diff --git a/app/src/main/jni/transition/PeerManager.c b/app/src/main/jni/transition/PeerManager.c index 4c2ce73e..8d249725 100644 --- a/app/src/main/jni/transition/PeerManager.c +++ b/app/src/main/jni/transition/PeerManager.c @@ -385,7 +385,7 @@ Java_com_brainwallet_wallet_BRPeerManager_create(JNIEnv *env, jobject thiz, BRPeerManagerSetCallbacks(_peerManager, NULL, syncStarted, syncStopped, txStatusUpdate, - saveBlocks, savePeers, networkIsReachable, threadCleanup, featureSelectedPeersEnabled, fetchSelectedPeers); + saveBlocks, savePeers, networkIsReachable, threadCleanup); } if (_peerManager == NULL) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4d2d53f..1b2f4e52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -825,6 +825,7 @@ Unlock Theme App version: + Sync metadata: Sync Sync duration: >20 minutes Game %1$d: From 3caa1a7702878a54fa87f326523197797a5fbdb0 Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Tue, 29 Apr 2025 12:33:29 +0700 Subject: [PATCH 22/44] feat: remove unused activity (ImportActivity) at AndroidManifest.xml (#71) --- app/src/main/AndroidManifest.xml | 5 ----- 1 file changed, 5 deletions(-) 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" /> - Date: Fri, 2 May 2025 13:23:05 +0700 Subject: [PATCH 23/44] fix: fix crash IllegalStateException: cannot make a new request because the previous response is still open (#73) --- app/src/main/java/com/brainwallet/di/Module.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index 4dce30f1..3b666d1f 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -97,16 +97,17 @@ private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() .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) + chain.proceed(request).use { response -> + if (response.isSuccessful.not()) { + throw HttpException( + retrofit2.Response.error( + response.code, + response.body ?: response.peekBody(Long.MAX_VALUE) + ) ) - ) + } + response } - result }.getOrElse { //retry using dev host val newRequest = request.newBuilder() From 1f9c5d03496b86e16e90db38fc88ee376040f357 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Fri, 2 May 2025 08:05:53 +0100 Subject: [PATCH 24/44] code bump --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3d4cb0e2..80cc7aec 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 34 - versionCode = 202504251 + versionCode = 202505021 versionName = "v4.4.7" multiDexEnabled = true From 7ca17d3615b80a71d0d004dc2e5eb45377e67196 Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Mon, 5 May 2025 20:52:16 +0700 Subject: [PATCH 25/44] Feat/move tx fee (#74) * chore: add new translations at strings.xml * feat: move network fee to HomeSettingDrawerSheet & remove unused code --- .../java/com/brainwallet/data/model/Fee.kt | 58 +++- .../data/repository/LtcRepository.kt | 30 +- .../main/java/com/brainwallet/di/Module.kt | 2 +- .../presenter/fragments/FragmentSend.kt | 296 +++++++++++------- .../brainwallet/tools/manager/FeeManager.java | 85 +---- .../ui/screens/home/SettingsState.kt | 5 + .../ui/screens/home/SettingsViewModel.kt | 8 +- .../home/composable/HomeSettingDrawerSheet.kt | 4 +- .../settingsrows/LitecoinBlockchainDetail.kt | 112 ++++++- .../brainwallet/wallet/BRWalletManager.java | 6 +- app/src/main/res/layout/fragment_send.xml | 94 +----- app/src/main/res/values-ar/strings.xml | 4 + app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values-es/strings.xml | 8 +- app/src/main/res/values-fr/strings.xml | 4 + app/src/main/res/values-hi/strings.xml | 4 + app/src/main/res/values-in/strings.xml | 4 + app/src/main/res/values-it/strings.xml | 4 + app/src/main/res/values-ja/strings.xml | 4 + app/src/main/res/values-ko/strings.xml | 4 + app/src/main/res/values-pt/strings.xml | 4 + app/src/main/res/values-ru/strings.xml | 4 + app/src/main/res/values-sv/strings.xml | 4 + app/src/main/res/values-tr/strings.xml | 4 + app/src/main/res/values-uk/strings.xml | 4 + app/src/main/res/values-zh-rCN/strings.xml | 6 +- app/src/main/res/values-zh-rTW/strings.xml | 4 + app/src/main/res/values/strings.xml | 7 +- 28 files changed, 462 insertions(+), 315 deletions(-) diff --git a/app/src/main/java/com/brainwallet/data/model/Fee.kt b/app/src/main/java/com/brainwallet/data/model/Fee.kt index 246100a4..0ea99667 100644 --- a/app/src/main/java/com/brainwallet/data/model/Fee.kt +++ b/app/src/main/java/com/brainwallet/data/model/Fee.kt @@ -1,20 +1,27 @@ package com.brainwallet.data.model +import android.annotation.SuppressLint +import com.brainwallet.R +import com.brainwallet.tools.manager.FeeManager.ECONOMY +import com.brainwallet.tools.manager.FeeManager.FeeType +import com.brainwallet.tools.manager.FeeManager.LUXURY +import com.brainwallet.tools.manager.FeeManager.REGULAR import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.math.ceil +import kotlin.math.round @Serializable data class Fee( @JvmField - @SerialName("fee_per_kb") + @SerialName("fee_per_kb_luxury") var luxury: Long, @JvmField - @SerialName("fee_per_kb_economy") + @SerialName("fee_per_kb") var regular: Long, @JvmField - @SerialName("fee_per_kb_luxury") + @SerialName("fee_per_kb_economy") var economy: Long, - var timestamp: Long ) { companion object { //from legacy @@ -25,13 +32,52 @@ data class Fee( private const val defaultLuxuryFeePerKB: Long = 66746L private const val defaultTimestamp: Long = 1583015199122L +// {"fee_per_kb":5289,"fee_per_kb_economy":2645,"fee_per_kb_luxury":10578} + @JvmStatic val Default = Fee( defaultLuxuryFeePerKB, defaultRegularFeePerKB, defaultEconomyFeePerKB, - defaultTimestamp ) - } } + + +data class FeeOption( + @FeeType + val type: String, + val feePerKb: Long, + val labelStringId: Int, +) + +fun Fee.toFeeOptions(): List = 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" +} \ 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..38b325fd 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -1,12 +1,16 @@ package com.brainwallet.data.repository import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Fee import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.di.json import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource +import kotlinx.serialization.encodeToString interface LtcRepository { suspend fun fetchRates(): List @@ -16,7 +20,8 @@ interface LtcRepository { 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 +47,33 @@ interface LtcRepository { } override suspend fun fetchFeePerKb(): Fee { + val lastUpdateTime = sharedPreferences.getLong(PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, 0) + val currentTime = System.currentTimeMillis() + val cachedFee = sharedPreferences.getString(PREF_KEY_NETWORK_FEE_PER_KB, null) + ?.let { json.decodeFromString(it) } + return runCatching { - val fee = remoteApiSource.getFeePerKb() + // Check if cache exists and is less than 6 hours old + if (cachedFee != null && (currentTime - lastUpdateTime) < 6 * 60 * 60 * 1000) { + return cachedFee + } - //todo: cache + val fee = remoteApiSource.getFeePerKb() + sharedPreferences.edit { + putString(PREF_KEY_NETWORK_FEE_PER_KB, json.encodeToString(fee)) + putLong(PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, currentTime) + } return fee - }.getOrElse { Fee.Default } + }.getOrElse { + cachedFee ?: Fee.Default + } } } 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" } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index 3b666d1f..8dcfb909 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -58,7 +58,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 { 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/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/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/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/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/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" /> - - - - - - - - - - - - - - - - - - - - + - مشاركة بيانات التحليلات تحديث الرقم السري عرض + رسوم الشبكة (لكل كيلو بايت): القيمة الأعلى تعني اكتمال المعاملة في وقت أقرب + قمة + واسطة + قليل diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4d0e68ac..6d272928 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -796,4 +796,8 @@ 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 diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9f12f07d..24bf3fc0 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,8 @@ 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 diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 93928b67..1571dd93 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -796,4 +796,8 @@ 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 diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 8a0158a5..a03d849c 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -434,4 +434,8 @@ विश्लेषण डेटा साझा करें पिन अपडेट करें दिखाएं + नेटवर्क शुल्क (प्रति केबी): उच्च मूल्य का मतलब है कि लेनदेन जल्दी पूरा हो जाएगा + शीर्ष + मध्यम + कम diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 7899cb2d..d3bb43e0 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -798,4 +798,8 @@ Bagikan data analitik Perbarui PIN Tampilkan + Biaya Jaringan (per kb): Nilai yang lebih tinggi berarti transaksi selesai lebih cepat + Atas + Sedang + Rendah diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6641145b..592ebba1 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -796,4 +796,8 @@ 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 diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 523cd7f2..c6ffb20c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -796,4 +796,8 @@ 分析データを共有 PIN を更新 表示 + ネットワーク料金 (kb あたり): 値が高いほど、トランザクションが早く完了することを意味します + トップ + 中くらい + 低い diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3deda254..8ec4b384 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -796,4 +796,8 @@ 분석 데이터 공유 PIN 업데이트 표시 + 네트워크 수수료(kb당): 값이 높을수록 거래가 더 빨리 완료됨을 의미합니다. + 맨 위 + 중간 + 낮은 diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 48a2dc0b..27b64935 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -795,4 +795,8 @@ 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 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 22df2082..4df74670 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -796,4 +796,8 @@ Делиться аналитическими данными Обновить PIN Показать + Сетевая плата (за КБ): более высокое значение означает, что транзакция завершится раньше. + Вершина + Середина + Низкий diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 12d62935..78b503fd 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -434,4 +434,8 @@ 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 diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d1e3c107..a526c60c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -819,4 +819,8 @@ 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 diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 3f7aca4a..b5d20546 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -802,4 +802,8 @@ Ділитися аналітичними даними Оновити PIN Показати + Плата за мережу (за кб): вищий показник означає, що транзакція завершується швидше + Топ + Середній + Низький diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b25903f6..b1517637 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -764,7 +764,7 @@ 你可以从开始的地方返回 恢复我的脑钱包 清除 - 不要猜测。\n\n这将需要你 \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000 次尝试 + 不要猜测。\n\n这将需要你 \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000,000 次尝试 确认 你没有忘记吧?再次输入。或者,返回重新开始。 准备好 @@ -796,4 +796,8 @@ 分享分析数据 更新 PIN 显示 + 网络费用(每 kb):值越高意味着交易完成得越快 + 顶部 + 中等的 + 低的 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 13e6b9cc..69514c55 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -796,4 +796,8 @@ 分享分析數據 更新 PIN 顯示 + 網路費用(每 kb):數值越高代表交易完成越快 + 頂部 + 中等的 + 低的 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b2f4e52..3e6f5bbe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -805,7 +805,7 @@ You can get back from where you started Restore my Brainwallet Clear - Don’t guess.\n\nIt would take you \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000 tries + Don’t guess.\n\nIt would take you \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000,000 tries Confirm You didn’t forget did you? Enter it again. Or, go back to start over. Ready @@ -839,5 +839,8 @@ Share analytics data Update PIN Show - + Network Fee (per kb): Higher value mean the transaction completes sooner + Top + Medium + Low From e7fb2b7873f4ddb5a9c511a0c0b91efe9041b77a Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Wed, 7 May 2025 12:45:38 +0700 Subject: [PATCH 26/44] fix: fix dismiss allow state loss (#76) --- .../com/brainwallet/presenter/fragments/FragmentSignal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } }; From 3d8ef981879bbcab1d1fa8cab54c2b08edfd29e0 Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Wed, 7 May 2025 12:46:41 +0700 Subject: [PATCH 27/44] fix: fix typo at strings.xml (#75) --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b1517637..53cd941f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -764,7 +764,7 @@ 你可以从开始的地方返回 恢复我的脑钱包 清除 - 不要猜测。\n\n这将需要你 \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000,000 次尝试 + 不要猜测。\n\n这将需要你 \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000 次尝试 确认 你没有忘记吧?再次输入。或者,返回重新开始。 准备好 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e6f5bbe..d107536c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -805,7 +805,7 @@ You can get back from where you started Restore my Brainwallet Clear - Don’t guess.\n\nIt would take you \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000,000 tries + Don’t guess.\n\nIt would take you \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000 tries Confirm You didn’t forget did you? Enter it again. Or, go back to start over. Ready From 25be1a85f036e502811c3e093398feee996dfe77 Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Mon, 12 May 2025 18:59:29 +0700 Subject: [PATCH 28/44] feat: new UI for receive and topup flow (moonpay integration) (#72) * feat: [WIP][UI] new UI for receive and topup flow * feat: [WIP][UI] implement copy address and some refactor * feat: replace webview using CustomTabsIntent from androidx.browser * feat: replace webview using CustomTabsIntent from androidx.browser * feat: add PickerWheel * chore: rename param at QRUtils.generateQR * chore: revert default startDestination at BrainwalletActivity * feat: wip moonpay integration * feat: wip moonpay integration * chore: new UI for ReceiveDialog * chore: parse error from response API at BrainwalletViewModel * chore: add /moonpay/buy-quote and remove dev base url * chore: make LegacyNavigation.showMoonPayWidget receive params for the widget url * chore: add GetMoonpayBuyQuoteResponse * chore: wip moonpay integration to call buy-quote * fix: fix wheel picker for IDR at ReceiveDialog * fix: fix invalid signature * feat: moonpay integration at onboarding flow at BuyLitecoinScreen also fix some stuffs * chore: replace base url for moonpay buy integration when debug * chore: introduce LocalCacheSource.kt and cache fetch limits * chore: adjustment receive dialog UI - debounce fiat currency to 2s - increment in 10 - remove decimal places - default value to 10% of range * chore: reorder bottom navigation item and add new strings * chore: add new translations * chore: refactor UI for buy and receive dialog * chore: cleanup ReceiveDialogViewModel * chore: using MoonpayBuyWidget * chore: adjustment moonpaywidget * chore: for now moonpay widget using CustomTabsIntent --- .../data/model/MoonpayCurrencyLimit.kt | 24 + .../data/model/QuickFiatAmountOption.kt | 11 + .../data/repository/LtcRepository.kt | 65 +- .../data/source/LocalCacheSource.kt | 45 ++ .../data/source/RemoteApiSource.kt | 22 +- .../response/GetMoonpayBuyQuoteResponse.kt | 17 + .../response/GetMoonpaySignUrlResponse.kt | 8 + .../main/java/com/brainwallet/di/Module.kt | 36 +- .../navigation/LegacyNavigation.kt | 61 ++ .../com/brainwallet/navigation/MainNav.kt | 5 + .../java/com/brainwallet/navigation/Route.kt | 3 + .../presenter/activities/BreadActivity.java | 23 +- .../com/brainwallet/tools/qrcode/QRUtils.java | 15 + .../tools/sqlite/CurrencyDataSource.java | 51 +- .../brainwallet/ui/BrainwalletViewModel.kt | 19 + .../brainwallet/ui/composable/Foundation.kt | 18 +- .../brainwallet/ui/composable/LargeButton.kt | 5 +- .../ui/composable/LoadingDialog.kt | 55 ++ .../ui/composable/MoonpayBuyButton.kt | 66 ++ .../ui/composable/MoonpayBuyWidget.kt | 56 ++ .../brainwallet/ui/composable/WheelPicker.kt | 634 ++++++++++++++++++ .../screens/buylitecoin/BuyLitecoinEvent.kt | 11 + .../screens/buylitecoin/BuyLitecoinScreen.kt | 167 +++++ .../screens/buylitecoin/BuyLitecoinState.kt | 19 + .../buylitecoin/BuyLitecoinViewModel.kt | 128 ++++ .../ui/screens/home/buy/BuyScreen.kt | 10 - .../ui/screens/home/receive/ReceiveDialog.kt | 452 +++++++++++++ .../home/receive/ReceiveDialogEvent.kt | 21 + .../home/receive/ReceiveDialogState.kt | 61 ++ .../home/receive/ReceiveDialogViewModel.kt | 179 +++++ .../ui/screens/topup/TopUpScreen.kt | 182 ++--- .../YourSeedProveItViewModel.kt | 4 +- .../java/com/brainwallet/ui/theme/Theme.kt | 4 + app/src/main/res/drawable/ic_copy.xml | 9 + app/src/main/res/drawable/ic_import.xml | 19 + app/src/main/res/menu/bottom_nav_menu.xml | 10 +- app/src/main/res/menu/bottom_nav_menu_us.xml | 23 +- app/src/main/res/values-ar/strings.xml | 11 + app/src/main/res/values-de/strings.xml | 11 + app/src/main/res/values-es/strings.xml | 11 + app/src/main/res/values-fr/strings.xml | 11 + app/src/main/res/values-hi/strings.xml | 11 + app/src/main/res/values-in/strings.xml | 11 + app/src/main/res/values-it/strings.xml | 11 + app/src/main/res/values-ja/strings.xml | 11 + app/src/main/res/values-ko/strings.xml | 11 + app/src/main/res/values-pt/strings.xml | 11 + app/src/main/res/values-ru/strings.xml | 11 + app/src/main/res/values-sv/strings.xml | 11 + app/src/main/res/values-tr/strings.xml | 12 + app/src/main/res/values-uk/strings.xml | 12 + app/src/main/res/values-zh-rCN/strings.xml | 11 + app/src/main/res/values-zh-rTW/strings.xml | 11 + app/src/main/res/values/strings.xml | 14 +- 54 files changed, 2479 insertions(+), 251 deletions(-) create mode 100644 app/src/main/java/com/brainwallet/data/model/MoonpayCurrencyLimit.kt create mode 100644 app/src/main/java/com/brainwallet/data/model/QuickFiatAmountOption.kt create mode 100644 app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt create mode 100644 app/src/main/java/com/brainwallet/data/source/response/GetMoonpayBuyQuoteResponse.kt create mode 100644 app/src/main/java/com/brainwallet/data/source/response/GetMoonpaySignUrlResponse.kt create mode 100644 app/src/main/java/com/brainwallet/ui/composable/LoadingDialog.kt create mode 100644 app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyButton.kt create mode 100644 app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyWidget.kt create mode 100644 app/src/main/java/com/brainwallet/ui/composable/WheelPicker.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinEvent.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinState.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt delete mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/buy/BuyScreen.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogEvent.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogState.kt create mode 100644 app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt create mode 100644 app/src/main/res/drawable/ic_copy.xml create mode 100644 app/src/main/res/drawable/ic_import.xml diff --git a/app/src/main/java/com/brainwallet/data/model/MoonpayCurrencyLimit.kt b/app/src/main/java/com/brainwallet/data/model/MoonpayCurrencyLimit.kt new file mode 100644 index 00000000..d8344438 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/model/MoonpayCurrencyLimit.kt @@ -0,0 +1,24 @@ +package com.brainwallet.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MoonpayCurrencyLimit( + @SerialName("data") val data: Data = Data() +) { + @Serializable + data class Data( + @SerialName("paymentMethod") val paymentMethod: String = "", + @SerialName("quoteCurrency") val quoteCurrency: CurrencyLimit = CurrencyLimit(), + @SerialName("baseCurrency") val baseCurrency: CurrencyLimit = CurrencyLimit(), + @SerialName("areFeesIncluded") val areFeesIncluded: Boolean = false + ) + + @Serializable + data class CurrencyLimit( + val code: String = "usd", + @SerialName("minBuyAmount") val min: Float = 21f, + @SerialName("maxBuyAmount") val max: Float = 29849f + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/model/QuickFiatAmountOption.kt b/app/src/main/java/com/brainwallet/data/model/QuickFiatAmountOption.kt new file mode 100644 index 00000000..20dc3368 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/model/QuickFiatAmountOption.kt @@ -0,0 +1,11 @@ +package com.brainwallet.data.model + + +data class QuickFiatAmountOption( + val symbol: String = "custom", + val value: Float = -1f +) + +fun QuickFiatAmountOption.isCustom(): Boolean = symbol == "custom" && value == -1f + +fun QuickFiatAmountOption.getFormattedText(): String = "${symbol}${value.toInt()}" \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt index 38b325fd..365bbc0d 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -2,21 +2,29 @@ package com.brainwallet.data.repository import android.content.Context import android.content.SharedPreferences -import androidx.core.content.edit +import androidx.core.net.toUri +import com.brainwallet.BuildConfig import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Fee +import com.brainwallet.data.model.MoonpayCurrencyLimit import com.brainwallet.data.source.RemoteApiSource -import com.brainwallet.di.json +import com.brainwallet.data.source.fetchWithCache +import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource -import kotlinx.serialization.encodeToString interface LtcRepository { suspend fun fetchRates(): List suspend fun fetchFeePerKb(): Fee + suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit + + suspend fun fetchBuyQuote(params: Map): GetMoonpayBuyQuoteResponse + + suspend fun fetchMoonpaySignedUrl(params: Map): String + class Impl( private val context: Context, private val remoteApiSource: RemoteApiSource, @@ -47,27 +55,42 @@ interface LtcRepository { } override suspend fun fetchFeePerKb(): Fee { - val lastUpdateTime = sharedPreferences.getLong(PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, 0) - val currentTime = System.currentTimeMillis() - val cachedFee = sharedPreferences.getString(PREF_KEY_NETWORK_FEE_PER_KB, null) - ?.let { json.decodeFromString(it) } + return sharedPreferences.fetchWithCache( + key = PREF_KEY_NETWORK_FEE_PER_KB, + cachedAtKey = PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, + cacheTimeMs = 6 * 60 * 60 * 1000, + fetchData = { + remoteApiSource.getFeePerKb() + }, + defaultValue = Fee.Default + ) + } - return runCatching { - // Check if cache exists and is less than 6 hours old - if (cachedFee != null && (currentTime - lastUpdateTime) < 6 * 60 * 60 * 1000) { - return cachedFee + override suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit { + return sharedPreferences.fetchWithCache( + key = "${PREF_KEY_BUY_LIMITS_PREFIX}${baseCurrencyCode.lowercase()}", + cachedAtKey = "${PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT}${baseCurrencyCode.lowercase()}", + cacheTimeMs = 5 * 60 * 1000, //5 minutes + fetchData = { + remoteApiSource.getMoonpayCurrencyLimit(baseCurrencyCode) } + ) + } - val fee = remoteApiSource.getFeePerKb() - sharedPreferences.edit { - putString(PREF_KEY_NETWORK_FEE_PER_KB, json.encodeToString(fee)) - putLong(PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, currentTime) - } + override suspend fun fetchBuyQuote(params: Map): GetMoonpayBuyQuoteResponse = + remoteApiSource.getBuyQuote(params) - return fee - }.getOrElse { - cachedFee ?: Fee.Default - } + override suspend fun fetchMoonpaySignedUrl(params: Map): String { + return remoteApiSource.getMoonpaySignedUrl(params) + .signedUrl.toUri() + .buildUpon() + .apply { + if (BuildConfig.DEBUG) { + authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com + } + } + .build() + .toString() } } @@ -75,5 +98,7 @@ interface LtcRepository { companion object { const val PREF_KEY_NETWORK_FEE_PER_KB = "network_fee_per_kb" const val PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT = "${PREF_KEY_NETWORK_FEE_PER_KB}_cached_at" + const val PREF_KEY_BUY_LIMITS_PREFIX = "buy_limits:" //e.g. buy_limits:usd + const val PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT = "buy_limits_cached_at:" } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt b/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt new file mode 100644 index 00000000..d86d312d --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/source/LocalCacheSource.kt @@ -0,0 +1,45 @@ +package com.brainwallet.data.source + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.brainwallet.di.json +import kotlinx.serialization.encodeToString + +/** + * Generic function to handle caching data in shared preferences + * @param key The key to store the data under + * @param cachedAtKey The key to store the timestamp under + * @param cacheTimeMs How long the cache should be valid in milliseconds + * @param fetchData Suspending function to fetch new data + * @return The data, either from cache or freshly fetched + */ +suspend inline fun SharedPreferences.fetchWithCache( + key: String, + cachedAtKey: String, + cacheTimeMs: Long, + crossinline fetchData: suspend () -> T, + defaultValue: T? = null +): T { + val lastUpdateTime = getLong(cachedAtKey, 0) + val currentTime = System.currentTimeMillis() + val cached = getString(key, null) + ?.let { runCatching { json.decodeFromString(it) }.getOrNull() } + + // Return cached value if it exists and is not expired + if (cached != null && (currentTime - lastUpdateTime) < cacheTimeMs) { + return cached + } + + return runCatching { + // Fetch fresh data + val result = fetchData() + // Save to cache + edit { + putString(key, json.encodeToString(result)) + putLong(cachedAtKey, currentTime) + } + result + }.getOrElse { + cached ?: (defaultValue ?: throw it) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt b/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt index 60c865e2..4e0a1c02 100644 --- a/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt +++ b/app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt @@ -2,7 +2,12 @@ package com.brainwallet.data.source import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Fee +import com.brainwallet.data.model.MoonpayCurrencyLimit +import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse +import com.brainwallet.data.source.response.GetMoonpaySignUrlResponse import retrofit2.http.GET +import retrofit2.http.Query +import retrofit2.http.QueryMap //TODO interface RemoteApiSource { @@ -13,6 +18,19 @@ interface RemoteApiSource { @GET("v1/fee-per-kb") suspend fun getFeePerKb(): Fee -// https://prod.apigsltd.net/moonpay/buy?address=ltc1qjnsg3p9rt4r4vy7ncgvrywdykl0zwhkhcp8ue0&code=USD&idate=1742331930290&uid=ec51fa950b271ff3 -// suspend fun getMoonPayBuy() + @GET("v1/moonpay/ltc-to-fiat-limits") + suspend fun getMoonpayCurrencyLimit( + @Query("baseCurrencyCode") baseCurrencyCode: String + ): MoonpayCurrencyLimit + + @GET("v1/moonpay/sign-url") + suspend fun getMoonpaySignedUrl( + @QueryMap params: Map + ): GetMoonpaySignUrlResponse + + @GET("v1/moonpay/buy-quote") + suspend fun getBuyQuote( + @QueryMap params: Map + ): GetMoonpayBuyQuoteResponse + } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/source/response/GetMoonpayBuyQuoteResponse.kt b/app/src/main/java/com/brainwallet/data/source/response/GetMoonpayBuyQuoteResponse.kt new file mode 100644 index 00000000..848732a9 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/source/response/GetMoonpayBuyQuoteResponse.kt @@ -0,0 +1,17 @@ +package com.brainwallet.data.source.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetMoonpayBuyQuoteResponse( + @SerialName("data") val data: Data = Data() + +) { + @Serializable + data class Data( + val totalAmount: Float = 0f, + val baseCurrencyCode: String = "usd", + val quoteCurrencyAmount: Float = 0f, + ) +} diff --git a/app/src/main/java/com/brainwallet/data/source/response/GetMoonpaySignUrlResponse.kt b/app/src/main/java/com/brainwallet/data/source/response/GetMoonpaySignUrlResponse.kt new file mode 100644 index 00000000..51e316cc --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/source/response/GetMoonpaySignUrlResponse.kt @@ -0,0 +1,8 @@ +package com.brainwallet.data.source.response + +import kotlinx.serialization.Serializable + +@Serializable +data class GetMoonpaySignUrlResponse( + val signedUrl: String +) diff --git a/app/src/main/java/com/brainwallet/di/Module.kt b/app/src/main/java/com/brainwallet/di/Module.kt index 8dcfb909..b95bbeec 100644 --- a/app/src/main/java/com/brainwallet/di/Module.kt +++ b/app/src/main/java/com/brainwallet/di/Module.kt @@ -10,7 +10,9 @@ import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.tools.util.BRConstants +import com.brainwallet.ui.screens.buylitecoin.BuyLitecoinViewModel import com.brainwallet.ui.screens.home.SettingsViewModel +import com.brainwallet.ui.screens.home.receive.ReceiveDialogViewModel import com.brainwallet.ui.screens.inputwords.InputWordsViewModel import com.brainwallet.ui.screens.ready.ReadyViewModel import com.brainwallet.ui.screens.setpasscode.SetPasscodeViewModel @@ -28,6 +30,8 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidApplication +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module @@ -70,6 +74,8 @@ val viewModelModule = module { viewModel { UnLockViewModel() } viewModel { YourSeedProveItViewModel() } viewModel { YourSeedWordsViewModel() } + viewModel { ReceiveDialogViewModel(get(), get()) } + viewModel { BuyLitecoinViewModel(get(), get()) } } val appModule = module { @@ -94,28 +100,6 @@ private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() .addHeader("Accept-Language", "en") chain.proceed(requestBuilder.build()) } - .addInterceptor { chain -> - val request = chain.request() - runCatching { - chain.proceed(request).use { response -> - if (response.isSuccessful.not()) { - throw HttpException( - retrofit2.Response.error( - response.code, - response.body ?: response.peekBody(Long.MAX_VALUE) - ) - ) - } - response - } - }.getOrElse { - //retry using dev host - val newRequest = request.newBuilder() - .url("${BRConstants.LEGACY_BW_API_DEV_HOST}/api${request.url.encodedPath}") //legacy dev api need prefix path /api - .build() - chain.proceed(newRequest) - } - } .addInterceptor(HttpLoggingInterceptor().apply { setLevel( when { @@ -140,4 +124,10 @@ internal fun provideRetrofit( .build() internal inline fun provideApi(retrofit: Retrofit): T = - retrofit.create(T::class.java) \ No newline at end of file + retrofit.create(T::class.java) + +inline fun getKoinInstance(): T { + return object : KoinComponent { + val value: T by inject() + }.value +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt index b50bef17..d38eaeb9 100644 --- a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt +++ b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt @@ -1,11 +1,22 @@ package com.brainwallet.navigation import android.app.Activity +import android.app.ProgressDialog import android.content.Context import android.content.Intent +import android.widget.Toast +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri +import com.brainwallet.BuildConfig import com.brainwallet.R +import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.di.getKoinInstance import com.brainwallet.presenter.activities.BreadActivity import com.brainwallet.ui.BrainwalletActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber @@ -55,4 +66,54 @@ object LegacyNavigation { context.startActivity(it) } + @JvmStatic + fun showMoonPayWidget( + context: Context, + params: Map = mapOf(), + isDarkMode: Boolean = true, + ) { + val remoteApiSource: RemoteApiSource = getKoinInstance() + + val progressDialog = ProgressDialog(context).apply { + setMessage(context.getString(R.string.loading)) + setCancelable(false) + show() + } + + CoroutineScope(Dispatchers.Main).launch { + try { + val result = withContext(Dispatchers.IO) { + remoteApiSource.getMoonpaySignedUrl( + params = params.toMutableMap().apply { + put("defaultCurrencyCode", "ltc") + put("currencyCode", "ltc") + put("themeId", "main-v1.0.0") + put("theme", if (isDarkMode) "dark" else "light") + } + ) + } + + val widgetUri = result.signedUrl.toUri().buildUpon() + .apply { + if (BuildConfig.DEBUG) { + authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com + } + } + .build() + val intent = CustomTabsIntent.Builder() + .setColorScheme(if (isDarkMode) CustomTabsIntent.COLOR_SCHEME_DARK else CustomTabsIntent.COLOR_SCHEME_LIGHT) + .build() + intent.launchUrl(context, widgetUri) + } catch (e: Exception) { + Toast.makeText( + context, + "Failed to load: ${e.message}, please try again later", + Toast.LENGTH_LONG + ).show() + } finally { + progressDialog.dismiss() + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/navigation/MainNav.kt b/app/src/main/java/com/brainwallet/navigation/MainNav.kt index f6e2e091..ec2a8a06 100644 --- a/app/src/main/java/com/brainwallet/navigation/MainNav.kt +++ b/app/src/main/java/com/brainwallet/navigation/MainNav.kt @@ -7,6 +7,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute +import com.brainwallet.ui.screens.buylitecoin.BuyLitecoinScreen import com.brainwallet.ui.screens.inputwords.InputWordsScreen import com.brainwallet.ui.screens.ready.ReadyScreen import com.brainwallet.ui.screens.setpasscode.SetPasscodeScreen @@ -113,6 +114,10 @@ fun NavGraphBuilder.mainNavGraph( UnLockScreen(onNavigate = onNavigate, isUpdatePin = route.isUpdatePin) } + composable { navBackStackEntry -> + BuyLitecoinScreen(onNavigate = onNavigate) + } + //todo add more composable screens } diff --git a/app/src/main/java/com/brainwallet/navigation/Route.kt b/app/src/main/java/com/brainwallet/navigation/Route.kt index 2e0e71a8..16f16dcf 100644 --- a/app/src/main/java/com/brainwallet/navigation/Route.kt +++ b/app/src/main/java/com/brainwallet/navigation/Route.kt @@ -45,4 +45,7 @@ sealed class Route : JavaSerializable { @Serializable data class UnLock(val isUpdatePin: Boolean = false) : Route() + @Serializable + object BuyLitecoin : Route() + } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java index aec322e1..b210395c 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java @@ -25,6 +25,7 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.browser.customtabs.CustomTabsIntent; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.app.ActivityCompat; @@ -61,6 +62,8 @@ import com.brainwallet.ui.BrainwalletActivity; import com.brainwallet.ui.screens.home.SettingsViewModel; import com.brainwallet.ui.screens.home.composable.HomeSettingDrawerComposeView; +import com.brainwallet.ui.screens.home.receive.ReceiveDialogFragment; +import com.brainwallet.ui.screens.home.receive.ReceiveDialogKt; import com.brainwallet.util.PermissionUtil; import com.brainwallet.wallet.BRPeerManager; import com.brainwallet.wallet.BRWalletManager; @@ -252,16 +255,22 @@ public boolean handleNavigationItemSelected(int menuItemId) { mSelectedBottomNavItem = 0; } else if (menuItemId == R.id.nav_receive) { if (BRAnimator.isClickAllowed()) { - BRAnimator.showReceiveFragment(BreadActivity.this, true); - } - mSelectedBottomNavItem = 0; - } - else if (menuItemId == R.id.nav_buy) { - if (BRAnimator.isClickAllowed()) { - BRAnimator.showMoonpayFragment(BreadActivity.this); +// BRAnimator.showReceiveFragment(BreadActivity.this, true); + //todo + ReceiveDialogFragment.show(getSupportFragmentManager()); } mSelectedBottomNavItem = 0; } +// else if (menuItemId == R.id.nav_buy) { +// if (BRAnimator.isClickAllowed()) { +//// BRAnimator.showMoonpayFragment(BreadActivity.this); +// } +// +// +// +// +// mSelectedBottomNavItem = 0; +// } return true; } diff --git a/app/src/main/java/com/brainwallet/tools/qrcode/QRUtils.java b/app/src/main/java/com/brainwallet/tools/qrcode/QRUtils.java index 5728657c..a0e9fd5f 100644 --- a/app/src/main/java/com/brainwallet/tools/qrcode/QRUtils.java +++ b/app/src/main/java/com/brainwallet/tools/qrcode/QRUtils.java @@ -83,6 +83,21 @@ public static boolean generateQR(Context ctx, String bitcoinURL, ImageView qrcod return true; } + public static Bitmap generateQR(Context ctx, String litecoinUrl) { + if (litecoinUrl == null || litecoinUrl.isEmpty()) return null; + WindowManager manager = (WindowManager) ctx.getSystemService(Activity.WINDOW_SERVICE); + Display display = manager.getDefaultDisplay(); + Point point = new Point(); + display.getSize(point); + int width = point.x; + int height = point.y; + int smallerDimension = Math.min(width, height); + smallerDimension = (int) (smallerDimension * 0.45f); + Bitmap bitmap = null; + bitmap = QRUtils.encodeAsBitmap(litecoinUrl, smallerDimension); + return bitmap; + } + private static String guessAppropriateEncoding(CharSequence contents) { // Very crude at the moment for (int i = 0; i < contents.length(); i++) { diff --git a/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java b/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java index 40ad36b1..7c15df6d 100644 --- a/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java +++ b/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java @@ -16,6 +16,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import timber.log.Timber; @@ -31,6 +32,27 @@ public class CurrencyDataSource implements BRDataSourceInterface { BRSQLiteHelper.CURRENCY_RATE }; + /// Set the most popular fiats + /// Hack: Database needs a symbol column. This is injected here. + private final HashMap codeSymbolsMap = new HashMap() {{ + put("USD","$"); + put("EUR","€"); + put("GBP","£"); + put("SGD","$"); + put("CAD","$"); + put("AUD","$"); + put("RUB","₽"); + put("KRW","₩"); + put("MXN","$"); + put("SAR","﷼"); + put("UAH","₴"); + put("NGN","₦"); + put("JPY","¥"); + put("CNY","¥"); + put("IDR","Rp"); + put("TRY","₺"); + }}; + private static CurrencyDataSource instance; public static CurrencyDataSource getInstance(Context context) { @@ -71,26 +93,6 @@ public List getAllCurrencies(Boolean shouldBeFiltered) { List currencies = new ArrayList<>(); - /// Set the most popular fiats - /// Hack: Database needs a symbol column. This is injected here. - HashMap codeSymbolsMap = new HashMap(); - codeSymbolsMap.put("USD","$"); - codeSymbolsMap.put("EUR","€"); - codeSymbolsMap.put("GBP","£"); - codeSymbolsMap.put("SGD","$"); - codeSymbolsMap.put("CAD","$"); - codeSymbolsMap.put("AUD","$"); - codeSymbolsMap.put("RUB","₽"); - codeSymbolsMap.put("KRW","₩"); - codeSymbolsMap.put("MXN","$"); - codeSymbolsMap.put("SAR","﷼"); - codeSymbolsMap.put("UAH","₴"); - codeSymbolsMap.put("NGN","₦"); - codeSymbolsMap.put("JPY","¥"); - codeSymbolsMap.put("CNY","¥"); - codeSymbolsMap.put("IDR","Rp"); - codeSymbolsMap.put("TRY","₺"); - /// Set the most popular fiats List filteredFiatCodes = Arrays.asList("USD","EUR","GBP", "SGD","CAD","AUD","RUB","KRW","MXN","SAR","UAH","NGN","JPY","CNY","IDR","TRY"); @@ -125,7 +127,7 @@ public List getAllCurrencies(Boolean shouldBeFiltered) { List completeCurrencyEntities = new ArrayList<>(); for(int i=0;i : ViewModel() { protected fun handleError(t: Throwable) { val errorMessage = t.message ?: "Oops, something went wrong" //todo more error handler + + if (t is retrofit2.HttpException) { + val message = t.response()?.errorBody()?.string()?.let { + runCatching { + json.decodeFromString(it)["message"]?.toString() + }.getOrNull() + } ?: "Oops, something went wrong" + + sendUiEffect( + UiEffect.ShowMessage( + type = UiEffect.ShowMessage.Type.Error, + message = message + ) + ) + return + } + sendUiEffect( UiEffect.ShowMessage( type = UiEffect.ShowMessage.Type.Error, diff --git a/app/src/main/java/com/brainwallet/ui/composable/Foundation.kt b/app/src/main/java/com/brainwallet/ui/composable/Foundation.kt index 2fabe5ae..f72137f5 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/Foundation.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/Foundation.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -18,6 +19,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.dp import com.brainwallet.ui.theme.BrainwalletTheme @@ -75,17 +77,21 @@ fun BrainwalletBottomSheet( fun BrainwalletButton( onClick: () -> Unit, modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = CircleShape, + colors: ButtonColors = ButtonDefaults.buttonColors( + containerColor = BrainwalletTheme.colors.surface, + contentColor = BrainwalletTheme.colors.content + ), content: @Composable RowScope.() -> Unit ) { Button( onClick = onClick, - shape = CircleShape, - colors = ButtonDefaults.buttonColors( - containerColor = BrainwalletTheme.colors.surface, - contentColor = BrainwalletTheme.colors.content - ), + enabled = enabled, + shape = shape, + colors = colors, modifier = modifier - .border(1.dp, BrainwalletTheme.colors.border, CircleShape) + .border(1.dp, BrainwalletTheme.colors.border, shape) .height(50.dp), content = content ) diff --git a/app/src/main/java/com/brainwallet/ui/composable/LargeButton.kt b/app/src/main/java/com/brainwallet/ui/composable/LargeButton.kt index a6f63965..48721758 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/LargeButton.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/LargeButton.kt @@ -10,16 +10,14 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledTonalButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.unit.dp -import com.brainwallet.ui.theme.BrainwalletColors import com.brainwallet.ui.theme.BrainwalletTheme @Composable fun LargeButton( modifier: Modifier = Modifier, + enabled: Boolean = true, onClick: () -> Unit, colors: ButtonColors = ButtonDefaults.buttonColors( containerColor = BrainwalletTheme.colors.background, @@ -32,6 +30,7 @@ fun LargeButton( modifier = modifier .fillMaxWidth() .height(56.dp), + enabled = enabled, onClick = onClick, colors = colors, shape = shape, diff --git a/app/src/main/java/com/brainwallet/ui/composable/LoadingDialog.kt b/app/src/main/java/com/brainwallet/ui/composable/LoadingDialog.kt new file mode 100644 index 00000000..8c6270ad --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/LoadingDialog.kt @@ -0,0 +1,55 @@ +package com.brainwallet.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.brainwallet.R +import com.brainwallet.ui.theme.BrainwalletTheme + +@Composable +fun LoadingDialog( + text: String = stringResource(R.string.loading), + onDismissRequest: () -> Unit = {}, +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + dismissOnClickOutside = false, + dismissOnBackPress = false + ) + ) { + Card( + shape = BrainwalletTheme.shapes.medium, + modifier = Modifier + .fillMaxWidth(0.8f) + .wrapContentHeight(), + ) { + Column( + modifier = Modifier + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = text, + style = BrainwalletTheme.typography.bodyLarge + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyButton.kt b/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyButton.kt new file mode 100644 index 00000000..e3513194 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyButton.kt @@ -0,0 +1,66 @@ +package com.brainwallet.ui.composable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.brainwallet.R +import com.brainwallet.ui.theme.BrainwalletTheme +import com.brainwallet.ui.theme.lavender +import com.brainwallet.ui.theme.nearBlack + +@Composable +fun MoonpayBuyButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: () -> Unit, +) { + Button( + modifier = modifier, + shape = BrainwalletTheme.shapes.large, + colors = ButtonDefaults.buttonColors( + containerColor = lavender, + contentColor = nearBlack + ), + enabled = enabled, + onClick = onClick, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.buy_ltc).uppercase(), + style = BrainwalletTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold) + ) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.powered_by_moonpay).uppercase(), + style = BrainwalletTheme.typography.labelSmall.copy(fontSize = 8.sp) + ) + Image( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_moonpay_logo), + contentDescription = "moonpay" + ) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyWidget.kt b/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyWidget.kt new file mode 100644 index 00000000..006e45bf --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/MoonpayBuyWidget.kt @@ -0,0 +1,56 @@ +package com.brainwallet.ui.composable + + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.viewinterop.AndroidView + +//TODO: wip here + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun MoonpayBuyWidget( + modifier: Modifier = Modifier, + signedUrl: String = "https://buy.moonpay.com", +) { + AndroidView( + modifier = modifier.clip(MaterialTheme.shapes.large), + factory = { ctx -> + WebView(ctx).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + setBackgroundColor(0) + + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.useWideViewPort = true + + webViewClient = object : WebViewClient() { + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + } + } + + } + }, + update = { + it.loadUrl(signedUrl) + } + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/composable/WheelPicker.kt b/app/src/main/java/com/brainwallet/ui/composable/WheelPicker.kt new file mode 100644 index 00000000..27e9ce40 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/WheelPicker.kt @@ -0,0 +1,634 @@ +package com.brainwallet.ui.composable + +import android.util.Log +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.calculateTargetValue +import androidx.compose.animation.core.exponentialDecay +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.math.absoluteValue + +interface WheelPickerContentScope { + val state: WheelPickerState +} + +interface WheelPickerDisplayScope : WheelPickerContentScope { + @Composable + fun Content(index: Int) +} + +@Composable +fun VerticalWheelPicker( + modifier: Modifier = Modifier, + count: Int, + state: WheelPickerState = rememberWheelPickerState(), + key: ((index: Int) -> Any)? = null, + itemHeight: Dp = 35.dp, + unfocusedCount: Int = 2, + userScrollEnabled: Boolean = true, + reverseLayout: Boolean = false, + debug: Boolean = false, + focus: @Composable () -> Unit = { WheelPickerFocusVertical() }, + display: @Composable WheelPickerDisplayScope.(index: Int) -> Unit = { + DefaultWheelPickerDisplay( + it + ) + }, + content: @Composable WheelPickerContentScope.(index: Int) -> Unit, +) { + WheelPicker( + modifier = modifier, + isVertical = true, + count = count, + state = state, + key = key, + itemSize = itemHeight, + unfocusedCount = unfocusedCount, + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + debug = debug, + focus = focus, + display = display, + content = content, + ) +} + + +@Composable +private fun WheelPicker( + modifier: Modifier, + isVertical: Boolean, + count: Int, + state: WheelPickerState, + key: ((index: Int) -> Any)?, + itemSize: Dp, + unfocusedCount: Int, + userScrollEnabled: Boolean, + reverseLayout: Boolean, + debug: Boolean, + focus: @Composable () -> Unit, + display: @Composable WheelPickerDisplayScope.(index: Int) -> Unit, + content: @Composable WheelPickerContentScope.(index: Int) -> Unit, +) { + require(count >= 0) { "Require count >= 0" } + require(unfocusedCount >= 0) { "Require unfocusedCount >= 0" } + require(itemSize > 0.dp) { "Require itemSize > 0.dp" } + + SafeBox( + modifier = modifier, + isVertical = isVertical, + itemSize = itemSize, + unfocusedCount = unfocusedCount, + ) { safeUnfocusedCount -> + InternalWheelPicker( + isVertical = isVertical, + count = count, + state = state, + key = key, + itemSize = itemSize, + unfocusedCount = safeUnfocusedCount, + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + debug = debug, + focus = focus, + display = display, + content = content, + ) + + if (debug && unfocusedCount != safeUnfocusedCount) { + LaunchedEffect(unfocusedCount, safeUnfocusedCount) { + logMsg(true) { "unfocusedCount $unfocusedCount -> $safeUnfocusedCount" } + } + } + } +} + +@Composable +private fun InternalWheelPicker( + isVertical: Boolean, + count: Int, + state: WheelPickerState, + key: ((index: Int) -> Any)?, + itemSize: Dp, + unfocusedCount: Int, + userScrollEnabled: Boolean, + reverseLayout: Boolean, + debug: Boolean, + focus: @Composable () -> Unit, + display: @Composable WheelPickerDisplayScope.(index: Int) -> Unit, + content: @Composable WheelPickerContentScope.(index: Int) -> Unit, +) { + state.debug = debug + LaunchedEffect(state, count) { + state.updateCount(count) + } + + val nestedScrollConnection = remember(state) { + WheelPickerNestedScrollConnection(state) + }.apply { + this.isVertical = isVertical + this.itemSizePx = with(LocalDensity.current) { itemSize.roundToPx() } + this.reverseLayout = reverseLayout + } + + val totalSize = remember(itemSize, unfocusedCount) { + itemSize * (unfocusedCount * 2 + 1) + } + + val displayScope = remember(state) { + WheelPickerDisplayScopeImpl(state) + }.apply { + this.content = content + } + + Box( + modifier = Modifier + .nestedScroll(nestedScrollConnection) + .graphicsLayer { + this.alpha = if (state.isReady) 1f else 0f + } + .run { + if (totalSize > 0.dp) { + if (isVertical) { + height(totalSize).widthIn(40.dp) + } else { + width(totalSize).heightIn(40.dp) + } + } else { + this + } + }, + contentAlignment = Alignment.Center, + ) { + + val lazyListScope: LazyListScope.() -> Unit = { + + repeat(unfocusedCount) { + item(contentType = "placeholder") { + ItemSizeBox( + isVertical = isVertical, + itemSize = itemSize, + ) + } + } + + items( + count = count, + key = key, + ) { index -> + ItemSizeBox( + isVertical = isVertical, + itemSize = itemSize, + ) { + displayScope.display(index) + } + } + + repeat(unfocusedCount) { + item(contentType = "placeholder") { + ItemSizeBox( + isVertical = isVertical, + itemSize = itemSize, + ) + } + } + } + + if (isVertical) { + LazyColumn( + state = state.lazyListState, + horizontalAlignment = Alignment.CenterHorizontally, + reverseLayout = reverseLayout, + userScrollEnabled = userScrollEnabled && state.isReady, + modifier = Modifier.matchParentSize(), + content = lazyListScope, + ) + } else { + LazyRow( + state = state.lazyListState, + verticalAlignment = Alignment.CenterVertically, + reverseLayout = reverseLayout, + userScrollEnabled = userScrollEnabled && state.isReady, + modifier = Modifier.matchParentSize(), + content = lazyListScope, + ) + } + + ItemSizeBox( + modifier = Modifier.align(Alignment.Center), + isVertical = isVertical, + itemSize = itemSize, + ) { + focus() + } + } +} + +@Composable +private fun SafeBox( + modifier: Modifier = Modifier, + isVertical: Boolean, + itemSize: Dp, + unfocusedCount: Int, + content: @Composable (safeUnfocusedCount: Int) -> Unit, +) { + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + val maxSize = if (isVertical) maxHeight else maxWidth + val result = remember(maxSize, itemSize, unfocusedCount) { + val totalSize = itemSize * (unfocusedCount * 2 + 1) + if (totalSize <= maxSize) { + unfocusedCount + } else { + (((maxSize - itemSize) / 2f) / itemSize).toInt().coerceAtLeast(0) + } + } + content(result) + } +} + +@Composable +private fun ItemSizeBox( + modifier: Modifier = Modifier, + isVertical: Boolean, + itemSize: Dp, + content: @Composable () -> Unit = { }, +) { + Box( + modifier + .run { + if (isVertical) { + height(itemSize) + } else { + width(itemSize) + } + }, + contentAlignment = Alignment.Center, + ) { + content() + } +} + +private class WheelPickerNestedScrollConnection( + private val state: WheelPickerState, +) : NestedScrollConnection { + var isVertical: Boolean = true + var itemSizePx: Int = 0 + var reverseLayout: Boolean = false + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + state.synchronizeCurrentIndexSnapshot() + return super.onPostScroll(consumed, available, source) + } + + override suspend fun onPreFling(available: Velocity): Velocity { + val currentIndex = state.synchronizeCurrentIndexSnapshot() + return if (currentIndex >= 0) { + available.flingItemCount( + isVertical = isVertical, + itemSize = itemSizePx, + decay = exponentialDecay(2f), + reverseLayout = reverseLayout, + ).let { flingItemCount -> + if (flingItemCount == 0) { + state.animateScrollToIndex(currentIndex) + } else { + state.animateScrollToIndex(currentIndex - flingItemCount) + } + } + available + } else { + super.onPreFling(available) + } + } +} + +private fun Velocity.flingItemCount( + isVertical: Boolean, + itemSize: Int, + decay: DecayAnimationSpec, + reverseLayout: Boolean, +): Int { + if (itemSize <= 0) return 0 + val velocity = if (isVertical) y else x + val targetValue = decay.calculateTargetValue(0f, velocity) + val flingItemCount = (targetValue / itemSize).toInt() + return if (reverseLayout) -flingItemCount else flingItemCount +} + +private class WheelPickerDisplayScopeImpl( + override val state: WheelPickerState, +) : WheelPickerDisplayScope { + + var content: @Composable WheelPickerContentScope.(index: Int) -> Unit by mutableStateOf({}) + + @Composable + override fun Content(index: Int) { + content(index) + } +} + +internal inline fun logMsg(debug: Boolean, block: () -> String) { + if (debug) { + Log.i("WheelPicker", block()) + } +} + +//default +/** + * The default implementation of focus view in vertical. + */ +@Composable +fun WheelPickerFocusVertical( + modifier: Modifier = Modifier, + dividerSize: Dp = 1.dp, + dividerColor: Color = DefaultDividerColor, +) { + Box( + modifier = modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .background(dividerColor) + .height(dividerSize) + .fillMaxWidth() + .align(Alignment.TopCenter), + ) + Box( + modifier = Modifier + .background(dividerColor) + .height(dividerSize) + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) + } +} + +/** + * Default divider color. + */ +private val DefaultDividerColor: Color + @Composable + get() { + val color = if (isSystemInDarkTheme()) Color.White else Color.Black + return color.copy(alpha = 0.2f) + } + +/** + * Default display. + */ +@Composable +fun WheelPickerDisplayScope.DefaultWheelPickerDisplay( + index: Int, +) { + val focused = index == state.currentIndexSnapshot + val animateScale by animateFloatAsState( + targetValue = if (focused) 1.0f else 0.8f, + label = "Wheel picker item scale", + ) + Box( + modifier = Modifier.graphicsLayer { + this.alpha = if (focused) 1.0f else 0.3f + this.scaleX = animateScale + this.scaleY = animateScale + } + ) { + Content(index) + } +} + +//state +@Composable +fun rememberWheelPickerState( + initialIndex: Int = 0, +): WheelPickerState { + return rememberSaveable(saver = WheelPickerState.Saver) { + WheelPickerState( + initialIndex = initialIndex, + ) + } +} + +@Composable +fun WheelPickerState.CurrentIndex( + block: suspend (Int) -> Unit, +) { + val blockUpdated by rememberUpdatedState(block) + LaunchedEffect(this) { + snapshotFlow { currentIndex } + .collect { blockUpdated(it) } + } +} + +class WheelPickerState internal constructor( + initialIndex: Int, +) { + internal var debug = false + internal val lazyListState = LazyListState() + internal var isReady by mutableStateOf(false) + + private var _count = 0 + private var _currentIndex by mutableIntStateOf(-1) + private var _currentIndexSnapshot by mutableIntStateOf(-1) + + private var _pendingIndex: Int? = initialIndex.coerceAtLeast(0) + private var _pendingIndexContinuation: CancellableContinuation? = null + set(value) { + field = value + if (value == null) _pendingIndex = null + } + + /** + * Index of picker when it is idle, -1 means that there is no data. + * + * Note that this property is observable and if you use it in the composable function + * it will be recomposed on every change. + */ + val currentIndex: Int get() = _currentIndex + + /** + * Index of picker when it is idle or drag but not fling, -1 means that there is no data. + * + * Note that this property is observable and if you use it in the composable function + * it will be recomposed on every change. + */ + val currentIndexSnapshot: Int get() = _currentIndexSnapshot + + /** + * [LazyListState.interactionSource] + */ + val interactionSource: InteractionSource get() = lazyListState.interactionSource + + /** + * [LazyListState.isScrollInProgress] + */ + val isScrollInProgress: Boolean get() = lazyListState.isScrollInProgress + + suspend fun animateScrollToIndex(index: Int) { + logMsg(debug) { "animateScrollToIndex index:$index count:$_count" } + @Suppress("NAME_SHADOWING") + val index = index.coerceAtLeast(0) + lazyListState.animateScrollToItem(index) + synchronizeCurrentIndex() + } + + suspend fun scrollToIndex(index: Int) { + logMsg(debug) { "scrollToIndex index:$index count:$_count" } + @Suppress("NAME_SHADOWING") + val index = index.coerceAtLeast(0) + + // Always cancel last continuation. + _pendingIndexContinuation?.let { + logMsg(debug) { "cancelAwaitIndex" } + _pendingIndexContinuation = null + it.cancel() + } + + awaitIndex(index) + + lazyListState.scrollToItem(index) + synchronizeCurrentIndex() + } + + private suspend fun awaitIndex(index: Int) { + if (_count > 0) return + logMsg(debug) { "awaitIndex:$index start" } + suspendCancellableCoroutine { cont -> + _pendingIndex = index + _pendingIndexContinuation = cont + cont.invokeOnCancellation { + logMsg(debug) { "awaitIndex:$index canceled" } + _pendingIndexContinuation = null + } + } + logMsg(debug) { "awaitIndex:$index finish" } + } + + internal suspend fun updateCount(count: Int) { + logMsg(debug) { "updateCount count:$count currentIndex:$_currentIndex" } + + // Update count + _count = count + + val maxIndex = count - 1 + if (maxIndex < _currentIndex) { + if (count > 0) { + scrollToIndex(maxIndex) + } else { + synchronizeCurrentIndex() + } + } + + if (count > 0) { + val pendingIndex = _pendingIndex + if (pendingIndex != null) { + logMsg(debug) { "Found pendingIndex:$pendingIndex pendingIndexContinuation:$_pendingIndexContinuation" } + val continuation = _pendingIndexContinuation + _pendingIndexContinuation = null + + if (continuation?.isActive == true) { + logMsg(debug) { "resume pendingIndexContinuation" } + continuation.resume(Unit) + } else { + scrollToIndex(pendingIndex) + } + } else { + if (_currentIndex < 0) { + synchronizeCurrentIndex() + } + } + } + + isReady = count > 0 + } + + private fun synchronizeCurrentIndex() { + val index = synchronizeCurrentIndexSnapshot() + if (_currentIndex != index) { + logMsg(debug) { "setCurrentIndex:$index" } + _currentIndex = index + _currentIndexSnapshot = index + } + } + + internal fun synchronizeCurrentIndexSnapshot(): Int { + return (mostStartItemInfo()?.index ?: -1).also { + _currentIndexSnapshot = it + } + } + + /** + * The item closest to the viewport start. + */ + private fun mostStartItemInfo(): LazyListItemInfo? { + if (_count <= 0) return null + + val layoutInfo = lazyListState.layoutInfo + val listInfo = layoutInfo.visibleItemsInfo + + if (listInfo.isEmpty()) return null + if (listInfo.size == 1) return listInfo.first() + + val firstItem = listInfo.first() + val firstOffsetDelta = (firstItem.offset - layoutInfo.viewportStartOffset).absoluteValue + return if (firstOffsetDelta < firstItem.size / 2) { + firstItem + } else { + listInfo[1] + } + } + + companion object { + val Saver = listSaver( + save = { listOf(it.currentIndex) }, + restore = { WheelPickerState(initialIndex = it[0]) }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinEvent.kt new file mode 100644 index 00000000..31e528fe --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinEvent.kt @@ -0,0 +1,11 @@ +package com.brainwallet.ui.screens.buylitecoin + +import android.content.Context + +sealed class BuyLitecoinEvent { + data class OnLoad(val context: Context) : BuyLitecoinEvent() + data class OnFiatAmountChange( + val fiatAmount: Float, + val needFetch: Boolean = true + ) : BuyLitecoinEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt new file mode 100644 index 00000000..97260328 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt @@ -0,0 +1,167 @@ +package com.brainwallet.ui.screens.buylitecoin + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.brainwallet.R +import com.brainwallet.navigation.LegacyNavigation +import com.brainwallet.navigation.OnNavigate +import com.brainwallet.navigation.UiEffect +import com.brainwallet.ui.composable.BrainwalletScaffold +import com.brainwallet.ui.composable.BrainwalletTopAppBar +import com.brainwallet.ui.composable.LargeButton +import com.brainwallet.ui.composable.LoadingDialog +import com.brainwallet.ui.screens.home.receive.ReceiveDialogEvent +import com.brainwallet.ui.theme.BrainwalletTheme +import org.koin.compose.koinInject + +//TODO: wip +@Composable +fun BuyLitecoinScreen( + onNavigate: OnNavigate, + viewModel: BuyLitecoinViewModel = koinInject() +) { + val state by viewModel.state.collectAsState() + val loadingState by viewModel.loadingState.collectAsState() + val appSetting by viewModel.appSetting.collectAsState() + val context = LocalContext.current + + LaunchedEffect(Unit) { + viewModel.onEvent(BuyLitecoinEvent.OnLoad(context)) + viewModel.uiEffect.collect { effect -> + when (effect) { + is UiEffect.ShowMessage -> + Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() + + else -> Unit + } + } + } + + BrainwalletScaffold( + topBar = { + BrainwalletTopAppBar( + navigationIcon = { + IconButton( + onClick = { onNavigate.invoke(UiEffect.Navigate.Back()) }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back), + ) + } + } + ) + } + ) { paddingValues -> + + Box( + modifier = Modifier + .padding(paddingValues) + .padding(16.dp) + .fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Text( + text = stringResource(R.string.buy_litecoin_title), + style = MaterialTheme.typography.titleLarge, + ) + + OutlinedTextField( + enabled = loadingState.visible.not(), + prefix = { + Text( + text = appSetting.currency.symbol, + style = BrainwalletTheme.typography.titleLarge.copy(color = BrainwalletTheme.colors.content) + ) + }, + textStyle = BrainwalletTheme.typography.titleLarge.copy(color = BrainwalletTheme.colors.content), + value = "${if (state.fiatAmount < 1) "" else state.fiatAmount.toInt()}", + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { input -> + val amount = input.toFloatOrNull() ?: 0f + viewModel.onEvent(BuyLitecoinEvent.OnFiatAmountChange(amount, false)) + }, + shape = BrainwalletTheme.shapes.extraLarge, + isError = state.isValid().not(), + supportingText = { + state.errorFiatAmountStringId?.let { + Text(stringResource(it, state.fiatAmount)) + } + } + ) + + Text( + text = state.getLtcAmountFormatted(loadingState.visible), + style = MaterialTheme.typography.titleLarge.copy( + color = BrainwalletTheme.colors.content.copy( + 0.7f + ) + ), + ) + + Text( + text = stringResource(R.string.buy_litecoin_desc), + style = MaterialTheme.typography.titleMedium + ) + + Text( + text = state.address, + style = MaterialTheme.typography.titleMedium.copy( + color = BrainwalletTheme.colors.content.copy( + 0.7f + ) + ) + ) + + } + + + LargeButton( + modifier = Modifier.align(Alignment.BottomCenter), + enabled = loadingState.visible.not(), + onClick = { + LegacyNavigation.showMoonPayWidget( + context = context, + params = mapOf( + "baseCurrencyCode" to appSetting.currency.code, + "baseCurrencyAmount" to state.fiatAmount.toString(), + "language" to appSetting.languageCode, + "walletAddress" to state.address + ) + ) + } + ) { + Text(stringResource(R.string.buy_litecoin_button_moonpay)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinState.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinState.kt new file mode 100644 index 00000000..628d6f0c --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinState.kt @@ -0,0 +1,19 @@ +package com.brainwallet.ui.screens.buylitecoin + +import com.brainwallet.data.model.MoonpayCurrencyLimit +import timber.log.Timber + +data class BuyLitecoinState( + val moonpayCurrencyLimit: MoonpayCurrencyLimit = MoonpayCurrencyLimit(), + val fiatAmount: Float = moonpayCurrencyLimit.data.baseCurrency.min, + val ltcAmount: Float = 0f, + val address: String = "", + val errorFiatAmountStringId: Int? = null +) + +fun BuyLitecoinState.isValid(): Boolean = errorFiatAmountStringId == null + +fun BuyLitecoinState.getLtcAmountFormatted(isLoading: Boolean): String = + (if (isLoading || ltcAmount < 0f) "x.xxxŁ" else "%.3fŁ".format(ltcAmount)).also { + Timber.d("TImber: ltcamount $ltcAmount") + } diff --git a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt new file mode 100644 index 00000000..5e6e6316 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinViewModel.kt @@ -0,0 +1,128 @@ +package com.brainwallet.ui.screens.buylitecoin + +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.brainwallet.R +import com.brainwallet.data.model.AppSetting +import com.brainwallet.data.repository.LtcRepository +import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.tools.manager.BRSharedPrefs +import com.brainwallet.ui.BrainwalletViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class BuyLitecoinViewModel( + private val settingRepository: SettingRepository, + private val ltcRepository: LtcRepository +) : BrainwalletViewModel() { + + private val _state = MutableStateFlow(BuyLitecoinState()) + val state: StateFlow = _state.asStateFlow() + + val appSetting = settingRepository.settings + .distinctUntilChanged() + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + AppSetting() + ) + + init { + viewModelScope.launch { + state.map { it.fiatAmount } + .debounce(1000) + .distinctUntilChanged() + .filter { + val (_, min, max) = state.value.moonpayCurrencyLimit.data.baseCurrency + it in min..max + } + .collect { + onEvent(BuyLitecoinEvent.OnFiatAmountChange(it)) + } + } + + } + + override fun onEvent(event: BuyLitecoinEvent) { + when (event) { + is BuyLitecoinEvent.OnLoad -> viewModelScope.launch { + delay(500) + _state.update { it.copy(address = BRSharedPrefs.getReceiveAddress(event.context)) } + try { + onLoading(true) + + _state.getAndUpdate { + val limitResult = ltcRepository.fetchLimits( + baseCurrencyCode = appSetting.value.currency.code + ) + + it.copy( + moonpayCurrencyLimit = limitResult, + fiatAmount = limitResult.data.baseCurrency.min, + ) + } + } catch (e: Exception) { + handleError(e) + } finally { + onLoading(false) + } + + } + + is BuyLitecoinEvent.OnFiatAmountChange -> viewModelScope.launch { + //do validation + val (_, min, max) = state.value.moonpayCurrencyLimit.data.baseCurrency + val errorStringId = when { + event.fiatAmount < min -> R.string.buy_litecoin_fiat_amount_validation_min + event.fiatAmount > max -> R.string.buy_litecoin_fiat_amount_validation_max + else -> null + } + _state.update { + it.copy( + errorFiatAmountStringId = errorStringId, + fiatAmount = event.fiatAmount + ) + } + + if (event.needFetch.not()) { + return@launch + } + + try { + onLoading(true) + + _state.update { + val result = ltcRepository.fetchBuyQuote( + mapOf( + "currencyCode" to "ltc", + "baseCurrencyCode" to appSetting.value.currency.code, + "baseCurrencyAmount" to event.fiatAmount.toString(), + ) + ) + + it.copy( + ltcAmount = result.data.quoteCurrencyAmount, + ) + } + + } catch (e: Exception) { + handleError(e) + } finally { + onLoading(false) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/buy/BuyScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/home/buy/BuyScreen.kt deleted file mode 100644 index 2e974ad2..00000000 --- a/app/src/main/java/com/brainwallet/ui/screens/home/buy/BuyScreen.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.brainwallet.ui.screens.home.buy - -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable - -//TODO: wip -@Composable -fun BuyScreen() { - Box {} -} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt new file mode 100644 index 00000000..291015e2 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt @@ -0,0 +1,452 @@ +@file:OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) + +package com.brainwallet.ui.screens.home.receive + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.AssistChip +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.OutlinedIconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.brainwallet.R +import com.brainwallet.data.model.getFormattedText +import com.brainwallet.data.model.isCustom +import com.brainwallet.navigation.LegacyNavigation +import com.brainwallet.navigation.UiEffect +import com.brainwallet.ui.composable.MoonpayBuyButton +import com.brainwallet.ui.composable.VerticalWheelPicker +import com.brainwallet.ui.composable.WheelPickerFocusVertical +import com.brainwallet.ui.composable.rememberWheelPickerState +import com.brainwallet.ui.theme.BrainwalletAppTheme +import com.brainwallet.ui.theme.BrainwalletTheme +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import org.koin.android.ext.android.inject +import org.koin.compose.koinInject +import timber.log.Timber + +@Composable +fun ReceiveDialog( + onDismissRequest: () -> Unit, + viewModel: ReceiveDialogViewModel = koinInject() +) { + val state by viewModel.state.collectAsState() + val loadingState by viewModel.loadingState.collectAsState() + val appSetting by viewModel.appSetting.collectAsState() + val context = LocalContext.current + val wheelPickerFiatCurrencyState = rememberWheelPickerState(0) + + LaunchedEffect(Unit) { + viewModel.onEvent(ReceiveDialogEvent.OnLoad(context)) + viewModel.uiEffect.collect { effect -> + when (effect) { + is UiEffect.ShowMessage -> Toast.makeText( + context, + effect.message, + Toast.LENGTH_SHORT + ).show() + + else -> Unit + } + } + } + + LaunchedEffect(Unit) { + delay(500) + wheelPickerFiatCurrencyState.scrollToIndex(state.getSelectedFiatCurrencyIndex()) + } + + LaunchedEffect(wheelPickerFiatCurrencyState) { + snapshotFlow { wheelPickerFiatCurrencyState.currentIndex } + .filter { it > -1 } + .distinctUntilChanged() + .debounce(700) + .collect { + Timber.i("wheelPickerFiatCurrencyState: currentIndex $it") + + viewModel.onEvent(ReceiveDialogEvent.OnFiatCurrencyChange(state.fiatCurrencies[it])) + } + } + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = BrainwalletTheme.colors.content //invert surface + ), + expandedHeight = 56.dp, + title = { + Text( + text = stringResource(R.string.bottom_nav_item_buy_receive_title).uppercase(), + style = BrainwalletTheme.typography.titleSmall + ) + }, + navigationIcon = { + if (state.moonpayWidgetVisible()) { + IconButton(onClick = { + viewModel.onEvent(ReceiveDialogEvent.OnSignedUrlClear) + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + } + }, + actions = { + IconButton(onClick = onDismissRequest) { + Icon( + Icons.Default.Close, + contentDescription = stringResource(R.string.AccessibilityLabels_close), + tint = BrainwalletTheme.colors.surface + ) + } + } + ) + + //moonpay widget + //todo: revisit this later +// AnimatedVisibility(visible = state.moonpayWidgetVisible()) { +// state.moonpayBuySignedUrl?.let { signedUrl -> +// MoonpayBuyWidget( +// modifier = Modifier.height(500.dp), +// signedUrl = signedUrl +// ) +// } +// } + + + //buy / receive +// AnimatedVisibility(visible = state.moonpayWidgetVisible().not()) { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + state.qrBitmap?.asImageBitmap()?.let { imageBitmap -> + Image( + modifier = Modifier + .weight(1f), + bitmap = imageBitmap, + contentDescription = "address" + ) + } ?: Box( + modifier = Modifier + .weight(1f) + .height(180.dp) + .background(Color.Gray) + ) + + Column( + modifier = Modifier + .weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = state.address, + style = BrainwalletTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Bold, + color = BrainwalletTheme.colors.surface + ), + maxLines = 4, + overflow = TextOverflow.Ellipsis + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(R.string.new_address).uppercase(), + style = BrainwalletTheme.typography.bodySmall.copy( + color = BrainwalletTheme.colors.surface, + ), + modifier = Modifier.weight(1f) + ) + OutlinedIconButton( + modifier = Modifier.size(32.dp), + onClick = { + viewModel.onEvent(ReceiveDialogEvent.OnCopyClick(context)) + Toast.makeText( + context, + R.string.Receive_copied, + Toast.LENGTH_SHORT + ) + .show() + }, + colors = IconButtonDefaults.outlinedIconButtonColors( + containerColor = BrainwalletTheme.colors.content.copy(alpha = 0.5f) + ), + ) { + Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.ic_copy), + contentDescription = stringResource(R.string.URLHandling_copy), + tint = BrainwalletTheme.colors.surface + ) + } + } + } + } + + HorizontalDivider() + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + VerticalWheelPicker( + modifier = Modifier.weight(1f), + focus = { + WheelPickerFocusVertical( + dividerColor = BrainwalletTheme.colors.surface.copy( + alpha = 0.5f + ) + ) + }, + unfocusedCount = 1, + count = state.fiatCurrencies.size, + state = wheelPickerFiatCurrencyState, + ) { index -> + Text( + text = state.fiatCurrencies[index].code, + fontWeight = FontWeight.Bold, + color = BrainwalletTheme.colors.surface + ) + } + + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = state.getLtcAmountFormatted(loadingState.visible), + style = BrainwalletTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + color = BrainwalletTheme.colors.surface + ) + ) + Text( + text = state.getRatesUpdatedAtFormatted(), + style = BrainwalletTheme.typography.bodySmall.copy( + color = BrainwalletTheme.colors.surface + ) + ) + } + } + + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(items = state.getQuickFiatAmountOptions()) { index, quickFiatAmountOption -> + AssistChip( + enabled = loadingState.visible.not(), + onClick = { + viewModel.onEvent( + ReceiveDialogEvent.OnFiatAmountOptionIndexChange( + index, + quickFiatAmountOption + ) + ) + }, + label = { + Text( + text = if (quickFiatAmountOption.isCustom()) + stringResource(R.string.custom) + else quickFiatAmountOption.getFormattedText() + ) + }, + leadingIcon = { + if (index == state.selectedQuickFiatAmountOptionIndex) { + Icon(Icons.Default.Check, contentDescription = null) + } + } + ) + } + } + + + AnimatedVisibility(visible = state.isQuickFiatAmountOptionCustom()) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + prefix = { + Text( + text = state.selectedFiatCurrency.symbol, + style = BrainwalletTheme.typography.bodyMedium.copy(color = BrainwalletTheme.colors.surface) + ) + }, + trailingIcon = { + IconButton(onClick = { + viewModel.onEvent(ReceiveDialogEvent.OnFiatAmountChange(state.fiatAmount)) + }) { + Icon(Icons.Default.Done, contentDescription = null) + } + }, + textStyle = BrainwalletTheme.typography.bodyMedium.copy(color = BrainwalletTheme.colors.surface), + value = "${if (state.fiatAmount < 1) "" else state.fiatAmount}", + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { input -> + val amount = input.toFloatOrNull() ?: 0f + viewModel.onEvent(ReceiveDialogEvent.OnFiatAmountChange(amount, false)) + }, + shape = BrainwalletTheme.shapes.large, + isError = state.errorFiatAmountStringId != null, + supportingText = { + state.errorFiatAmountStringId?.let { + Text(stringResource(it, state.fiatAmount)) + } + } + ) + } + + MoonpayBuyButton( + modifier = Modifier.fillMaxWidth(), + enabled = loadingState.visible.not(), + onClick = { + + //todo: revisit this later + //viewModel.onEvent(ReceiveDialogEvent.OnMoonpayButtonClick) + + LegacyNavigation.showMoonPayWidget( + context = context, + params = mapOf( + "baseCurrencyCode" to state.selectedFiatCurrency.code, + "baseCurrencyAmount" to state.fiatAmount.toString(), + "language" to appSetting.languageCode, + "walletAddress" to state.address, + ), + isDarkMode = appSetting.isDarkMode + ) + onDismissRequest.invoke() + }, + ) + +// } + } + + + } +} + +/** + * describe [ReceiveDialogFragment] for backward compat, + * since we are still using [com.brainwallet.presenter.activities.BreadActivity] + */ +class ReceiveDialogFragment : DialogFragment() { + + private val viewModel: ReceiveDialogViewModel by inject() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setContent { + val appSetting by viewModel.appSetting.collectAsState() + /** + * we need this theme inside this fragment, + * because we are still using fragment to display ReceiveDialog composable + * pls check BreadActivity.handleNavigationItemSelected + */ + BrainwalletAppTheme(appSetting = appSetting) { + Box( + modifier = Modifier + .padding(12.dp) + .background( + BrainwalletTheme.colors.content, + shape = BrainwalletTheme.shapes.large + ) + .border( + width = 1.dp, + color = BrainwalletTheme.colors.surface, + shape = BrainwalletTheme.shapes.large + ) + .padding(12.dp), + ) { + ReceiveDialog( + viewModel = viewModel, + onDismissRequest = { dismiss() } + ) + } + } + } + } + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) + isCancelable = false + } + + companion object { + @JvmStatic + fun show(manager: FragmentManager) { + ReceiveDialogFragment().show(manager, "ReceiveDialogFragment") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogEvent.kt new file mode 100644 index 00000000..f1968756 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogEvent.kt @@ -0,0 +1,21 @@ +package com.brainwallet.ui.screens.home.receive + +import android.content.Context +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.QuickFiatAmountOption + +sealed class ReceiveDialogEvent { + data class OnLoad(val context: Context) : ReceiveDialogEvent() + data class OnCopyClick(val context: Context) : ReceiveDialogEvent() + data class OnFiatAmountChange(val fiatAmount: Float, val needFetch: Boolean = true) : + ReceiveDialogEvent() + + data class OnFiatCurrencyChange(val fiatCurrency: CurrencyEntity) : ReceiveDialogEvent() + data class OnFiatAmountOptionIndexChange( + val index: Int, + val quickFiatAmountOption: QuickFiatAmountOption + ) : ReceiveDialogEvent() + + object OnMoonpayButtonClick : ReceiveDialogEvent() + object OnSignedUrlClear : ReceiveDialogEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogState.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogState.kt new file mode 100644 index 00000000..745d172a --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogState.kt @@ -0,0 +1,61 @@ +package com.brainwallet.ui.screens.home.receive + +import android.graphics.Bitmap +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.model.MoonpayCurrencyLimit +import com.brainwallet.data.model.QuickFiatAmountOption +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + + +data class ReceiveDialogState( + val address: String = "", + val qrBitmap: Bitmap? = null, + val fiatCurrencies: List = listOf(), + val selectedFiatCurrency: CurrencyEntity = CurrencyEntity( + "USD", + "US Dollar", + -1f, + "$" + ), // default from shared prefs then the user can override using picker wheel at [ReceiveDialog] + val moonpayCurrencyLimit: MoonpayCurrencyLimit = MoonpayCurrencyLimit(), + val fiatAmount: Float = 0f, + val ltcAmount: Float = 0f, + val ratesUpdatedAt: Long = System.currentTimeMillis(), + val selectedQuickFiatAmountOptionIndex: Int = 1, //default is 10X, other [min, 10x, max, custom] + val errorFiatAmountStringId: Int? = null, + val moonpayBuySignedUrl: String? = null, +) + +fun ReceiveDialogState.getSelectedFiatCurrencyIndex(): Int = fiatCurrencies + .indexOfFirst { it.code.lowercase() == selectedFiatCurrency.code.lowercase() } + +fun ReceiveDialogState.getDefaultFiatAmount(): Float { + val (_, min, max) = moonpayCurrencyLimit.data.baseCurrency + return min * 10 //default is 10X +} + +fun ReceiveDialogState.getRatesUpdatedAtFormatted(): String { + val date = Date(ratesUpdatedAt) + val format = SimpleDateFormat("d MMM yyyy HH:mm:ss", Locale.ENGLISH) + return format.format(date).uppercase() +} + +fun ReceiveDialogState.getLtcAmountFormatted(isLoading: Boolean): String = + (if (isLoading || ltcAmount < 0f) "x.xxxŁ" else "%.3fŁ".format(ltcAmount)).also { + Timber.d("TImber: ltcamount $ltcAmount") + } + +fun ReceiveDialogState.getQuickFiatAmountOptions(): List { + val selectedFiatSymbol = selectedFiatCurrency.symbol + val (_, min, max) = moonpayCurrencyLimit.data.baseCurrency + return listOf(min, min * 10, max) + .map { QuickFiatAmountOption(symbol = selectedFiatSymbol, it) } + QuickFiatAmountOption() +} + +fun ReceiveDialogState.isQuickFiatAmountOptionCustom(): Boolean = + selectedQuickFiatAmountOptionIndex == 3 //3 will be custom + +fun ReceiveDialogState.moonpayWidgetVisible(): Boolean = moonpayBuySignedUrl != null \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt new file mode 100644 index 00000000..80ab1984 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt @@ -0,0 +1,179 @@ +package com.brainwallet.ui.screens.home.receive + +import androidx.lifecycle.viewModelScope +import com.brainwallet.R +import com.brainwallet.data.model.AppSetting +import com.brainwallet.data.model.isCustom +import com.brainwallet.data.repository.LtcRepository +import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.tools.manager.BRClipboardManager +import com.brainwallet.tools.manager.BRSharedPrefs +import com.brainwallet.tools.qrcode.QRUtils +import com.brainwallet.tools.sqlite.CurrencyDataSource +import com.brainwallet.ui.BrainwalletViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.launch + +//todo: wip +class ReceiveDialogViewModel( + private val settingRepository: SettingRepository, + private val ltcRepository: LtcRepository, +) : BrainwalletViewModel() { + + private val _state = MutableStateFlow(ReceiveDialogState()) + val state: StateFlow = _state.asStateFlow() + + val appSetting = settingRepository.settings + .distinctUntilChanged() + .onEach { setting -> + onEvent(ReceiveDialogEvent.OnFiatCurrencyChange(setting.currency)) + } + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + AppSetting() + ) + + override fun onEvent(event: ReceiveDialogEvent) { + when (event) { + is ReceiveDialogEvent.OnLoad -> _state.update { + val address = BRSharedPrefs.getReceiveAddress(event.context) + it.copy( + address = address, + qrBitmap = QRUtils.generateQR(event.context, "litecoin:${address}"), + fiatCurrencies = CurrencyDataSource.getInstance(event.context) + .getAllCurrencies(true), + ) + } + + is ReceiveDialogEvent.OnCopyClick -> BRClipboardManager.putClipboard( + event.context, + state.value.address + ) + + is ReceiveDialogEvent.OnFiatAmountChange -> viewModelScope.launch { + //do validation + val (_, min, max) = state.value.moonpayCurrencyLimit.data.baseCurrency + val errorStringId = when { + event.fiatAmount < min -> R.string.buy_litecoin_fiat_amount_validation_min + event.fiatAmount > max -> R.string.buy_litecoin_fiat_amount_validation_max + else -> null + } + _state.update { + it.copy( + errorFiatAmountStringId = errorStringId, + fiatAmount = event.fiatAmount + ) + } + + if (event.needFetch.not()) { + return@launch + } + + try { + onLoading(true) + + _state.update { + val result = ltcRepository.fetchBuyQuote( + mapOf( + "currencyCode" to "ltc", + "baseCurrencyCode" to it.selectedFiatCurrency.code, + "baseCurrencyAmount" to event.fiatAmount.toString(), + ) + ) + + it.copy( + ltcAmount = result.data.quoteCurrencyAmount, + ) + } + + } catch (e: Exception) { + handleError(e) + } finally { + onLoading(false) + } + + } + + is ReceiveDialogEvent.OnFiatCurrencyChange -> viewModelScope.launch { + try { + onLoading(true) + val currencyLimit = ltcRepository.fetchLimits(event.fiatCurrency.code) + + _state.updateAndGet { + it.copy( + selectedFiatCurrency = event.fiatCurrency, + moonpayCurrencyLimit = currencyLimit, + selectedQuickFiatAmountOptionIndex = 1, //default to 10X + fiatAmount = it.getDefaultFiatAmount(), + ) + }.let { + onEvent( + ReceiveDialogEvent.OnFiatAmountOptionIndexChange( + index = it.selectedQuickFiatAmountOptionIndex, + quickFiatAmountOption = it.getQuickFiatAmountOptions()[it.selectedQuickFiatAmountOptionIndex] + ) + ) + } + + } catch (e: Exception) { + handleError(e) + } finally { + onLoading(false) + } + } + + is ReceiveDialogEvent.OnFiatAmountOptionIndexChange -> _state.updateAndGet { + it.copy( + selectedQuickFiatAmountOptionIndex = event.index, + fiatAmount = if (event.quickFiatAmountOption.isCustom()) it.fiatAmount + else event.quickFiatAmountOption.value + ) + }.let { + if (event.quickFiatAmountOption.isCustom().not()) { + onEvent(ReceiveDialogEvent.OnFiatAmountChange(it.fiatAmount)) + } + } + + ReceiveDialogEvent.OnMoonpayButtonClick -> viewModelScope.launch { + try { + onLoading(true) + + val currentState = state.value + val signedUrl = ltcRepository.fetchMoonpaySignedUrl( + mapOf( + "baseCurrencyCode" to currentState.selectedFiatCurrency.code, + "baseCurrencyAmount" to currentState.fiatAmount.toString(), + "language" to appSetting.value.languageCode, + "walletAddress" to currentState.address, + "defaultCurrencyCode" to "ltc", + "currencyCode" to "ltc", + "themeId" to "main-v1.0.0", + "theme" to if (appSetting.value.isDarkMode) "dark" else "light" + ) + ) + + _state.update { it.copy(moonpayBuySignedUrl = signedUrl) } + + } catch (e: Exception) { + handleError(e) + } finally { + + onLoading(false) + + } + + } + + ReceiveDialogEvent.OnSignedUrlClear -> _state.update { it.copy(moonpayBuySignedUrl = null) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/topup/TopUpScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/topup/TopUpScreen.kt index ddc14ded..22f5722d 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/topup/TopUpScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/topup/TopUpScreen.kt @@ -3,10 +3,6 @@ package com.brainwallet.ui.screens.topup -import android.graphics.Bitmap -import android.view.ViewGroup -import android.webkit.WebView -import android.webkit.WebViewClient import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -15,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForward @@ -25,12 +20,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate @@ -39,27 +28,25 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.viewmodel.compose.viewModel import com.brainwallet.R import com.brainwallet.navigation.OnNavigate +import com.brainwallet.navigation.Route import com.brainwallet.navigation.UiEffect import com.brainwallet.tools.manager.AnalyticsManager import com.brainwallet.tools.util.BRConstants import com.brainwallet.ui.composable.BorderedLargeButton import com.brainwallet.ui.composable.BrainwalletScaffold import com.brainwallet.ui.composable.BrainwalletTopAppBar -import com.brainwallet.ui.composable.MediumTextButton import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItEvent import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItViewModel -import kotlinx.coroutines.delay @Composable fun TopUpScreen( onNavigate: OnNavigate, viewModel: YourSeedProveItViewModel = viewModel() ) { - val state by viewModel.state.collectAsState() + val context = LocalContext.current /// Layout values @@ -69,13 +56,6 @@ fun TopUpScreen( val spacerHeight = 90 val skipButtonWidth = 100 - var shouldShowWebView by remember { mutableStateOf(false) } - var backEnabled by remember { mutableStateOf(false) } - var shouldSkipBeVisible by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - } - BrainwalletScaffold( topBar = { BrainwalletTopAppBar( @@ -93,7 +73,6 @@ fun TopUpScreen( } ) { paddingValues -> - Spacer(modifier = Modifier.height(spacerHeight.dp)) Column( modifier = Modifier @@ -103,129 +82,54 @@ fun TopUpScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), ) { - - if (!shouldShowWebView) { + Spacer(modifier = Modifier.weight(1f)) + Row { + Icon( + Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = stringResource(R.string.down_left_arrow), + modifier = Modifier + .rotate(45f) + .graphicsLayer( + scaleX = 2f, + scaleY = 2f + ) + ) Spacer(modifier = Modifier.weight(1f)) - Row { - Icon( - Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = stringResource(R.string.down_left_arrow), - modifier = Modifier - .rotate(45f) - .graphicsLayer( - scaleX = 2f, - scaleY = 2f - ) - ) - Spacer(modifier = Modifier.weight(1f)) - } + } - Text( - text = stringResource(R.string.top_up_title), - style = MaterialTheme.typography.displaySmall.copy(textAlign = TextAlign.Left), - modifier = Modifier.fillMaxWidth() - ) + Text( + text = stringResource(R.string.top_up_title), + style = MaterialTheme.typography.displaySmall.copy(textAlign = TextAlign.Left), + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = stringResource(R.string.top_up_detail_1), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxWidth() + ) + BorderedLargeButton( + onClick = { onNavigate.invoke(UiEffect.Navigate(destinationRoute = Route.BuyLitecoin)) }, + modifier = Modifier.fillMaxWidth() + ) { Text( - text = stringResource(R.string.top_up_detail_1), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.fillMaxWidth() + text = stringResource(R.string.top_up_button_1), + style = MaterialTheme.typography.bodyLarge ) } - if (shouldShowWebView) { - AndroidView( - factory = { - WebView(it).apply { - setInitialScale(99) - setLayerType(ViewGroup.LAYER_TYPE_SOFTWARE, null) - settings.apply { - javaScriptEnabled = true - useWideViewPort = true - loadWithOverviewMode = true - builtInZoomControls = true - domStorageEnabled = true - allowContentAccess = true - } - webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { - backEnabled = view.canGoBack() - } - } - webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - shouldSkipBeVisible = true - } - } - - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT) - - } - }, - update = { - it.loadUrl(BRConstants.MOBILE_MP_LINK) - }, - modifier = Modifier - .height(600.dp) - .weight(1f) + BorderedLargeButton( + onClick = { + viewModel.onEvent(YourSeedProveItEvent.OnGameAndSync) + AnalyticsManager.logCustomEvent(BRConstants._20250303_DSTU) + }, + modifier = Modifier.fillMaxWidth() + + ) { + Text( + text = stringResource(R.string.top_up_button_2), + style = MaterialTheme.typography.bodyMedium ) - Row( - modifier = Modifier - .fillMaxWidth() - .height(buttonRowHeight.dp), - - ){ - Spacer(modifier = Modifier.weight(1f)) - if(shouldSkipBeVisible) { - MediumTextButton( - onClick = { - viewModel.onEvent(YourSeedProveItEvent.OnGameAndSync) - AnalyticsManager.logCustomEvent(BRConstants._20250303_DSTU) - }, - modifier = Modifier - .width(skipButtonWidth.dp) - .padding(vertical = horizontalVerticalSpacing.dp), - - ) { - - Text( - text = stringResource(R.string.top_up_button_3), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Right - ) - } - } - } - - } - if (!shouldShowWebView) { - BorderedLargeButton( - onClick = { - shouldShowWebView = true - }, - modifier = Modifier.fillMaxWidth() - - ) { - Text( - text = stringResource(R.string.top_up_button_1), - style = MaterialTheme.typography.bodyLarge - ) - } - BorderedLargeButton( - onClick = { - viewModel.onEvent(YourSeedProveItEvent.OnGameAndSync) - AnalyticsManager.logCustomEvent(BRConstants._20250303_DSTU) - }, - modifier = Modifier.fillMaxWidth() - - ) { - Text( - text = stringResource(R.string.top_up_button_2), - style = MaterialTheme.typography.bodyMedium - ) - } - } Spacer(modifier = Modifier.weight(0.05f)) diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt index 6f4d19c4..998ddb00 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItViewModel.kt @@ -18,8 +18,8 @@ class YourSeedProveItViewModel : BrainwalletViewModel() { when (event) { is YourSeedProveItEvent.OnLoad -> _state.update { it.copy( - correctSeedWords = event.seedWords.mapIndexed { index, word -> - index to SeedWordItem(expected = word) + correctSeedWords = event.seedWords.mapIndexed { index, word -> + index to SeedWordItem(expected = word) }.toMap(), shuffledSeedWords = event.seedWords.shuffled() ) diff --git a/app/src/main/java/com/brainwallet/ui/theme/Theme.kt b/app/src/main/java/com/brainwallet/ui/theme/Theme.kt index 819eb43e..36afabbe 100644 --- a/app/src/main/java/com/brainwallet/ui/theme/Theme.kt +++ b/app/src/main/java/com/brainwallet/ui/theme/Theme.kt @@ -1,6 +1,7 @@ package com.brainwallet.ui.theme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -84,6 +85,9 @@ object BrainwalletTheme { val typography: Typography @Composable @ReadOnlyComposable get() = MaterialTheme.typography + val shapes: Shapes + @Composable @ReadOnlyComposable get() = MaterialTheme.shapes + //todo: add typography, shape? for the design system } diff --git a/app/src/main/res/drawable/ic_copy.xml b/app/src/main/res/drawable/ic_copy.xml new file mode 100644 index 00000000..6a0293ec --- /dev/null +++ b/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml new file mode 100644 index 00000000..33f6260c --- /dev/null +++ b/app/src/main/res/drawable/ic_import.xml @@ -0,0 +1,19 @@ + + + + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index 0d1f4a90..dc0df45e 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -19,10 +19,10 @@ android:icon="@drawable/ic_nav_receive" android:title="@string/Button.receive" /> - + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu_us.xml b/app/src/main/res/menu/bottom_nav_menu_us.xml index 2d26a165..187ac237 100644 --- a/app/src/main/res/menu/bottom_nav_menu_us.xml +++ b/app/src/main/res/menu/bottom_nav_menu_us.xml @@ -1,27 +1,28 @@ + + - + android:title="@string/bottom_nav_item_buy_receive_title" /> - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8a3ee8f0..cf9095d7 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -439,4 +439,15 @@ قمة واسطة قليل + مزامنة البيانات التعريفية: + شراء لايتكوين + مدعوم من مونباي + تحميل... + المبلغ أقل من الحد الأدنى (%1$f) + يتجاوز المبلغ الحد الأقصى (%1$f) + شراء لايتكوين + يتم إيداعها في عنوان LTC الخاص بك: + الشراء باستخدام Moonpay + العنوان الجديد + شراء / تلقي diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6d272928..a07b7400 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -800,4 +800,15 @@ Spitze Medium Niedrig + Metadaten synchronisieren: + Kaufen Sie LTC + Unterstützt von Moonpay + Laden... + Betrag liegt unter dem Mindestlimit (%1$f) + Betrag überschreitet Höchstgrenze (%1$f) + Litecoin kaufen + Bitte überweisen Sie es an Ihre LTC-Adresse: + Kaufen Sie mit Moonpay + Neue Adresse + Kaufen / Empfangen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 24bf3fc0..1a918843 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -799,4 +799,15 @@ Arriba Medio Bajo + Sincronizar metadatos: + Comprar LTC + Desarrollado por Moonpay + Cargando... + El monto está por debajo del límite mínimo (%1$f) + El importe supera el límite máximo (%1$f) + Comprar Litecoin + Se depositará en su dirección LTC: + Compra con Moonpay + Nueva dirección + Comprar / Recibir diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1571dd93..b0e92d08 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -800,4 +800,15 @@ Haut Moyen Faible + Synchroniser les métadonnées : + Acheter des SLD + Propulsé par Moonpay + Chargement... + Le montant est inférieur à la limite minimale (%1$f) + Le montant dépasse la limite maximale (%1$f) + Acheter du Litecoin + Soyez déposé à votre adresse LTC : + Acheter avec Moonpay + Nouvelle adresse + Acheter / Recevoir diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index a03d849c..3c263e70 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -438,4 +438,15 @@ शीर्ष मध्यम कम + मेटाडेटा सिंक करें: + एलटीसी खरीदें + मूनपे द्वारा संचालित + लोड हो रहा है... + राशि न्यूनतम सीमा से कम है (%1$f) + राशि अधिकतम सीमा से अधिक है (%1$f) + लाइटकॉइन खरीदें + अपने एलटीसी पते पर जमा करें: + मूनपे से खरीदें + नया पता + खरीदें/प्राप्त करें diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index d3bb43e0..371a07fa 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -802,4 +802,15 @@ Atas Sedang Rendah + Sinkronkan metadata: + Beli LTC + Didukung oleh Moonpay + Memuat... + Jumlahnya di bawah batas minimum (%1$f) + Jumlah melebihi batas maksimum (%1$f) + Beli Litecoin + Disetorkan ke Alamat LTC Anda: + Beli dengan Moonpay + Alamat Baru + Beli / Terima diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 592ebba1..0246634a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -800,4 +800,15 @@ Superiore Medio Basso + Sincronizza i metadati: + Acquista LTC + Alimentato da Moonpay + Caricamento... + L\'importo è inferiore al limite minimo (%1$f) + L\'importo supera il limite massimo (%1$f) + Acquista Litecoin + Essere depositato al tuo indirizzo LTC: + Acquista con Moonpay + Nuovo indirizzo + Acquista/Ricevi diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c6ffb20c..9523482b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -800,4 +800,15 @@ トップ 中くらい 低い + メタデータを同期する: + LTCを購入する + ムーンペイ提供 + 読み込み中... + 金額が下限値 (%1$f) を下回っています + 量が上限を超えています (%1$f) + ライトコインを購入する + LTCアドレスに入金してください: + Moonpay で購入する + 新しい住所 + 買う/受け取る diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 8ec4b384..0f409eb0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -800,4 +800,15 @@ 맨 위 중간 낮은 + 메타데이터 동기화: + 라이트코인 구매 + 문페이 제공 + 로드 중... + 금액이 최소 한도(%1$f)보다 낮습니다. + 금액이 최대 한도(%1$f)를 초과했습니다. + 라이트코인 구매 + LTC 주소로 입금하세요: + 문페이로 구매하세요 + 새 주소 + 구매 / 받기 diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 27b64935..535904d3 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -799,4 +799,15 @@ Principal Médio Baixo + Sincronizar metadados: + Comprar LTC + Desenvolvido por Moonpay + Carregando... + O valor está abaixo do limite mínimo (%1$f) + O valor excede o limite máximo (%1$f) + Comprar Litecoin + Seja depositado em seu endereço LTC: + Compre com Moonpay + Novo endereço + Comprar / Receber diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4df74670..5434b353 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -800,4 +800,15 @@ Вершина Середина Низкий + Синхронизировать метаданные: + Купить LTC + При поддержке Moonpay + Загрузка... + Сумма ниже минимального лимита (%1$f) + Сумма превышает максимальный лимит (%1$f) + Купить Лайткоин + Депонируйте на свой адрес LTC: + Купить с помощью Moonpay + Новый адрес + Купить/получить diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 78b503fd..eef96cac 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -438,4 +438,15 @@ Bästa Medium Låg + Synkronisera metadata: + Köp LTC + Drivs av Moonpay + Belastning... + Beloppet är under minimigränsen (%1$f) + Beloppet överskrider maxgränsen (%1$f) + Köp Litecoin + Sätt in till din LTC-adress: + Köp med Moonpay + Ny adress + Köp/ta emot diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a526c60c..b67506d4 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -823,4 +823,16 @@ Tepe Orta Düşük + + Meta verileri senkronize et: + LTC satın al + Moonpay tarafından desteklenmektedir + Yükleniyor... + Tutar minimum sınırın altında (%1$f) + Tutar maksimum sınırı aşıyor (%1$f) + Litecoin satın al + LTC Adresinize yatırın: + Moonpay ile satın alın + Yeni Adres + Satın Al / Al diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b5d20546..f8899d68 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -806,4 +806,16 @@ Топ Середній Низький + + Синхронізувати метадані: + Купити LTC + На основі Moonpay + Завантаження... + Сума нижче мінімального ліміту (%1$f) + Сума перевищує максимальний ліміт (%1$f) + Купуйте Litecoin + Зробіть депозит на свою адресу LTC: + Купуйте за допомогою Moonpay + Нова адреса + Купити / Отримати diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 53cd941f..763288ac 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -800,4 +800,15 @@ 顶部 中等的 低的 + 同步元数据: + 购买莱特币 + 由 月付 提供支持 + 加载中... + 金额低于最低限额 (%1$f) + 金额超过最大限制 (%1$f) + 购买莱特币 + 请存入您的 LTC 地址: + 使用 Moonpay 购买 + 新地址 + 购买/接收 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 69514c55..fe1abc56 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -800,4 +800,15 @@ 頂部 中等的 低的 + 同步元資料: + 購買萊特幣 + 由 月付 提供支持 + 載入中... + 金額低於最低限額 (%1$f) + 金額超過最大限制 (%1$f) + 購買萊特幣 + 請存入您的 LTC 地址: + 使用 Moonpay 購買 + 新地址 + 購買/接收 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d107536c..f7c7deb0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,7 +74,7 @@ receive - send + Send Settings @@ -839,8 +839,20 @@ Share analytics data Update PIN Show + Buy LTC + Powered by Moonpay + Loading... Network Fee (per kb): Higher value mean the transaction completes sooner Top Medium Low + Amount is below minimum limit (%f) + Amount exceeds maximum limit (%f) + Buy Litecoin + Do be deposited to your LTC Address: + Buy with Moonpay + New Address + + Buy / Receive + Custom From 18eda469fb568e492d9f7a6f3a5b95d240872478 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 12 May 2025 13:29:20 +0100 Subject: [PATCH 29/44] version and code bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 80cc7aec..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 = 202505021 - versionName = "v4.4.7" + versionCode = 202505121 + versionName = "v4.5.0" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") From dbb6a5fee697fa6d29f0fdd32e77f378029b4a7a Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 19 May 2025 01:22:10 -0700 Subject: [PATCH 30/44] Added support url (#81) - changed the language - code bump --- app/build.gradle.kts | 2 +- app/src/main/java/com/brainwallet/tools/util/BRConstants.java | 1 + .../ui/screens/home/composable/HomeSettingDrawerSheet.kt | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 172a0a87..8add0963 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 34 - versionCode = 202505121 + versionCode = 202505141 versionName = "v4.5.0" multiDexEnabled = true diff --git a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java index 1c4d4a53..7c989533 100644 --- a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java +++ b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java @@ -98,6 +98,7 @@ private BRConstants() { public static final String TWITTER_LINK = "https://twitter.com/Brainwallet_App"; public static final String INSTAGRAM_LINK = "https://www.instagram.com/brainwalletapp"; public static final String WEB_LINK = "https://brainwallet.co"; + public static final String SUPPORT_WEB_LINK = "https://brainwallet.co/support.html"; public static final String TOS_LINK = "https://brainwallet.co/privacy-policy.html"; public static final String MOBILE_MP_LINK = "https://brainwallet.co/mobile-top-up.html"; public static String BITREFILL_AFFILIATE_LINK = "https://www.bitrefill.com/"; 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 5c0c6a9c..1daa4b33 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 @@ -144,11 +144,11 @@ fun HomeSettingDrawerSheet( item { SettingRowItem( title = stringResource(R.string.settings_title_support), - description = "brainwallet.co", + description = "brainwallet.co/support.html", onClick = { val builder = CustomTabsIntent.Builder() val customTabsIntent = builder.build() - customTabsIntent.launchUrl(context, Uri.parse(BRConstants.WEB_LINK)) + customTabsIntent.launchUrl(context, Uri.parse(BRConstants.SUPPORT_WEB_LINK)) } ) } From c82a5e3316282bc3d7d31d3c5e8d7ebb5774f521 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 19 May 2025 09:26:16 +0100 Subject: [PATCH 31/44] version and code bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8add0963..290ed2dd 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 = 202505141 - versionName = "v4.5.0" + versionCode = 202505191 + versionName = "v4.5.3" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") From 2981ec62d3974cbbc12ad70617612588b181135b Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Sun, 25 May 2025 19:32:34 +0700 Subject: [PATCH 32/44] Revert from eda0f532 & cherry pick (#86) * version bump * feat: new UI for receive and topup flow (moonpay integration) (#72) * feat: [WIP][UI] new UI for receive and topup flow * feat: [WIP][UI] implement copy address and some refactor * feat: replace webview using CustomTabsIntent from androidx.browser * feat: replace webview using CustomTabsIntent from androidx.browser * feat: add PickerWheel * chore: rename param at QRUtils.generateQR * chore: revert default startDestination at BrainwalletActivity * feat: wip moonpay integration * feat: wip moonpay integration * chore: new UI for ReceiveDialog * chore: parse error from response API at BrainwalletViewModel * chore: add /moonpay/buy-quote and remove dev base url * chore: make LegacyNavigation.showMoonPayWidget receive params for the widget url * chore: add GetMoonpayBuyQuoteResponse * chore: wip moonpay integration to call buy-quote * fix: fix wheel picker for IDR at ReceiveDialog * fix: fix invalid signature * feat: moonpay integration at onboarding flow at BuyLitecoinScreen also fix some stuffs * chore: replace base url for moonpay buy integration when debug * chore: introduce LocalCacheSource.kt and cache fetch limits * chore: adjustment receive dialog UI - debounce fiat currency to 2s - increment in 10 - remove decimal places - default value to 10% of range * chore: reorder bottom navigation item and add new strings * chore: add new translations * chore: refactor UI for buy and receive dialog * chore: cleanup ReceiveDialogViewModel * chore: using MoonpayBuyWidget * chore: adjustment moonpaywidget * chore: for now moonpay widget using CustomTabsIntent * chore: set allowSpend to false when recommend rescan click * fix: add delete transaction data from local database * Removed chatty event * chore: add analytics at BRWalletManager.publishCallback * chore: make sure calculation and static fee same as iOS, add setting for selected fee type --------- Co-authored-by: Kerry Washington --- .../java/com/brainwallet/data/model/Fee.kt | 27 ++++++++---- .../data/repository/LtcRepository.kt | 18 ++++---- .../data/repository/SettingRepository.kt | 17 ++++++++ .../presenter/fragments/FragmentSend.kt | 32 ++++++++------- .../brainwallet/tools/manager/FeeManager.java | 27 +++++++++--- .../tools/manager/PromptManager.java | 2 + .../brainwallet/tools/util/BRConstants.java | 6 +-- .../brainwallet/tools/util/BRExchange.java | 12 ++++-- .../com/brainwallet/tools/util/Utils.java | 41 ++++++++----------- .../ui/screens/home/SettingsEvent.kt | 2 + .../ui/screens/home/SettingsState.kt | 2 + .../ui/screens/home/SettingsViewModel.kt | 28 ++++++++++++- .../home/composable/HomeSettingDrawerSheet.kt | 1 + .../settingsrows/LitecoinBlockchainDetail.kt | 18 ++------ .../brainwallet/wallet/BRWalletManager.java | 17 +++++--- .../tools/database/DatabaseTests.kt | 1 - .../brainwallet/tools/util/BRConstantsTest.kt | 2 - 17 files changed, 160 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/com/brainwallet/data/model/Fee.kt b/app/src/main/java/com/brainwallet/data/model/Fee.kt index 0ea99667..b1d086ae 100644 --- a/app/src/main/java/com/brainwallet/data/model/Fee.kt +++ b/app/src/main/java/com/brainwallet/data/model/Fee.kt @@ -8,8 +8,6 @@ import com.brainwallet.tools.manager.FeeManager.LUXURY import com.brainwallet.tools.manager.FeeManager.REGULAR import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlin.math.ceil -import kotlin.math.round @Serializable data class Fee( @@ -22,23 +20,31 @@ data class Fee( @JvmField @SerialName("fee_per_kb_economy") var economy: Long, + var timestamp: Long ) { companion object { - //from legacy - // this is the default that matches the mobile-api if the server is unavailable - private const val defaultEconomyFeePerKB: Long = - 2500L // From legacy minimum. default min is 1000 as Litecoin Core version v0.17.1 + /** + * Default value for economy fee rate per kilobyte. + * Used as a fallback when fee rate cannot be determined dynamically. + * + * Previous value: 2500L (2.5 satoshis per byte). From legacy minimum. default min is 1000 as Litecoin Core version v0.17.1 + * Updated economy to 8000L (8 satoshis per byte) on 2023-11-16 (same as iOS) + */ + private const val defaultEconomyFeePerKB: Long = 8000L private const val defaultRegularFeePerKB: Long = 25000L private const val defaultLuxuryFeePerKB: Long = 66746L private const val defaultTimestamp: Long = 1583015199122L -// {"fee_per_kb":5289,"fee_per_kb_economy":2645,"fee_per_kb_luxury":10578} - + /** + * currently we are using this static [Default] for our fee + * maybe we need to update core if we need dynamic fee? + */ @JvmStatic val Default = Fee( defaultLuxuryFeePerKB, defaultRegularFeePerKB, defaultEconomyFeePerKB, + defaultTimestamp ) } } @@ -80,4 +86,9 @@ fun FeeOption.getFiatFormatted(currencyEntity: CurrencyEntity): String { val fiatValue = getFiat(currencyEntity) val formatted = String.format("%.3f", fiatValue) return "${currencyEntity.symbol}$formatted" +} + +fun List.getSelectedIndex(selectedFeeType: String): Int { + return indexOfFirst { it.type == selectedFeeType }.takeIf { it >= 0 } + ?: 2 //2 -> index of top, since we have [low,medium,top] } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt index 365bbc0d..962602f7 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -54,17 +54,13 @@ interface LtcRepository { } - override suspend fun fetchFeePerKb(): Fee { - return sharedPreferences.fetchWithCache( - key = PREF_KEY_NETWORK_FEE_PER_KB, - cachedAtKey = PREF_KEY_NETWORK_FEE_PER_KB_CACHED_AT, - cacheTimeMs = 6 * 60 * 60 * 1000, - fetchData = { - remoteApiSource.getFeePerKb() - }, - defaultValue = Fee.Default - ) - } + /** + * for now we just using [Fee.Default] + * will move to [RemoteApiSource.getFeePerKb] after fix the calculation when we do send + * + * maybe need updaete core if we need to use dynamic fee? + */ + override suspend fun fetchFeePerKb(): Fee = Fee.Default //using static fee override suspend fun fetchLimits(baseCurrencyCode: String): MoonpayCurrencyLimit { return sharedPreferences.fetchWithCache( diff --git a/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt b/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt index 2e4b3388..3f217918 100644 --- a/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/SettingRepository.kt @@ -5,6 +5,7 @@ import androidx.core.content.edit import com.brainwallet.data.model.AppSetting import com.brainwallet.data.model.CurrencyEntity import com.brainwallet.data.model.Language +import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +37,10 @@ interface SettingRepository { fun toggleDarkMode(isDarkMode: Boolean) + fun putSelectedFeeType(feeType: String) + + fun getSelectedFeeType(): String + class Impl( private val sharedPreferences: SharedPreferences, private val currencyDataSource: CurrencyDataSource @@ -77,6 +82,17 @@ interface SettingRepository { _state.update { it.copy(isDarkMode = isDarkMode) } } + override fun putSelectedFeeType(feeType: String) { + sharedPreferences.edit { + putString(KEY_SELECTED_FEE_TYPE, feeType) + } + } + + override fun getSelectedFeeType(): String = + sharedPreferences.getString(KEY_SELECTED_FEE_TYPE, FeeManager.REGULAR) + ?: FeeManager.REGULAR + + private fun load(): AppSetting { return AppSetting( isDarkMode = sharedPreferences.getBoolean(KEY_IS_DARK_MODE, true), @@ -100,5 +116,6 @@ interface SettingRepository { const val KEY_IS_DARK_MODE = "is_dark_mode" const val KEY_LANGUAGE_CODE = "language_code" const val KEY_FIAT_CURRENCY_CODE = "fiat_currency_code" + const val KEY_SELECTED_FEE_TYPE = "selected_fee_type" } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt index 9b47690e..83d08dcb 100644 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt +++ b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt @@ -135,6 +135,9 @@ class FragmentSend : Fragment() { updateText() + //update fee + BRWalletManager.getInstance().setFeePerKb(FeeManager.getInstance().currentFeeValue) + return rootView } @@ -496,7 +499,6 @@ class FragmentSend : Fragment() { override fun onStop() { super.onStop() - FeeManager.getInstance().resetFeeType() } override fun onResume() { @@ -611,9 +613,22 @@ class FragmentSend : Fragment() { } Timber.d("timber: updateText: currentAmountInLitoshis %d", currentAmountInLitoshis) + // Service Fee depending on ISOSymbol + var serviceFee = Utils.tieredOpsFee(activity, currentAmountInLitoshis) + val serviceFeeForISOSymbol = + BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(serviceFee)) + .setScale(scaleValue, RoundingMode.HALF_UP) + val formattedServiceFee = BRCurrency.getFormattedCurrencyString( + activity, + selectedISOSymbol, + serviceFeeForISOSymbol + ) + + val totalAmountToCalculateFees = currentAmountInLitoshis + serviceFee + // Network Fee depending on ISOSymbol - var networkFee = if (currentAmountInLitoshis > 0) { - BRWalletManager.getInstance().feeForTransactionAmount(currentAmountInLitoshis) + var networkFee = if (totalAmountToCalculateFees > 0) { + BRWalletManager.getInstance().feeForTransactionAmount(totalAmountToCalculateFees) } else { 0 } //Amount is zero so network fee is also zero @@ -626,17 +641,6 @@ class FragmentSend : Fragment() { networkFeeForISOSymbol ) - // Service Fee depending on ISOSymbol - var serviceFee = Utils.tieredOpsFee(activity, currentAmountInLitoshis) - val serviceFeeForISOSymbol = - BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(serviceFee)) - .setScale(scaleValue, RoundingMode.HALF_UP) - val formattedServiceFee = BRCurrency.getFormattedCurrencyString( - activity, - selectedISOSymbol, - serviceFeeForISOSymbol - ) - // Total Fees depending on ISOSymbol val totalFees = networkFee + serviceFee val totalFeeForISOSymbol = diff --git a/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java b/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java index f9457baa..bf927976 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/FeeManager.java @@ -4,9 +4,10 @@ import androidx.annotation.StringDef; +import com.brainwallet.data.model.Fee; import com.brainwallet.data.repository.LtcRepository; +import com.brainwallet.data.repository.SettingRepository; import com.brainwallet.tools.threads.BRExecutor; -import com.brainwallet.data.model.Fee; import org.koin.java.KoinJavaComponent; @@ -14,7 +15,6 @@ import java.lang.annotation.RetentionPolicy; //we are still using this, maybe in the future will deprecate? -@Deprecated public final class FeeManager { @@ -56,8 +56,24 @@ public boolean isLuxuryFee() { public static final String REGULAR = "regular";//medium public static final String ECONOMY = "economy";//low - public void setFees(long luxuryFee, long regularFee, long economyFee) { - currentFeeOptions = new Fee(luxuryFee, regularFee, economyFee); + public void setFees(Fee fee) { + currentFeeOptions = fee; + } + + public long getCurrentFeeValue() { + SettingRepository settingRepository = KoinJavaComponent.get(SettingRepository.class); + String feeType = settingRepository.getSelectedFeeType(); + + switch (feeType) { + case LUXURY: + return currentFeeOptions.luxury; + case REGULAR: + return currentFeeOptions.regular; + case ECONOMY: + return currentFeeOptions.economy; + default: + return currentFeeOptions.regular; // Default to regular fee + } } public static void updateFeePerKb(Context app) { @@ -66,8 +82,7 @@ public static void updateFeePerKb(Context app) { (coroutineScope, continuation) -> ltcRepository.fetchFeePerKb(continuation) ).whenComplete((fee, throwable) -> { - //legacy logic - FeeManager.getInstance().setFees(fee.luxury, fee.regular, fee.economy); + FeeManager.getInstance().setFees(fee); BRSharedPrefs.putFeeTime(app, System.currentTimeMillis()); //store the time of the last successful fee fetch }); } diff --git a/app/src/main/java/com/brainwallet/tools/manager/PromptManager.java b/app/src/main/java/com/brainwallet/tools/manager/PromptManager.java index e6c9a029..a716d7f7 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/PromptManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/PromptManager.java @@ -9,6 +9,7 @@ import android.content.Intent; import android.view.View; +import com.brainwallet.presenter.activities.settings.SyncBlockchainActivity; import com.brainwallet.tools.security.BRKeyStore; import com.brainwallet.tools.threads.BRExecutor; import com.brainwallet.R; @@ -88,6 +89,7 @@ public void run() { BRSharedPrefs.putStartHeight(app, 0); BRPeerManager.getInstance().rescan(); BRSharedPrefs.putScanRecommended(app, false); + BRSharedPrefs.putAllowSpend(app, false); } }); } diff --git a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java index 7c989533..bd8f810b 100644 --- a/app/src/main/java/com/brainwallet/tools/util/BRConstants.java +++ b/app/src/main/java/com/brainwallet/tools/util/BRConstants.java @@ -129,7 +129,6 @@ private BRConstants() { public static final String _20201118_DTGS = "did_tap_get_support"; public static final String _20200217_DUWP = "did_unlock_with_pin"; public static final String _20200217_DUWB = "did_unlock_with_biometrics"; - public static final String _20200301_DUDFPK = "did_use_default_fee_per_kb"; public static final String _20201121_SIL = "started_IFPS_lookup"; public static final String _20201121_DRIA = "did_resolve_IPFS_address"; public static final String _20201121_FRIA = "failed_resolve_IPFS_address"; @@ -137,6 +136,7 @@ private BRConstants() { public static final String _20230407_DCS = "did_complete_sync"; public static final String _20250303_DSTU = "did_skip_top_up"; + public static final String _20250517_WCINFO = "wallet_callback_info"; ///Dev: These events not yet used public static final String _20200207_DTHB = "did_tap_header_balance"; @@ -170,7 +170,6 @@ private BRConstants() { _20201118_DTGS, _20200217_DUWP, _20200217_DUWB, - _20200301_DUDFPK, _20201121_SIL, _20201121_DRIA, _20201121_FRIA, @@ -187,7 +186,8 @@ private BRConstants() { _20241006_UCR, _HOME_OPEN, _20250222_PAC, - _20250303_DSTU + _20250303_DSTU, + _20250517_WCINFO }) public @interface Event { } diff --git a/app/src/main/java/com/brainwallet/tools/util/BRExchange.java b/app/src/main/java/com/brainwallet/tools/util/BRExchange.java index 52b6df6b..c199b115 100644 --- a/app/src/main/java/com/brainwallet/tools/util/BRExchange.java +++ b/app/src/main/java/com/brainwallet/tools/util/BRExchange.java @@ -102,11 +102,17 @@ public static BigDecimal getLitoshisFromAmount(Context app, String iso, BigDecim if (iso.equalsIgnoreCase("LTC")) { result = BRExchange.getLitoshisForLitecoin(app, amount); } else { - //multiply by 100 because core function localAmount accepts the smallest amount e.g. cents CurrencyEntity ent = CurrencyDataSource.getInstance(app).getCurrencyByIso(iso); if (ent == null) return new BigDecimal(0); - BigDecimal rate = new BigDecimal(ent.rate).multiply(new BigDecimal(100)); - result = new BigDecimal(BRWalletManager.getInstance().bitcoinAmount(amount.multiply(new BigDecimal(100)).longValue(), rate.doubleValue())); + + // Get the exchange rate + BigDecimal rate = new BigDecimal(ent.rate); + + result = amount.divide(rate, 8, BRConstants.ROUNDING_MODE) + .multiply(new BigDecimal("100000000")); + + // Round to a whole number of litoshis + result = result.setScale(0, BRConstants.ROUNDING_MODE); } return result; } diff --git a/app/src/main/java/com/brainwallet/tools/util/Utils.java b/app/src/main/java/com/brainwallet/tools/util/Utils.java index 8e7e6062..fb03d0c3 100644 --- a/app/src/main/java/com/brainwallet/tools/util/Utils.java +++ b/app/src/main/java/com/brainwallet/tools/util/Utils.java @@ -267,38 +267,29 @@ else if (name == ServiceItems.CLIENTCODE) { } /// Description: 1715876807 public static long tieredOpsFee(Context app, long sendAmount) { - - double doubleRate = 83.000; - double sendAmountDouble = new Double(String.valueOf(sendAmount)); - String usIso = Currency.getInstance(new Locale("en", "US")).getCurrencyCode(); - CurrencyEntity currency = CurrencyDataSource.getInstance(app).getCurrencyByIso(usIso); - if (currency != null) { - doubleRate = currency.rate; + if (sendAmount < 1_398_000) { + return 69900; } - double usdInLTC = sendAmountDouble * doubleRate / 100_000_000.0; - usdInLTC = Math.floor(usdInLTC * 100) / 100; - - if (isBetween(usdInLTC, 0.00, 20.00)) { - double lowRate = usdInLTC * 0.01; - return (long) ((lowRate / doubleRate) * 100_000_000.0); + else if (sendAmount < 6_991_000) { + return 111_910; + } + else if (sendAmount < 27_965_000) { + return 279_700; } - else if (isBetween(usdInLTC, 20.00, 50.00)) { - return (long) ((0.30 / doubleRate) * 100_000_000.0); + else if (sendAmount < 139_820_000) { + return 699_540; } - else if (isBetween(usdInLTC, 50.00, 100.00)) { - return (long) ((1.00 / doubleRate) * 100_000_000.0); - } - else if (isBetween(usdInLTC, 100.00, 500.00)) { - return (long) ((2.00 / doubleRate) * 100_000_000.0); + else if (sendAmount < 279_653_600) { + return 1_049_300; } - else if (isBetween(usdInLTC, 500.00, 1000.00)) { - return (long) ((2.50 / doubleRate) * 100_000_000.0); + else if (sendAmount < 699_220_000) { + return 1_398_800; } - else if ( usdInLTC > 1000.00) { - return (long) ((3.00 / doubleRate) * 100_000_000.0); + else if (sendAmount < 1_398_440_000) { + return 2_797_600; } else { - return (long) ((3.00 / doubleRate) * 100_000_000.0); + return 2_797_600; } } private static boolean isBetween(double x, double lower, double upper) { diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt index 48e0750a..61a8ced4 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsEvent.kt @@ -8,6 +8,7 @@ sealed class SettingsEvent { val shareAnalyticsDataEnabled: Boolean = false, val lastSyncMetadata: String? = null, ) : SettingsEvent() + object OnSecurityUpdatePinClick : SettingsEvent() object OnSecuritySeedPhraseClick : SettingsEvent() object OnSecurityShareAnalyticsDataClick : SettingsEvent() @@ -20,4 +21,5 @@ sealed class SettingsEvent { object OnFiatSelectorDismiss : SettingsEvent() data class OnFiatChange(val currency: CurrencyEntity) : SettingsEvent() object OnBlockchainSyncClick : SettingsEvent() + data class OnFeeTypeChange(val feeType: String) : SettingsEvent() } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsState.kt b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsState.kt index 60a209bd..d0336bff 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsState.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsState.kt @@ -5,6 +5,7 @@ import com.brainwallet.data.model.Fee import com.brainwallet.data.model.FeeOption import com.brainwallet.data.model.Language import com.brainwallet.data.model.toFeeOptions +import com.brainwallet.tools.manager.FeeManager data class SettingsState( val darkMode: Boolean = true, @@ -20,4 +21,5 @@ data class SettingsState( val shareAnalyticsDataEnabled: Boolean = false, val lastSyncMetadata: String? = null, val currentFeeOptions: List = Fee.Default.toFeeOptions(), + val selectedFeeType: String = FeeManager.LUXURY ) \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt index 8859d395..738af4be 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/SettingsViewModel.kt @@ -8,8 +8,10 @@ import com.brainwallet.data.model.Language import com.brainwallet.data.model.toFeeOptions import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.tools.manager.FeeManager import com.brainwallet.ui.BrainwalletViewModel import com.brainwallet.util.EventBus +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -47,6 +49,25 @@ class SettingsViewModel( AppSetting() ) + init { + viewModelScope.launch { + while (true) { + /** + * need update fee options every 4s, since we are fetching every 4s + * pls check at + * - [CurrencyUpdateWorker] + * - [LtcRepository.fetchRates] + * - [LtcRepository.fetchFeePerKb] + */ + + _state.update { + it.copy(currentFeeOptions = FeeManager.getInstance().currentFeeOptions.toFeeOptions()) + } + delay(4000) + } + } + } + override fun onEvent(event: SettingsEvent) { when (event) { is SettingsEvent.OnLoad -> viewModelScope.launch { @@ -54,7 +75,7 @@ class SettingsViewModel( it.copy( shareAnalyticsDataEnabled = event.shareAnalyticsDataEnabled, lastSyncMetadata = event.lastSyncMetadata, - currentFeeOptions = ltcRepository.fetchFeePerKb().toFeeOptions() + selectedFeeType = settingRepository.getSelectedFeeType() ) } } @@ -143,6 +164,11 @@ class SettingsViewModel( EventBus.emit(EventBus.Event.Message(LEGACY_EFFECT_ON_SHARE_ANALYTICS_DATA_TOGGLE)) } + + is SettingsEvent.OnFeeTypeChange -> _state.update { + settingRepository.putSelectedFeeType(event.feeType) + it.copy(selectedFeeType = event.feeType) + } } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt b/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt index 1daa4b33..b3559014 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt @@ -135,6 +135,7 @@ fun HomeSettingDrawerSheet( .fillMaxSize() .wrapContentHeight(), selectedCurrency = state.selectedCurrency, + selectedFeeType = state.selectedFeeType, feeOptions = state.currentFeeOptions, onEvent = { viewModel.onEvent(it) diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LitecoinBlockchainDetail.kt b/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LitecoinBlockchainDetail.kt index 138ab31c..f964e418 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LitecoinBlockchainDetail.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LitecoinBlockchainDetail.kt @@ -14,10 +14,6 @@ import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -26,24 +22,21 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.brainwallet.R import com.brainwallet.data.model.CurrencyEntity -import com.brainwallet.data.model.Fee import com.brainwallet.data.model.FeeOption import com.brainwallet.data.model.getFiatFormatted -import com.brainwallet.data.model.toFeeOptions -import com.brainwallet.tools.manager.FeeManager +import com.brainwallet.data.model.getSelectedIndex import com.brainwallet.ui.screens.home.SettingsEvent import com.brainwallet.ui.theme.BrainwalletTheme -import com.brainwallet.wallet.BRWalletManager //TODO @Composable fun LitecoinBlockchainDetail( modifier: Modifier = Modifier, selectedCurrency: CurrencyEntity, + selectedFeeType: String, feeOptions: List, onEvent: (SettingsEvent) -> Unit, ) { - var feeOptionsState by remember { mutableIntStateOf(2) } //2 -> index of top, since we have [low,medium,top] /// Layout values val contentHeight = 60 @@ -81,12 +74,9 @@ fun LitecoinBlockchainDetail( NetworkFeeSelector( selectedCurrency = selectedCurrency, feeOptions = feeOptions, - selectedIndex = feeOptionsState + selectedIndex = feeOptions.getSelectedIndex(selectedFeeType) ) { newSelectedIndex -> - feeOptionsState = newSelectedIndex - - //just update inside BRWalletManager.setFeePerKb - BRWalletManager.getInstance().setFeePerKb(feeOptions[newSelectedIndex].feePerKb) + onEvent.invoke(SettingsEvent.OnFeeTypeChange(feeOptions[newSelectedIndex].type)) } } diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index 507a5125..59aaed33 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -9,6 +9,7 @@ import android.media.MediaPlayer; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.os.Bundle; import android.os.Handler; import android.os.NetworkOnMainThreadException; import android.os.SystemClock; @@ -334,6 +335,14 @@ public void onClick(DialogInterface dialog, int which) { */ public static void publishCallback(final String message, final int error, byte[] txHash) { Timber.d("timber: publishCallback: " + message + ", err:" + error + ", txHash: " + Arrays.toString(txHash)); + + Bundle params = new Bundle(); + params.putString("function", "BRWalletManager.publishCallback"); + params.putString("message", message); + params.putInt("error", error); + params.putString("txHash", Arrays.toString(txHash)); + AnalyticsManager.logCustomEventWithParams(BRConstants._20250517_WCINFO, params); + final Context app = BrainwalletApp.getBreadContext(); BRExecutor.getInstance().forMainThreadTasks().execute(new Runnable() { @Override @@ -440,6 +449,7 @@ public static void onTxDeleted(String hash, int notifyUser, final int recommendR final Context ctx = BrainwalletApp.getBreadContext(); if (ctx != null) { BRSharedPrefs.putScanRecommended(ctx, true); + TransactionDataSource.getInstance(ctx).deleteTxByHash(hash); } else { Timber.i("timber: onTxDeleted: Failed! ctx is null"); } @@ -513,11 +523,8 @@ public void initWallet(final Context ctx) { m.createWallet(transactionsCount, pubkeyEncoded); String firstAddress = BRWalletManager.getFirstAddress(pubkeyEncoded); BRSharedPrefs.putFirstAddress(ctx, firstAddress); - FeeManager feeManager = FeeManager.getInstance(); - if (feeManager.isLuxuryFee()) { - FeeManager.updateFeePerKb(ctx); - BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.luxury); - } + //set fee here + BRWalletManager.getInstance().setFeePerKb(FeeManager.getInstance().getCurrentFeeValue()); } if (!pm.isCreated()) { diff --git a/app/src/test/java/com/brainwallet/tools/database/DatabaseTests.kt b/app/src/test/java/com/brainwallet/tools/database/DatabaseTests.kt index 9a3a7d4f..3b8821d0 100644 --- a/app/src/test/java/com/brainwallet/tools/database/DatabaseTests.kt +++ b/app/src/test/java/com/brainwallet/tools/database/DatabaseTests.kt @@ -68,7 +68,6 @@ class DatabaseTests { // Assert.assertSame(BRConstants._20201118_DTGS,"did_tap_get_support"); // Assert.assertSame(BRConstants._20200217_DUWP,"did_unlock_with_pin"); // Assert.assertSame(BRConstants._20200217_DUWB,"did_unlock_with_biometrics"); -// Assert.assertSame(BRConstants._20200301_DUDFPK,"did_use_default_fee_per_kb"); // Assert.assertSame(BRConstants._20201121_SIL,"started_IFPS_lookup"); // Assert.assertSame(BRConstants._20201121_DRIA,"did_resolve_IPFS_address"); // Assert.assertSame(BRConstants._20201121_FRIA,"failed_resolve_IPFS_address"); diff --git a/app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt b/app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt index 82eabe93..5779b8df 100644 --- a/app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt +++ b/app/src/test/java/com/brainwallet/tools/util/BRConstantsTest.kt @@ -43,7 +43,6 @@ class BRConstantsTest { assertSame(BRConstants._20201118_DTGS,"did_tap_get_support"); assertSame(BRConstants._20200217_DUWP,"did_unlock_with_pin"); assertSame(BRConstants._20200217_DUWB,"did_unlock_with_biometrics"); - assertSame(BRConstants._20200301_DUDFPK,"did_use_default_fee_per_kb"); assertSame(BRConstants._20201121_SIL,"started_IFPS_lookup"); assertSame(BRConstants._20201121_DRIA,"did_resolve_IPFS_address"); assertSame(BRConstants._20201121_FRIA,"failed_resolve_IPFS_address"); @@ -128,7 +127,6 @@ class BRConstantsTest { // Assert.assertSame(BRConstants._20201118_DTGS,"did_tap_get_support"); // Assert.assertSame(BRConstants._20200217_DUWP,"did_unlock_with_pin"); // Assert.assertSame(BRConstants._20200217_DUWB,"did_unlock_with_biometrics"); -// Assert.assertSame(BRConstants._20200301_DUDFPK,"did_use_default_fee_per_kb"); // Assert.assertSame(BRConstants._20201121_SIL,"started_IFPS_lookup"); // Assert.assertSame(BRConstants._20201121_DRIA,"did_resolve_IPFS_address"); // Assert.assertSame(BRConstants._20201121_FRIA,"failed_resolve_IPFS_address"); From c161fedb924a437b22b6b905ada4303a0ed51011 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Sun, 25 May 2025 15:48:06 +0100 Subject: [PATCH 33/44] build bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 290ed2dd..bfc2e94d 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 = 202505191 - versionName = "v4.5.3" + versionCode = 202505251 + versionName = "v4.5.4" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") From 63d7494d3e476e60fe645523b954fd004276638a Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Mon, 26 May 2025 17:04:48 +0700 Subject: [PATCH 34/44] chore: open bread activity first then open moonpay widget (#88) --- .../com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index 97260328..89fd9473 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/buylitecoin/BuyLitecoinScreen.kt @@ -149,6 +149,8 @@ fun BuyLitecoinScreen( modifier = Modifier.align(Alignment.BottomCenter), enabled = loadingState.visible.not(), onClick = { + //open bread activity first then open moonpay widget + LegacyNavigation.restartBreadActivity(context) LegacyNavigation.showMoonPayWidget( context = context, params = mapOf( From f09eb286c60f23ad52f1400ef7f29118f27c7c99 Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Thu, 29 May 2025 14:16:53 +0700 Subject: [PATCH 35/44] Adjustment for circleci (#89) * chore: wip adjustment for screengrab * chore: [circleci] adjust config.yml * chore: [circleci] update config.yml, Fastfile, Gemfile.lock, RecoverWalletScreenGrabsTest.kt * chore: [circleci] for now just unit-test --- .circleci/config.yml | 139 ++++++++++-- Gemfile.lock | 2 + .../flow/RecoverWalletScreenGrabsTest.kt | 205 ++++++++++++------ .../java/com/brainwallet/BrainwalletApp.kt | 4 +- .../ui/composable/PasscodeKeypad.kt | 3 +- .../home/composable/HomeSettingDrawerSheet.kt | 13 +- .../settingsrows/LockSettingRowItem.kt | 3 + .../ui/screens/home/receive/ReceiveDialog.kt | 6 +- .../ui/screens/welcome/WelcomeScreen.kt | 2 + app/src/main/res/menu/bottom_nav_menu.xml | 12 +- .../brainwallet/BrainwalletScreengrabApp.kt | 26 ++- fastlane/Fastfile | 6 +- 12 files changed, 317 insertions(+), 104 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1ad5baa3..c074e195 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,60 @@ version: 2.1 orbs: - android: circleci/android@3.0.2 + android: circleci/android@3.1.0 + ruby: circleci/ruby@2.5.3 + +# Define reusable commands +commands: + setup_environment: + description: "Sets up the basic environment for all jobs (checkout, credentials, submodules)" + steps: + - checkout + - run: + name: "Setup environment..." + command: "echo Setting up environment for brainwallet-android!" + - run: + name: "Create google-services.json from env" + command: | + mkdir -p app + echo "$GOOGLE_SERVICES_JSON" | base64 --decode > app/google-services.json + ls -la app/google-services.json + - run: + name: "create service-data.json from env" + command: | + mkdir -p app/src/main/assets + echo "$SERVICE_DATA_JSON" | base64 --decode > app/src/main/assets/service-data.json + ls -la app/src/main/assets/service-data.json + - run: + name: "Create keystore files from env" + command: | + echo "$DEBUG_STORE_FILE" | base64 --decode > debug.keystore + echo "$RELEASE_STORE_FILE" | base64 --decode > release.keystore + ls -la *.keystore + - run: + name: "Create local.properties file" + command: | + cat > local.properties \<< EOL + #adjust with your location + DEBUG_STORE_FILE=${CIRCLE_WORKING_DIRECTORY}/debug.keystore + DEBUG_STORE_PASSWORD=${DEBUG_STORE_PASSWORD} + DEBUG_KEY_ALIAS=${DEBUG_KEY_ALIAS} + DEBUG_KEY_PASSWORD=${DEBUG_KEY_PASSWORD} + + #adjust with your location + RELEASE_STORE_FILE=${CIRCLE_WORKING_DIRECTORY}/release.keystore + RELEASE_STORE_PASSWORD=${RELEASE_STORE_PASSWORD} + RELEASE_KEY_ALIAS=${RELEASE_KEY_ALIAS} + RELEASE_KEY_PASSWORD=${RELEASE_KEY_PASSWORD} + + # screengrab paperkey + SCREENGRAB_PAPERKEY=${SCREENGRAB_PAPERKEY} + EOL + ls && cat local.properties + - run: + name: "Initialize submodule" + command: "git submodule init && git submodule update --init --recursive" + - android/restore_gradle_cache # Define a job to be invoked later in a workflow. # See: https://circleci.com/docs/2.0/configuration-reference/#jobs @@ -14,23 +67,7 @@ jobs: resource_class: large tag: 2024.07.1-ndk steps: - - checkout - - run: - name: "brainwallet-android unit test setup..." - command: "echo Building tests for brainwallet-android! && ls" - - run: - name: "Default for gradle.properties" - command: "echo \"RELEASE_STORE_FILE=/\nRELEASE_STORE_PASSWORD=\nRELEASE_KEY_ALIAS=\nRELEASE_KEY_PASSWORD=\nandroid.useAndroidX=true\nandroid.enableJetifier=true\" >> gradle.properties && ls && cat gradle.properties" - - run: - name: "Initialize submodule" - command: "git submodule init && git submodule update --init --recursive" - - run: - name: "Export google-services.json to env" - command: echo 'export $GOOGLE_SERVICES_JSON="$GOOGLE_SERVICES_JSON"' >> $BASH_ENV - - run: - name: "decode $GOOGLE_SERVICES_JSON to google-services.json" - command: echo "$GOOGLE_SERVICES_JSON" | base64 --decode > app/google-services.json - - android/restore_gradle_cache + - setup_environment - run: name: "Execute Unit Test" command: ./gradlew testBrainwalletDebugUnitTest @@ -46,6 +83,57 @@ jobs: - store_artifacts: path: ~/test-results/junit + screengrab: + executor: + name: android/android_machine + resource_class: large + tag: default + steps: + - setup_environment + - ruby/install: + version: '3.2.0' + - ruby/install-deps + - run: + name: "Install fastlane" + command: | + gem install bundler + bundle install + - android/create_avd: + avd_name: bw_avd + install: true + system_image: system-images;android-34;google_apis;x86_64 + additional_args: '-d "pixel_7_pro"' + - android/start_emulator: + avd_name: bw_avd + no_window: true + memory: 4096 + delay_adb: true + run_logcat: true + # additional_args: "-skin 1080x1920 -memory 2048" + post_emulator_launch_assemble_command: | + # bundle exec fastlane android build_and_screengrab + echo "skipping assemble command, as we only need the emulator running for screengrab" + - run: + name: "Wait for emulator to boot and set PIN" + command: | + echo "Waiting for emulator to boot..." + timeout 300 sh -c 'until adb shell getprop sys.boot_completed | grep -m 1 "1"; do sleep 5; done' + echo "Emulator is ready!" + adb shell svc power stayon true + adb shell locksettings set-pin 123456 + adb shell input keyevent 82 + sleep 5 + - run: + name: "Run Screengrab" + command: | + bundle exec fastlane android build_and_screengrab + - run: + name: "List fastlane android contents" + command: ls -R fastlane/android/ + - android/save_gradle_cache + - store_artifacts: + path: fastlane/metadata/android/ + destination: android-screenshots # Invoke jobs via workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows @@ -53,3 +141,18 @@ workflows: test-and-build: jobs: - unit-test +# - screengrab: +# requires: +# - unit-test + + # Daily scheduled screengrab workflow + # daily-screengrab: + # triggers: + # - schedule: + # cron: "0 0 * * *" # Run at midnight UTC every day + # filters: + # branches: + # only: + # - develop # Adjust to your main branch name + # jobs: + # - screengrab diff --git a/Gemfile.lock b/Gemfile.lock index f322ed56..03d4f21e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -214,6 +214,8 @@ GEM PLATFORMS arm64-darwin-23 arm64-darwin-24 + x86-linux + x86_64-linux DEPENDENCIES fastlane diff --git a/app/src/androidTest/kotlin/flow/RecoverWalletScreenGrabsTest.kt b/app/src/androidTest/kotlin/flow/RecoverWalletScreenGrabsTest.kt index 30060516..56e381e6 100644 --- a/app/src/androidTest/kotlin/flow/RecoverWalletScreenGrabsTest.kt +++ b/app/src/androidTest/kotlin/flow/RecoverWalletScreenGrabsTest.kt @@ -1,33 +1,46 @@ package flow import android.Manifest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onChild import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performTextInput import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import androidx.test.uiautomator.UiDevice -import com.brainwallet.BuildConfig import com.brainwallet.R +import com.brainwallet.test.BuildConfig import com.brainwallet.ui.BrainwalletActivity +import org.junit.After +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import timber.log.Timber import tools.fastlane.screengrab.FalconScreenshotStrategy import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.cleanstatusbar.BluetoothState +import tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar +import tools.fastlane.screengrab.cleanstatusbar.IconVisibility +import tools.fastlane.screengrab.cleanstatusbar.MobileDataType import tools.fastlane.screengrab.locale.LocaleTestRule /** * TODO: revisit this, since breaking with new navigation + * + * this will require pixel 7 pro */ @RunWith(JUnit4::class) @LargeTest @@ -47,23 +60,47 @@ class RecoverWalletScreenGrabsTest { @get:Rule val composeTestRule = createAndroidComposeRule() + private val uiDevice + get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + @Before + fun setUp() { + CleanStatusBar() + .setBluetoothState(BluetoothState.DISCONNECTED) + .setMobileNetworkDataType(MobileDataType.LTE) + .setWifiVisibility(IconVisibility.HIDE) + .setShowNotifications(false) + .setClock("0900") + .setBatteryLevel(100) + .enable() + } + + @After + fun tearDown() { + CleanStatusBar.disable() + } + + + @OptIn(ExperimentalTestApi::class) @Test fun onRecoverFlowSuccess() { + composeTestRule.activityRule.scenario.onActivity { Screengrab.setDefaultScreenshotStrategy(FalconScreenshotStrategy(it)) } - val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + waitUntilReady() - Screengrab.screenshot("1_intro_screen") + uiDevice.waitForIdle(60_000) + composeTestRule.waitForIdle() - onView(withId(R.id.button_recover_wallet)).perform(click()) + Screengrab.screenshot("1_welcome_screen") - Screengrab.screenshot("2_recover_screen") + composeTestRule.onNode(hasTestTag("buttonRestore")).performClick() - onView(withId(R.id.send_button)).perform(click()) //actually next button + uiDevice.waitForIdle() - Screengrab.screenshot("3_input_paperkey_screen") + Screengrab.screenshot("2_input_words_screen") //seed words input val editTextTags = (0..11).map { index -> "textFieldSeedWord$index" } @@ -71,123 +108,159 @@ class RecoverWalletScreenGrabsTest { val paperKey = BuildConfig.SCREENGRAB_PAPERKEY editTextTags.zip(paperKey).forEachIndexed { index, (textFieldTag, value) -> - device.waitForIdle(100) + uiDevice.waitForIdle() val textForTyping = value.dropLast(1) composeTestRule.onNodeWithTag(textFieldTag).onChild().performTextInput(textForTyping) composeTestRule.onNodeWithText(value).performClick() - } - composeTestRule.onNodeWithTag("buttonRestore").performClick() + Screengrab.screenshot("3_input_words_screen_2") - device.waitForIdle(300) + composeTestRule.onNodeWithTag("buttonRestore").performScrollTo() + composeTestRule.onNodeWithTag("buttonRestore").assertExists() + composeTestRule.onNodeWithTag("buttonRestore").performClick() - Screengrab.screenshot("4_input_paperkey_screen_2") + uiDevice.waitForIdle() - Screengrab.screenshot("5_setpin_screen") + composeTestRule.waitUntilAtLeastOneExists(hasTestTag("keypad1")) - repeat(6) { - onView(withId(R.id.num1)).perform(click()) + repeat(4) { + uiDevice.waitForIdle() + composeTestRule.onNodeWithTag("keypad1").assertExists() + composeTestRule.onNodeWithTag("keypad1").performClick() } - Screengrab.screenshot("6_setpin_confirm_screen") + Screengrab.screenshot("4_set_passcode_screen") - repeat(6) { - onView(withId(R.id.num1)).perform(click()) - } + composeTestRule.waitUntilAtLeastOneExists(hasTestTag("keypad1")) - device.waitForIdle(3000) + repeat(4) { + uiDevice.waitForIdle() + composeTestRule.onNodeWithTag("keypad1").assertExists() + composeTestRule.onNodeWithTag("keypad1").performClick() + } - Screengrab.screenshot("7_main_screen") + Screengrab.screenshot("5_set_passcode_confirm_screen") - onView(withId(R.id.menuBut)).perform(click()) + uiDevice.waitForIdle() - Screengrab.screenshot("8_menu_bottom_sheet") + Thread.sleep(1000) - onView(withText(R.string.MenuButton_security)).perform(click()) + Screengrab.screenshot("6_main_screen") - Screengrab.screenshot("9_security_center_screen") + //setting drawer + onView(withId(R.id.menuBut)).perform(click()) - onView(withId(R.id.close_button)).perform(click()) + Screengrab.screenshot("7_setting_drawer_open") - onView(withId(R.id.menuBut)).perform(click()) + composeTestRule.onNodeWithTag("settingSecurity").assertExists() + composeTestRule.onNodeWithTag("settingSecurity").performClick() - onView(withText(R.string.MenuButton_settings)).perform(click()) + composeTestRule.waitForIdle() - Screengrab.screenshot("10_settings_screen") + Screengrab.screenshot("8_setting_drawer_open_security") - onView(withText(R.string.settings_show_seed)).perform(click()) + composeTestRule.waitForIdle() - onView(withId(R.id.show_seed_button)).perform(click()) + composeTestRule.onNodeWithTag("settingLanguage").assertExists() + composeTestRule.onNodeWithTag("settingLanguage").performClick() - Screengrab.screenshot("11_settings_seed_phrase") + composeTestRule.waitForIdle() - device.pressBack() + Screengrab.screenshot("9_setting_drawer_open_language") - onView(withText(R.string.Settings_wipe)).perform(click()) + composeTestRule.waitForIdle() - Screengrab.screenshot("12_settings_recover_another_wallet") + composeTestRule.onNodeWithTag("settingCurrency").assertExists() + composeTestRule.onNodeWithTag("settingCurrency").performClick() - onView(withId(R.id.close_button)).perform(click()) + composeTestRule.waitForIdle() - onView(withText(R.string.Settings_languages)).perform(click()) + Screengrab.screenshot("10_setting_drawer_open_currency") - Screengrab.screenshot("13_settings_language") + composeTestRule.waitForIdle() - device.pressBack() + composeTestRule.onNodeWithTag("settingGames").assertExists() + composeTestRule.onNodeWithTag("settingGames").performClick() - onView(withText(R.string.Settings_currency)).perform(click()) + composeTestRule.waitForIdle() - Screengrab.screenshot("14_settings_currency") + Screengrab.screenshot("11_setting_drawer_open_games") - device.pressBack() + composeTestRule.waitForIdle() - onView(withText(R.string.Settings_sync)).perform(click()) + composeTestRule.onNodeWithTag("lazyColumnSetting").performScrollToNode(hasTestTag("settingBlockchain")) + composeTestRule.onNodeWithTag("settingBlockchain").assertExists() + composeTestRule.onNodeWithTag("settingBlockchain").performClick() - Screengrab.screenshot("15_settings_sync") + composeTestRule.waitForIdle() - device.pressBack() + Screengrab.screenshot("12_setting_drawer_open_blockchain") - onView(withText(R.string.Settings_shareData)).perform(click()) + composeTestRule.waitForIdle() - Screengrab.screenshot("16_settings_share_anonymous_data") + composeTestRule.onNodeWithTag("lazyColumnSetting").performScrollToNode(hasTestTag("settingLock")) + composeTestRule.onNodeWithTag("settingLock").assertExists() + composeTestRule.onNodeWithTag("settingLock").performClick() - device.pressBack() + composeTestRule.waitForIdle() - onView(withText(R.string.Settings_about)).perform(click()) + Screengrab.screenshot("13_setting_drawer_lock") - Screengrab.screenshot("17_settings_about") + repeat(4) { + uiDevice.waitForIdle() + composeTestRule.onNodeWithTag("keypad1").assertExists() + composeTestRule.onNodeWithTag("keypad1").performClick() + } - device.pressBack() + uiDevice.waitForIdle() + Thread.sleep(1000) - device.pressBack() + //tx send ui onView(withId(R.id.nav_send)).perform(click()) onView(withId(R.id.amount_edit)).perform(click()) - Screengrab.screenshot("18_transaction_send") + Screengrab.screenshot("14_transaction_send") onView(withId(R.id.close_button)).perform(click()) - onView(withId(R.id.nav_receive)).perform(click()) - - Screengrab.screenshot("19_transaction_receive") - onView(withId(R.id.close_button)).perform(click()) - - onView(withId(R.id.nav_buy)).perform(click()) - - Screengrab.screenshot("20_transaction_buy") + //tx buy/receive ui + onView(withId(R.id.nav_receive)).perform(click()) - onView(withId(R.id.nav_history)).perform(click()) + Screengrab.screenshot("15_transaction_buy_receive") - onView(withId(R.id.menuBut)).perform(click()) + composeTestRule.onNodeWithTag("buttonClose").performClick() - onView(withText(R.string.MenuButton_lock)).perform(click()) + uiDevice.waitForIdle() - Screengrab.screenshot("21_lock_screen") + } + private fun waitUntilReady() { + var attempts = 0 + val maxAttempts = 5 + + while (attempts < maxAttempts) { + try { + composeTestRule.waitUntil(timeoutMillis = 60_000) { + try { + composeTestRule.onRoot().fetchSemanticsNode() + true + } catch (e: Exception) { + Timber.e("[waitForComposeHierarchy] Error1 : ${e.message}", e) + false + } + } + return + } catch (e: Exception) { + Timber.e("[waitForComposeHierarchy] Error2 : ${e.message}", e) + attempts++ + if (attempts >= maxAttempts) throw e + Thread.sleep(1000) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/BrainwalletApp.kt b/app/src/main/java/com/brainwallet/BrainwalletApp.kt index 237bf5a1..03fd029e 100644 --- a/app/src/main/java/com/brainwallet/BrainwalletApp.kt +++ b/app/src/main/java/com/brainwallet/BrainwalletApp.kt @@ -31,7 +31,7 @@ import java.util.Timer import java.util.TimerTask import java.util.concurrent.atomic.AtomicInteger -class BrainwalletApp : Application() { +open class BrainwalletApp : Application() { override fun onCreate() { super.onCreate() @@ -73,7 +73,7 @@ class BrainwalletApp : Application() { appsFlyerLib.start(this) } - protected fun initializeModule() { + protected open fun initializeModule() { startKoin { androidLogger(if (BuildConfig.DEBUG) Level.DEBUG else Level.ERROR) androidContext(this@BrainwalletApp) diff --git a/app/src/main/java/com/brainwallet/ui/composable/PasscodeKeypad.kt b/app/src/main/java/com/brainwallet/ui/composable/PasscodeKeypad.kt index 4fac2149..a0607cbc 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/PasscodeKeypad.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/PasscodeKeypad.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp @Composable @@ -34,7 +35,7 @@ fun PasscodeKeypad( repeat(9) { index -> val number = index + 1 CircleButton( - modifier = modifierCircleButton, + modifier = modifierCircleButton.testTag("keypad$number"), onClick = { onEvent.invoke(PasscodeKeypadEvent.OnPressed(number)) }, 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 b3559014..d96cda42 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 @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.AbstractComposeView import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.coroutineScope @@ -76,6 +77,7 @@ fun HomeSettingDrawerSheet( ) { LazyColumn( modifier = Modifier + .testTag("lazyColumnSetting") .weight(1f) .padding(top = headerPadding.dp) .wrapContentHeight(align = Alignment.Top) @@ -83,6 +85,7 @@ fun HomeSettingDrawerSheet( item { SecurityDetail( modifier = Modifier + .testTag("settingSecurity") .fillMaxSize() .wrapContentHeight(), shareAnalyticsDataEnabled = state.shareAnalyticsDataEnabled, @@ -94,6 +97,7 @@ fun HomeSettingDrawerSheet( item { LanguageDetail( modifier = Modifier + .testTag("settingLanguage") .fillMaxSize() .wrapContentHeight(), selectedLanguage = state.selectedLanguage, @@ -110,6 +114,7 @@ fun HomeSettingDrawerSheet( item { CurrencyDetail( modifier = Modifier + .testTag("settingCurrency") .fillMaxSize() .wrapContentHeight(), selectedCurrency = state.selectedCurrency, @@ -125,6 +130,7 @@ fun HomeSettingDrawerSheet( item { GamesDetail( modifier = Modifier + .testTag("settingGames") .fillMaxSize() .wrapContentHeight() ) @@ -132,6 +138,7 @@ fun HomeSettingDrawerSheet( item { LitecoinBlockchainDetail( modifier = Modifier + .testTag("settingBlockchain") .fillMaxSize() .wrapContentHeight(), selectedCurrency = state.selectedCurrency, @@ -144,6 +151,7 @@ fun HomeSettingDrawerSheet( } item { SettingRowItem( + modifier = Modifier.testTag("settingSupport"), title = stringResource(R.string.settings_title_support), description = "brainwallet.co/support.html", onClick = { @@ -155,6 +163,7 @@ fun HomeSettingDrawerSheet( } item { SettingRowItem( + modifier = Modifier.testTag("settingSocialMedia"), title = stringResource(R.string.settings_title_social_media), description = "linktr.ee/brainwallet", onClick = { @@ -166,7 +175,9 @@ fun HomeSettingDrawerSheet( } item { // Lock / Unlock - LockSettingRowItem { + LockSettingRowItem( + modifier = Modifier.testTag("settingLock"), + ) { viewModel.onEvent(SettingsEvent.OnToggleLock) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LockSettingRowItem.kt b/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LockSettingRowItem.kt index 8f0ab2eb..871b466f 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LockSettingRowItem.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LockSettingRowItem.kt @@ -4,14 +4,17 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.brainwallet.R @Composable fun LockSettingRowItem( + modifier: Modifier = Modifier, onClick: () -> Unit, ) { SettingRowItem( + modifier = modifier, title = stringResource(R.string.settings_title_lock), onClick = onClick ) { 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 index 291015e2..8292904c 100644 --- 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 @@ -52,6 +52,7 @@ 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.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType @@ -152,7 +153,10 @@ fun ReceiveDialog( } }, actions = { - IconButton(onClick = onDismissRequest) { + IconButton( + modifier = Modifier.testTag("buttonClose"), + onClick = onDismissRequest + ) { Icon( Icons.Default.Close, contentDescription = stringResource(R.string.AccessibilityLabels_close), diff --git a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt index f4d8ba3a..b0370f4b 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -202,6 +203,7 @@ fun WelcomeScreen( }, shape = RoundedCornerShape(50), modifier = Modifier + .testTag("buttonRestore") .padding(horizontal = leadTrailPadding.dp) .padding(vertical = rowPadding.dp) .height(activeRowHeight.dp) diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index dc0df45e..947c8a79 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -1,18 +1,18 @@ - - + + Date: Thu, 29 May 2025 15:45:24 +0700 Subject: [PATCH 36/44] fix: android: Footer version label is obfuscated (#92) * fix: android: Footer version label is obfuscated * fix: [#92] android: Footer version label is obfuscated --- .../ui/screens/welcome/WelcomeScreen.kt | 212 +++++++++--------- 1 file changed, 110 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt index b0370f4b..f7c32e61 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt @@ -3,6 +3,7 @@ package com.brainwallet.ui.screens.welcome import androidx.compose.foundation.Image import androidx.compose.foundation.background 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.Spacer @@ -10,10 +11,12 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -26,6 +29,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -61,7 +65,8 @@ fun WelcomeScreen( val configuration = LocalConfiguration.current val screenHeight = configuration.screenHeightDp - var mainBoxFactor = 0.5 + val density = LocalDensity.current.density + val mainBoxFactor = if (density > 2) 0.5 else 0.4 val thirdOfScreenHeight = (screenHeight * mainBoxFactor).toInt() LaunchedEffect(Unit) { @@ -73,11 +78,11 @@ fun WelcomeScreen( val buttonFontSize = 24 val thinButtonFontSize = 22 val toggleButtonSize = 45 - val leadTrailPadding = 18 + val leadTrailPadding = 8 val halfLeadTrailPadding = leadTrailPadding / 2 val doubleLeadTrailPadding = leadTrailPadding * 2 val rowPadding = 8 - val versionPadding = 12 + val versionPadding = 8 val activeRowHeight = 58 val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.welcomeemoji20250212)) @@ -86,16 +91,12 @@ fun WelcomeScreen( iterations = LottieConstants.IterateForever ) - Column( + Box( modifier = Modifier .fillMaxSize() - .background(BrainwalletTheme.colors.surface), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + .background(BrainwalletTheme.colors.surface) + .verticalScroll(rememberScrollState()), ) { - - Spacer(modifier = Modifier.weight(0.2f)) - Image( painterResource(R.drawable.brainwallet_logotype_white), contentDescription = "brainwallet_logotype_white", @@ -104,124 +105,131 @@ fun WelcomeScreen( BrainwalletTheme.colors.content, ), modifier = Modifier + .align(Alignment.TopCenter) .fillMaxWidth() .padding(doubleLeadTrailPadding.dp) ) // Animation Placeholder - Card( + LottieAnimation( modifier = Modifier + .offset(y = 120.dp) .fillMaxWidth() - .height(thirdOfScreenHeight.dp) .padding(leadTrailPadding.dp) - ) { - - LottieAnimation( - modifier = Modifier.background(BrainwalletTheme.colors.surface), - composition = composition, - contentScale = ContentScale.Fit, - progress = { progress } - ) - } - // TODO: implement later, for now just comment this - Row( - modifier = Modifier - .fillMaxWidth() - .height(activeRowHeight.dp) - .padding(horizontal = leadTrailPadding.dp) - .padding(vertical = rowPadding.dp), - horizontalArrangement = Arrangement.SpaceEvenly + .background( + BrainwalletTheme.colors.surface, + BrainwalletTheme.shapes.large + ) + .height(thirdOfScreenHeight.dp) + .clip(BrainwalletTheme.shapes.large), + composition = composition, + contentScale = ContentScale.FillWidth, + alignment = Alignment.Center, + progress = { progress } + ) + Column( + modifier = Modifier.align(Alignment.BottomCenter), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - - BrainwalletButton( + Row( modifier = Modifier - .weight(1f) - .fillMaxWidth(), - onClick = { - viewModel.onEvent(WelcomeEvent.OnLanguageSelectorButtonClick) - } + .fillMaxWidth() + .height(activeRowHeight.dp) + .padding(horizontal = leadTrailPadding.dp) + .padding(vertical = rowPadding.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { - Text( - text = state.selectedLanguage.title, - fontSize = 14.sp, - color = BrainwalletTheme.colors.content + + BrainwalletButton( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + onClick = { + viewModel.onEvent(WelcomeEvent.OnLanguageSelectorButtonClick) + } + ) { + Text( + text = state.selectedLanguage.title, + fontSize = 14.sp, + color = BrainwalletTheme.colors.content + ) + } + + Spacer(modifier = Modifier.weight(0.1f)) + + DarkModeToggleButton( + modifier = Modifier + .width(toggleButtonSize.dp) + .aspectRatio(1f), + checked = state.darkMode, + onCheckedChange = { + viewModel.onEvent(WelcomeEvent.OnToggleDarkMode) + } ) - } - Spacer(modifier = Modifier.weight(0.1f)) + Spacer(modifier = Modifier.weight(0.1f)) - DarkModeToggleButton( - modifier = Modifier - .width(toggleButtonSize.dp) - .aspectRatio(1f), - checked = state.darkMode, - onCheckedChange = { - viewModel.onEvent(WelcomeEvent.OnToggleDarkMode) + BrainwalletButton( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + onClick = { viewModel.onEvent(WelcomeEvent.OnFiatButtonClick) } + ) { + Text( + text = state.selectedCurrency.name, + fontSize = 14.sp, + color = BrainwalletTheme.colors.content + ) } - ) - - Spacer(modifier = Modifier.weight(0.1f)) - BrainwalletButton( + } + // Ready Button + BorderedLargeButton( + onClick = { + onNavigate.invoke(UiEffect.Navigate(Route.Ready)) + }, + shape = RoundedCornerShape(50), modifier = Modifier - .weight(1f) - .fillMaxWidth(), - onClick = { viewModel.onEvent(WelcomeEvent.OnFiatButtonClick) } + .padding(horizontal = leadTrailPadding.dp) + .height(activeRowHeight.dp) + ) { Text( - text = state.selectedCurrency.name, - fontSize = 14.sp, - color = BrainwalletTheme.colors.content + text = stringResource(R.string.ready), + fontSize = buttonFontSize.sp, + fontWeight = FontWeight.SemiBold, ) } - } - // Ready Button - BorderedLargeButton( - onClick = { - onNavigate.invoke(UiEffect.Navigate(Route.Ready)) - }, - shape = RoundedCornerShape(50), - modifier = Modifier - .padding(horizontal = leadTrailPadding.dp) - .padding(vertical = rowPadding.dp) - .height(activeRowHeight.dp) - - ) { - Text( - text = stringResource(R.string.ready), - fontSize = buttonFontSize.sp, - fontWeight = FontWeight.SemiBold, - ) - } + // Restore Button + BorderedLargeButton( + onClick = { + onNavigate.invoke(UiEffect.Navigate(Route.InputWords())) + }, + shape = RoundedCornerShape(50), + modifier = Modifier + .testTag("buttonRestore") + .padding(horizontal = leadTrailPadding.dp) + .height(activeRowHeight.dp) + .clip(RoundedCornerShape(50)) + ) { + Text( + text = stringResource(R.string.restore), + fontSize = thinButtonFontSize.sp, + fontWeight = FontWeight.Thin, + ) + } - // Restore Button - BorderedLargeButton( - onClick = { - onNavigate.invoke(UiEffect.Navigate(Route.InputWords())) - }, - shape = RoundedCornerShape(50), - modifier = Modifier - .testTag("buttonRestore") - .padding(horizontal = leadTrailPadding.dp) - .padding(vertical = rowPadding.dp) - .height(activeRowHeight.dp) - .clip(RoundedCornerShape(50)) - ) { Text( - text = stringResource(R.string.restore), - fontSize = thinButtonFontSize.sp, - fontWeight = FontWeight.Thin, + modifier = Modifier.padding(vertical = versionPadding.dp), + text = BRConstants.APP_VERSION_NAME_CODE, + fontSize = 13.sp, + color = BrainwalletTheme.colors.content ) } - - Text( modifier = Modifier - .padding(vertical = versionPadding.dp), - text = BRConstants.APP_VERSION_NAME_CODE, - fontSize = 13.sp, - color = BrainwalletTheme.colors.content - ) } //language selector From a9c4a63d604a8c7e6252757766a7c26168fd5db5 Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Thu, 29 May 2025 17:35:10 +0700 Subject: [PATCH 37/44] fix: You saved it right screen reset button covers words (#93) * fix: [#84] change seed words layout to lazy vertical grid * fix: [#84] refactor seed words layout --- .../brainwallet/ui/composable/SeedWordItem.kt | 18 ++-- .../ui/composable/SeedWordsLayout.kt | 27 ++++++ .../yourseedproveit/YourSeedProveItScreen.kt | 83 ++++++++----------- .../yourseedwords/YourSeedWordsScreen.kt | 31 +++---- app/src/main/res/values/strings.xml | 1 + 5 files changed, 82 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/com/brainwallet/ui/composable/SeedWordsLayout.kt diff --git a/app/src/main/java/com/brainwallet/ui/composable/SeedWordItem.kt b/app/src/main/java/com/brainwallet/ui/composable/SeedWordItem.kt index 040457fd..202f21f8 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/SeedWordItem.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/SeedWordItem.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -22,7 +21,6 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -32,18 +30,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.PopupProperties -import com.brainwallet.ui.screens.welcome.WelcomeScreen import com.brainwallet.ui.theme.BrainwalletTheme import com.brainwallet.ui.theme.chilli @@ -56,10 +48,14 @@ fun SeedWordItem( ) { SeedWordItemBox(modifier = modifier) { Text( - modifier = Modifier.padding(vertical = 12.dp), + modifier = Modifier + .padding(vertical = 12.dp) + .weight(1f), text = label, style = MaterialTheme.typography.bodyMedium, - color = if (isError) chilli else BrainwalletTheme.colors.content + color = if (isError) chilli else BrainwalletTheme.colors.content, + overflow = TextOverflow.Ellipsis, + maxLines = 1 ) trailingIcon?.let { icon -> Spacer(modifier = Modifier.width(8.dp)) diff --git a/app/src/main/java/com/brainwallet/ui/composable/SeedWordsLayout.kt b/app/src/main/java/com/brainwallet/ui/composable/SeedWordsLayout.kt new file mode 100644 index 00000000..a8f3d98c --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/SeedWordsLayout.kt @@ -0,0 +1,27 @@ +package com.brainwallet.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun SeedWordsLayout( + modifier: Modifier = Modifier, + content: LazyGridScope.() -> Unit +) { + LazyVerticalGrid( + modifier = modifier + .height(220.dp), + columns = GridCells.Fixed(3), //fixed 3 + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt index 171186ab..214982a7 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt @@ -9,27 +9,24 @@ import android.content.ClipData import android.content.ClipDescription import android.media.MediaPlayer import android.view.View -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.draganddrop.dragAndDropSource import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer 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.lazy.grid.itemsIndexed import androidx.compose.foundation.rememberScrollState 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.Clear import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -60,6 +57,7 @@ import com.brainwallet.ui.composable.BrainwalletScaffold import com.brainwallet.ui.composable.BrainwalletTopAppBar import com.brainwallet.ui.composable.LargeButton import com.brainwallet.ui.composable.SeedWordItem +import com.brainwallet.ui.composable.SeedWordsLayout import org.koin.compose.koinInject @Composable @@ -72,7 +70,7 @@ fun YourSeedProveItScreen( val context = LocalContext.current /// Layout values - val columnPadding = 18 + val columnPadding = 12 val horizontalVerticalSpacing = 8 val spacerHeight = 48 val maxItemsPerRow = 3 @@ -104,23 +102,12 @@ fun YourSeedProveItScreen( } }) }, - floatingActionButton = { - if (state.orderCorrected.not()) { - FloatingActionButton( - onClick = { - viewModel.onEvent(YourSeedProveItEvent.OnClear) - } - ) { - Icon(Icons.Default.Clear, contentDescription = null) - } - } - } ) { paddingValues -> Column( modifier = Modifier + .fillMaxSize() .padding(paddingValues) .padding(columnPadding.dp) - .fillMaxSize() .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), @@ -138,17 +125,10 @@ fun YourSeedProveItScreen( ) ) - Spacer(modifier = Modifier.height(spacerHeight.dp)) - - FlowRow( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - maxItemsInEachRow = maxItemsPerRow - ) { - state.correctSeedWords.values.forEachIndexed { index, (expectedWord, actualWord) -> + Spacer(modifier = Modifier.weight(0.1f)) + SeedWordsLayout { + itemsIndexed(items = state.correctSeedWords.values.toList()) { index: Int, (expectedWord, actualWord): SeedWordItem -> val label = if (expectedWord != actualWord && actualWord.isEmpty()) { "${index + 1}" } else { @@ -158,7 +138,6 @@ fun YourSeedProveItScreen( SeedWordItem( modifier = Modifier .fillMaxWidth() - .weight(1f) .dragAndDropTarget( shouldStartDragAndDrop = { event -> event @@ -195,17 +174,20 @@ fun YourSeedProveItScreen( Spacer(modifier = Modifier.weight(1f)) - AnimatedVisibility(visible = state.orderCorrected.not()) { - FlowRow( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - maxItemsInEachRow = maxItemsPerRow - ) { - state.shuffledSeedWords.forEachIndexed { index, word -> + SeedWordsLayout { + itemsIndexed(items = state.shuffledSeedWords) { index, word -> + + val isWordUsedCorrectly = + state.correctSeedWords.values.any { (expectedWord, actualWord) -> + expectedWord == word && actualWord == word + } + + if (isWordUsedCorrectly) { + Box(modifier = Modifier.fillMaxWidth()) + } else { SeedWordItem( modifier = Modifier + .fillMaxWidth() .dragAndDropSource { detectTapGestures( onLongPress = { @@ -230,20 +212,25 @@ fun YourSeedProveItScreen( } ) } + + } } - AnimatedVisibility(visible = state.orderCorrected) { - LargeButton( - onClick = { + + LargeButton( + onClick = { + if (state.orderCorrected) { onNavigate.invoke(UiEffect.Navigate(Route.TopUp)) - }, - ) { - Text( - text = stringResource(R.string.game_and_sync), - style = MaterialTheme.typography.labelLarge - ) - } + } else { + viewModel.onEvent(YourSeedProveItEvent.OnClear) + } + }, + ) { + Text( + text = stringResource(if (state.orderCorrected) R.string.game_and_sync else R.string.reset_start_over).uppercase(), + style = MaterialTheme.typography.labelLarge + ) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt index c541f588..17bdaf18 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt @@ -5,12 +5,11 @@ package com.brainwallet.ui.screens.yourseedwords import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer 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.lazy.grid.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -36,6 +35,7 @@ import com.brainwallet.ui.composable.BrainwalletScaffold import com.brainwallet.ui.composable.BrainwalletTopAppBar import com.brainwallet.ui.composable.LargeButton import com.brainwallet.ui.composable.SeedWordItem +import com.brainwallet.ui.composable.SeedWordsLayout import org.koin.compose.koinInject @Composable @@ -45,12 +45,11 @@ fun YourSeedWordsScreen( viewModel: YourSeedWordsViewModel = koinInject() ) { /// Layout values - val columnPadding = 16 + val columnPadding = 12 val horizontalVerticalSpacing = 8 - val spacerHeight = 48 - val maxItemsPerRow = 3 - val leadingCopyPadding = 16 - val detailLineHeight = 28 + val spacerHeight = 36 + val leadingCopyPadding = 8 + val detailLineHeight = 24 LaunchedEffect(Unit) { viewModel.uiEffect.collect { effect -> @@ -78,9 +77,9 @@ fun YourSeedWordsScreen( ) { paddingValues -> Column( modifier = Modifier + .fillMaxSize() .padding(paddingValues) .padding(columnPadding.dp) - .fillMaxSize() .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), @@ -102,16 +101,10 @@ fun YourSeedWordsScreen( ) ) - Spacer(modifier = Modifier.height(spacerHeight.dp)) + Spacer(modifier = Modifier.weight(0.1f)) - FlowRow( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - maxItemsInEachRow = maxItemsPerRow - ) { - seedWords.forEachIndexed { index, word -> + SeedWordsLayout(modifier = Modifier.weight(1f)) { + itemsIndexed(items = seedWords) { index, word -> SeedWordItem( modifier = Modifier .fillMaxWidth() @@ -121,14 +114,14 @@ fun YourSeedWordsScreen( } } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(0.1f)) Text( text = stringResource(R.string.blockchain_litecoin), style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center) ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(0.2f)) LargeButton( onClick = { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7c7deb0..12fca16d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -855,4 +855,5 @@ Buy / Receive Custom + Reset / Start Over From d8f08adf506c7166f1c930dc7c9da923c3e714d3 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Thu, 29 May 2025 04:29:26 -0700 Subject: [PATCH 38/44] tiny resizing (#94) --- .../java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt index f7c32e61..845a9073 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt @@ -79,8 +79,6 @@ fun WelcomeScreen( val thinButtonFontSize = 22 val toggleButtonSize = 45 val leadTrailPadding = 8 - val halfLeadTrailPadding = leadTrailPadding / 2 - val doubleLeadTrailPadding = leadTrailPadding * 2 val rowPadding = 8 val versionPadding = 8 val activeRowHeight = 58 @@ -107,7 +105,8 @@ fun WelcomeScreen( modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() - .padding(doubleLeadTrailPadding.dp) + .padding(horizontal = 55.dp) + .padding(vertical = 30.dp) ) // Animation Placeholder From ef935858b84faa6723120b009b0f7b58128c8a62 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Thu, 29 May 2025 12:32:00 +0100 Subject: [PATCH 39/44] code and version bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bfc2e94d..08429835 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 = 202505251 - versionName = "v4.5.4" + versionCode = 202505291 + versionName = "v4.5.5" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") From 174d612e80724d5d4c1cc6f9bb03c8752ef39b72 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Fri, 30 May 2025 14:14:02 -0700 Subject: [PATCH 40/44] change break (#97) adds a android user agent and externalID --- .../main/java/com/brainwallet/navigation/LegacyNavigation.kt | 5 ++++- app/src/main/java/com/brainwallet/tools/util/Utils.java | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt index d38eaeb9..08bb8258 100644 --- a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt +++ b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt @@ -12,6 +12,7 @@ import com.brainwallet.R import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.di.getKoinInstance import com.brainwallet.presenter.activities.BreadActivity +import com.brainwallet.tools.util.Utils import com.brainwallet.ui.BrainwalletActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -66,6 +67,7 @@ object LegacyNavigation { context.startActivity(it) } + @JvmOverloads @JvmStatic fun showMoonPayWidget( context: Context, @@ -73,7 +75,7 @@ object LegacyNavigation { isDarkMode: Boolean = true, ) { val remoteApiSource: RemoteApiSource = getKoinInstance() - + val agentString = Utils.getAgentString(context, "android/HttpURLConnection") val progressDialog = ProgressDialog(context).apply { setMessage(context.getString(R.string.loading)) setCancelable(false) @@ -86,6 +88,7 @@ object LegacyNavigation { remoteApiSource.getMoonpaySignedUrl( params = params.toMutableMap().apply { put("defaultCurrencyCode", "ltc") + put("externalTransactionId", agentString) put("currencyCode", "ltc") put("themeId", "main-v1.0.0") put("theme", if (isDarkMode) "dark" else "light") diff --git a/app/src/main/java/com/brainwallet/tools/util/Utils.java b/app/src/main/java/com/brainwallet/tools/util/Utils.java index fb03d0c3..21b556dd 100644 --- a/app/src/main/java/com/brainwallet/tools/util/Utils.java +++ b/app/src/main/java/com/brainwallet/tools/util/Utils.java @@ -171,6 +171,9 @@ public static void hideKeyboard(Context app) { public static String getAgentString(Context app, String cfnetwork) { int versionNumber = 0; + String deviceCode = Build.MANUFACTURER + "-|-" + Build.MODEL; + + if (app != null) { try { PackageInfo pInfo = app.getPackageManager().getPackageInfo(app.getPackageName(), 0); @@ -179,7 +182,7 @@ public static String getAgentString(Context app, String cfnetwork) { Timber.e(e); } } - return String.format(Locale.ENGLISH, "%s/%d %s Android/%s", "Brainwallet", versionNumber, cfnetwork, Build.VERSION.RELEASE); + return String.format(Locale.ENGLISH, "%s/%d %s Android/%s Device/%s", "Brainwallet", versionNumber, cfnetwork, Build.VERSION.RELEASE, deviceCode); } public static String reverseHex(String hex) { From a4521d7028495031bb2ad208323b7a1b35517590 Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Sat, 31 May 2025 13:19:07 +0700 Subject: [PATCH 41/44] build code number fix: [#96] remove bottom_nav_menu_us and just using bottom_nav_menu for consistency (#98) --- app/build.gradle.kts | 2 +- .../presenter/activities/BreadActivity.java | 7 +---- app/src/main/res/layout/activity_bread.xml | 2 +- app/src/main/res/menu/bottom_nav_menu.xml | 8 +----- app/src/main/res/menu/bottom_nav_menu_us.xml | 28 ------------------- 5 files changed, 4 insertions(+), 43 deletions(-) delete mode 100644 app/src/main/res/menu/bottom_nav_menu_us.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 08429835..326d7062 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 34 - versionCode = 202505291 + versionCode = 202505311 versionName = "v4.5.5" multiDexEnabled = true 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 b210395c..2fd05ab0 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java @@ -421,7 +421,7 @@ private void initializeViews() { bottomNav = findViewById(R.id.bottomNav); bottomNav.getMenu().clear(); - bottomNav.inflateMenu(isInUsa() ? R.menu.bottom_nav_menu_us : R.menu.bottom_nav_menu); + bottomNav.inflateMenu(R.menu.bottom_nav_menu); balanceTxtV = findViewById(R.id.balanceTxtV); primaryPrice = findViewById(R.id.primary_price); @@ -447,11 +447,6 @@ public void onGlobalLayout() { balanceTxtV.append(":"); } - private boolean isInUsa() { - TelephonyManager telManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - return "us".equals(telManager.getSimCountryIso()); - } - @Override public void onBalanceChanged(final long balance) { updateUI(); diff --git a/app/src/main/res/layout/activity_bread.xml b/app/src/main/res/layout/activity_bread.xml index 7ad85ca9..e17e37f7 100644 --- a/app/src/main/res/layout/activity_bread.xml +++ b/app/src/main/res/layout/activity_bread.xml @@ -146,7 +146,7 @@ android:layout_height="64dp" android:layout_alignParentBottom="true" app:labelVisibilityMode="labeled" - tools:menu="@menu/bottom_nav_menu_us" /> + tools:menu="@menu/bottom_nav_menu" /> diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index 947c8a79..e5323f61 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -17,12 +17,6 @@ android:id="@+id/nav_receive" android:enabled="true" android:icon="@drawable/ic_nav_receive" - android:title="@string/Button.receive" /> - - - - - - + android:title="@string/bottom_nav_item_buy_receive_title" /> \ 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 deleted file mode 100644 index 187ac237..00000000 --- a/app/src/main/res/menu/bottom_nav_menu_us.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file From 6fec1e25e9cf1f35d4737112351cac4b59f7e05e Mon Sep 17 00:00:00 2001 From: Andhika Yuana Date: Mon, 2 Jun 2025 13:04:59 +0700 Subject: [PATCH 42/44] fix: [#137] fix: Reset fiat options in Buy / Receive modal (#99) --- .../tools/sqlite/CurrencyDataSource.java | 20 +++++++++++++++++++ .../home/receive/ReceiveDialogViewModel.kt | 3 +-- .../com/brainwallet/currency/CurrencyTests.kt | 7 +++++++ 3 files changed, 28 insertions(+), 2 deletions(-) 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 7c15df6d..6640916f 100644 --- a/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java +++ b/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java @@ -89,6 +89,26 @@ public void putCurrencies(Collection currencyEntities) { } } + public List getCurrenciesForBuy() { + List supportedFiatCodes = Arrays.asList( + "AUD", + "BRL", + "CAD", + "CHF", + "EUR", + "GBP", + "IDR", + "MXN", + "NGN", + "TRY", + "USD", + "ZAR" + ); + return getAllCurrencies(true).stream() + .filter(currencyEntity -> supportedFiatCodes.contains(currencyEntity.code)) + .collect(Collectors.toList()); + } + public List getAllCurrencies(Boolean shouldBeFiltered) { List currencies = new ArrayList<>(); 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 index 80ab1984..ea6df431 100644 --- 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 @@ -49,8 +49,7 @@ class ReceiveDialogViewModel( it.copy( address = address, qrBitmap = QRUtils.generateQR(event.context, "litecoin:${address}"), - fiatCurrencies = CurrencyDataSource.getInstance(event.context) - .getAllCurrencies(true), + fiatCurrencies = CurrencyDataSource.getInstance(event.context).getCurrenciesForBuy(), ) } diff --git a/app/src/test/java/com/brainwallet/currency/CurrencyTests.kt b/app/src/test/java/com/brainwallet/currency/CurrencyTests.kt index 37b7e46a..6be7752e 100644 --- a/app/src/test/java/com/brainwallet/currency/CurrencyTests.kt +++ b/app/src/test/java/com/brainwallet/currency/CurrencyTests.kt @@ -50,6 +50,13 @@ class CurrencyTests { assertEquals(currencyDataSource?.getAllCurrencies(true)?.count(), 0) } + @Test + fun `invoke CurrencyDataSource instance and Brainwallet filtered Fiats for Buy, should return the correct number of supported currencies (moonpay)`() { + //The actual number of BW currencies is 16. The 0 is a placeholder and needs to be replaced with a db query. + mockCursorDataFromDatabase() + assertEquals(currencyDataSource?.currenciesForBuy?.count(), 0) + } + @Test fun `invoke CurrencyDataSource instance and open database, then should validate database was not null`() { val database = currencyDataSource?.openDatabase() From c8fdf8d73be2753e5f772be5d38ee0ec169afde4 Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 2 Jun 2025 20:38:32 +0100 Subject: [PATCH 43/44] Chore/update device data (#100) * change break adds a android user agent and externalID * Update ReceiveDialogViewModel.kt * chore: refactor request params for fetchMoonpaySignedUrl --------- Co-authored-by: andhikayuana --- .../brainwallet/data/repository/LtcRepository.kt | 10 +++++++++- .../com/brainwallet/navigation/LegacyNavigation.kt | 13 ++++--------- .../screens/home/receive/ReceiveDialogViewModel.kt | 6 ++---- 3 files changed, 15 insertions(+), 14 deletions(-) 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 962602f7..b5983c3c 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -13,6 +13,7 @@ import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource +import com.brainwallet.tools.util.Utils interface LtcRepository { suspend fun fetchRates(): List @@ -77,7 +78,14 @@ interface LtcRepository { remoteApiSource.getBuyQuote(params) override suspend fun fetchMoonpaySignedUrl(params: Map): String { - return remoteApiSource.getMoonpaySignedUrl(params) + val agentString = Utils.getAgentString(context, "android/HttpURLConnection") + val finalParams = params + mapOf( + "defaultCurrencyCode" to "ltc", + "externalTransactionId" to agentString, + "currencyCode" to "ltc", + "themeId" to "main-v1.0.0", + ) + return remoteApiSource.getMoonpaySignedUrl(finalParams) .signedUrl.toUri() .buildUpon() .apply { diff --git a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt index 08bb8258..26e1b5dc 100644 --- a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt +++ b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt @@ -9,10 +9,10 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.core.net.toUri import com.brainwallet.BuildConfig import com.brainwallet.R +import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.di.getKoinInstance import com.brainwallet.presenter.activities.BreadActivity -import com.brainwallet.tools.util.Utils import com.brainwallet.ui.BrainwalletActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -74,8 +74,7 @@ object LegacyNavigation { params: Map = mapOf(), isDarkMode: Boolean = true, ) { - val remoteApiSource: RemoteApiSource = getKoinInstance() - val agentString = Utils.getAgentString(context, "android/HttpURLConnection") + val ltcRepository: LtcRepository = getKoinInstance() val progressDialog = ProgressDialog(context).apply { setMessage(context.getString(R.string.loading)) setCancelable(false) @@ -85,18 +84,14 @@ object LegacyNavigation { CoroutineScope(Dispatchers.Main).launch { try { val result = withContext(Dispatchers.IO) { - remoteApiSource.getMoonpaySignedUrl( + ltcRepository.fetchMoonpaySignedUrl( params = params.toMutableMap().apply { - put("defaultCurrencyCode", "ltc") - put("externalTransactionId", agentString) - put("currencyCode", "ltc") - put("themeId", "main-v1.0.0") put("theme", if (isDarkMode) "dark" else "light") } ) } - val widgetUri = result.signedUrl.toUri().buildUpon() + val widgetUri = result.toUri().buildUpon() .apply { if (BuildConfig.DEBUG) { authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com 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 index ea6df431..f6b2ba8a 100644 --- 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 @@ -49,7 +49,8 @@ class ReceiveDialogViewModel( it.copy( address = address, qrBitmap = QRUtils.generateQR(event.context, "litecoin:${address}"), - fiatCurrencies = CurrencyDataSource.getInstance(event.context).getCurrenciesForBuy(), + fiatCurrencies = CurrencyDataSource.getInstance(event.context) + .getCurrenciesForBuy(), ) } @@ -153,9 +154,6 @@ class ReceiveDialogViewModel( "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" ) ) From 2d6e1777d8806d2311f5609741967c964fc8120e Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Tue, 3 Jun 2025 14:36:30 +0100 Subject: [PATCH 44/44] Update build.gradle.kts --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 326d7062..94d8b60a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 34 - versionCode = 202505311 + versionCode = 202506031 versionName = "v4.5.5" multiDexEnabled = true