Merge branch 'master' into modal

This commit is contained in:
Marcos Rodriguez Velez 2024-07-16 12:29:17 -04:00
commit dc0fa26395
No known key found for this signature in database
GPG Key ID: 6030B2F48CCE86D7
25 changed files with 502 additions and 691 deletions

View File

@ -104,6 +104,7 @@ android {
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
}
}
}
task copyFiatUnits(type: Copy) {
@ -117,6 +118,8 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation files("../../node_modules/rn-ldk/android/libs/LDK-release.aar")
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-remoteviews:1.1.0'
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
@ -129,7 +132,9 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.work:work-runtime:2.9.0"
implementation "androidx.work:work-runtime-ktx:2.9.0"
}
apply plugin: 'com.google.gms.google-services' // Google Services plugin
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle")
apply plugin: 'org.jetbrains.kotlin.android'; applyNativeModulesAppBuildGradle(project)

View File

@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACTION_SCREEN_OFF"/>
<uses-permission android:name="android.permission.ACTION_SCREEN_ON"/>
<application
android:name=".MainApplication"
android:label="@string/app_name"
@ -62,16 +63,16 @@
</intent-filter>
</receiver>
<receiver android:name=".BitcoinPriceWidget" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<action android:name="android.intent.action.SCREEN_ON"/>
<action android:name="android.intent.action.SCREEN_OFF"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/bitcoin_price_widget_info"/>
</receiver>
<receiver android:name=".BitcoinPriceWidget" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<action android:name="android.intent.action.SCREEN_ON"/>
<action android:name="android.intent.action.SCREEN_OFF"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/bitcoin_price_widget_info"/>
</receiver>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"

View File

@ -1,125 +0,0 @@
package io.bluewallet.bluewallet;
import android.app.Application;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.PowerManager;
import android.util.Log;
import android.widget.RemoteViews;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class BitcoinPriceWidget extends AppWidgetProvider {
private static final String TAG = "BitcoinPriceWidget";
private static final String ACTION_UPDATE = "io.bluewallet.bluewallet.UPDATE_WIDGET";
private static final long UPDATE_INTERVAL_MINUTES = 15; // Update interval in minutes
private static final int MAX_RETRIES = 3;
private static PowerManager.WakeLock wakeLock;
private static int retryCount = 0;
private static boolean isScreenOn = true;
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
registerScreenReceiver(context);
schedulePeriodicUpdates(context);
}
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
unregisterScreenReceiver(context);
WorkManager.getInstance(context).cancelUniqueWork("UpdateWidgetWork");
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
if (isScreenOn) {
scheduleWork(context);
}
}
private void schedulePeriodicUpdates(Context context) {
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(WidgetUpdateWorker.class,
UPDATE_INTERVAL_MINUTES, TimeUnit.MINUTES)
.setInitialDelay(UPDATE_INTERVAL_MINUTES, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"UpdateWidgetWork",
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
);
}
private void registerScreenReceiver(Context context) {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
context.getApplicationContext().registerReceiver(screenReceiver, filter);
}
private void unregisterScreenReceiver(Context context) {
context.getApplicationContext().unregisterReceiver(screenReceiver);
}
private final BroadcastReceiver screenReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() != null) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
switch (intent.getAction()) {
case Intent.ACTION_SCREEN_ON:
isScreenOn = true;
Log.d(TAG, "Screen ON");
acquireWakeLock(context);
scheduleWork(context);
break;
case Intent.ACTION_SCREEN_OFF:
isScreenOn = false;
Log.d(TAG, "Screen OFF");
releaseWakeLock();
break;
}
}
}
}
};
private void acquireWakeLock(Context context) {
if (wakeLock == null) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/);
}
}
}
private void releaseWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
}
private void scheduleWork(Context context) {
Log.d(TAG, "Scheduling work for widget update");
WorkManager.getInstance(context).enqueue(WidgetUpdateWorker.createWorkRequest());
}
}

View File

@ -0,0 +1,34 @@
package io.bluewallet.bluewallet
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.WorkManager
class BitcoinPriceWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d("BitcoinPriceWidget", "onUpdate called")
WidgetUpdateWorker.scheduleWork(context)
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
Log.d("BitcoinPriceWidget", "onEnabled called")
WidgetUpdateWorker.scheduleWork(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
Log.d("BitcoinPriceWidget", "onDisabled called")
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
Log.d("BitcoinPriceWidget", "onReceive called with action: ${intent.action}")
}
}

View File

