From ba8ea45b3465947eb7e33c9d41f9e90771f7386d Mon Sep 17 00:00:00 2001 From: Kerry Washington Date: Mon, 31 Mar 2025 17:57:14 +0100 Subject: [PATCH 01/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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})")