@ -1,42 +0,0 @@
package io.bluewallet.bluewallet;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "BlueWallet";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
if (getResources().getBoolean(R.bool.portrait_only)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
/**
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
* (aka React 18) with two boolean flags.
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new DefaultReactActivityDelegate(
this,
getMainComponentName(),
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled());
}
}

View File

@ -0,0 +1,39 @@
package io.bluewallet.bluewallet
import android.content.pm.ActivityInfo
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
override fun getMainComponentName(): String {
return "BlueWallet"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
if (resources.getBoolean(R.bool.portrait_only)) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
/**
* Returns the instance of the [ReactActivityDelegate]. Here we use a util class [DefaultReactActivityDelegate]
* which allows you to easily enable Fabric and Concurrent React (aka React 18) with two boolean flags.
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return DefaultReactActivityDelegate(
this,
mainComponentName,
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.fabricEnabled
)
}
}

View File

@ -1,85 +0,0 @@
package io.bluewallet.bluewallet;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import com.bugsnag.android.Bugsnag;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import com.facebook.react.modules.i18nmanager.I18nUtil;
import java.util.List;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import java.util.concurrent.TimeUnit;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new DefaultReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
return packages;
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
sharedI18nUtilInstance.allowRTL(getApplicationContext(), true);
SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
DefaultNewArchitectureEntryPoint.load();
}
SharedPreferences sharedPref = getApplicationContext().getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE);
String isDoNotTrackEnabled = sharedPref.getString("donottrack", "0");
if (!isDoNotTrackEnabled.equals("1")) {
Bugsnag.start(this);
}
// Schedule periodic widget updates
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(WidgetUpdateWorker.class, 4, TimeUnit.MINUTES).build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork("UpdateWidgetWork", ExistingPeriodicWorkPolicy.REPLACE, workRequest);
}
}

View File

@ -0,0 +1,57 @@
package io.bluewallet.bluewallet
import android.app.Application
import android.content.Context
import com.bugsnag.android.Bugsnag
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader
import com.facebook.react.modules.i18nmanager.I18nUtil
class MainApplication : Application(), ReactApplication {
private val mReactNativeHost = object : DefaultReactNativeHost(this) {
override fun getUseDeveloperSupport() = BuildConfig.DEBUG
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
// Packages that cannot be autolinked yet can be added manually here, for example:
return packages
}
override fun getJSMainModuleName() = "index"
override val isNewArchEnabled = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled = BuildConfig.IS_HERMES_ENABLED
}
override fun getReactNativeHost() = mReactNativeHost
override fun onCreate() {
super.onCreate()
val sharedI18nUtilInstance = I18nUtil.getInstance()
sharedI18nUtilInstance.allowRTL(applicationContext, true)
SoLoader.init(this, /* native exopackage */ false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load()
}
val sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
// Retrieve the "donottrack" value. Default to "0" if not found.
val isDoNotTrackEnabled = sharedPref.getString("donottrack", "0")
// Check if do not track is not enabled and initialize Bugsnag if so
if (isDoNotTrackEnabled != "1") {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this)
}
}
}

View File

@ -1,122 +0,0 @@
package io.bluewallet.bluewallet;
import android.content.Context;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
public class MarketAPI {
private static final String TAG = "MarketAPI";
private static final String HARD_CODED_JSON = "{\n" +
" \"USD\": {\n" +
" \"endPointKey\": \"USD\",\n" +
" \"locale\": \"en-US\",\n" +
" \"source\": \"Kraken\",\n" +
" \"symbol\": \"$\",\n" +
" \"country\": \"United States (US Dollar)\"\n" +
" }\n" +
"}";
public static String fetchPrice(Context context, String currency) {
try {
JSONObject json = new JSONObject(HARD_CODED_JSON);
JSONObject currencyInfo = json.getJSONObject(currency);
String source = currencyInfo.getString("source");
String endPointKey = currencyInfo.getString("endPointKey");
String urlString = buildURLString(source, endPointKey);
Log.d(TAG, "Fetching from URL: " + urlString);
URI uri = new URI(urlString);
URL url = uri.toURL();
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.connect();
int responseCode = urlConnection.getResponseCode();
if (responseCode != 200) {
return null;
}
InputStreamReader reader = new InputStreamReader(urlConnection.getInputStream());
StringBuilder jsonResponse = new StringBuilder();
int read;
char[] buffer = new char[1024];
while ((read = reader.read(buffer)) != -1) {
jsonResponse.append(buffer, 0, read);
}
return parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static String buildURLString(String source, String endPointKey) {
switch (source) {
case "Yadio":
return "https://api.yadio.io/json/" + endPointKey;
case "YadioConvert":
return "https://api.yadio.io/convert/1/BTC/" + endPointKey;
case "Exir":
return "https://api.exir.io/v1/ticker?symbol=btc-irt";
case "wazirx":
return "https://api.wazirx.com/api/v2/tickers/btcinr";
case "Bitstamp":
return "https://www.bitstamp.net/api/v2/ticker/btc" + endPointKey.toLowerCase();
case "Coinbase":
return "https://api.coinbase.com/v2/prices/BTC-" + endPointKey.toUpperCase() + "/buy";
case "CoinGecko":
return "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=" + endPointKey.toLowerCase();
case "BNR":
return "https://www.bnr.ro/nbrfxrates.xml";
case "Kraken":
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ" + endPointKey.toUpperCase();
default:
return "https://api.coindesk.com/v1/bpi/currentprice/" + endPointKey + ".json";
}
}
private static String parseJSONBasedOnSource(String jsonString, String source, String endPointKey) {
try {
JSONObject json = new JSONObject(jsonString);
switch (source) {
case "Yadio":
JSONObject rateDict = json.getJSONObject(endPointKey);
return rateDict.getString("price");
case "YadioConvert":
return json.getString("rate");
case "CoinGecko":
JSONObject bitcoinDict = json.getJSONObject("bitcoin");
return bitcoinDict.getString(endPointKey.toLowerCase());
case "Exir":
return json.getString("last");
case "Bitstamp":
return json.getString("last");
case "wazirx":
JSONObject tickerDict = json.getJSONObject("ticker");
return tickerDict.getString("buy");
case "Coinbase":
JSONObject data = json.getJSONObject("data");
return data.getString("amount");
case "Kraken":
JSONObject result = json.getJSONObject("result");
JSONObject tickerData = result.getJSONObject("XXBTZ" + endPointKey.toUpperCase());
JSONArray c = tickerData.getJSONArray("c");
return c.getString(0);
default:
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

View File

@ -0,0 +1,99 @@
package io.bluewallet.bluewallet
import android.content.Context
import android.util.Log
import org.json.JSONObject
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
object MarketAPI {
private const val TAG = "MarketAPI"
private const val HARD_CODED_JSON = "{\n" +
" \"USD\": {\n" +
" \"endPointKey\": \"USD\",\n" +
" \"locale\": \"en-US\",\n" +
" \"source\": \"Kraken\",\n" +
" \"symbol\": \"$\",\n" +
" \"country\": \"United States (US Dollar)\"\n" +
" }\n" +
"}"
var baseUrl: String? = null
fun fetchPrice(context: Context, currency: String): String? {
return try {
val json = JSONObject(HARD_CODED_JSON)
val currencyInfo = json.getJSONObject(currency)
val source = currencyInfo.getString("source")
val endPointKey = currencyInfo.getString("endPointKey")
val urlString = buildURLString(source, endPointKey)
Log.d(TAG, "Fetching price from URL: $urlString")
val url = URL(urlString)
val urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "GET"
urlConnection.connect()
val responseCode = urlConnection.responseCode
if (responseCode != 200) {
Log.e(TAG, "Failed to fetch price. Response code: $responseCode")
return null
}
val reader = InputStreamReader(urlConnection.inputStream)
val jsonResponse = StringBuilder()
val buffer = CharArray(1024)
var read: Int
while (reader.read(buffer).also { read = it } != -1) {
jsonResponse.append(buffer, 0, read)
}
parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey)
} catch (e: Exception) {
Log.e(TAG, "Error fetching price", e)
null
}
}
private fun buildURLString(source: String, endPointKey: String): String {
return if (baseUrl != null) {
baseUrl + endPointKey
} else {
when (source) {
"Yadio" -> "https://api.yadio.io/json/$endPointKey"
"YadioConvert" -> "https://api.yadio.io/convert/1/BTC/$endPointKey"
"Exir" -> "https://api.exir.io/v1/ticker?symbol=btc-irt"
"wazirx" -> "https://api.wazirx.com/api/v2/tickers/btcinr"
"Bitstamp" -> "https://www.bitstamp.net/api/v2/ticker/btc${endPointKey.lowercase()}"
"Coinbase" -> "https://api.coinbase.com/v2/prices/BTC-${endPointKey.uppercase()}/buy"
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
}
}
}
private fun parseJSONBasedOnSource(jsonString: String, source: String, endPointKey: String): String? {
return try {
val json = JSONObject(jsonString)
when (source) {
"Yadio" -> json.getJSONObject(endPointKey).getString("price")
"YadioConvert" -> json.getString("rate")
"CoinGecko" -> json.getJSONObject("bitcoin").getString(endPointKey.lowercase())
"Exir" -> json.getString("last")
"Bitstamp" -> json.getString("last")
"wazirx" -> json.getJSONObject("ticker").getString("buy")
"Coinbase" -> json.getJSONObject("data").getString("amount")
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
else -> null
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing price", e)
null
}
}
}

View File

@ -1,80 +0,0 @@
package io.bluewallet.bluewallet;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
public class WidgetUpdateManager extends Worker {
private static final String TAG = "WidgetUpdateManager";
private static final String PREFS_NAME = "BitcoinPriceWidgetPrefs";
private static final String PREF_PREFIX_KEY = "appwidget_";
private static final String PREFS_LAST_UPDATE_TIME_KEY = "lastUpdateTime_";
private static final String PREFS_LAST_PRICE_KEY = "lastPrice_";
private static final String PREFS_PREV_PRICE_KEY = "prevPrice_";
private static final String PREFS_PREV_UPDATE_TIME_KEY = "prevUpdateTime_";
public WidgetUpdateManager(Context context, WorkerParameters params) {
super(context, params);
}
@Override
public Result doWork() {
Context context = getApplicationContext();
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, BitcoinPriceWidget.class));
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
return Result.success();
}
public static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0);
String previousPrice = prefs.getString(PREFS_PREV_PRICE_KEY + appWidgetId, "N/A");
String lastPrice = prefs.getString(PREFS_LAST_PRICE_KEY + appWidgetId, "N/A");
String lastUpdateTime = prefs.getString(PREFS_LAST_UPDATE_TIME_KEY + appWidgetId, "N/A");
String previousUpdateTime = prefs.getString(PREFS_PREV_UPDATE_TIME_KEY + appWidgetId, "N/A");
Log.d(TAG, "Fetch completed with price: " + lastPrice + " at " + lastUpdateTime + ". Previous price: " + previousPrice + " at " + previousUpdateTime);
String price = MarketAPI.fetchPrice(context, "USD");
if (price != null) {
Log.d(TAG, "Fetch completed with price: " + price);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREFS_PREV_PRICE_KEY + appWidgetId, lastPrice);
editor.putString(PREFS_PREV_UPDATE_TIME_KEY + appWidgetId, lastUpdateTime);
editor.putString(PREFS_LAST_PRICE_KEY + appWidgetId, price);
editor.putString(PREFS_LAST_UPDATE_TIME_KEY + appWidgetId, DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date()));
editor.apply();
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
views.setTextViewText(R.id.price_value, price);
views.setTextViewText(R.id.last_updated_time, DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date()));
views.setViewVisibility(R.id.price_value, View.VISIBLE);
views.setViewVisibility(R.id.last_updated_label, View.VISIBLE);
views.setViewVisibility(R.id.last_updated_time, View.VISIBLE);
if (!"N/A".equals(previousPrice)) {
views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE);
views.setViewVisibility(R.id.previous_price, View.VISIBLE);
} else {
views.setViewVisibility(R.id.price_arrow_container, View.GONE);
views.setViewVisibility(R.id.previous_price, View.GONE);
}
appWidgetManager.updateAppWidget(appWidgetId, views);
} else {
Log.e(TAG, "Failed to fetch Bitcoin price");
}
}
}

View File

@ -1,108 +0,0 @@
package io.bluewallet.bluewallet;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import androidx.work.OneTimeWorkRequest;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class WidgetUpdateWorker extends Worker {
private static final String TAG = "WidgetUpdateWorker";
private static final String PREFS_NAME = "BitcoinPriceWidgetPrefs";
private static final String PREF_PREFIX_KEY_CURRENT = "appwidget_current_";
private static final String PREF_PREFIX_KEY_PREVIOUS = "appwidget_previous_";
public WidgetUpdateWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
Context context = getApplicationContext();
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName thisWidget = new ComponentName(context, BitcoinPriceWidget.class);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
return Result.success();
}
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0);
String prevPrice = prefs.getString(PREF_PREFIX_KEY_PREVIOUS + appWidgetId, "N/A");
String currentPrice = prefs.getString(PREF_PREFIX_KEY_CURRENT + appWidgetId, "N/A");
Log.d(TAG, "Previous price: " + prevPrice);
Log.d(TAG, "Current price: " + currentPrice);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
// Fetch the latest price
String newPrice = MarketAPI.fetchPrice(context, "USD");
if (newPrice != null) {
String currentTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date());
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault());
// Update the widget views
views.setTextViewText(R.id.price_value, currencyFormat.format(Double.parseDouble(newPrice)));
views.setTextViewText(R.id.last_updated_time, currentTime);
views.setViewVisibility(R.id.last_updated_label, View.VISIBLE);
views.setViewVisibility(R.id.last_updated_time, View.VISIBLE);
if (!prevPrice.equals("N/A") && !prevPrice.equals(newPrice)) {
views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE);
views.setTextViewText(R.id.previous_price, currencyFormat.format(Double.parseDouble(prevPrice)));
views.setViewVisibility(R.id.previous_price_label, View.VISIBLE);
views.setViewVisibility(R.id.previous_price, View.VISIBLE);
if (Double.parseDouble(newPrice) > Double.parseDouble(prevPrice)) {
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_up_float);
} else {
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_down_float);
}
} else {
views.setViewVisibility(R.id.price_arrow_container, View.GONE);
views.setViewVisibility(R.id.previous_price_label, View.GONE);
views.setViewVisibility(R.id.previous_price, View.GONE);
}
// Save the new price and time
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREF_PREFIX_KEY_PREVIOUS + appWidgetId, currentPrice);
editor.putString(PREF_PREFIX_KEY_CURRENT + appWidgetId, newPrice);
editor.apply();
Log.d(TAG, "Fetch completed with price: " + newPrice + " at " + currentTime + ". Previous price: " + prevPrice);
appWidgetManager.updateAppWidget(appWidgetId, views);
// Log the next update time
long nextUpdateTimeMillis = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(4);
String nextUpdateTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date(nextUpdateTimeMillis));
Log.d(TAG, "Next fetch scheduled at: " + nextUpdateTime);
} else {
Log.e(TAG, "Failed to fetch Bitcoin price");
views.setTextViewText(R.id.price_value, "Error");
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
public static OneTimeWorkRequest createWorkRequest() {
return new OneTimeWorkRequest.Builder(WidgetUpdateWorker.class).build();
}
}

View File

@ -0,0 +1,107 @@
package io.bluewallet.bluewallet
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.work.*
import org.json.JSONObject
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
companion object {
const val TAG = "WidgetUpdateWorker"
const val WORK_NAME = "widget_update_work"
const val REPEAT_INTERVAL_MINUTES = 15L
fun scheduleWork(context: Context) {
val workRequest = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(
REPEAT_INTERVAL_MINUTES, TimeUnit.MINUTES
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
)
Log.d(TAG, "Scheduling work for widget updates, will run every $REPEAT_INTERVAL_MINUTES minutes")
}
}
override fun doWork(): Result {
Log.d(TAG, "Widget update worker running")
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
val sharedPref = applicationContext.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
val price = fetchPrice()
val previousPrice = sharedPref.getString("previous_price", null)
// Log fetched data
Log.d(TAG, "Fetch completed with price: $price at $currentTime. Previous price: $previousPrice")
// Update views
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault()).apply {
maximumFractionDigits = 0
}
views.setTextViewText(R.id.price_value, currencyFormat.format(price.toDouble()))
views.setTextViewText(R.id.last_updated_time, currentTime)
views.setViewVisibility(R.id.last_updated_label, View.VISIBLE)
views.setViewVisibility(R.id.last_updated_time, View.VISIBLE)
if (previousPrice != null) {
views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE)
views.setTextViewText(R.id.previous_price, currencyFormat.format(previousPrice.toDouble()))
if (price.toDouble() > previousPrice.toDouble()) {
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_up_float)
} else {
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_down_float)
}
} else {
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
}
appWidgetManager.updateAppWidget(appWidgetIds, views)
savePrice(sharedPref, price)
return Result.success()
}
private fun fetchPrice(): String {
val urlString = "https://api.kraken.com/0/public/Ticker?pair=XXBTZUSD"
val url = URL(urlString)
val urlConnection = url.openConnection() as HttpURLConnection
return try {
val reader = InputStreamReader(urlConnection.inputStream)
val jsonResponse = StringBuilder()
val buffer = CharArray(1024)
var read: Int
while (reader.read(buffer).also { read = it } != -1) {
jsonResponse.append(buffer, 0, read)
}
val json = JSONObject(jsonResponse.toString())
json.getJSONObject("result").getJSONObject("XXBTZUSD").getJSONArray("c").getString(0)
} finally {
urlConnection.disconnect()
}
}
private fun savePrice(sharedPref: SharedPreferences, price: String) {
sharedPref.edit().putString("previous_price", price).apply()
}
}

View File

@ -1,19 +0,0 @@
package io.bluewallet.bluewallet;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.widget.RemoteViews;
public class WidgetUtils {
public static void updateAppWidget(Context context, RemoteViews views) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName thisWidget = new ComponentName(context, BitcoinPriceWidget.class);
int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
for (int widgetId : allWidgetIds) {
appWidgetManager.updateAppWidget(widgetId, views);
}
}
}

View File

@ -1,3 +1,6 @@
<resources>
<string name="app_name">BlueWallet</string>
<string name="loading">Loading...</string>
<string name="last_updated">Last Updated</string>
<string name="from">From</string>
</resources>

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minWidth="130dp"
android:minHeight="40dp"
android:minWidth="160dp"
android:minHeight="80dp"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen"
android:previewImage="@drawable/widget_preview"
android:resizeMode="none" />
android:resizeMode="none"
/>

View File

@ -15,7 +15,7 @@ buildscript {
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
kotlin_version = '1.9.23'
kotlinVersion = '1.9.20'
kotlinVersion = '1.9.23'
}
repositories {
google()

View File

@ -22,6 +22,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DetailViewStackParamList } from '../navigation/DetailViewStackParamList';
import { useStorage } from '../hooks/context/useStorage';
import ToolTipMenu from './TooltipMenu';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
interface TransactionListItemProps {
itemPriceUnit: BitcoinUnit;
@ -255,17 +256,17 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const onToolTipPress = useCallback(
(id: any) => {
if (id === actionKeys.CopyAmount) {
if (id === CommonToolTipActions.CopyAmount.id) {
handleOnCopyAmountTap();
} else if (id === actionKeys.CopyNote) {
} else if (id === CommonToolTipActions.CopyNote.id) {
handleOnCopyNote();
} else if (id === actionKeys.OpenInBlockExplorer) {
} else if (id === CommonToolTipActions.OpenInBlockExplorer.id) {
handleOnViewOnBlockExplorer();
} else if (id === actionKeys.ExpandNote) {
} else if (id === CommonToolTipActions.ExpandNote.id) {
handleOnExpandNote();
} else if (id === actionKeys.CopyBlockExplorerLink) {
} else if (id === CommonToolTipActions.CopyBlockExplorerLink.id) {
handleCopyOpenInBlockExplorerPress();
} else if (id === actionKeys.CopyTXID) {
} else if (id === CommonToolTipActions.CopyTXID.id) {
handleOnCopyTransactionID();
}
},
@ -282,51 +283,19 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const actions: (Action | Action[])[] = [];
if (rowTitle !== loc.lnd.expired) {
actions.push({
id: actionKeys.CopyAmount,
text: loc.transactions.details_copy_amount,
icon: actionIcons.Clipboard,
});
actions.push(CommonToolTipActions.CopyAmount);
}
if (subtitle) {
actions.push({
id: actionKeys.CopyNote,
text: loc.transactions.details_copy_note,
icon: actionIcons.Clipboard,
});
actions.push(CommonToolTipActions.CopyNote);
}
if (item.hash) {
actions.push(
{
id: actionKeys.CopyTXID,
text: loc.transactions.details_copy_txid,
icon: actionIcons.Clipboard,
},
{
id: actionKeys.CopyBlockExplorerLink,
text: loc.transactions.details_copy_block_explorer_link,
icon: actionIcons.Clipboard,
},
[
{
id: actionKeys.OpenInBlockExplorer,
text: loc.transactions.details_show_in_block_explorer,
icon: actionIcons.Link,
},
],
);
actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]);
}
if (subtitle && subtitleNumberOfLines === 1) {
actions.push([
{
id: actionKeys.ExpandNote,
text: loc.transactions.expand_note,
icon: actionIcons.Note,
},
]);
actions.push([CommonToolTipActions.ExpandNote]);
}
return actions as Action[] | Action[][];
@ -363,30 +332,3 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
</ToolTipMenu>
);
});
const actionKeys = {
CopyTXID: 'copyTX_ID',
CopyBlockExplorerLink: 'copy_blockExplorer',
ExpandNote: 'expandNote',
OpenInBlockExplorer: 'open_in_blockExplorer',
CopyAmount: 'copyAmount',
CopyNote: 'copyNote',
};
const actionIcons = {
Eye: {
iconValue: 'eye',
},
EyeSlash: {
iconValue: 'eye.slash',
},
Clipboard: {
iconValue: 'doc.on.doc',
},
Link: {
iconValue: 'link',
},
Note: {
iconValue: 'note.text',
},
};

View File

@ -169,8 +169,19 @@
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */; };
B4D0B2682C1DED67006B6B1B /* ReceiveMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */; };
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
B4EFF73C2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
B4EFF73D2C3F6C6C0095D655 /* MarketData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E82BCC371A00162242 /* MarketData.swift */; };
B4EFF7412C3F6C960095D655 /* PriceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA4BC255872E3009312A5 /* PriceWidget.swift */; };
B4EFF7422C3F6C990095D655 /* PriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5272558EC52009312A5 /* PriceView.swift */; };
B4EFF7432C3F6F650095D655 /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033C92BCC350A00162242 /* Currency.swift */; };
B4EFF7442C3F6F6A0095D655 /* FiatUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2AA8072568B8F40090B089 /* FiatUnit.swift */; };
B4EFF7452C3F6FF30095D655 /* Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4BFA254FBA0E00E9F9AA /* Placeholders.swift */; };
B4EFF7462C3F6FF90095D655 /* WalletData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E32BCC36FF00162242 /* WalletData.swift */; };
B4EFF7472C3F70010095D655 /* LatestTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033DC2BCC36C300162242 /* LatestTransaction.swift */; };
B4EFF7482C3F70090095D655 /* BitcoinUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033BE2BCC32F800162242 /* BitcoinUnit.swift */; };
C59F90CE0D04D3E4BB39BC5D /* libPods-BlueWalletUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F02C2F7CA3591E4E0B06EBA /* libPods-BlueWalletUITests.a */; };
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -461,6 +472,7 @@
B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveMethod.swift; sourceTree = "<group>"; };
B4D3235A177F4580BA52F2F9 /* libRNCSlider.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCSlider.a; sourceTree = "<group>"; };
B4EFF73A2C3F6C5E0095D655 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = "<group>"; };
B642AFB13483418CAB6FF25E /* libRCTQRCodeLocalImage.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTQRCodeLocalImage.a; sourceTree = "<group>"; };
B68F8552DD4428F64B11DCFB /* Pods-BlueWallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BlueWallet.debug.xcconfig"; path = "Target Support Files/Pods-BlueWallet/Pods-BlueWallet.debug.xcconfig"; sourceTree = "<group>"; };
B9D9B3A7B2CB4255876B67AF /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
@ -485,7 +497,7 @@
files = (
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
773E382FE62E836172AAB98B /* libPods-BlueWallet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -550,6 +562,7 @@
children = (
00E356F01AD99517003FC87E /* Supporting Files */,
B49038D82B8FBAD300A8164A /* BlueWalletUITest.swift */,
B4EFF73A2C3F6C5E0095D655 /* MockData.swift */,
);
path = BlueWalletTests;
sourceTree = "<group>";
@ -1627,6 +1640,7 @@
B44033DB2BCC369B00162242 /* Colors.swift in Sources */,
B40D4E632258425500428FCC /* ReceiveInterfaceController.swift in Sources */,
B43D0378225847C500FBAA95 /* WalletGradient.swift in Sources */,
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */,
B44033C02BCC32F800162242 /* BitcoinUnit.swift in Sources */,
B44033E52BCC36FF00162242 /* WalletData.swift in Sources */,
B44033EF2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
@ -1645,6 +1659,16 @@
buildActionMask = 2147483647;
files = (
B49038D92B8FBAD300A8164A /* BlueWalletUITest.swift in Sources */,
B4EFF7472C3F70010095D655 /* LatestTransaction.swift in Sources */,
B4EFF7422C3F6C990095D655 /* PriceView.swift in Sources */,
B4EFF7482C3F70090095D655 /* BitcoinUnit.swift in Sources */,
B4EFF73D2C3F6C6C0095D655 /* MarketData.swift in Sources */,
B4EFF73C2C3F6C5E0095D655 /* MockData.swift in Sources */,
B4EFF7432C3F6F650095D655 /* Currency.swift in Sources */,
B4EFF7412C3F6C960095D655 /* PriceWidget.swift in Sources */,
B4EFF7462C3F6FF90095D655 /* WalletData.swift in Sources */,
B4EFF7452C3F6FF30095D655 /* Placeholders.swift in Sources */,
B4EFF7442C3F6F6A0095D655 /* FiatUnit.swift in Sources */,
B47B21EC2B2128B8001F6690 /* BlueWalletUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -0,0 +1,15 @@
//
// MockData.swift
// BlueWallet
//
// Created by Marcos Rodriguez on 7/10/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
import Foundation
struct MockData {
static let currentMarketData = MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2023-01-01T00:00:00+00:00")
static let previousMarketData = MarketData(nextBlock: "", sats: "", price: "$9,000", rate: 9000, dateString: "2022-12-31T00:00:00+00:00")
static let noChangeMarketData = MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2023-01-01T00:00:00+00:00")
}

View File

@ -15,9 +15,13 @@ struct PriceView: View {
var body: some View {
switch entry.family {
case .accessoryInline, .accessoryCircular, .accessoryRectangular:
wrappedView(for: getView(for: entry.family), family: entry.family)
if #available(iOSApplicationExtension 16.0, *) {
wrappedView(for: getView(for: entry.family), family: entry.family)
} else {
getView(for: entry.family)
}
default:
defaultView.background(Color.widgetBackground)
defaultView.background(Color(UIColor.systemBackground))
}
}
@ -40,7 +44,7 @@ struct PriceView: View {
ZStack {
if family == .accessoryRectangular {
AccessoryWidgetBackground()
.background(Color(.systemBackground))
.background(Color(UIColor.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
} else {
AccessoryWidgetBackground()
@ -118,26 +122,26 @@ struct PriceView: View {
}
.padding(.all, 8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.systemBackground))
.background(Color(UIColor.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
private var defaultView: some View {
VStack(alignment: .trailing, spacing: nil, content: {
Text("Last Updated").font(Font.system(size: 11, weight: .regular, design: .default)).foregroundColor(.textColorLightGray)
Text("Last Updated").font(Font.system(size: 11, weight: .regular)).foregroundColor(Color(UIColor.lightGray))
HStack(alignment: .lastTextBaseline, spacing: nil, content: {
Text(entry.currentMarketData?.formattedDate ?? "").lineLimit(1).foregroundColor(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01).transition(.opacity)
Text(entry.currentMarketData?.formattedDate ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01).transition(.opacity)
})
Spacer()
VStack(alignment: .trailing, spacing: 16, content: {
HStack(alignment: .lastTextBaseline, spacing: nil, content: {
Text(entry.currentMarketData?.price ?? "").lineLimit(1).foregroundColor(.textColor).font(Font.system(size:28, weight: .bold, design: .default)).minimumScaleFactor(0.01).transition(.opacity)
Text(entry.currentMarketData?.price ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 28, weight: .bold)).minimumScaleFactor(0.01).transition(.opacity)
})
if let previousMarketDataPrice = entry.previousMarketData?.price, let currentMarketDataRate = entry.currentMarketData?.rate, let previousMarketDataRate = entry.previousMarketData?.rate, previousMarketDataRate > 0, currentMarketDataRate != previousMarketDataRate {
HStack(alignment: .lastTextBaseline, spacing: nil, content: {
Image(systemName: currentMarketDataRate > previousMarketDataRate ? "arrow.up" : "arrow.down")
Text("from").lineLimit(1).foregroundColor(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01)
Text(previousMarketDataPrice).lineLimit(1).foregroundColor(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01)
Image(systemName: currentMarketDataRate > previousMarketDataRate ? "arrow.up" : "arrow.down")
Text("from").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01)
Text(previousMarketDataPrice).lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01)
}).transition(.slide)
}
})
@ -178,7 +182,7 @@ struct PriceView_Previews: PreviewProvider {
.previewContext(WidgetPreviewContext(family: .accessoryInline))
PriceView(entry: PriceWidgetEntry(date: Date(), family: .accessoryRectangular, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData))
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
}
}
}
}
}
}
}
}

14
package-lock.json generated
View File

@ -46,7 +46,7 @@
"coinselect": "3.1.13",
"crypto-js": "4.2.0",
"dayjs": "1.11.11",
"detox": "20.25.0",
"detox": "20.25.1",
"ecpair": "2.0.1",
"ecurve": "1.0.6",
"electrum-client": "github:BlueWallet/rn-electrum-client#1bfe3cc",
@ -9650,9 +9650,9 @@
}
},
"node_modules/detox": {
"version": "20.25.0",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.25.0.tgz",
"integrity": "sha512-PI4JR7XZQM/VPLLscf07WHdjHu/n9zRRKajEFrgIIpaZK2txDJZnv1rqaIC+4yFGEvCVSbxYQFrjjihLERDWrA==",
"version": "20.25.1",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.25.1.tgz",
"integrity": "sha512-l+VhhtwfIR8DR4Htvo3KUIx1VPQkYanwjxsKUlXrOM60e47GB45U87la3VYVpFEvUQ5liD46tWWDkY0fkhoOOw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@ -29881,9 +29881,9 @@
"dev": true
},
"detox": {
"version": "20.25.0",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.25.0.tgz",
"integrity": "sha512-PI4JR7XZQM/VPLLscf07WHdjHu/n9zRRKajEFrgIIpaZK2txDJZnv1rqaIC+4yFGEvCVSbxYQFrjjihLERDWrA==",
"version": "20.25.1",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.25.1.tgz",
"integrity": "sha512-l+VhhtwfIR8DR4Htvo3KUIx1VPQkYanwjxsKUlXrOM60e47GB45U87la3VYVpFEvUQ5liD46tWWDkY0fkhoOOw==",
"requires": {
"ajv": "^8.6.3",
"bunyan": "^1.8.12",

View File

@ -64,7 +64,7 @@
"e2e:release-build": "detox build -c android.release",
"e2e:release-test": "detox test -c android.release",
"tslint": "tsc",
"lint": " npm run tslint && node scripts/find-unused-loc.js && eslint --ext .js,.ts,.tsx '*.@(js|ts|tsx)' screen 'blue_modules/*.@(js|ts|tsx)' class models loc tests components navigation",
"lint": " npm run tslint && node scripts/find-unused-loc.js && eslint --ext .js,.ts,.tsx '*.@(js|ts|tsx)' screen 'blue_modules/*.@(js|ts|tsx)' class models loc tests components navigation typings",
"lint:fix": "npm run lint -- --fix",
"lint:quickfix": "git status --porcelain | grep -v '\\.json' | grep -E '\\.js|\\.ts' --color=never | awk '{print $2}' | xargs eslint --fix; exit 0",
"unit": "jest -b -i tests/unit/*"
@ -130,7 +130,7 @@
"coinselect": "3.1.13",
"crypto-js": "4.2.0",
"dayjs": "1.11.11",
"detox": "20.25.0",
"detox": "20.25.1",
"ecpair": "2.0.1",
"ecurve": "1.0.6",
"electrum-client": "github:BlueWallet/rn-electrum-client#1bfe3cc",

View File

@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');
const mainLocFile = './loc/en.json';
const dirsToInterate = ['components', 'screen', 'blue_modules', 'class', 'hooks', 'helpers', 'navigation'];
const dirsToInterate = ['components', 'screen', 'blue_modules', 'class', 'hooks', 'helpers', 'navigation', 'typings'];
const addFiles = ['BlueComponents.js', 'App.tsx', 'navigation/index.tsx'];
const allowedLocPrefixes = ['loc.lnurl_auth', 'loc.units'];

View File

@ -0,0 +1,61 @@
import loc from '../loc';
const keys = {
CopyTXID: 'copyTX_ID',
CopyBlockExplorerLink: 'copy_blockExplorer',
ExpandNote: 'expandNote',
OpenInBlockExplorer: 'open_in_blockExplorer',
CopyAmount: 'copyAmount',
CopyNote: 'copyNote',
};
const icons = {
Eye: {
iconValue: 'eye',
},
EyeSlash: {
iconValue: 'eye.slash',
},
Clipboard: {
iconValue: 'doc.on.doc',
},
Link: {
iconValue: 'link',
},
Note: {
iconValue: 'note.text',
},
};
export const CommonToolTipActions = {
CopyTXID: {
id: keys.CopyTXID,
text: loc.transactions.details_copy_txid,
icon: icons.Clipboard,
},
CopyBlockExplorerLink: {
id: keys.CopyBlockExplorerLink,
text: loc.transactions.details_copy_block_explorer_link,
icon: icons.Clipboard,
},
OpenInBlockExplorer: {
id: keys.OpenInBlockExplorer,
text: loc.transactions.details_show_in_block_explorer,
icon: icons.Link,
},
ExpandNote: {
id: keys.ExpandNote,
text: loc.transactions.expand_note,
icon: icons.Note,
},
CopyAmount: {
id: keys.CopyAmount,
text: loc.transactions.details_copy_amount,
icon: icons.Clipboard,
},
CopyNote: {
id: keys.CopyNote,
text: loc.transactions.details_copy_note,
icon: icons.Clipboard,
},
};