Merge branch 'master' into renovate/gradle-8.x

This commit is contained in:
Marcos Rodriguez Vélez 2025-01-08 09:13:37 -04:00 committed by GitHub
commit bd9340c756
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 5014 additions and 1674 deletions

View file

@ -97,6 +97,7 @@ jobs:
with:
name: signed-apk
path: ${{ env.APK_PATH }}
if-no-files-found: error
browserstack:
runs-on: ubuntu-latest

View file

@ -125,6 +125,13 @@
"symbol": "£",
"country": "United Kingdom (British Pound)"
},
"HKD": {
"endPointKey": "HKD",
"locale": "zh-HK",
"source": "CoinGecko",
"symbol": "HK$",
"country": "Hong Kong (Hong Kong Dollar)"
},
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",

View file

@ -8,28 +8,55 @@ import androidx.work.WorkManager
class BitcoinPriceWidget : AppWidgetProvider() {
companion object {
private const val TAG = "BitcoinPriceWidget"
private const val SHARED_PREF_NAME = "group.io.bluewallet.bluewallet"
private const val WIDGET_COUNT_KEY = "widget_count"
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d("BitcoinPriceWidget", "onUpdate called")
WidgetUpdateWorker.scheduleWork(context)
for (widgetId in appWidgetIds) {
Log.d(TAG, "Updating widget with ID: $widgetId")
WidgetUpdateWorker.scheduleWork(context)
}
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
Log.d("BitcoinPriceWidget", "onEnabled called")
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 0)
if (widgetCount >= 1) {
Log.e(TAG, "Only one widget instance is allowed.")
return
}
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount + 1).apply()
Log.d(TAG, "onEnabled called")
WidgetUpdateWorker.scheduleWork(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
Log.d("BitcoinPriceWidget", "onDisabled called")
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - 1).apply()
Log.d(TAG, "onDisabled called")
clearCache(context)
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
}
private fun clearCache(context: Context) {
val sharedPref = context.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
sharedPref.edit().clear().apply() // Clear all preferences in the group
Log.d("BitcoinPriceWidget", "Cache cleared from group.io.bluewallet.bluewallet")
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - appWidgetIds.size).apply()
Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}")
}
private fun clearCache(context: Context) {
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
sharedPref.edit().clear().apply()
Log.d(TAG, "Cache cleared from $SHARED_PREF_NAME")
}
}

View file

@ -2,6 +2,7 @@ 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
@ -16,6 +17,14 @@ import com.facebook.react.modules.i18nmanager.I18nUtil
class MainApplication : Application(), ReactApplication {
private lateinit var sharedPref: SharedPreferences
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
if (key == "preferredCurrency") {
prefs.edit().remove("previous_price").apply()
WidgetUpdateWorker.scheduleWork(this)
}
}
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
@ -35,10 +44,10 @@ class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
sharedPref = getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
val sharedI18nUtilInstance = I18nUtil.getInstance()
sharedI18nUtilInstance.allowRTL(applicationContext, true)
SoLoader.init(this, /* native exopackage */ false)
@ -48,14 +57,17 @@ class MainApplication : Application(), ReactApplication {
load()
}
val sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
initializeBugsnag()
}
// Retrieve the "donottrack" value. Default to "0" if not found.
override fun onTerminate() {
super.onTerminate()
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
}
private fun initializeBugsnag() {
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

@ -2,20 +2,21 @@ package io.bluewallet.bluewallet
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
object MarketAPI {
private const val TAG = "MarketAPI"
private val client = OkHttpClient()
var baseUrl: String? = null
fun fetchPrice(context: Context, currency: String): String? {
suspend fun fetchPrice(context: Context, currency: String): String? {
return try {
// Load the JSON data from the assets
val fiatUnitsJson = context.assets.open("fiatUnits.json").bufferedReader().use { it.readText() }
val json = JSONObject(fiatUnitsJson)
val currencyInfo = json.getJSONObject(currency)
@ -25,26 +26,16 @@ object MarketAPI {
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 request = Request.Builder().url(urlString).build()
val response = withContext(Dispatchers.IO) { client.newCall(request).execute() }
val responseCode = urlConnection.responseCode
if (responseCode != 200) {
Log.e(TAG, "Failed to fetch price. Response code: $responseCode")
if (!response.isSuccessful) {
Log.e(TAG, "Failed to fetch price. Response code: ${response.code}")
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)
val jsonResponse = response.body?.string() ?: return null
parseJSONBasedOnSource(jsonResponse, source, endPointKey)
} catch (e: Exception) {
Log.e(TAG, "Error fetching price", e)
null

View file

@ -1,8 +1,10 @@
package io.bluewallet.bluewallet
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.util.Log
import android.view.View
@ -13,8 +15,10 @@ import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
companion object {
const val TAG = "WidgetUpdateWorker"
@ -35,66 +39,57 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
}
private lateinit var sharedPref: SharedPreferences
private lateinit var preferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener
override fun doWork(): Result {
override suspend fun doWork(): Result {
Log.d(TAG, "Widget update worker running")
sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
registerPreferenceChangeListener()
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 intent = Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widget_layout, pendingIntent)
// Show loading indicator
views.setViewVisibility(R.id.loading_indicator, View.VISIBLE)
views.setViewVisibility(R.id.price_value, View.GONE)
views.setViewVisibility(R.id.last_updated_label, View.GONE)
views.setViewVisibility(R.id.last_updated_time, View.GONE)
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
appWidgetManager.updateAppWidget(appWidgetIds, views)
val preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
val previousPrice = sharedPref.getString("previous_price", null)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
fetchPrice(preferredCurrency) { fetchedPrice, error ->
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
)
}
val fetchedPrice = fetchPrice(preferredCurrency)
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
return Result.success()
}
private fun registerPreferenceChangeListener() {
preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == "preferredCurrency" || key == "preferredCurrencyLocale" || key == "previous_price") {
Log.d(TAG, "Preference changed: $key")
updateWidgetOnPreferenceChange()
}
}
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
}
override fun onStopped() {
super.onStopped()
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
}
private fun updateWidgetOnPreferenceChange() {
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 preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
val previousPrice = sharedPref.getString("previous_price", null)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
fetchPrice(preferredCurrency) { fetchedPrice, error ->
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
)
private suspend fun fetchPrice(currency: String?): String? {
return withContext(Dispatchers.IO) {
MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
}
}
@ -107,24 +102,27 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
previousPrice: String?,
currentTime: String,
preferredCurrency: String?,
preferredCurrencyLocale: String?,
error: String?
preferredCurrencyLocale: String?
) {
val isPriceFetched = fetchedPrice != null
val isPriceCached = previousPrice != null
if (error != null || !isPriceFetched) {
Log.e(TAG, "Error fetching price: $error")
if (!isPriceFetched) {
Log.e(TAG, "Error fetching price.")
if (!isPriceCached) {
showLoadingError(views)
} else {
displayCachedPrice(views, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale)
}
} else {
displayFetchedPrice(
views, fetchedPrice!!, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
savePrice(sharedPref, fetchedPrice)
if (fetchedPrice != null) {
displayFetchedPrice(
views, fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
}
if (fetchedPrice != null) {
savePrice(sharedPref, fetchedPrice)
}
}
appWidgetManager.updateAppWidget(appWidgetIds, views)
@ -132,7 +130,7 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
private fun showLoadingError(views: RemoteViews) {
views.apply {
setViewVisibility(R.id.loading_indicator, View.VISIBLE)
setViewVisibility(R.id.loading_indicator, View.GONE)
setViewVisibility(R.id.price_value, View.GONE)
setViewVisibility(R.id.last_updated_label, View.GONE)
setViewVisibility(R.id.last_updated_time, View.GONE)
@ -216,15 +214,6 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
return currencyFormat
}
private fun fetchPrice(currency: String?, callback: (String?, String?) -> Unit) {
val price = MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
if (price == null) {
callback(null, "Failed to fetch price")
} else {
callback(price, null)
}
}
private fun savePrice(sharedPref: SharedPreferences, price: String) {
sharedPref.edit().putString("previous_price", price).apply()
}

View file

@ -13,13 +13,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
android:visibility="visible"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
android:gravity="end"
android:layout_gravity="end">
<TextView
android:id="@+id/last_updated_label"
style="@style/WidgetTextSecondary"
@ -29,69 +30,85 @@
android:textSize="12sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:visibility="gone"/>
android:visibility="gone"
android:layout_gravity="end"/>
<TextView
android:id="@+id/last_updated_time"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:text="--:--"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginTop="2dp"
android:visibility="gone"/>
android:visibility="gone"
android:layout_gravity="end"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
android:gravity="end"
android:layout_gravity="end">
<TextView
android:id="@+id/price_value"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:visibility="gone">
<ImageView
android:id="@+id/price_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price_label"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="From:"
android:textSize="12sp"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price"
style="@style/WidgetTextPrimary"
android:layout_marginBottom="8dp"
android:autoSizeMaxTextSize="24sp"
android:autoSizeMinTextSize="12sp"
android:autoSizeStepGranularity="2sp"
android:autoSizeTextType="uniform"
android:duplicateParentState="false"
android:editable="false"
android:lines="1"
android:text="Loading..."
android:textSize="24sp"
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout></LinearLayout>
android:orientation="horizontal"
android:gravity="end"
android:visibility="gone"
android:layout_gravity="end">
<ImageView
android:id="@+id/price_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"
android:layout_gravity="end"/>
<TextView
android:id="@+id/previous_price_label"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="From:"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:layout_gravity="end"/>
<TextView
android:id="@+id/previous_price"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:layout_gravity="end"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -1,4 +1,3 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import DefaultPreference from 'react-native-default-preference';
@ -9,6 +8,8 @@ import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet, TaprootWallet } fro
import presentAlert from '../components/Alert';
import loc from '../loc';
import { GROUP_IO_BLUEWALLET } from './currency';
import { ElectrumServerItem } from '../screen/settings/ElectrumSettings';
import { triggerWarningHapticFeedback } from './hapticFeedback';
const ElectrumClient = require('electrum-client');
const net = require('net');
@ -67,17 +68,11 @@ type MempoolTransaction = {
fee: number;
};
type Peer =
| {
host: string;
ssl: string;
tcp?: undefined;
}
| {
host: string;
tcp: string;
ssl?: undefined;
};
type Peer = {
host: string;
ssl?: number;
tcp?: number;
};
export const ELECTRUM_HOST = 'electrum_host';
export const ELECTRUM_TCP_PORT = 'electrum_tcp_port';
@ -85,16 +80,20 @@ export const ELECTRUM_SSL_PORT = 'electrum_ssl_port';
export const ELECTRUM_SERVER_HISTORY = 'electrum_server_history';
const ELECTRUM_CONNECTION_DISABLED = 'electrum_disabled';
const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: '443' };
const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: 443 };
export const hardcodedPeers: Peer[] = [
{ host: 'mainnet.foundationdevices.com', ssl: '50002' },
// { host: 'bitcoin.lukechilds.co', ssl: '50002' },
{ host: 'mainnet.foundationdevices.com', ssl: 50002 },
// { host: 'bitcoin.lukechilds.co', ssl: 50002 },
// { host: 'electrum.jochen-hoenicke.de', ssl: '50006' },
{ host: 'electrum1.bluewallet.io', ssl: '443' },
{ host: 'electrum.acinq.co', ssl: '50002' },
{ host: 'electrum.bitaroo.net', ssl: '50002' },
{ host: 'electrum1.bluewallet.io', ssl: 443 },
{ host: 'electrum.acinq.co', ssl: 50002 },
{ host: 'electrum.bitaroo.net', ssl: 50002 },
];
export const suggestedServers: Peer[] = hardcodedPeers.map(peer => ({
...peer,
}));
let mainClient: typeof ElectrumClient | undefined;
let mainConnected: boolean = false;
let wasConnectedAtLeastOnce: boolean = false;
@ -137,23 +136,56 @@ async function _getRealm() {
return _realm;
}
export const getPreferredServer = async (): Promise<ElectrumServerItem | undefined> => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
console.log('Getting preferred server:', { host, tcpPort, sslPort });
if (!host) {
console.warn('Preferred server host is undefined');
return;
}
return {
host,
tcp: tcpPort ? Number(tcpPort) : undefined,
ssl: sslPort ? Number(sslPort) : undefined,
};
};
export const removePreferredServer = async () => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('Removing preferred server');
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
};
export async function isDisabled(): Promise<boolean> {
let result;
try {
const savedValue = await AsyncStorage.getItem(ELECTRUM_CONNECTION_DISABLED);
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const savedValue = await DefaultPreference.get(ELECTRUM_CONNECTION_DISABLED);
console.log('Getting Electrum connection disabled state:', savedValue);
if (savedValue === null) {
result = false;
} else {
result = savedValue;
}
} catch {
} catch (error) {
console.error('Error getting Electrum connection disabled state:', error);
result = false;
}
return !!result;
}
export async function setDisabled(disabled = true) {
return AsyncStorage.setItem(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : '');
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('Setting Electrum connection disabled state to:', disabled);
return DefaultPreference.set(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : '');
}
function getCurrentPeer() {
@ -171,20 +203,23 @@ function getNextPeer() {
}
async function getSavedPeer(): Promise<Peer | null> {
const host = await AsyncStorage.getItem(ELECTRUM_HOST);
const tcpPort = await AsyncStorage.getItem(ELECTRUM_TCP_PORT);
const sslPort = await AsyncStorage.getItem(ELECTRUM_SSL_PORT);
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
console.log('Getting saved peer:', { host, tcpPort, sslPort });
if (!host) {
return null;
}
if (sslPort) {
return { host, ssl: sslPort };
return { host, ssl: Number(sslPort) };
}
if (tcpPort) {
return { host, tcp: tcpPort };
return { host, tcp: Number(tcpPort) };
}
return null;
@ -201,6 +236,8 @@ export async function connectMain(): Promise<void> {
usingPeer = savedPeer;
}
console.log('Using peer:', JSON.stringify(usingPeer));
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
try {
if (usingPeer.host.endsWith('onion')) {
@ -208,10 +245,6 @@ export async function connectMain(): Promise<void> {
await DefaultPreference.set(ELECTRUM_HOST, randomPeer.host);
await DefaultPreference.set(ELECTRUM_TCP_PORT, randomPeer.tcp ?? '');
await DefaultPreference.set(ELECTRUM_SSL_PORT, randomPeer.ssl ?? '');
} else {
await DefaultPreference.set(ELECTRUM_HOST, usingPeer.host);
await DefaultPreference.set(ELECTRUM_TCP_PORT, usingPeer.tcp ?? '');
await DefaultPreference.set(ELECTRUM_SSL_PORT, usingPeer.ssl ?? '');
}
} catch (e) {
// Must be running on Android
@ -292,6 +325,39 @@ export async function connectMain(): Promise<void> {
}
}
export async function presentResetToDefaultsAlert(): Promise<boolean> {
return new Promise(resolve => {
triggerWarningHapticFeedback();
presentAlert({
title: loc.settings.electrum_reset,
message: loc.settings.electrum_reset_to_default,
buttons: [
{
text: loc._.cancel,
style: 'cancel',
onPress: () => resolve(false),
},
{
text: loc._.ok,
style: 'destructive',
onPress: async () => {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
} catch (e) {
console.log(e); // Must be running on Android
}
resolve(true);
},
},
],
options: { cancelable: true },
});
});
}
const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
if (await isDisabled()) {
console.log(
@ -299,6 +365,7 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
);
return;
}
presentAlert({
allowRepeat: false,
title: loc.errors.network,
@ -319,39 +386,13 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
{
text: loc.settings.electrum_reset,
onPress: () => {
presentAlert({
title: loc.settings.electrum_reset,
message: loc.settings.electrum_reset_to_default,
buttons: [
{
text: loc._.cancel,
style: 'cancel',
onPress: () => {},
},
{
text: loc._.ok,
style: 'destructive',
onPress: async () => {
await AsyncStorage.setItem(ELECTRUM_HOST, '');
await AsyncStorage.setItem(ELECTRUM_TCP_PORT, '');
await AsyncStorage.setItem(ELECTRUM_SSL_PORT, '');
try {
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
} catch (e) {
console.log(e); // Must be running on Android
}
presentAlert({ message: loc.settings.electrum_saved });
setTimeout(connectMain, 500);
},
},
],
options: { cancelable: true },
presentResetToDefaultsAlert().then(result => {
if (result) {
connectionAttempt = 0;
mainClient.close() && mainClient.close();
setTimeout(connectMain, 500);
}
});
connectionAttempt = 0;
mainClient.close() && mainClient.close();
},
style: 'destructive',
},
@ -377,13 +418,18 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function getRandomDynamicPeer(): Promise<Peer> {
try {
let peers = JSON.parse((await AsyncStorage.getItem(storageKey)) as string);
let peers = JSON.parse((await DefaultPreference.get(storageKey)) as string);
peers = peers.sort(() => Math.random() - 0.5); // shuffle
for (const peer of peers) {
const ret = {
host: peer[1] as string,
tcp: '',
};
const ret: Peer = { host: peer[0], ssl: peer[1] };
ret.host = peer[1];
if (peer[1] === 's') {
ret.ssl = peer[2];
} else {
ret.tcp = peer[2];
}
for (const item of peer[2]) {
if (item.startsWith('t')) {
ret.tcp = item.replace('t', '');

View file

@ -132,13 +132,19 @@ export const showImagePickerAndReadImage = async (): Promise<string | undefined>
return undefined;
} else if (response.errorCode) {
throw new Error(response.errorMessage);
} else if (response.assets?.[0]?.uri) {
} else if (response.assets) {
try {
const result = await RNQRGenerator.detect({ uri: decodeURI(response.assets[0].uri.toString()) });
return result?.values[0];
const uri = response.assets[0].uri;
if (uri) {
const result = await RNQRGenerator.detect({ uri: decodeURI(uri.toString()) });
if (result?.values.length > 0) {
return result?.values[0];
}
}
throw new Error(loc.send.qr_error_no_qrcode);
} catch (error) {
console.error(error);
throw new Error(loc.send.qr_error_no_qrcode);
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
}
@ -187,9 +193,11 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
if (result) {
return { data: result.values[0], uri: fileCopyUri };
}
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
} catch (error) {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
}
}

View file

@ -14,6 +14,8 @@ export const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK
let alreadyConfigured = false;
let baseURI = groundControlUri;
const deepClone = obj => JSON.parse(JSON.stringify(obj));
const checkAndroidNotificationPermission = async () => {
try {
const { status } = await checkNotifications();
@ -323,8 +325,8 @@ export const configureNotifications = async onProcessNotifications => {
};
const handleNotification = async notification => {
// Deep clone to avoid modifying the original object
const payload = structuredClone({
// Deep clone to avoid modifying the original object
const payload = deepClone({
...notification,
...notification.data,
});

View file

@ -348,11 +348,31 @@ const startImport = (
// maybe its a watch-only address?
yield { progress: 'watch only' };
const watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(text);
if (watchOnly.valid()) {
await fetch(watchOnly, true);
yield { wallet: watchOnly };
const wo1 = new WatchOnlyWallet();
wo1.setSecret(text);
if (wo1.valid()) {
wo1.init();
if (text.startsWith('xpub')) {
// for xpub we also check ypub and zpub. If any of them was used, we import it.
let found = false;
const pubs = [text, wo1._xpubToYpub(text), wo1._xpubToZpub(text)];
for (const pub of pubs) {
const wo2 = new WatchOnlyWallet();
wo2.setSecret(pub);
wo2.init();
if (await wasUsed(wo2)) {
yield { wallet: wo2 };
found = true;
}
}
if (!found) {
await fetch(wo1, true);
yield { wallet: wo1 };
}
} else {
await fetch(wo1, true);
yield { wallet: wo1 };
}
}
// electrum p2wpkh-p2sh

View file

@ -1417,6 +1417,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (!this.allowBIP47()) {
return false;
}
try {
// watch-only wallet will throw an error here
this.getDerivationPath();
} catch (_) {
return false;
}
// only check BIP47 if derivation path is regular, otherwise too many wallets will be found
if (!["m/84'/0'/0'", "m/44'/0'/0'", "m/49'/0'/0'"].includes(this.getDerivationPath() as string)) {
return false;

View file

@ -629,7 +629,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
hexFingerprint = Buffer.from(hexFingerprint, 'hex').toString('hex');
}
const path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
let path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
if (path === 'm/') {
// not considered valid by Bip32 lib
path = 'm/0';
}
let xpub = m[2];
if (xpub.indexOf('/') !== -1) {
xpub = xpub.substr(0, xpub.indexOf('/'));

View file

@ -88,6 +88,12 @@ export type LightningTransaction = {
expire_time?: number;
ispaid?: boolean;
walletID?: string;
value?: number;
amt?: number;
fee?: number;
payment_preimage?: string;
payment_request?: string;
description?: string;
};
export type Transaction = {

View file

@ -312,4 +312,9 @@ export class WatchOnlyWallet extends LegacyWallet {
if (this._hdWalletInstance) return this._hdWalletInstance.isSegwit();
return super.isSegwit();
}
wasEverUsed(): Promise<boolean> {
if (this._hdWalletInstance) return this._hdWalletInstance.wasEverUsed();
return super.wasEverUsed();
}
}

View file

@ -1,15 +1,16 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import ToolTipMenu from './TooltipMenu';
import loc from '../loc';
import { scanQrHelper } from '../helpers/scan-qr';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
import presentAlert from './Alert';
import { useTheme } from './themes';
import RNQRGenerator from 'rn-qr-generator';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import { useSettings } from '../hooks/context/useSettings';
import { useRoute } from '@react-navigation/native';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
interface AddressInputScanButtonProps {
isLoading: boolean;
@ -19,6 +20,10 @@ interface AddressInputScanButtonProps {
onChangeText: (text: string) => void;
}
interface RouteParams {
onBarScanned?: any;
}
export const AddressInputScanButton = ({
isLoading,
launchedBy,
@ -28,6 +33,9 @@ export const AddressInputScanButton = ({
}: AddressInputScanButtonProps) => {
const { colors } = useTheme();
const { isClipboardGetContentEnabled } = useSettings();
const navigation = useExtendedNavigation();
const params = useRoute().params as RouteParams;
const stylesHook = StyleSheet.create({
scan: {
backgroundColor: colors.scanLabel,
@ -40,8 +48,10 @@ export const AddressInputScanButton = ({
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
Keyboard.dismiss();
if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value }));
}, [launchedBy, onBarScanned, scanButtonTapped]);
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation, scanButtonTapped]);
const actions = useMemo(() => {
const availableActions = [
@ -57,20 +67,23 @@ export const AddressInputScanButton = ({
return availableActions;
}, [isClipboardGetContentEnabled]);
useEffect(() => {
const data = params.onBarScanned;
if (data) {
onBarScanned({ data });
navigation.setParams({ onBarScanned: undefined });
}
});
const onMenuItemPressed = useCallback(
async (action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) {
case CommonToolTipActions.ScanQR.id:
scanButtonTapped();
if (launchedBy) {
scanQrHelper(launchedBy)
.then(value => onBarScanned({ data: value }))
.catch(error => {
presentAlert({ message: error.message });
});
}
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
break;
case CommonToolTipActions.PasteFromClipboard.id:
try {
@ -134,7 +147,7 @@ export const AddressInputScanButton = ({
}
Keyboard.dismiss();
},
[launchedBy, onBarScanned, onChangeText, scanButtonTapped],
[navigation, onBarScanned, onChangeText, scanButtonTapped],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);

View file

@ -3,7 +3,7 @@ import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Image, LayoutAnimation, Pressable, StyleSheet, TextInput, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
import { Image, LayoutAnimation, Pressable, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import { Badge, Icon, Text } from '@rneui/themed';
import {
@ -254,7 +254,7 @@ class AmountInput extends Component {
});
return (
<TouchableWithoutFeedback
<Pressable
accessibilityRole="button"
accessibilityLabel={loc._.enter_amount}
disabled={this.props.pointerEvents === 'none'}
@ -340,7 +340,7 @@ class AmountInput extends Component {
</View>
)}
</>
</TouchableWithoutFeedback>
</Pressable>
);
}
}

View file

@ -1,9 +1,9 @@
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType } from 'react';
import { SheetSize, SizeInfo, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
import { Keyboard, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
import { Keyboard, Image, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
import SaveFileButton from './SaveFileButton';
import { useTheme } from './themes';
import { Icon, Image } from '@rneui/base';
import { Icon } from '@rneui/base';
interface BottomModalProps extends TrueSheetProps {
children?: React.ReactNode;

222
components/CameraScreen.tsx Normal file
View file

@ -0,0 +1,222 @@
import React, { useState, useRef } from 'react';
import { Animated, SafeAreaView, StatusBar, StyleSheet, TouchableOpacity, View } from 'react-native';
import { Camera, CameraApi, CameraType, Orientation } from 'react-native-camera-kit';
import loc from '../loc';
import { Icon } from '@rneui/base';
const AnimatedIcon = Animated.createAnimatedComponent(Icon);
interface CameraScreenProps {
onCancelButtonPress: () => void;
showImagePickerButton?: boolean;
showFilePickerButton?: boolean;
onImagePickerButtonPress?: () => void;
onFilePickerButtonPress?: () => void;
onReadCode?: (event: any) => void;
}
const CameraScreen: React.FC<CameraScreenProps> = ({
onCancelButtonPress,
showImagePickerButton,
showFilePickerButton,
onImagePickerButtonPress,
onFilePickerButtonPress,
onReadCode,
}) => {
const cameraRef = useRef<CameraApi>(null);
const [torchMode, setTorchMode] = useState(false);
const [cameraType, setCameraType] = useState(CameraType.Back);
const [zoom, setZoom] = useState<number | undefined>();
const [orientationAnim] = useState(new Animated.Value(3));
const onSwitchCameraPressed = () => {
const direction = cameraType === CameraType.Back ? CameraType.Front : CameraType.Back;
setCameraType(direction);
setZoom(1); // When changing camera type, reset to default zoom for that camera
};
const onSetTorch = () => {
setTorchMode(!torchMode);
};
// Counter-rotate the icons to indicate the actual orientation of the captured photo.
// For this example, it'll behave incorrectly since UI orientation is allowed (and already-counter rotates the entire screen)
// For real phone apps, lock your UI orientation using a library like 'react-native-orientation-locker'
const rotateUi = true;
const uiRotation = orientationAnim.interpolate({
inputRange: [1, 4],
outputRange: ['180deg', '-90deg'],
});
const uiRotationStyle = rotateUi ? { transform: [{ rotate: uiRotation }] } : {};
function rotateUiTo(rotationValue: number) {
Animated.timing(orientationAnim, {
toValue: rotationValue,
useNativeDriver: true,
duration: 200,
isInteraction: false,
}).start();
}
return (
<View style={styles.screen}>
<StatusBar hidden />
<SafeAreaView style={styles.topButtons}>
<TouchableOpacity style={styles.topButton} onPress={onSetTorch}>
<AnimatedIcon
name={torchMode ? 'flashlight-on' : 'flashlight-off'}
type="font-awesome-6"
color="#ffffff"
style={{ ...styles.topButtonImg, ...uiRotationStyle }}
/>
</TouchableOpacity>
<View style={styles.rightButtonsContainer}>
{showImagePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_image}
style={[styles.topButton, styles.spacing, uiRotationStyle]}
onPress={onImagePickerButtonPress}
>
<AnimatedIcon name="image" type="font-awesome" color="#ffffff" style={{ ...styles.topButtonImg, ...uiRotationStyle }} />
</TouchableOpacity>
)}
{showFilePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_file}
style={[styles.topButton, styles.spacing, uiRotationStyle]}
onPress={onFilePickerButtonPress}
>
<AnimatedIcon
name="file-import"
type="font-awesome-5"
color="#ffffff"
style={{ ...styles.topButtonImg, ...uiRotationStyle }}
/>
</TouchableOpacity>
)}
</View>
</SafeAreaView>
<View style={styles.cameraContainer}>
<Camera
ref={cameraRef}
style={styles.cameraPreview}
cameraType={cameraType}
resetFocusWhenMotionDetected
zoom={zoom}
maxZoom={10}
onZoom={e => {
console.debug('zoom', e.nativeEvent.zoom);
setZoom(e.nativeEvent.zoom);
}}
onReadCode={onReadCode}
torchMode={torchMode ? 'on' : 'off'}
shutterPhotoSound
maxPhotoQualityPrioritization="quality"
onOrientationChange={e => {
// We recommend locking the camera UI to portrait (using a different library)
// and rotating the UI elements counter to the orientation
// However, we include onOrientationChange so you can match your UI to what the camera does
switch (e.nativeEvent.orientation) {
case Orientation.PORTRAIT_UPSIDE_DOWN:
console.debug('orientationChange', 'PORTRAIT_UPSIDE_DOWN');
rotateUiTo(1);
break;
case Orientation.LANDSCAPE_LEFT:
console.debug('orientationChange', 'LANDSCAPE_LEFT');
rotateUiTo(2);
break;
case Orientation.PORTRAIT:
console.debug('orientationChange', 'PORTRAIT');
rotateUiTo(3);
break;
case Orientation.LANDSCAPE_RIGHT:
console.debug('orientationChange', 'LANDSCAPE_RIGHT');
rotateUiTo(4);
break;
default:
console.debug('orientationChange', e.nativeEvent);
break;
}
}}
/>
</View>
<SafeAreaView style={styles.bottomButtons}>
<TouchableOpacity onPress={onCancelButtonPress}>
<Animated.Text style={[styles.backTextStyle, uiRotationStyle]}>{loc._.cancel}</Animated.Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bottomButton} onPress={onSwitchCameraPressed}>
<AnimatedIcon name="cameraswitch" type="font-awesome-6" color="#ffffff" style={{ ...styles.topButtonImg, ...uiRotationStyle }} />
</TouchableOpacity>
</SafeAreaView>
</View>
);
};
export default CameraScreen;
const styles = StyleSheet.create({
screen: {
height: '100%',
backgroundColor: '#000000',
},
topButtons: {
margin: 10,
zIndex: 10,
flexDirection: 'row',
justifyContent: 'space-between',
},
topButton: {
backgroundColor: '#222',
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
},
topButtonImg: {
margin: 10,
width: 24,
height: 24,
},
cameraContainer: {
justifyContent: 'center',
flex: 1,
},
cameraPreview: {
width: '100%',
height: '100%',
},
bottomButtons: {
margin: 10,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
backTextStyle: {
padding: 10,
color: 'white',
fontSize: 20,
},
rightButtonsContainer: {
flexDirection: 'row',
alignItems: 'center',
},
bottomButton: {
backgroundColor: '#222',
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10,
},
spacing: {
marginLeft: 20,
},
});

View file

@ -183,6 +183,9 @@ const CompanionDelegates = () => {
if (fileName && /\.(jpe?g|png)$/i.test(fileName)) {
try {
if (!decodedUrl) {
throw new Error(loc.send.qr_error_no_qrcode);
}
const values = await RNQRGenerator.detect({
uri: decodedUrl,
});
@ -200,11 +203,12 @@ const CompanionDelegates = () => {
},
);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.send.qr_error_no_qrcode });
throw new Error(loc.send.qr_error_no_qrcode);
}
} catch (error) {
console.error('Error detecting QR code:', error);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
} else {
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {

View file

@ -1,11 +1,13 @@
import React, { useCallback } from 'react';
import { View, StyleSheet, ViewStyle, TouchableOpacity } from 'react-native';
import React, { useCallback, useState } from 'react';
import { View, StyleSheet, ViewStyle, TouchableOpacity, ActivityIndicator } from 'react-native';
import { Icon, ListItem } from '@rneui/base';
import { ExtendedTransaction, LightningTransaction, TWallet } from '../class/wallets/types';
import { WalletCarouselItem } from './WalletsCarousel';
import { TransactionListItem } from './TransactionListItem';
import { useTheme } from './themes';
import { BitcoinUnit } from '../models/bitcoinUnits';
import { TouchableOpacityWrapper } from './ListItem';
import loc from '../loc';
enum ItemType {
WalletSection = 'wallet',
@ -29,11 +31,15 @@ interface ManageWalletsListItemProps {
isDraggingDisabled: boolean;
drag?: () => void;
isPlaceHolder?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
state: { wallets: TWallet[]; searchQuery: string };
navigateToWallet: (wallet: TWallet) => void;
renderHighlightedText: (text: string, query: string) => JSX.Element;
handleDeleteWallet: (wallet: TWallet) => void;
handleToggleHideBalance: (wallet: TWallet) => void;
isActive?: boolean;
style?: ViewStyle;
}
interface SwipeContentProps {
@ -46,13 +52,20 @@ const LeftSwipeContent: React.FC<SwipeContentProps> = ({ onPress, hideBalance, c
<TouchableOpacity
onPress={onPress}
style={[styles.leftButtonContainer, { backgroundColor: colors.buttonAlternativeTextColor } as ViewStyle]}
accessibilityRole="button"
accessibilityLabel={hideBalance ? loc.transactions.details_balance_show : loc.transactions.details_balance_hide}
>
<Icon name={hideBalance ? 'eye-slash' : 'eye'} color={colors.brandingColor} type="font-awesome-5" />
</TouchableOpacity>
);
const RightSwipeContent: React.FC<Partial<SwipeContentProps>> = ({ onPress }) => (
<TouchableOpacity onPress={onPress} style={styles.rightButtonContainer as ViewStyle}>
<TouchableOpacity
onPress={onPress}
style={styles.rightButtonContainer as ViewStyle}
accessibilityRole="button"
accessibilityLabel="Delete Wallet"
>
<Icon name="delete-outline" color="#FFFFFF" />
</TouchableOpacity>
);
@ -67,49 +80,54 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
renderHighlightedText,
handleDeleteWallet,
handleToggleHideBalance,
onPressIn,
onPressOut,
isActive,
style,
}) => {
const { colors } = useTheme();
const [isLoading, setIsLoading] = useState(false);
const onPress = useCallback(() => {
if (item.type === ItemType.WalletSection) {
setIsLoading(true);
navigateToWallet(item.data);
setIsLoading(false);
}
}, [item, navigateToWallet]);
const leftContent = useCallback(
(reset: () => void) => (
<LeftSwipeContent
onPress={() => {
handleToggleHideBalance(item.data as TWallet);
reset();
}}
hideBalance={(item.data as TWallet).hideBalance}
colors={colors}
/>
),
[colors, handleToggleHideBalance, item.data],
const handleLeftPress = (reset: () => void) => {
handleToggleHideBalance(item.data as TWallet);
reset();
};
const leftContent = (reset: () => void) => (
<LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />
);
const rightContent = useCallback(
(reset: () => void) => (
<RightSwipeContent
onPress={() => {
handleDeleteWallet(item.data as TWallet);
reset();
}}
/>
),
[handleDeleteWallet, item.data],
);
const handleRightPress = (reset: () => void) => {
handleDeleteWallet(item.data as TWallet);
reset();
};
const rightContent = (reset: () => void) => <RightSwipeContent onPress={() => handleRightPress(reset)} />;
if (isLoading) {
return <ActivityIndicator size="large" color={colors.brandingColor} />;
}
if (item.type === ItemType.WalletSection) {
return (
<ListItem.Swipeable
leftWidth={80}
rightWidth={90}
containerStyle={{ backgroundColor: colors.background }}
containerStyle={[{ backgroundColor: colors.background }, style]}
leftContent={leftContent}
rightContent={rightContent}
Component={TouchableOpacityWrapper}
onPressOut={onPressOut}
onPressIn={onPressIn}
style={isActive ? styles.activeItem : undefined}
>
<ListItem.Content
style={{
@ -121,6 +139,8 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
item={item.data}
handleLongPress={isDraggingDisabled ? undefined : drag}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
animationsEnabled={false}
searchQuery={state.searchQuery}
isPlaceHolder={isPlaceHolder}
@ -145,7 +165,6 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
);
}
console.error('Unrecognized item type:', item);
return null;
};
@ -164,6 +183,10 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: 'red',
},
activeItem: {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
},
});
export { ManageWalletsListItem, LeftSwipeContent, RightSwipeContent };
export { LeftSwipeContent, RightSwipeContent };
export default ManageWalletsListItem;

View file

@ -155,9 +155,13 @@ const MultipleStepsListItem = props => {
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
onPress={props.button.onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
{props.button.showActivityIndicator ? (
<ActivityIndicator />
) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
)}
</TouchableOpacity>
</View>
)}
@ -171,7 +175,11 @@ const MultipleStepsListItem = props => {
style={styles.rightButton}
onPress={props.rightButton.onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
{props.rightButton.showActivityIndicator ? (
<ActivityIndicator />
) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
)}
</TouchableOpacity>
</View>
)}
@ -194,11 +202,13 @@ MultipleStepsListItem.propTypes = {
disabled: PropTypes.bool,
buttonType: PropTypes.number,
leftText: PropTypes.string,
showActivityIndicator: PropTypes.bool,
}),
rightButton: PropTypes.shape({
text: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
showActivityIndicator: PropTypes.bool,
}),
};

View file

@ -55,6 +55,14 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
subactions: subaction.subactions?.map(subsubaction => ({
id: subsubaction.id.toString(),
title: subsubaction.text,
subtitle: subsubaction.subtitle,
image: subsubaction.icon?.iconValue ? subsubaction.icon.iconValue : undefined,
state: subsubaction.menuState === undefined ? undefined : ((subsubaction.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: subsubaction.disabled, destructive: subsubaction.destructive, hidden: subsubaction.hidden },
})),
})) || [];
return {

View file

@ -172,6 +172,8 @@ interface WalletCarouselItemProps {
searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
animationsEnabled?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
}
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
@ -186,6 +188,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
renderHighlightedText,
animationsEnabled = true,
isPlaceHolder = false,
onPressIn,
onPressOut,
}) => {
const scaleValue = useRef(new Animated.Value(1.0)).current;
const { colors } = useTheme();
@ -203,7 +207,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
tension: 100,
}).start();
}
}, [scaleValue, animationsEnabled]);
if (onPressIn) onPressIn();
}, [scaleValue, animationsEnabled, onPressIn]);
const onPressedOut = useCallback(() => {
if (animationsEnabled) {
@ -214,7 +219,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
tension: 100,
}).start();
}
}, [scaleValue, animationsEnabled]);
if (onPressOut) onPressOut();
}, [scaleValue, animationsEnabled, onPressOut]);
const handlePress = useCallback(() => {
onPressedOut();

View file

@ -44,6 +44,7 @@ platform :android do
# Extract versionName from build.gradle
version_name = sh("grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '\"'").strip
UI.user_error!("Failed to extract versionName from build.gradle") if version_name.nil? || version_name.empty?
# Update versionCode in build.gradle
UI.message("Updating versionCode in build.gradle to #{build_number}...")
@ -52,26 +53,25 @@ platform :android do
new_build_gradle_contents = build_gradle_contents.gsub(/versionCode\s+\d+/, "versionCode #{build_number}")
File.write(build_gradle_path, new_build_gradle_contents)
# Determine branch name
branch_name = ENV['GITHUB_HEAD_REF'] || `git rev-parse --abbrev-ref HEAD`.strip.gsub(/[\/\\:?*"<>|]/, '_')
# Determine branch name and sanitize it
branch_name = ENV['GITHUB_HEAD_REF'] || `git rev-parse --abbrev-ref HEAD`.strip
branch_name = branch_name.gsub(/[^a-zA-Z0-9_-]/, '_') # Replace non-alphanumeric characters with underscore
branch_name = 'master' if branch_name.nil? || branch_name.empty?
# Define APK name based on branch
signed_apk_name = branch_name != 'master' ? "BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk" : "BlueWallet-#{version_name}-#{build_number}.apk"
# Build APK
UI.message("Building APK...")
sh("cd android && ./gradlew assembleRelease")
UI.message("APK build completed.")
# Define APK name based on branch
signed_apk_name = branch_name != 'master' ?
"BlueWallet-#{version_name}-#{build_number}-#{branch_name}".gsub(/[\/\\:?*"<>|]/, '_') + ".apk" :
"BlueWallet-#{version_name}-#{build_number}.apk"
"BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk" :
"BlueWallet-#{version_name}-#{build_number}.apk"
# Define paths
unsigned_apk_path = "android/app/build/outputs/apk/release/app-release-unsigned.apk"
signed_apk_path = "android/app/build/outputs/apk/release/#{signed_apk_name}"
# Build APK
UI.message("Building APK...")
sh("cd android && ./gradlew assembleRelease --no-daemon")
UI.message("APK build completed.")
# Rename APK
if File.exist?(unsigned_apk_path)
UI.message("Renaming APK to #{signed_apk_name}...")
@ -84,11 +84,13 @@ platform :android do
# Sign APK
UI.message("Signing APK with apksigner...")
apksigner_path = "#{ENV['ANDROID_HOME']}/build-tools/34.0.0/apksigner"
apksigner_path = Dir.glob("#{ENV['ANDROID_HOME']}/build-tools/*/apksigner").sort.last
UI.user_error!("apksigner not found in Android build-tools") if apksigner_path.nil? || apksigner_path.empty?
sh("#{apksigner_path} sign --ks #{project_root}/bluewallet-release-key.keystore --ks-pass=pass:#{ENV['KEYSTORE_PASSWORD']} #{signed_apk_path}")
UI.message("APK signed successfully: #{signed_apk_path}")
end
end
end
desc "Upload APK to BrowserStack and post result as PR comment"
lane :upload_to_browserstack_and_comment do
@ -592,4 +594,3 @@ lane :update_release_notes do |options|
end
end
end
end

View file

@ -1,54 +1,5 @@
import { Platform } from 'react-native';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { navigationRef } from '../NavigationService';
/**
* Helper function that navigates to ScanQR screen, and returns promise that will resolve with the result of a scan,
* and then navigates back. If QRCode scan was closed, promise resolves to null.
*
* @param currentScreenName {string}
* @param showFileImportButton {boolean}
*
* @param onDismiss {function} - if camera is closed via X button it gets triggered
* @param useMerge {boolean} - if true, will merge the new screen with the current screen, otherwise will replace the current screen
* @return {Promise<string>}
*/
function scanQrHelper(
currentScreenName: string,
showFileImportButton = true,
onDismiss?: () => void,
useMerge = true,
): Promise<string | null> {
return requestCameraAuthorization().then(() => {
return new Promise(resolve => {
let params = {};
if (useMerge) {
const onBarScanned = function (data: any) {
setTimeout(() => resolve(data.data || data), 1);
navigationRef.navigate({ name: currentScreenName, params: data, merge: true });
};
params = {
showFileImportButton: Boolean(showFileImportButton),
onDismiss,
onBarScanned,
};
} else {
params = { launchedBy: currentScreenName, showFileImportButton: Boolean(showFileImportButton) };
}
navigationRef.navigate({
name: 'ScanQRCodeRoot',
params: {
screen: 'ScanQRCode',
params,
},
merge: true,
});
});
});
}
const isCameraAuthorizationStatusGranted = async () => {
const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
@ -59,4 +10,4 @@ const requestCameraAuthorization = () => {
return request(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
};
export { scanQrHelper, isCameraAuthorizationStatusGranted, requestCameraAuthorization };
export { isCameraAuthorizationStatusGranted, requestCameraAuthorization };

View file

@ -3,6 +3,7 @@ import { navigationRef } from '../NavigationService';
import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder';
import { unlockWithBiometrics, useBiometrics } from './useBiometrics';
import { useStorage } from './context/useStorage';
import { requestCameraAuthorization } from '../helpers/scan-qr';
// List of screens that require biometrics
const requiresBiometrics = ['WalletExportRoot', 'WalletXpubRoot', 'ViewEditMultisigCosignersRoot', 'ExportMultisigCoordinationSetupRoot'];
@ -90,6 +91,10 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
return; // Prevent proceeding with the original navigation if the reminder is shown
}
}
if (screenName === 'ScanQRCode') {
await requestCameraAuthorization();
}
proceedWithNavigation();
})();
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -162,7 +162,7 @@
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 */; };
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -427,7 +427,7 @@
files = (
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
17CDA0718F42DB2CE856C872 /* libPods-BlueWallet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

@ -1,5 +1,5 @@
{
"originHash" : "89509f555bc90a15b96ca0a326a69850770bdaac04a46f9cf482d81533702e3c",
"originHash" : "52530e6b1e3a85c8854952ef703a6d1bbe1acd82713be2b3166476b9b277db23",
"pins" : [
{
"identity" : "bugsnag-cocoa",
@ -19,6 +19,15 @@
"version" : "6.2.2"
}
},
{
"identity" : "keychain-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/evgenyneu/keychain-swift.git",
"state" : {
"revision" : "5e1b02b6a9dac2a759a1d5dbc175c86bd192a608",
"version" : "24.0.0"
}
},
{
"identity" : "swift_qrcodejs",
"kind" : "remoteSourceControl",

View file

@ -137,7 +137,7 @@
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionDomains</key>

View file

@ -56,7 +56,5 @@
<key>apiKey</key>
<string>17ba9059f676f1cc4f45d98182388b01</string>
</dict>
<key>WKCompanionAppBundleIdentifier</key>
<string>io.bluewallet.bluewallet</string>
</dict>
</plist>

View file

@ -1315,7 +1315,7 @@ PODS:
- Yoga
- react-native-ios-context-menu (1.15.3):
- React-Core
- react-native-menu (1.1.7):
- react-native-menu (1.2.0):
- React
- react-native-randombytes (3.6.1):
- React-Core
@ -1588,8 +1588,27 @@ PODS:
- React-logger (= 0.75.4)
- React-perflogger (= 0.75.4)
- React-utils (= 0.75.4)
- ReactNativeCameraKit (13.0.0):
- ReactNativeCameraKit (14.1.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RealmJS (20.1.0):
- React
- RNCAsyncStorage (2.1.0):
@ -1650,7 +1669,7 @@ PODS:
- Yoga
- RNLocalize (3.3.0):
- React-Core
- RNPermissions (5.2.1):
- RNPermissions (5.2.2):
- React-Core
- RNQrGenerator (1.4.2):
- React
@ -1680,7 +1699,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNReanimated (3.16.5):
- RNReanimated (3.16.6):
- DoubleConversion
- glog
- hermes-engine
@ -1700,10 +1719,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNReanimated/reanimated (= 3.16.5)
- RNReanimated/worklets (= 3.16.5)
- RNReanimated/reanimated (= 3.16.6)
- RNReanimated/worklets (= 3.16.6)
- Yoga
- RNReanimated/reanimated (3.16.5):
- RNReanimated/reanimated (3.16.6):
- DoubleConversion
- glog
- hermes-engine
@ -1723,9 +1742,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNReanimated/reanimated/apple (= 3.16.5)
- RNReanimated/reanimated/apple (= 3.16.6)
- Yoga
- RNReanimated/reanimated/apple (3.16.5):
- RNReanimated/reanimated/apple (3.16.6):
- DoubleConversion
- glog
- hermes-engine
@ -1746,7 +1765,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNReanimated/worklets (3.16.5):
- RNReanimated/worklets (3.16.6):
- DoubleConversion
- glog
- hermes-engine
@ -2226,7 +2245,7 @@ SPEC CHECKSUMS:
react-native-document-picker: 530879d9e89b490f0954bcc4ab697c5b5e35d659
react-native-image-picker: 19a8d8471a239890675726f88f9c18dd213656d5
react-native-ios-context-menu: 986da6dcba70094bcc2a8049f68410fe7d25aff1
react-native-menu: 5779f6bd7a4e58d457ca5e0a6f164651dd26cd7f
react-native-menu: 74230a5879e0ca697e98ee7c3087297dc774bf06
react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116
react-native-safe-area-context: 758e894ca5a9bd1868d2a9cfbca7326a2b6bf9dc
react-native-screen-capture: 7b6121f529681ed2fde36cdedadd0bb39e9a3796
@ -2258,7 +2277,7 @@ SPEC CHECKSUMS:
React-utils: 02526ea15628a768b8db9517b6017a1785c734d2
ReactCodegen: 8b5341ecb61898b8bd40a73ebc443c6bf2d14423
ReactCommon: 36d48f542b4010786d6b2bcee615fe5f906b7105
ReactNativeCameraKit: f058d47e0b1e55fd819bb55ee16505a2e0ca53db
ReactNativeCameraKit: e72b838dac4ea2da19b7eb5d00b23125072790fd
RealmJS: 9fd51c849eb552ade9f7b11db42a319b4f6cab4c
RNCAsyncStorage: c91d753ede6dc21862c4922cd13f98f7cfde578e
RNCClipboard: dbcf25b8f666b4685c02eeb65be981d30198e505
@ -2270,12 +2289,12 @@ SPEC CHECKSUMS:
RNHandoff: bc8af5a86853ff13b033e7ba1114c3c5b38e6385
RNKeychain: 4df48b5186ca2b6a99f5ead69ad587154e084a32
RNLocalize: d024afa9204c13885e61dc88b8190651bcaabac9
RNPermissions: 979aa94a1a2091e3b2c3e7130ef0a1a6e331e05a
RNPermissions: 6f08c623b0c8ca7d95faa71c3956b159b34f25c3
RNQrGenerator: 7c604c0eb608af64ff586ab0c040796a04eff247
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNRate: 7641919330e0d6688ad885a985b4bd697ed7d14c
RNReactNativeHapticFeedback: 00ba111b82aa266bb3ee1aa576831c2ea9a9dfad
RNReanimated: ae56eba247f82fa0d8bbf52bb0e7a34a218482de
RNReanimated: 000b758cfbcd9c20c15b7ef305f98f036b288feb
RNScreens: 35bb8e81aeccf111baa0ea01a54231390dbbcfd9
RNShare: 6204e6a1987ba3e7c47071ef703e5449a0e3548a
RNSVG: 3421710ac15f4f2dc47e5c122f2c2e4282116830

View file

@ -16,6 +16,7 @@ struct CompactPriceView: View {
.bold()
.multilineTextAlignment(.center)
.dynamicTypeSize(.large ... .accessibility5)
.foregroundColor(textColor())
.accessibilityLabel("Bitcoin price: \(price)")
VStack(alignment: .center, spacing: 8) {
@ -27,7 +28,7 @@ struct CompactPriceView: View {
.shadow(color: shadowColor(), radius: 1, x: 0, y: 1)
}
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundColor(textColor())
.multilineTextAlignment(.center)
.accessibilityElement(children: .combine)
}
@ -35,8 +36,12 @@ struct CompactPriceView: View {
.frame(maxWidth: .infinity)
}
private func textColor() -> Color {
colorScheme == .dark ? .white : .black
}
private func shadowColor() -> Color {
colorScheme == .dark ? .white.opacity(0.2) : .black.opacity(0.2)
textColor().opacity(0.2)
}
}
@ -45,7 +50,6 @@ struct CompactPriceView: View {
struct CompactPriceView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
// Example vibrant background
LinearGradient(
gradient: Gradient(colors: [.blue, .purple]),
startPoint: .top,

View file

@ -19,6 +19,7 @@
"close": "اغلاق",
"change_input_currency": "تغيير عملة الادخال",
"refresh": "تحديث",
"pick_file": "اختر ملف",
"enter_amount": "أدخل القيمة",
"qr_custom_input_button": "أنقر ١٠ مرات لإدخال قيمة مخصصة"
},
@ -384,7 +385,6 @@
"select_wallet": "اختيار محفظة",
"xpub_copiedToClipboard": "تم النسخ إلى الحافظة.",
"pull_to_refresh": "اسحب للتحديث",
"warning_do_not_disclose": "تحذير! لا تنشر هذا.",
"add_ln_wallet_first": "يجب عليك أولاً إضافة محفظة برق.",
"identity_pubkey": "هوية Pubkey",
"xpub_title": "عنوان XPUB للمحفظة"

View file

@ -481,7 +481,14 @@
"select_wallet": "Vyberte peněženku",
"xpub_copiedToClipboard": "Zkopírováno do schránky.",
"pull_to_refresh": "Zatáhněte pro obnovení",
"warning_do_not_disclose": "Varování! Nezveřejňujte.",
"warning_do_not_disclose": "Níže uvedené informace nikdy nesdílejte",
"scan_import": "Chcete-li importovat vaši peněženku do jiné aplikace, naskenujte tento QR kód.",
"write_down_header": "Vytvořit ruční zálohu",
"write_down": "Zapište si a bezpečně uložte tato slova. Lze je použít pro pozdější obnovu vaší peněženky.",
"wallet_type_this": "Typ této peněženky je {type}.",
"share_number": "Sdílet {number}",
"copy_ln_url": "Zkopírujte a bezpečně si uložte tuto URL pro pozdější obnovení vaší peněženky.",
"copy_ln_public": "Zkopírujte a bezpečně si tuto informaci uložte pro pozdější obnovení vaší peněženky.",
"add_ln_wallet_first": "Nejdříve musíte přidat Lightning peněženku.",
"identity_pubkey": "Identity Pubkey",
"xpub_title": "XPUB peněženky",

View file

@ -187,8 +187,7 @@
"list_tryagain": "Trio eto",
"select_wallet": "Dewis waled",
"xpub_copiedToClipboard": "Wedi gopio i'r clipfwrdd.",
"pull_to_refresh": "Tynnu i Adnewyddu",
"warning_do_not_disclose": "Rhybudd! Paid ei ddatgelu."
"pull_to_refresh": "Tynnu i Adnewyddu"
},
"total_balance_view": {
"title": "Balans Llawn"

View file

@ -10,6 +10,7 @@
"never": "nie",
"of": "{number} von {total}",
"ok": "OK",
"customize": "Anpassen",
"enter_url": "URL eingeben",
"storage_is_encrypted": "Zum Entschlüsseln des Speichers das Passwort eingeben.",
"yes": "Ja",
@ -22,9 +23,12 @@
"close": "Schließen",
"change_input_currency": "Eingangswährung ändern",
"refresh": "Aktualisieren",
"pick_image": "Aus der Bibliothek wählen",
"pick_file": "Datei auswählen",
"enter_amount": "Betrag eingeben",
"qr_custom_input_button": "10x antippen für individuelle Eingabe",
"unlock": "Entsperren",
"port": "Port",
"suggested": "Vorgeschlagen"
},
"azteco": {
@ -71,6 +75,7 @@
"please_pay": "Bitte zahle",
"preimage": "Urbild",
"sats": "sats",
"date_time": "Datum und Zeit",
"wasnt_paid_and_expired": "Diese Rechnung ist unbezahlt abgelaufen."
},
"plausibledeniability": {
@ -128,7 +133,7 @@
"details_insert_contact": "Kontakt einfügen",
"details_add_rec_add": "Empfänger hinzufügen",
"details_add_rec_rem": "Empfänger entfernen",
"details_add_recc_rem_all_alert_description": "Bist du sicher, dass du alle Empfänger entfernen willst?",
"details_add_recc_rem_all_alert_description": "Wirklich alle Empfänger entfernen?",
"details_add_rec_rem_all": "Alle Empfänger entfernen",
"details_recipients_title": "Empfänger",
"details_address": "Adresse",
@ -212,6 +217,7 @@
"block_explorer_invalid_custom_url": "Ungültige URL. Geben Sie eine gültige URL ein, die mit http:// oder https:// beginnt.",
"about_selftest_electrum_disabled": "Deaktiviere den Electrum Offline-Modus, um den Selbsttest durchführen zu können.",
"about_selftest_ok": "Alle internen Tests verliefen erfolgreich. Das Wallet funktioniert gut.",
"about_sm_github": "GitHub",
"about_sm_discord": "Discord Server",
"about_sm_telegram": "Telegram-Channel",
@ -225,6 +231,7 @@
"biom_no_passcode": "Um fortzufahren müssen auf dem Gerät entweder ein Sicherheitscode oder biometrische Daten aktiviert sein. Dies lässt sich in der App \"Einstellungen\" vornehmen.",
"biom_remove_decrypt": "Alle Deine Wallets werden entfernt und der Speicher wird entschlüsselt. Bist du sicher, dass du fortfahren möchten?",
"currency": "Währung",
"currency_source": "Der Kurs wird bezogen von",
"currency_fetch_error": "Beim Abrufen des Wechselkurses für die ausgewählte Währung trat ein Fehler auf.",
"default_desc": "Wenn deaktiviert öffnet BlueWallet beim Start die ausgewählte Wallet.",
"default_info": "Standard Info",
@ -244,13 +251,16 @@
"set_lndhub_as_default": "{url} als Standard LNDhub-Server festlegen?",
"electrum_settings_server": "Electrum Server",
"electrum_status": "Status",
"electrum_clear_alert_title": "Historie löschen?",
"electrum_preferred_server": "Präferierter Server",
"electrum_preferred_server_description": "Den Server eingeben, der die Wallet für alle Bitcoin-Aktivitäten verwenden soll. Sobald festgelegt, wird die Wallet um Salden abzufragen, Transaktionen zu senden und Netzwerkdaten abzurufen ausschliesslich diesen Server verwenden. Prüfen sie vorher, dass der Server vertrauenswürdig ist.", "electrum_clear_alert_title": "Historie löschen?",
"electrum_clear_alert_message": "Electrum-Serverhistorie löschen?",
"electrum_clear_alert_cancel": "Abbrechen",
"electrum_clear_alert_ok": "Ok",
"electrum_reset": "Zurücksetzten",
"only_use_preferred": "Nur mit bevorzugtem Server verbinden",
"electrum_unable_to_connect": "Verbindung zu {server} kann nicht hergestellt werden.",
"electrum_reset_to_default": "Sollen die Electrum-Einstellungen wirklich auf die Standardwerte zurückgesetzt werden?",
"electrum_history": "Historie",
"electrum_reset_to_default": "BlueWallet wählt zufällig einen Server aus der Vorschlagsliste oder Historie aus. Der Serververlauf bleibt dadurch unverändert.",
"electrum_reset": "Zurücksetzten",
"electrum_clear": "Historie löschen",
"encrypt_decrypt": "Speicher entschlüsseln",
"encrypt_decrypt_q": "Willst du die Speicherverschlüsselung wirklich aufheben? Hiermit wird dein Wallet ohne Passwortschutz direkt benutzbar. ",
"encrypt_enc_and_pass": "Verschlüsselt und passwortgeschützt",
@ -264,6 +274,7 @@
"encrypt_title": "Sicherheit",
"encrypt_tstorage": "Speicher",
"encrypt_use": "Benutze {type}",
"set_as_preferred": "Als bevorzugt festlegen",
"encrypted_feature_disabled": "Diese Funktion kann bei verschlüsseltem Speicher nicht genutzt werden.",
"encrypt_use_expl": "{type} wird zur Transaktionsdurchführung, zum Entsperren, dem Export oder der Löschung einer Wallet benötigt. {type} ersetzt nicht die Passworteingabe bei verschlüsseltem Speicher.",
"biometrics_fail": "Wenn {type} nicht aktiviert ist oder entsperrt werden kann, alternativ Ihren Gerätepasscode verwenden.",
@ -283,6 +294,7 @@
"network": "Netzwerk",
"network_broadcast": "Transaktion publizieren",
"network_electrum": "Electrum Server",
"electrum_suggested_description": "Ist kein Bevorzugter festgelegt, wird zufällig einer der vorgeschlagenen Server genutzt.",
"not_a_valid_uri": "Keine gültige URI",
"notifications": "Benachrichtigungen",
"open_link_in_explorer": "Link in Explorer öffnen",
@ -298,7 +310,7 @@
"privacy_do_not_track": "Diagnosedaten ausschalten",
"privacy_do_not_track_explanation": "Leistungs- und Zuverlässigkeitsinformationen nicht zur Analyse einreichen.",
"rate": "Kurs",
"push_notifications_explanation": "Durch das Aktivieren von Benachrichtigungen wird dein Geräte-Token zusammen mit Wallet-Adressen und Transaktions-IDs für alle Wallets und Transaktionen, die nach dem Aktivieren von Benachrichtigungen durchgeführt wurden, an den Server gesendet. Das Gerätetoken wird verwendet, um Benachrichtigungen zu senden, und die Wallet-Informationen ermöglichen es uns, dich über eingehende Bitcoin oder Transaktionsbestätigungen zu informieren.\n\nEs werden nur Informationen übertragen, nachdem du die Benachrichtigungen aktiviert hast - nichts von früher wird gesammelt.\n\nWenn du die Benachrichtigungen deaktivierst, werden alle diese Informationen vom Server entfernt. Wenn Sie eine Wallet aus der App löschen, werden auch die zugehörigen Informationen vom Server entfernt.",
"push_notifications_explanation": "Durch das Aktivieren von Benachrichtigungen wird das Gerätetoken zusammen mit den Wallet-Adressen inkl. künftigen Transaktions-IDs, an den Benachrichtigungsdienst gesendet. Das Gerätetoken erlaubt Benachrichtigungen an das Gerät zu adressieren, die Wallet-Informationen ermöglichen, eingehende Transaktionen und Bestätigungen zu notifizieren.\n\nNach der Aktivierung werden nur künftige, nicht aber vergangene Transaktions-IDs übertragen.\n\nMit der Deaktivierung, werden alle diese Informationen wieder vom Server entfernt. Das Gleiche passiert beim löschen der Wallet-App.",
"selfTest": "Selbsttest",
"save": "Speichern",
"saved": "Gespeichert",
@ -311,17 +323,20 @@
"notifications": {
"would_you_like_to_receive_notifications": "Möchten Sie bei Zahlungseingängen eine Benachrichtigung erhalten?",
"notifications_subtitle": "Zahlungseingänge und Transaktionsbestätigungen",
"no_and_dont_ask": "Nein und nicht erneut fragen."
"no_and_dont_ask": "Nein und nicht erneut fragen.",
"permission_denied_message": "Die App-Berechtigung Benachrichtigungen zu erhalten ist nicht gesetzt. Zum Erhalt diese in den Geräteeinstellungen erteilen."
},
"transactions": {
"cancel_explain": "BlueWallet ersetzt diese Transaktion durch eine mit höherer Gebühr, welche den Betrag an Dich zurücküberweist. Die aktuelle Transaktion wird dadurch effektiv abgebrochen. Dieses Verfahren wird RBF - Replace By Fee - genannt.",
"cancel_no": "Diese Transaktion ist nicht ersetzbar.",
"cancel_title": "Diese Transaktion abbrechen (RBF)",
"transaction_loading_error": "Es gab ein Problem beim Laden der Transaktion. Bitte später erneut versuchen.",
"transaction_not_available": "Transaktion ist nicht verfügbar",
"confirmations_lowercase": "{confirmations} Bestätigungen",
"copy_link": "Link kopieren",
"expand_note": "Bezeichnung erweitern",
"cpfp_create": "Erstellen",
"cpfp_exp": "BlueWallet erzeugt eine weitere Transaktion, welche deine unbestätigte Transaktion ausgibt. Die Gesamtgebühren werden höher als die der ursprünglichen Transaktion sein, daher sollte sie schneller ausgeführt werden. Dies wird CPFP genannt - Child Pays For Parent.",
"cpfp_exp": "BlueWallet erzeugt eine weitere Transaktion, welche die unbestätigte Transaktion ausgibt. Das höhere Gebührentotal beider Transaktionen führt zu einer schnelleren Verarbeitung (Child Pays for Parent).",
"cpfp_no_bump": "Keine TRX-Gebührenerhöhung möglich",
"cpfp_title": "TRX-Gebühr erhöhen (CPFP)",
"details_balance_hide": "Guthaben verbergen",
@ -335,6 +350,7 @@
"details_outputs": "Ausgänge",
"date": "Datum",
"details_received": "Empfangen",
"details_view_in_browser": "Im Browser anzeigen",
"details_title": "Transaktion",
"incoming_transaction": "Eingehende Transaktion",
"outgoing_transaction": "Ausgehende Transaktion",
@ -372,6 +388,8 @@
"add_bitcoin_explain": "Einfache und leistungsstarke Bitcoin Wallet",
"add_create": "Erstellen",
"total_balance": "Gesamtes Guthaben",
"add_entropy_reset_title": "Entropie zurücksetzten",
"add_entropy_reset_message": "Ein Wechsel des Wallet Typs wird die Entropie zurücksetzten. Wirklich weiterfahren? ",
"add_entropy": "Entropie",
"add_entropy_bytes": "{bytes} Bytes Entropie",
"add_entropy_generated": "{gen} Bytes an generierter Entropie ",
@ -393,6 +411,7 @@
"add_wallet_seed_length_24": "24 Wörter",
"clipboard_bitcoin": "Willst Du die Bitcoin Adresse in der Zwischenablage für eine Transaktion verwenden?",
"clipboard_lightning": "Willst Du die Lightning Rechnung in der Zwischenablage für eine Transaktion verwenden?",
"clear_clipboard_on_import": "Zwischenablage beim Import löschen",
"details_address": "Adresse",
"details_advanced": "Fortgeschritten",
"details_are_you_sure": "Bist du dir sicher?",
@ -434,6 +453,7 @@
"import_discovery_subtitle": "Wallet aus Trefferliste wählen",
"import_discovery_derivation": "Eigener Ableitungspfad wählen",
"import_discovery_no_wallets": "Es wurden keine Wallets gefunden.",
"import_discovery_offline": "BlueWallet ist derzeit im Offline-Modus und kann die Existenz der Wallet nicht überprüfen. Bitte die richtige Wallet manuell auswählen.",
"import_derivation_found": "Gefunden",
"import_derivation_found_not": "Nicht gefunden",
"import_derivation_loading": "Lade...",
@ -465,7 +485,14 @@
"select_wallet": "Wähle eine Wallet",
"xpub_copiedToClipboard": "In die Zwischenablage kopiert.",
"pull_to_refresh": "Zum Aktualisieren ziehen",
"warning_do_not_disclose": "Warnung! Nicht veröffentlichen",
"warning_do_not_disclose": "Niemals die nachfolgenden Informationen teilen",
"scan_import": "Diesen QR-Code zum Import der Wallet in einer anderen App scannen.",
"write_down_header": "Manuelles Backup erstellen",
"write_down": "Diese Worte aufschreiben und sicher verwahren. Sie sind nötig um die Wallet später wiederherzustellen.",
"wallet_type_this": "Wallet vom Typ {type}.",
"share_number": "Teil {number}",
"copy_ln_url": "Diese URL kopieren und sicher speichern. Sie ist um nötig die Wallet später wiederherzustellen.",
"copy_ln_public": "Diese Information kopieren und sicher speichern. Sie ist nötig um die Wallet später wiederherzustellen.",
"add_ln_wallet_first": "Bitte zuerst ein Lightning-Wallet hinzufügen.",
"identity_pubkey": "Pubkey-Identität",
"xpub_title": "Wallet xPub",
@ -473,6 +500,10 @@
"more_info": "Mehr Infos"
},
"total_balance_view": {
"display_in_bitcoin": "In bitcoin anzeigen",
"hide": "Verbergen",
"display_in_sats": "In Sats anzeigen",
"display_in_fiat": "In {currency} anzeigen",
"title": "Gesamtes Guthaben",
"explanation": "Auf dem Übersichtsbildschirm den Gesamtsaldo aller Wallets anzeigen."
},
@ -538,15 +569,15 @@
"input_path_explain": "Überspringen, um den Standard zu verwenden ({default})",
"ms_help": "Hilfe",
"ms_help_title": "Tipps und Tricks zur Funktionsweise von Multisig",
"ms_help_text": "Ein wallet mit mehreren Schlüsseln zur gemeinsamen Verwahrung oder um die Sicherheit exponentiell zu erhöhen.",
"ms_help_text": "Ein Wallet mit mehreren Schlüsseln zur gemeinsamen Verwahrung oder zur Erhöhung der Sicherheit.",
"ms_help_title1": "Dazu sind mehrere Geräte empfohlen.",
"ms_help_1": "Der Tresor funktioniert mit weiteren BlueWallet Apps und PSBT kompatiblen wallets wie Electrum, Specter, Coldcard, Cobovault, etc.",
"ms_help_1": "Der Tresor funktioniert mit weiteren BlueWallet Apps und PSBT kompatiblen Wallets wie Electrum, Specter, Coldcard, Keystone, etc.",
"ms_help_title2": "Schlüssel bearbeiten",
"ms_help_2": "Alle Tresor Schlüssel lassen sich auf diesem Geräts erstellen und später löschen. Dazu in den Wallet-Einstellungen die Mitsignierer bearbeiten. Sind alle Tresorschlüssel auf dem gleichen Gerät, ist die Sicherheit, die eines regulären Bitcoin Wallet.",
"ms_help_title3": "Tresor-Sicherungen",
"ms_help_3": "Die Tresor Backup und Watch-only Export Funktion ist in den Wallet-Optionen. Geht ein Seed verloren, ist das Backup zur Wiederherstellung des Wallet essenziell. Es ist wie eine Karte zu Deinem Vermögen.",
"ms_help_title4": "Tresor importieren",
"ms_help_4": "Um ein Tresor zu importieren, lade deine Multisignatur Backupdatei mittels Import-Funktion. Hast du nur die Seeds der erweiterten Schlüssel, fügst Du diese während der Tresor-Erstellung hinzu.",
"ms_help_4": "Um ein Tresor zu importieren, die Multisignatur Backupdatei mittels Import-Funktion laden. Seeds der erweiterten Schlüssel während der Tresor-Erstellung hinzufügen.",
"ms_help_title5": "Erweiterte Optionen",
"ms_help_5": "Der geplante Tresor erfordert 2 von 3 Signaturen. Zum Ändern der Anzahl oder des Adresstyps unter Einstellungen > Allgemein den erweiterten Modus aktivieren."
},
@ -576,8 +607,13 @@
"use_coin": "Münzen benutzen",
"use_coins": "Benutze Münzen",
"tip": "Wallet-Verwaltung zum Anzeigen, Beschriften, Einfrieren oder Auswählen von Münzen. Zur Mehrfachselektion auf die Farbkreise tippen.",
"sort_asc": "Aufsteigend",
"sort_desc": "Absteigend",
"sort_height": "Höhe",
"sort_value": "Wert",
"sort_label": "Bezeichnung",
"sort_status": "Status"
"sort_status": "Status",
"sort_by": "Sortiert nach"
},
"units": {
"BTC": "BTC",
@ -622,7 +658,9 @@
"bip47": {
"payment_code": "Zahlungscode",
"contacts": "Kontakte",
"purpose": "Wiederverwendbarer und teilbarer Code (BIP47)",
"bip47_explain": "Teil- und wiederverwendbarer Code",
"bip47_explain_subtitle": "BIP47",
"purpose": "Teil- und wiederverwendbarer Code (BIP47)",
"pay_this_contact": "An diesen Kontakt zahlen",
"rename_contact": "Kontakt umbenennen",
"copy_payment_code": "Zahlungscode kopieren",
@ -635,7 +673,7 @@
"notification_tx_unconfirmed": "Benachrichtigungstransaktion noch unbestätigt. Bitte warten",
"failed_create_notif_tx": "On-Chain Transaktion konnte nicht in erstellt werden",
"onchain_tx_needed": "On-Chain Transaktion benötigt.",
"notif_tx_sent": "Benachrichtigungstransaktion ist gesendet. Auf Bestätigung warten.",
"notif_tx_sent" : "Benachrichtigungstransaktion ist gesendet. Auf Bestätigung warten.",
"notif_tx": "Benachrichtigungstransaktion",
"not_found": "Zahlungscode nicht gefunden"
}

View file

@ -28,6 +28,8 @@
"enter_amount": "Enter amount",
"qr_custom_input_button": "Tap 10 times to enter custom input",
"unlock": "Unlock",
"port": "Port",
"ssl_port": "SSL Port",
"suggested": "Suggested"
},
"azteco": {
@ -74,6 +76,7 @@
"please_pay": "Please pay",
"preimage": "Pre-image",
"sats": "sats.",
"date_time": "Date and Time",
"wasnt_paid_and_expired": "This invoice was not paid and has expired."
},
"plausibledeniability": {
@ -190,7 +193,7 @@
"outdated_rate": "Rate was last updated: {date}",
"psbt_tx_open": "Open Signed Transaction",
"psbt_tx_scan": "Scan Signed Transaction",
"qr_error_no_qrcode": "We were unable to find a QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text or buttons.",
"qr_error_no_qrcode": "We were unable to find a valid QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text or buttons.",
"reset_amount": "Reset Amount",
"reset_amount_confirm": "Would you like to reset the amount?",
"success_done": "Done",
@ -215,7 +218,6 @@
"block_explorer_invalid_custom_url": "The URL provided is invalid. Please enter a valid URL starting with http:// or https://.",
"about_selftest_electrum_disabled": "Self-testing is not available with Electrum Offline Mode. Please disable offline mode and try again.",
"about_selftest_ok": "All internal tests have passed successfully. The wallet works well.",
"about_sm_github": "GitHub",
"about_sm_discord": "Discord Server",
"about_sm_telegram": "Telegram channel",
@ -250,15 +252,11 @@
"electrum_settings_server": "Electrum Server",
"electrum_status": "Status",
"electrum_preferred_server": "Preferred Server",
"electrum_preferred_server_description": "Enter the server you want your wallet to use for all Bitcoin activities. Once set, your wallet will exclusively use this server to check balances, send transactions, and fetch network data. Ensure you trust this server before setting it.", "electrum_clear_alert_title": "Clear history?",
"electrum_clear_alert_message": "Do you want to clear electrum servers history?",
"electrum_clear_alert_cancel": "Cancel",
"electrum_clear_alert_ok": "Ok",
"electrum_reset": "Reset to default",
"electrum_preferred_server_description": "Enter the server you want your wallet to use for all Bitcoin activities. Once set, your wallet will exclusively use this server to check balances, send transactions, and fetch network data. Ensure you trust this server before setting it.",
"electrum_unable_to_connect": "Unable to connect to {server}.",
"electrum_history": "History",
"electrum_reset_to_default": "Are you sure to want to reset your Electrum settings to default?",
"electrum_clear": "Clear History",
"electrum_reset_to_default": "This will let BlueWallet randomly choose a server from the suggested list and history. Your server history will remain unchanged.",
"electrum_reset": "Reset to default",
"encrypt_decrypt": "Decrypt Storage",
"encrypt_decrypt_q": "Are you sure you want to decrypt your storage? This will allow your wallets to be accessed without a password.",
"encrypt_enc_and_pass": "Encrypted and Password Protected",
@ -272,6 +270,8 @@
"encrypt_title": "Security",
"encrypt_tstorage": "Storage",
"encrypt_use": "Use {type}",
"set_as_preferred": "Set as preferred",
"set_as_preferred_electrum": "Setting {host}:{port} as preferred server will disable connecting to a suggested server at random.",
"encrypted_feature_disabled": "This feature cannot be used with encrypted storage enabled.",
"encrypt_use_expl": "{type} will be used to confirm your identity before making a transaction, unlocking, exporting, or deleting a wallet. {type} will not be used to unlock encrypted storage.",
"biometrics_fail": "If {type} is not enabled, or fails to unlock, you can use your device passcode as an alternative.",
@ -291,6 +291,7 @@
"network": "Network",
"network_broadcast": "Broadcast Transaction",
"network_electrum": "Electrum Server",
"electrum_suggested_description": "When a preferred server is not set, a suggested server will be selected for use at random.",
"not_a_valid_uri": "Invalid URI",
"notifications": "Notifications",
"open_link_in_explorer": "Open link in explorer",
@ -654,6 +655,8 @@
"bip47": {
"payment_code": "Payment Code",
"contacts": "Contacts",
"bip47_explain": "Reusable and shareable code",
"bip47_explain_subtitle": "BIP47",
"purpose": "Reusable and shareable code (BIP47)",
"pay_this_contact": "Pay this contact",
"rename_contact": "Rename contact",

View file

@ -374,7 +374,6 @@
"select_wallet": "Selecciona una cartera",
"xpub_copiedToClipboard": "Copiado a portapapeles.",
"pull_to_refresh": "Desliza el dedo de arriba a abajo para actualizar",
"warning_do_not_disclose": "¡Advertencia! No comparta esta información",
"add_ln_wallet_first": "Primero tienes que agregar una cartera Lightning.",
"identity_pubkey": "Identity Pubkey",
"xpub_title": "XPUB de la cartera"

View file

@ -28,6 +28,7 @@
"enter_amount": "Ingresa la cantidad",
"qr_custom_input_button": "Pulsa 10 veces para ingresar una entrada personalizada",
"unlock": "Desbloquear",
"port": "Puerto",
"suggested": "Sugerido"
},
"azteco": {
@ -74,6 +75,7 @@
"please_pay": "Pagar por favor",
"preimage": "Imagen previa",
"sats": "sats.",
"date_time": "Fecha y hora",
"wasnt_paid_and_expired": "Esta factura no se pagó y ha caducado."
},
"plausibledeniability": {
@ -190,7 +192,7 @@
"outdated_rate": "La tarifa se actualizó por última vez: {date}",
"psbt_tx_open": "Abrir transacción firmada",
"psbt_tx_scan": "Escanear transacción firmada",
"qr_error_no_qrcode": "No pudimos encontrar un código QR en la imagen seleccionada. Asegúrate de que la imagen contenga solo un código QR y ningún contenido adicional como texto o botones.",
"qr_error_no_qrcode": "No pudimos encontrar un código QR válido en la imagen seleccionada. Asegúrate de que la imagen contenga solo un código QR y ningún contenido adicional, como texto o botones.",
"reset_amount": "Restablecer monto",
"reset_amount_confirm": "¿Te gustaría restablecer la cantidad?",
"success_done": "Hecho",
@ -253,11 +255,11 @@
"electrum_preferred_server_description": "Introduce el servidor que deseas que tu billetera utilice para todas las actividades de Bitcoin. Una vez configurado, tu billetera utilizará exclusivamente este servidor para comprobar saldos, enviar transacciones y obtener datos de la red. Asegúrate de que confías en este servidor antes de configurarlo.", "electrum_clear_alert_title": "¿Borrar historial?",
"electrum_clear_alert_message": "¿Quieres borrar el historial de los servidores de Electrum?",
"electrum_clear_alert_cancel": "Cancelar",
"electrum_clear_alert_ok": "Ok",
"electrum_reset": "Restablecer a predeterminado",
"only_use_preferred": "Conectarse únicamente al servidor preferido",
"electrum_unable_to_connect": "No se puede conectar al {server}.",
"electrum_history": "Historial",
"electrum_reset_to_default": "¿Estás seguro de querer restablecer la configuración de Electrum a los valores predeterminados?",
"electrum_reset_to_default": "Esto permitirá que BlueWallet elija aleatoriamente un servidor de la lista sugerida y del historial. El historial de tu servidor permanecerá sin cambios",
"electrum_reset": "Restablecer a predeterminado",
"electrum_clear": "Borrar historial",
"encrypt_decrypt": "Descifrar Almacenamiento",
"encrypt_decrypt_q": "¿Estás seguro de que deseas descifrar tu almacenamiento? Esto permitirá acceder a tus billeteras sin una contraseña.",
@ -272,6 +274,7 @@
"encrypt_title": "Seguridad",
"encrypt_tstorage": "Almacenamiento",
"encrypt_use": "Usar {type}",
"set_as_preferred": "Establecer como preferido",
"encrypted_feature_disabled": "Esta función no se puede utilizar con el almacenamiento cifrado habilitado.",
"encrypt_use_expl": "{type} se utilizará para confirmar tu identidad antes de realizar una transacción, desbloquear, exportar o eliminar una billetera. {type} no se utilizará para desbloquear el almacenamiento encriptado.",
"biometrics_fail": "Si {type} no está activado o no se desbloquea, puedes utilizar el código de acceso de tu dispositivo como alternativa.",
@ -291,6 +294,7 @@
"network": "Red",
"network_broadcast": "Publicar transacción",
"network_electrum": "Servidor Electrum",
"electrum_suggested_description": "Cuando no se establece un servidor preferido, se seleccionará un servidor sugerido para su uso al azar.",
"not_a_valid_uri": "URI inválido",
"notifications": "Notificaciones",
"open_link_in_explorer": "Abrir enlace en el explorador",
@ -323,7 +327,7 @@
"permission_denied_message": "Has denegado el envío de notificaciones. Si deseas recibirlas, actívalas en la configuración de tu dispositivo."
},
"transactions": {
"cancel_explain": "Reemplazaremos esta transacción con una que te pague y tenga tarifas más altas. Esto cancela efectivamente la transacción actual. Esto se llama RBF—Replace by Fee.",
"cancel_explain": "Reemplazaremos esta transacción con una que te pague y tenga tarifas más altas. Esto cancela efectivamente la transacción actual. Esto se llama RBF (Replace by Fee).",
"cancel_no": "Esta transacción no es reemplazable.",
"cancel_title": "Cancelar ésta transacción (RBF)",
"transaction_loading_error": "Se ha producido un problema al cargar la transacción. Vuelve a intentarlo más tarde.",
@ -332,7 +336,7 @@
"copy_link": "Copiar enlace",
"expand_note": "Expandir Nota",
"cpfp_create": "Crear",
"cpfp_exp": "Crearemos otra transacción que gaste tu transacción no confirmada. La tarifa total será más alta que la tarifa de la transacción original, por lo que debería extraerse más rápido. Esto se llama CPFP — Child Pays for Parent.",
"cpfp_exp": "Crearemos otra transacción que gaste tu transacción no confirmada. La tarifa total será más alta que la tarifa de la transacción original, por lo que debería extraerse más rápido. Esto se llama CPFP (Child Pays for Parent).",
"cpfp_no_bump": "Esta transacción no se puede acelerar.",
"cpfp_title": "Aumentar Comisión (CPFP)",
"details_balance_hide": "Ocultar Balance",
@ -367,7 +371,7 @@
"list_title": "Transacciones",
"transaction": "Transacción",
"open_url_error": "No se puede abrir el enlace con el navegador predeterminado. Cambia tu navegador predeterminado y vuelve a intentarlo.",
"rbf_explain": "Reemplazaremos esta transacción con una con una tarifa más alta para que se extraiga más rápido. Esto se llama RBF—Replace by Fee.",
"rbf_explain": "Reemplazaremos esta transacción con una con una tarifa más alta para que se extraiga más rápido. Esto se llama RBF (Replace by Fee)",
"rbf_title": "Aumentar Comisión (RBF)",
"status_bump": "Aumentar Comisión",
"status_cancel": "Cancelar Transacción",
@ -567,7 +571,7 @@
"ms_help_title": "Cómo funcionan las Bóvedas Multifirma: Consejos y Trucos",
"ms_help_text": "Una billetera con varias llaves para mayor seguridad o custodia compartida",
"ms_help_title1": "Se recomiendan varios dispositivos.",
"ms_help_1": "La Bóveda funcionará con otras apps de BlueWallet instalada en otros dispositivos y billeteras compatibles con PSBT, como Electrum, Spectre, Coldcard, Cobo Vault, etc.",
"ms_help_1": "La Bóveda funcionará con BlueWallet instalada en otros dispositivos y billeteras compatibles con PSBT, como Electrum, Spectre, Coldcard, Keystone, etc.",
"ms_help_title2": "Editar Claves",
"ms_help_2": "Puedes crear todas las claves de la Bóveda en este dispositivo y eliminarlas o editarlas después. Tener todas las claves en el mismo dispositivo tiene la seguridad equivalente a la de un monedero de Bitcoin normal.",
"ms_help_title3": "Copias de seguridad de la Bóveda",
@ -654,6 +658,8 @@
"bip47": {
"payment_code": "Código de pago",
"contacts": "Contactos",
"bip47_explain": "Código reutilizable y compartible",
"bip47_explain_subtitle": "BIP47",
"purpose": "Código reutilizable y compartible (BIP47)",
"pay_this_contact": "Paga a este contacto",
"rename_contact": "Renombrar contacto",

View file

@ -390,7 +390,6 @@
"select_wallet": "انتخاب کیف پول",
"xpub_copiedToClipboard": "در کلیپ‌بورد کپی شد.",
"pull_to_refresh": "برای به‌روزسانی به پایین بکشید",
"warning_do_not_disclose": "هشدار! فاش نکنید.",
"add_ln_wallet_first": "ابتدا باید یک کیف پول لایتنینگ اضافه کنید.",
"identity_pubkey": "هویت/کلید عمومی",
"xpub_title": "کلید XPUB کیف پول"

View file

@ -412,7 +412,6 @@
"select_wallet": "Valitse Lompakko",
"xpub_copiedToClipboard": "Kopioitu leikepöydälle.",
"pull_to_refresh": "vedä päivittääksesi",
"warning_do_not_disclose": "Varoitus! Älä paljasta",
"add_ln_wallet_first": "Sinun on ensin lisättävä Salamalompakko.",
"identity_pubkey": "Tunnus Pubkey",
"xpub_title": "lompakon XPUB"

View file

@ -5,10 +5,13 @@
"continue": "Continuer",
"clipboard": "Presse-papier",
"discard_changes": "Supprimer les changements ?",
"discard_changes_explain": "Certaines modifications n'ont pas été enregistrées. Êtes-vous sûr de vouloir les supprimer et quitter cet écran ?",
"enter_password": "Saisir le mot de passe",
"never": "Jamais",
"of": "{number} sur {total}",
"ok": "OK",
"customize": "Personnaliser",
"enter_url": "Entrer une URL",
"storage_is_encrypted": "L'espace de stockage est chiffré. le mot de passe est requis pour le déchiffrer.",
"yes": "Oui",
"no": "Non",
@ -17,7 +20,14 @@
"success": "Succès",
"wallet_key": "Clé du portefeuille",
"invalid_animated_qr_code_fragment": "Fragment du QR Code animé invalide. Veuillez réessayer.",
"enter_amount": "Entrer un montant"
"close": "Fermer",
"change_input_currency": "Changer la devise d'entrée",
"refresh": "Actualiser",
"pick_image": "Copier depuis la librairie",
"pick_file": "Choisir un fichier",
"enter_amount": "Entrer un montant",
"unlock": "Deverouiller",
"suggested": "Suggéré"
},
"azteco": {
"codeIs": "Votre code promo est",
@ -43,6 +53,7 @@
"expired": "Expiré",
"expiresIn": "Expire dans {time} minutes",
"payButton": "Payer",
"payment": "Paiement",
"placeholder": "Facture ou adresse",
"potentialFee": "Frais potentiels : {fee}",
"refill": "Déposer des fonds",
@ -85,6 +96,7 @@
"details_create": "Créer",
"details_label": "Description",
"details_setAmount": "Recevoir avec un montant spécifique",
"details_share": "partager",
"header": "Recevoir",
"maxSats": "Le montant maximal est de {max} sats",
"maxSatsFull": "Le montant maximal est de {max} sats ou {currency}",
@ -233,6 +245,7 @@
"language": "Langue",
"last_updated": "Dernière mise à jour",
"language_isRTL": "Il est nécessaire de redémarrer BlueWallet pour que le changement de langue prenne effet.",
"license": "License",
"lightning_saved": "Les changements ont bien été enregistrés",
"lightning_settings": "Paramètres Lightning",
"network": "Réseau",
@ -285,8 +298,11 @@
"details_from": "De",
"details_inputs": "Inputs",
"details_outputs": "Outputs",
"date": "Date",
"details_received": "Reçu",
"details_title": "Transaction",
"offchain": "Offchain",
"onchain": "Onchain",
"details_to": "À",
"enable_offline_signing": "Ce portefeuille n'est pas utilisé en conjonction avec une signature hors ligne. Voulez-vous l'activer maintenant ? ",
"list_conf": "Conf: {number}",
@ -393,12 +409,13 @@
"select_wallet": "Choix du portefeuille",
"xpub_copiedToClipboard": "Copié dans le presse-papiers.",
"pull_to_refresh": "Tirer pour rafraichir",
"warning_do_not_disclose": "Attention! Ne pas divulguer",
"add_ln_wallet_first": "Vous devez d'abord ajouter un portefeuille Lightning.",
"identity_pubkey": "Clé publique identité",
"xpub_title": "XPUB portefeuille"
"xpub_title": "XPUB portefeuille",
"more_info": "Plus d'information"
},
"total_balance_view": {
"hide": "Cacher",
"title": "Solde total"
},
"multisig": {
@ -412,6 +429,7 @@
"fee_btc": "{number} BTC",
"confirm": "Confirmer",
"header": "Envoyer",
"share": "partager",
"view": "Vue",
"manage_keys": "Gérer les clés",
"how_many_signatures_can_bluewallet_make": "Combien de signatures BlueWallet peut-il faire",
@ -491,6 +509,10 @@
"use_coin": "Utiliser l'UTXO",
"use_coins": "Utiliser les UTXO",
"tip": "Permet de voir, étiqueter, bloquer ou sélectionner des UTXOs pour une meilleure gestion du portefeuille. Vous pouvez sélectionner plusieurs UTXOs en appuyant sur les cercles colorés.",
"sort_asc": "Ascendant",
"sort_desc": "Descendant",
"sort_height": "Hauteur",
"sort_value": "Valeur",
"sort_label": "Libelé",
"sort_status": "Statut"
},
@ -531,5 +553,10 @@
"auth_answer": "Vous vous êtes authentifié avec succès à {hostname}!",
"could_not_auth": "Nous n'avons pas pu vous authentifier à {hostname}.",
"authenticate": "Authentifier"
},
"bip47": {
"payment_code": "Code de paiement",
"contacts": "Contacts",
"rename": "Renommer"
}
}

View file

@ -469,7 +469,6 @@
"select_wallet": "בחירת ארנק",
"xpub_copiedToClipboard": "הועתק ללוח.",
"pull_to_refresh": "משכו כדי לרענן",
"warning_do_not_disclose": "אזהרה! אין לחשוף.",
"add_ln_wallet_first": "עלייך להוסיף ארנק ברק קודם.",
"identity_pubkey": "מפתח זהות ציבורי",
"xpub_title": "מפתח צפייה של הארנק",

View file

@ -387,7 +387,6 @@
"select_wallet": "Válassz tárcát",
"xpub_copiedToClipboard": "Vágólapra másolva",
"pull_to_refresh": "Húzza le a frissítéshez",
"warning_do_not_disclose": "VESZÉLY! Ezt soha senkivel ne ossza meg!",
"add_ln_wallet_first": "Először egy Lightning Tárcát kell létrehoznod vagy beadnod.",
"identity_pubkey": "Nyilvános kulcs",
"xpub_title": "a tárca XPUB kulcsa"

View file

@ -380,7 +380,6 @@
"select_wallet": "Seleziona Portafoglio",
"xpub_copiedToClipboard": "Copiata negli appunti.",
"pull_to_refresh": "Tira verso il basso per aggiornare",
"warning_do_not_disclose": "Attenzione! Non rivelare.",
"add_ln_wallet_first": "Devi prima aggiungere un portafoglio Lightning.",
"identity_pubkey": "Identity Pubkey",
"xpub_title": "XPUB del Portafoglio"

View file

@ -481,7 +481,14 @@
"select_wallet": "ウォレット選択",
"xpub_copiedToClipboard": "クリップボードにコピーしました。",
"pull_to_refresh": "引っ張って更新する",
"warning_do_not_disclose": "警告! 公開しないこと。",
"warning_do_not_disclose": "以下の情報を絶対に他人に共有しないでください",
"scan_import": "このQRコードをスキャンして他のアプリにウォレットをインポートします。",
"write_down_header": "手動バックアップ作成",
"write_down": "これらの言葉を書きとめ、安全に保管してください。あとでウォレットを復元するために使います。",
"wallet_type_this": "ウォレットの種類は{type}。",
"share_number": "{number}を共有",
"copy_ln_url": "あとでウォレットを復元するために、このURLをコピーして安全に保管してください。",
"copy_ln_public": "あとでウォレットを復元するために、この情報を安全に保管してください。",
"add_ln_wallet_first": "先にライトニングウォレットを追加する必要があります。",
"identity_pubkey": "識別用公開鍵",
"xpub_title": "ウォレット XPUB",

View file

@ -386,7 +386,6 @@
"select_wallet": "지갑 선택",
"xpub_copiedToClipboard": "클립보드에 복사완료",
"pull_to_refresh": "갱신하려면 당기세요",
"warning_do_not_disclose": "경고! 공개하지 마십시오.",
"add_ln_wallet_first": "먼저 라이트닝 월렛을 추가해야합니다.",
"identity_pubkey": "아이덴티티 퍼브키",
"xpub_title": "지갑 XPUB"

View file

@ -339,7 +339,6 @@
"select_wallet": "Pilih Dompet",
"xpub_copiedToClipboard": "Disalin ke papan sepit.",
"pull_to_refresh": "Tarik untuk Segar Semula",
"warning_do_not_disclose": "Amaran! Jangan dedahkan.",
"add_ln_wallet_first": "Anda perlu menambah dompet Lightning terlebih dahulu.",
"identity_pubkey": "Kunci Umum Keperibadian",
"xpub_title": "Dompet XPUB"

View file

@ -368,7 +368,6 @@
"select_wallet": "Velg Lommebok",
"xpub_copiedToClipboard": "Kopiert til utklippstavlen.",
"pull_to_refresh": "Dra for å oppdatere",
"warning_do_not_disclose": "Advarsel! Ikke avslør.",
"add_ln_wallet_first": "Du må først legge til en Lightning-lommebok.",
"identity_pubkey": "Identity Pubkey",
"xpub_title": "Wallet XPUB"

View file

@ -369,7 +369,6 @@
"select_wallet": "Selecteer wallet",
"xpub_copiedToClipboard": "Gekopieerd naar het klembord.",
"pull_to_refresh": "Pull om te refreshen.",
"warning_do_not_disclose": "Waarschuwing! Niet bekendmaken",
"add_ln_wallet_first": "U moet eerst een Lightning-wallet toevoegen.",
"identity_pubkey": "Identiteit PubKey",
"xpub_title": "Wallet XPUB"

View file

@ -49,7 +49,6 @@
"default_info": "Normal info",
"default_title": "As you launch",
"default_wallets": "See all your wallets",
"electrum_clear_alert_title": "You wan clear history?",
"electrum_clear_alert_message": "You wan clear electrum servers history?",
"electrum_clear_alert_cancel": "Cancel",
"electrum_clear_alert_ok": "Ok",

View file

@ -10,6 +10,8 @@
"never": "Nunca",
"of": "{number} de {total}",
"ok": "OK",
"customize": "Personalizar",
"enter_url": "Insira URL",
"storage_is_encrypted": "Os arquivos estão criptografados, uma senha é necessária para descriptografá-los.",
"yes": "Sim",
"no": "Não",
@ -21,9 +23,12 @@
"close": "Fechar",
"change_input_currency": "Alterar moeda de entrada",
"refresh": "Atualizar",
"pick_image": "Escolher da biblioteca",
"pick_file": "Escolher arquivo",
"enter_amount": "Insira o valor",
"qr_custom_input_button": "Toque 10 vezes para inserir uma entrada personalizada",
"unlock": "Desbloquear"
"unlock": "Desbloquear",
"suggested": "Sugerido"
},
"azteco": {
"codeIs": "Seu código voucher é",
@ -126,6 +131,9 @@
"details_insert_contact": "Inserir Contato",
"details_add_rec_add": "Adicionar Destinatário",
"details_add_rec_rem": "Remover Destinatário",
"details_add_recc_rem_all_alert_description": "Você tem certeza que deseja remover estes destinatários?",
"details_add_rec_rem_all": "Remover todos os destinatários",
"details_recipients_title": "Destinatários",
"details_address": "Endereço",
"details_address_field_is_not_valid": "O endereço não é válido.",
"details_adv_fee_bump": "Permitir aumento de taxa",
@ -204,6 +212,7 @@
"performance_score": "Pontuação de performance: {num}",
"run_performance_test": "Teste de performance",
"about_selftest": "Executar autoteste",
"block_explorer_invalid_custom_url": "A URL é inválida. Insira uma URL válida começando com http:// ou https://.",
"about_selftest_electrum_disabled": "Autoteste indisponível no modo Offline da Electrum. Desative o modo Offline e tente novamente.",
"about_selftest_ok": "Todos os testes internos passaram com sucesso. A carteira funciona bem.",
"about_sm_github": "GitHub",
@ -219,6 +228,7 @@
"biom_no_passcode": "Seu dispositivo não possui senha ou biometria ativada. Para prosseguir, configure uma senha ou biometria nas configurações do aplicativo.",
"biom_remove_decrypt": "Todas suas carteiras serão removidas e seu armazenamento será descritptografado. Você tem certeza que deseja proceder?",
"currency": "Moeda",
"currency_source": "A taxa é obtida de",
"currency_fetch_error": "Ocorreu um erro ao tentar obter o índice da moeda selecionada.",
"default_desc": "Quando desativado, a BlueWallet abrirá imediatamente a carteira selecionada ao ser iniciada.",
"default_info": "Informação Padrão",
@ -454,7 +464,6 @@
"select_wallet": "Escolher carteira",
"xpub_copiedToClipboard": "Copiado para a área de transferência",
"pull_to_refresh": "Puxe para atualizar",
"warning_do_not_disclose": "Cuidado! Não divulgue.",
"add_ln_wallet_first": "Primeiro você deve adicionar uma carteira Lightning.",
"identity_pubkey": "Identificar chave pública",
"xpub_title": "XPUB da Carteira",

View file

@ -347,7 +347,6 @@
"select_wallet": "Alege portofel",
"xpub_copiedToClipboard": "Copiat în clipboard.",
"pull_to_refresh": "Trage pentru a reîncărca",
"warning_do_not_disclose": "Atenție! Nu dezvălui.",
"add_ln_wallet_first": " Trebuie mai întîi să adaugi un portofel Lightning.",
"identity_pubkey": "Pubkey identitate",
"xpub_title": "XPUB-ul portofelului"

View file

@ -465,7 +465,6 @@
"select_wallet": "Выбрать кошелёк",
"xpub_copiedToClipboard": "Скопировано в буфер обмена.",
"pull_to_refresh": "Потяните, чтобы обновить",
"warning_do_not_disclose": "Внимание! Не разглашать.",
"add_ln_wallet_first": "Сначала добавьте Лайтнинг-кошелёк.",
"identity_pubkey": "Identity Pubkey",
"xpub_title": "XPUB кошелька",

View file

@ -364,7 +364,6 @@
"select_wallet": "පසුම්බිය තෝරන්න",
"xpub_copiedToClipboard": "පසුරු පුවරුවට පිටපත් කර ඇත.",
"pull_to_refresh": "නැවුම් කිරීමට අදින්න",
"warning_do_not_disclose": "අනතුරු ඇඟවීම! හෙළි නොකරන්න.",
"add_ln_wallet_first": "ඔබ මුලින්ම ලයිට්නින් පසුම්බියක් එකතු කළ යුතුයි.",
"identity_pubkey": "අනන්‍යතා Pubkey",
"xpub_title": "XPUB පසුම්බිය"

View file

@ -373,7 +373,6 @@
"select_wallet": "Izberite Denarnico",
"xpub_copiedToClipboard": "Kopirano v odložišče.",
"pull_to_refresh": "Povlecite za osvežitev",
"warning_do_not_disclose": "Opozorilo! Ne razkrivajte",
"add_ln_wallet_first": "Najprej morate dodati Lightning denarnico.",
"identity_pubkey": "Identity Pubkey",
"xpub_title": "XPUB denarnice"

350
loc/sq_AL.json Normal file
View file

@ -0,0 +1,350 @@
{
"_": {
"bad_password": "Fjalkalimi është i gabuar. Provojeni përsëri.",
"cancel": "Anullo",
"continue": "Vazhdo",
"clipboard": " Memoria e përkohshme",
"discard_changes": "Anullo ndryshimet?",
"discard_changes_explain": "Ju keni ndryshime të pashpëtuara. Jeni i sigurtë që nuk doni ti ruani dhe te dilni?",
"enter_password": "Fusni fjalëkalimin",
"never": "Kurrë",
"of": "{numri} nga {totali}",
"ok": "OK",
"customize": "Personalizo",
"enter_url": "Fusni URL",
"storage_is_encrypted": "Memoria është e kriptuar. Duhet të fusni fjalkalimin për të pasur vizibilitet.",
"yes": "Po",
"no": "Jo",
"save": "Ruaj",
"seed": "Fara",
"success": "Sukses",
"wallet_key": "Fjalkalimi i portofolit",
"invalid_animated_qr_code_fragment": "Kodi QR nuk është i vlefshëm. Provo sërisht.",
"close": "Mbylle",
"change_input_currency": "Ndrysho valuten e hyrjes",
"refresh": "Rifresko",
"pick_image": "Zgjidh nga biblioteka",
"pick_file": "Zgjidh file-in",
"enter_amount": "Fut sasinë",
"qr_custom_input_button": "Kliko 10 herë për futur info të pesonalizuar",
"unlock": "Hap",
"suggested": "I sugjeruar"
},
"azteco": {
"codeIs": "Kodi promocional është",
"errorBeforeRefeem": "Para përdorimit, duhet të shtoni një portofol Bitcoin-i.",
"errorSomething": "Diçka shkoi gabim. Eshtë ky kod promocional akoma i vlefshëm?",
"redeem": "Përdore në portofol",
"redeemButton": "Përdor",
"success": "Sukses",
"title": "Përdor kodin promocional të Azte.co"
},
"entropy": {
"save": "Ruaj",
"title": "Entropia",
"undo": "Anullo",
"amountOfEntropy": "{bits} nga {limit} bits"
},
"errors": {
"broadcast": "Shpërndarja nuk funksjonoi.",
"error": "Gabim",
"network": "Problem me Internetin"
},
"lnd": {
"errorInvoiceExpired": "Fatura ka skaduar.",
"expired": "Skaduar",
"expiresIn": "Skadon në {time} minuta",
"payButton": "Paguaj",
"payment": "Pagesa",
"placeholder": "Fatura ose adresa",
"potentialFee": "Komisioni potencial: {fee}",
"refill": "Rimbush",
"refill_create": "Krijoni në portofol Bitcoin-i për të vazhduar.",
"refill_external": "Rimbush nga një portofol tjetër",
"refill_lnd_balance": "Rimbush ballancën e portofolit Lightning.",
"sameWalletAsInvoiceError": "Nuk mund të paguash një faturë me të njëjtin portofol që e ka krijuar atë",
"title": "Menaxho fondet"
},
"lndViewInvoice": {
"additional_info": "Informacione të tjera",
"for": "Për:",
"lightning_invoice": "Fatura Lightning",
"open_direct_channel": "Hap një kanal direkt me këtë nyje:",
"please_pay_between_and": "Ju lutem paguani ndërmjet {min} dhe {max}",
"please_pay": "Ju lutem paguani",
"preimage": "Pre-imazh",
"sats": "sats.",
"wasnt_paid_and_expired": "Fatura nuk është paguar dhe ka skaduar."
},
"plausibledeniability": {
"create_fake_storage": "Krijo një memorie të kriptuar",
"create_password_explanation": "Fjalekalimi per depozizen fallco nuk duhet te jete i njejti me ate te depozites se vertete.",
"help": "Në rrethana të caktuara, mund të detyroheni të zbuloni një fjalëkalim. Për të mbajtur monedhat tuaja të sigurta, BlueWallet mund të krijojë një hapësirë tjetër të koduar me një fjalëkalim të ndryshëm. Nën presion, mund ta zbuloni këtë fjalëkalim një pale të tretë. Nëse futet në BlueWallet, ai do të hapë një hapësirë të re “të rreme”. Kjo do të duket e ligjshme për palën e tretë, por në mënyrë të fshehtë do të mbajë të sigurt hapësirën tuaj kryesore me monedhat.",
"help2": "Hapësira e re do të jetë plotësisht funksionale dhe mund të ruani aty shuma minimale në mënyrë që të duket më e besueshme.",
"password_should_not_match": "Fjalekalimi eshte ne perdorim. Ju lutem, provin nje fjalekalim tjeter.",
"title": "Mohim i besueshëm"
},
"pleasebackup": {
"ask": "A i keni shpetuar fjalet per rigjenerimin e portofolit? Keto fjale do ju duhen per te pasur akses ne fondet tuaja ne rastin se humbni kete dispozitiv. Pa keto fjale, fondet tuaja do jene te humbura.",
"ask_no": "Jo, nuk kam.",
"ask_yes": "Po, e kam.",
"ok": "Ok, i shkruajta.",
"ok_lnd": "Ok, i shpëtova.",
"text": "Ju lutem merrni nje moment per te shkruajtur keto fjale ne nje cope letre.\nEshte mundesia juaj e vetme per te rigjeneruar portofolin.",
"text_lnd": "Ju letem shpetoni nje kopje te portofolit. Do ju nevojitet te rigjneroni portofolin ne rast humbje.",
"title": "Portofoli juaj u kriua"
},
"receive": {
"details_create": "Krijo",
"details_label": "Përshkrim",
"details_setAmount": "Merr me sasi",
"details_share": "Shpërndaj",
"header": "Merr",
"reset": "Rezeto",
"maxSats": "Sasia maksimale është {max} sats",
"maxSatsFull": "Sasia maksimale është {max} sats ose {currency}",
"minSats": "Sasia minimale është {min} sats",
"minSatsFull": "Sasia maksimale është {min} sats ose {currency}",
"qrcode_for_the_address": "QR kodi për adresën"
},
"send": {
"broadcastButton": "Transmetim",
"broadcastError": "Gabim",
"broadcastNone": "Fut Hex-in e transfertës",
"broadcastPending": "Në pritje",
"broadcastSuccess": "Sukses",
"confirm_header": "Konfirmo",
"confirm_sendNow": "Dërgo tani",
"create_amount": "Sasia",
"create_broadcast": "Transmetim",
"create_copy": "Kopjoje dhe trasmetoje më vonë",
"create_details": "Detajet",
"create_fee": "Komision",
"create_memo": "Memo",
"create_satoshi_per_vbyte": "Satoshi per vByte",
"create_to": "Te",
"create_tx_size": "Madhësia e Transfertës",
"create_verify": "Verifiko ne coinb.in",
"details_insert_contact": "Fut Kontaktin",
"details_add_rec_add": "Shto një marrës",
"details_add_rec_rem": "Hiq një marrës",
"details_add_recc_rem_all_alert_description": "Jeni i sigurte qe doni te fshini te gjithe marresit?",
"details_add_rec_rem_all": "Hiq të gjithë marrësit",
"details_recipients_title": "Marrësi",
"details_address": "Adresa",
"details_address_field_is_not_valid": "Adresa nuk eshte e vlefshme",
"details_adv_fee_bump": "Lejo rritjen e komisionit",
"details_adv_full": "Perdor te gjithe Ballancen",
"details_adv_full_sure": "Jeni i sigurt që doni të përdorni gjithë bilancin e portofolit tuaj për këtë transferte?",
"details_adv_full_sure_frozen": "Jeni i sigurt që doni të përdorni gjithë bilancin e portofolit tuaj për këtë transaksion? Vini re se monedhat e ngrira nuk mund te perdoren. ",
"details_adv_import": "Importo nje Transferte",
"details_adv_import_qr": "Imprto nje Transferte (QR)",
"details_amount_field_is_not_valid": "Sasia e perdorur nuk eshte e vlefshme",
"details_amount_field_is_less_than_minimum_amount_sat": "Sasia e percaktuar eshte shume e vogel. Ju lutem fusni nje sasi me te madhe se 500 sats.",
"details_create": "Krijo një Faturë",
"details_error_decode": "E pamundur me de-koduar adresen e Bitcoin-it",
"details_fee_field_is_not_valid": "Komisioni nuk eshte i vlefshem",
"details_frozen": "{amount} BTC jane te ngrira.",
"details_next": "Pas",
"details_no_signed_tx": "File-at e zgjedhur nuk permbajne asnje transferte e cila mund te importohet.",
"details_note_placeholder": "Shenim per veten",
"counterparty_label_placeholder": "Ndrysho emrin e kontaktit",
"details_scan": "Skano",
"details_scan_hint": "kliko dy here per skanim ose importo destinacionin.",
"details_total_exceeds_balance": "Sasia qe deshironi te dergoni e tejkalon ballancen tuaj",
"details_total_exceeds_balance_frozen": "Shuma që po dërgoni tejkalon ballancen e disponueshëm. Ju lutemi vini re se monedhat e ngrira nuk mund te dergohen.",
"details_unrecognized_file_format": "Formati i file-it eshte i panjohur.",
"details_wallet_before_tx": "Para se te krijoni nje transferte, duhet te keni nje portofolo Bitcoin-i.",
"dynamic_init": "Duke filluar",
"dynamic_next": "Pas",
"dynamic_prev": "Para",
"dynamic_start": "Fillo",
"dynamic_stop": "Ndalo",
"fee_10m": "10m",
"fee_1d": "1d",
"fee_3h": "3h",
"fee_custom": "Peronal",
"insert_custom_fee": "Fut komisionin e deshiruar",
"fee_fast": "Shpejt",
"fee_medium": "Mesatar",
"fee_satvbyte": "ne sat/vByte",
"fee_slow": "Avashët",
"header": "Dërgo",
"input_clear": "Fshi",
"input_done": "U kry",
"input_paste": "Ngjit",
"input_total": "Total:",
"psbt_sign": "Firmos një transfertë",
"psbt_tx_export": "Exporto ne file",
"success_done": "U kry"
},
"settings": {
"about": "Rreth",
"about_license": "Licenca e tipit MIT",
"about_review": "Na leni një review",
"about_sm_github": "GitHub",
"about_sm_discord": "Serveri Discord",
"about_sm_telegram": "Knali Telegram",
"about_sm_twitter": "Na ndiq ne X",
"biometrics": "Te dhenat Biometrike",
"biom_conf_identity": "Ju lutem konfirmoni identitetin",
"currency": "Valuta",
"electrum_connected": "I lidhur",
"electrum_offline_mode": "Modaliteti Off-Line",
"use_ssl": "Perdor SSL",
"electrum_settings_server": "Serveri Elektrum",
"electrum_status": "Gjëndja",
"electrum_preferred_server": "Serveri i Preferuar",
"electrum_clear_alert_title": "Fshi Historine",
"electrum_clear_alert_message": "Deshironi te fshini historin e serverit Elektrum?",
"electrum_clear_alert_cancel": "Anullo",
"encrypt_tstorage": "Depozita",
"general": "Gjenerale",
"language": "Gjuha",
"last_updated": "Perditesimi i Fundit",
"license": "Licenca",
"lightning_saved": "Ndryshimet tuaja jane shpetuar me sukses.",
"lightning_settings": "Serveri Lightning",
"network": "Rrjeti",
"network_broadcast": "Publiko Transferten",
"network_electrum": "Serveri Elektrum",
"password": "Fjalekalim",
"plausible_deniability": "Mohim i besueshëm",
"privacy": "Privatesia",
"push_notifications_explanation": "Duke aktivizuar njoftimet, kodi i pajisjes suaj do të dërgohet në server, së bashku me adresat e portofolit dhe ID-të e transaksioneve për të gjitha portofolat dhe transfertat e kryera pasi të aktivizoni njoftimet. Kodi i pajisjes përdoret për të dërguar njoftime, ndërsa informacioni i portofolit na mundëson tju njoftojmë për Bitcoin-in që ju vjen ose për konfirmimet e transaksioneve.\n\nVetëm informacioni i krijuar pas aktivizimit të njoftimeve transmetohet—asnjanjë të dhënë nga më parë nuk mblidhet.\n\nÇaktivizimi i njoftimeve do të fshijë të gjithë këtë informacion nga serveri. Gjithashtu, fshirja e një portofoli nga aplikacioni do të heqë edhe informacionin e tij përkatës nga serveri.",
"save": "Ruaj",
"saved": "Shpetuar",
"total_balance": "Ballanca Totale"
},
"transactions": {
"cpfp_create": "Krijo",
"details_copy": "Kopjo",
"details_title": "Transferte",
"details_to": "Dalje",
"pending": "Në pritje",
"list_title": "Transfertat",
"transaction": "Transferte",
"updating": "Duke u perditesuar..."
},
"wallets": {
"add_bitcoin": "Bitcoin",
"add_create": "Krijo",
"total_balance": "Ballanca Totale",
"add_entropy": "Entropia",
"add_lightning": "Lightning",
"add_title": "Shto Portofol",
"add_wallet_name": "Emer",
"add_wallet_type": "Tipi",
"add_wallet_seed_length_12": "12 fjalet",
"add_wallet_seed_length_24": "24 fjalet",
"details_address": "Adresa",
"details_no_cancel": "Jo, anullo",
"details_show_addresses": "Trego adresen",
"details_title": "Portofol",
"wallets": "Portofola",
"details_type": "Tipi",
"import_discovery_title": "Zbulo",
"import_discovery_no_wallets": "Asnje portofol nuk u gjet.",
"import_derivation_found": "U gjet",
"import_derivation_found_not": "Nuk u gjet",
"import_derivation_loading": "Duke u ngarkuar",
"import_derivation_unknown": "I panjohur",
"list_create_a_button": "Shto tani",
"list_create_a_wallet": "Shto nje portofol",
"list_create_a_wallet_text": "Eshte fala dhe mund te krijoni \nsa te deshironi.",
"list_long_choose": "Zgjidh Foton",
"paste_from_clipboard": "Ngjit",
"list_long_scan": "Skano QR kodin",
"list_title": "Portofola",
"list_tryagain": "Provojeni perseri",
"manage_title": "Menaxho Portofolat",
"no_results_found": "Asnje rezultat",
"please_continue_scanning": "Vazhdo skanimin.",
"select_no_bitcoin": "Per momentin nuk ka asnje portofol Bitcoin-i.",
"select_wallet": "Zgjidh Portofolin",
"share_number": "Shperndaj {number}",
"more_info": "Me shum Info"
},
"total_balance_view": {
"hide": "Mçihe",
"title": "Ballanca Totale"
},
"multisig": {
"confirm": "Konfirmo",
"header": "Dërgo",
"share": "Shpërndaj",
"view": "Shiko",
"manage_keys": "Menaxho Fjalekalimet",
"signatures_required_to_spend": "Firmat janë të nevojshme {number}",
"signatures_we_can_make": "mund të bjmë {number}",
"scan_or_import_file": "Skano ose importo nga një file",
"lets_start": "Le të fillojmë",
"create": "Krijo",
"native_segwit_title": "Praktikat më të mira",
"co_sign_transaction": "Firmos një transfertë",
"what_is_vault_wallet": "portofol.",
"needs": "I duhet",
"quorum_header": "Kuorumi",
"wallet_type": "Tipi i Portofolit",
"create_new_key": "Krijo te ri",
"scan_or_open_file": "Skano ose hap file-in",
"input_fp": "Fut shenjat e gishtave",
"ms_help": "Ndihme",
"ms_help_title2": "Ndrysho Celësat"
},
"is_it_my_address": {
"title": "A eshte adresa ime?",
"enter_address": "Fut adresen",
"check_address": "Kontrollo adresen",
"view_qrcode": "Shiko QR kodin"
},
"autofill_word": {
"generate_word": "Gjenero fjalën përfundimtare."
},
"cc": {
"change": "Ndrysho",
"empty": "Ky portofol ska asnjë xheton për momentin",
"freeze": "Ngrije",
"freezeLabel": "Ngrije",
"freezeLabel_un": "Shkrije",
"header": "Kontrollo Xhetonin",
"use_coin": "Përdor Xhetonin",
"use_coins": "Përdor Xhetonat",
"sort_height": "Lartësia",
"sort_value": "Vlerë",
"sort_label": "Etiketë",
"sort_status": "Gjëndja",
"sort_by": "Radhiti sipas"
},
"units": {
"BTC": "BTC",
"MAX": "Max",
"sat_vbyte": "sat/vByte",
"sats": "sats"
},
"addresses": {
"copy_private_key": "Kopjo celësin privat",
"sign_sign": "Firmos",
"sign_verify": "Verifiko",
"sign_signature_correct": "Verifikimi u krye me Sukses",
"sign_signature_incorrect": "Verifikimi nuk pati Sukses",
"sign_placeholder_address": "Adresa",
"sign_placeholder_message": "Mesazhi",
"sign_placeholder_signature": "Firma",
"addresses_title": "Adresat",
"type_change": "Ndrysho",
"type_receive": "Merr",
"type_used": "I përdorur",
"transactions": "Transfertat"
},
"bip47": {
"payment_code": "Kodi i Pagesës",
"contacts": "Kontaktet",
"pay_this_contact": "Paguaj këtë kontakt",
"rename_contact": "Riemëro kontaktin",
"hide_contact": "Mçife kontaktin",
"rename": "Riemëro",
"add_contact": "Shto Kontakt",
"provide_payment_code": "Fut kodin e pagesës"
}
}

View file

@ -23,11 +23,11 @@
},
"azteco": {
"codeIs": "Din kupong är",
"errorBeforeRefeem": "Innan inlösen måste du först skapa en Bitcoin plånbok.",
"errorBeforeRefeem": "Innan du löser in måste du skapa en bitcoin-plånbok.",
"errorSomething": "Något gick fel. Är din kupong fortfarande giltig?",
"redeem": "Lös in till en plånbok",
"redeemButton": "Lös in",
"success": "Framgång",
"success": "Klart",
"title": "Lös in Azte.co kupong"
},
"entropy": {
@ -38,7 +38,7 @@
"errors": {
"broadcast": "Sändning misslyckades",
"error": "Fel",
"network": "Nätverks fel"
"network": "Nätverksfel"
},
"lnd": {
"expired": "Förfallen",
@ -47,7 +47,7 @@
"payment": "Betalning",
"placeholder": "Faktura eller adress",
"potentialFee": "Potentiell avgift: {fee}",
"refill": "Sätt in",
"refill": "Fyll på",
"refill_create": "För att fortsätta, vänligen skapa en Bitcoin plånbok att fylla på med",
"refill_external": "Fyll på från extern plånbok",
"refill_lnd_balance": "Fyll på Lightning-plånbok",
@ -57,7 +57,7 @@
"lndViewInvoice": {
"additional_info": "Ytterligare information",
"for": "För:",
"lightning_invoice": "Lightning faktura",
"lightning_invoice": "Lightning-faktura",
"open_direct_channel": "Öppna en direkt kanal med denna nod:",
"please_pay_between_and": "Betala mellan {min} och {max}",
"please_pay": "Var god betala",
@ -172,9 +172,9 @@
"about": "Om",
"about_awesome": "Byggd med det fantastiska",
"about_backup": "Backa alltid upp dina nycklar!",
"about_free": "BlueWallet är ett fritt och öppen källkods projekt. Skapad av Bitcoin användare.",
"about_free": "BlueWallet är gratis och byggs av bitcoin-användare med hjälp av öppen källkod.",
"about_license": "MIT-licens",
"about_release_notes": "Release notes",
"about_release_notes": "Versionsinformation",
"about_review": "Lämna oss en recension",
"performance_score": "Prestandapoäng: {num}",
"run_performance_test": "Testa prestanda",
@ -182,8 +182,8 @@
"about_selftest_electrum_disabled": "Självtest är inte tillgänglig med Electrum Offline Mode. Inaktivera offlineläget och försök igen.",
"about_selftest_ok": "Alla interna tester har godkänts. Plånboken fungerar bra.",
"about_sm_github": "GitHub",
"about_sm_discord": "Discord Server",
"about_sm_telegram": "Telegram chatt",
"about_sm_discord": "Discord-server",
"about_sm_telegram": "Telegramkanal",
"about_sm_twitter": "Följ oss på Twitter",
"biometrics": "Biometri",
"biom_10times": "Du har försökt ange ditt lösenord 10 gånger. Vill du återställa din lagring? Detta tar bort alla plånböcker och dekrypterar din lagring.",
@ -196,7 +196,7 @@
"default_title": "Vid uppstart",
"default_wallets": "Visa alla plånböcker",
"electrum_connected": "Ansluten",
"electrum_connected_not": "Inte Ansluten",
"electrum_connected_not": "Inte ansluten",
"lndhub_uri": "E.g., {example}",
"electrum_host": "E.g., {example}",
"electrum_offline_mode": "Offline läge",
@ -217,15 +217,15 @@
"encrypt_decrypt": "Dekryptera lagring",
"encrypt_decrypt_q": "Är du säker på att du vill dekryptera din lagring? Detta gör att dina plånböcker kan nås utan lösenord.",
"encrypt_enc_and_pass": "Krypterad och lösenordsskyddad",
"encrypt_title": "säkerhet",
"encrypt_title": "Säkerhet",
"encrypt_tstorage": "Lagring",
"encrypt_use": "Använd {type}",
"encrypt_use_expl": "{type} kommer att användas för att bekräfta din identitet innan du gör en transaktion, låser upp, exporterar eller tar bort en plånbok. {type} kommer inte att användas för att låsa upp krypterad lagring.",
"general": "Allmän",
"general": "Allmänt",
"general_continuity": "Kontinuitet",
"general_continuity_e": "När det är aktiverat kommer du att kunna se utvalda plånböcker och transaktioner med dina andra Apple iCloud-anslutna enheter.",
"groundcontrol_explanation": "GroundControl är en gratis push-meddelandeserver med öppen källkod för Bitcoin-plånböcker. Du kan installera din egen GroundControl-server och lägga in dess URL här för att inte lita på BlueWallets infrastruktur. Lämna tomt för att använda GroundControls standardserver.",
"header": "inställningar",
"header": "Inställningar",
"language": "Språk",
"last_updated": "Senast uppdaterad",
"language_isRTL": "Att starta om BlueWallet krävs för att språkorienteringen ska träda i kraft.",
@ -363,6 +363,7 @@
"import_derivation_title": "Derivation path",
"list_create_a_button": "Ny plånbok",
"list_create_a_wallet": "Ny plånbok",
"list_create_a_wallet_text": "Det är gratis och du kan\nskapa hur många du vill.",
"list_empty_txs1": "Dina transaktioner kommer att visas här",
"list_empty_txs1_lightning": "Lightningplånboken ska användas för dagliga småtransaktioner. Avgifterna är minimala och transaktioner sker direkt.",
"list_empty_txs2": "Börja med din plånbok.",
@ -382,7 +383,6 @@
"select_wallet": "Välj plånbok",
"xpub_copiedToClipboard": "Kopierad till urklipp",
"pull_to_refresh": "Dra för att uppdatera",
"warning_do_not_disclose": "Varning! Avslöja inte.",
"add_ln_wallet_first": "Du måste först lägga till en Lightning-plånbok.",
"identity_pubkey": "Identitet Pubkey",
"xpub_title": "plånbokens XPUB"

View file

@ -1,21 +1,27 @@
{
"_": {
"bad_password": "Hatalı şifre, lütfen tekrar deneyiniz.",
"cancel": "Vazgeç",
"bad_password": "Yanlış şifre. Lütfen tekrar deneyin.",
"cancel": "İptal",
"continue": "Devam et",
"clipboard": "Pano",
"discard_changes": "Değişiklikleri iptal et?",
"enter_password": "Şifre gir",
"discard_changes": "Değişiklikleri silmek istiyor musunuz?",
"discard_changes_explain": "Kaydedilmemiş değişiklikleriniz var. Bunları silip ekrandan çıkmak istediğinizden emin misiniz?",
"enter_password": "Şifreyi girin",
"never": "Asla",
"of": "{number} / {total}",
"ok": "Tamam",
"storage_is_encrypted": "Depolama alanınız şifrelidir, kilidi kaldırmak için şifrenizi girin.",
"ok": "Evet",
"customize": "Özelleştir",
"enter_url": "URL girin",
"storage_is_encrypted": "Depolama alanınız şifrelenmiş. Şifrelemeyi çözmek için şifre gereklidir.",
"yes": "Evet",
"no": "Hayır",
"save": "Kaydet",
"seed": "Seed",
"success": "Başarılı",
"wallet_key": "Cüzdan anahtarı"
"wallet_key": "Cüzdan anahtarı",
"invalid_animated_qr_code_fragment": "Geçersiz animasyonlu QR Kod parçası. Lütfen tekrar deneyin.",
"close": "Kapat",
"change_input_currency": "Giriş para birimini değiştir"
},
"azteco": {
"codeIs": "Bilet kodunuz ",

View file

@ -375,7 +375,6 @@
"select_wallet": "Chọn ví",
"xpub_copiedToClipboard": "Đã sao chép vào bảng tạm.",
"pull_to_refresh": "Kéo để làm mới",
"warning_do_not_disclose": "Cảnh báo! Không tiết lộ.",
"add_ln_wallet_first": "Đầu tiên bạn phải thêm một ví Lightning.",
"identity_pubkey": "PubKey danh tính",
"xpub_title": "XPUB của ví"

View file

@ -196,6 +196,9 @@
"success_done": "完成",
"txSaved": "交易文件({filepath})已被保存。",
"file_saved_at_path": "文件 ({filePath}) 已被保存。",
"cant_send_to_silentpayment_adress": "该钱包无法向 SilentPayment 地址发送",
"cant_send_to_bip47": "该钱包无法发送至 BIP47 付款代码",
"cant_find_bip47_notification": "先将此付款代码添加到联系人",
"problem_with_psbt": "PSBT的问题"
},
"settings": {
@ -206,77 +209,125 @@
"about_license": "麻省理工学院许可证",
"about_release_notes": "发布说明",
"about_review": "给我们评论",
"performance_score": "性能得分:{num}",
"run_performance_test": "测试性能",
"about_selftest": "运行自检",
"block_explorer_invalid_custom_url": "所提供的网址无效。请输入以 http:// 或 https:// 开头的有效网址。",
"about_selftest_electrum_disabled": "在 Electrum 离线模式下,自我检测不可用。请禁用离线模式并重试。",
"about_selftest_ok": "所有内部测试均已成功通过,钱包运作良好。",
"about_sm_github": "Github",
"about_sm_discord": "Discord 服务器",
"about_sm_telegram": "电报频道",
"about_sm_twitter": "在推特上关注我们",
"privacy_temporary_screenshots": "允许截屏",
"privacy_temporary_screenshots_instructions": "屏幕截图保护将在此会话中关闭,允许你截屏。关闭并重新打开应用后,保护将自动开启。",
"biometrics": "生物识别",
"biometrics_no_longer_available": "您的设备设置已更改,不再与应用程序中选择的安全设置相匹配。请重新启用生物识别或密码,然后重启应用程序以应用这些更改。",
"biom_10times": "您已尝试输入密码10次。 您想重置存储空间吗? 这将删除所有钱包并解密您的存储。",
"biom_conf_identity": "请确认您的身份。",
"biom_no_passcode": "您的设备未启用密码或生物识别。要继续操作,请在 “设置 ”应用程序中配置密码或生物识别。",
"biom_remove_decrypt": "您的所有钱包将被删除,您的存储空间将被解密。您确定要继续吗?",
"currency": "货币",
"currency_source": "汇率报价提供者",
"currency_fetch_error": "在获取所选货币的汇率时出现错误。",
"default_desc": "停用后BlueWallet将在启动时立即打开选定的钱包。",
"default_info": "默认信息",
"default_title": "启动时",
"default_wallets": "查看所有钱包",
"electrum_connected": "已连接",
"electrum_connected_not": "未连接",
"electrum_error_connect": "无法连接到所提供的 Electrum 服务器",
"lndhub_uri": "举例:{example}",
"electrum_host": "举例:{example}",
"electrum_offline_mode": "离线模式",
"electrum_offline_description": "启用后,您的比特币钱包将不会尝试获取余额或交易。",
"electrum_port": "端口,通常 {example}",
"use_ssl": "使用 SSL",
"electrum_saved": "您的更变已成功保存。要使更变生效可能需要重新启动BlueWallet。",
"set_electrum_server_as_default": "将{server}设置为默认的Electrum服务器",
"electrum_settings_server": "Electrum Server",
"set_lndhub_as_default": "是否将 {url} 设置为默认的 LNDhub 服务器?",
"electrum_settings_server": "Electrum 服务器",
"electrum_status": "状态",
"electrum_preferred_server": "首选的服务器",
"electrum_preferred_server_description": "输入您希望钱包用于所有比特币活动的服务器。设置后,您的钱包将只使用该服务器检查余额、发送交易和获取网络数据。在设置之前,请确保您信任该服务器。",
"electrum_clear_alert_title": "清除历史记录?",
"electrum_clear_alert_message": "您是否要清除electrum 服务器的历史记录?",
"electrum_clear_alert_cancel": "取消",
"electrum_clear_alert_ok": "好的",
"electrum_reset": "重置为默认",
"electrum_unable_to_connect": "无法连接至 {server}。",
"electrum_history": "历史",
"electrum_reset_to_default": "您确定要重置您的Electrum设置为默认值吗",
"electrum_clear": "清除历史",
"encrypt_decrypt": "解密存储",
"encrypt_decrypt_q": "您确定要解密存储吗? 这样一来,无需密码即可访问您的钱包。",
"encrypt_enc_and_pass": "加密和密码保护的",
"encrypt_storage_explanation_headline": "启用存储加密功能",
"encrypt_storage_explanation_description_line1": "启用 “存储加密功能” 可确保您的数据在设备上的存储方式安全,从而为您的应用程序增加一层额外的保护。这样,任何人就很难在未经允许的情况下访问您的信息。",
"encrypt_storage_explanation_description_line2": "不过,重要的是要知道,这种加密只保护对存储在设备钥匙串上的钱包的访问。它不会为钱包本身设置密码或任何额外保护。",
"i_understand": "我明白了",
"block_explorer": "区块浏览器",
"block_explorer_preferred": "使用首选区块浏览器",
"block_explorer_error_saving_custom": "保存首选区块浏览器出错",
"encrypt_title": "安全",
"encrypt_tstorage": "存储",
"encrypt_use": "使用{type}",
"encrypted_feature_disabled": "启用加密存储时不能使用此功能。",
"encrypt_use_expl": "在进行交易、解锁、导出或删除钱包之前,{type} 将用于确认您的身份。{type} 不会用于解锁加密存储。",
"biometrics_fail": "如果未启用 {type} 或无法解锁,可以使用设备密码作为替代。",
"general": "一般的",
"general_continuity": "连续性",
"general_continuity_e": "启用后您将能够查看选定的钱包、交易及使用您其他Apple iCloud连接的设备。",
"groundcontrol_explanation": "GroundControl是一款免费的开源推送通知服务器用于比特币钱包。 您可以安装自己的GroundControl服务器并将其URL放在此处而不依赖BlueWallet的基础结构。 保留空白以使用GroundControl的默认服务器。",
"header": "设置",
"language": "语言",
"last_updated": "最近一次更新",
"language_isRTL": "需要重启 BlueWallet 才能使语言定位生效。",
"license": "许可证",
"lightning_error_lndhub_uri": "无效的 LNDhub URI",
"lightning_saved": "您的更变已成功保存。",
"lightning_settings": "闪电网络设置",
"lightning_settings_explain": "要连接到您自己的 LND 节点,请安装 LNDhub 并在设置中将其网址置于此处。请注意,只有保存更改后创建的钱包才能连接到指定的 LNDhub。",
"network": "网络",
"network_broadcast": "广播交易",
"network_electrum": "Electrum服务器",
"not_a_valid_uri": "无效的 URI",
"notifications": "通知事项",
"open_link_in_explorer": "在资源管理器中打开链接",
"password": "密码",
"password_explain": "输入用于解锁存储的密码。",
"plausible_deniability": "合理推诿",
"privacy": "隐私",
"privacy_read_clipboard": "阅读剪贴板",
"privacy_system_settings": "系统设置",
"privacy_quickactions": "钱包捷径",
"privacy_quickactions_explanation": "轻触并按住 BlueWallet 应用程序图标,即可快速查看钱包余额。",
"privacy_clipboard_explanation": "如果在剪贴板中找到地址或发票,请提供捷径。",
"privacy_do_not_track": "禁用数据分析功能",
"privacy_do_not_track_explanation": "性能与可靠性信息将不提交为数据分析。",
"rate": "汇率",
"push_notifications_explanation": "启用通知后,您的设备令牌将连同所有钱包地址和交易 ID 以及启用通知后的所有钱包和交易发送至服务器。设备令牌用于发送通知,而钱包信息允许我们通知您收到的比特币或交易确认。\n\n只传输您启用通知后的信息不会收集之前的任何信息。\n\n禁用通知将从服务器上删除所有这些信息。此外从应用程序中删除钱包也会从服务器中删除其相关信息。",
"selfTest": "自行测试",
"save": "保存",
"saved": "已保存",
"success_transaction_broadcasted": "您的交易已成功广播!",
"total_balance": "总余额",
"total_balance_explanation": "在主屏幕小工具上显示您所有钱包的总余额。",
"widgets": "小工具",
"tools": "工具"
},
"notifications": {
"would_you_like_to_receive_notifications": "您想在收到款项时得到通知吗?"
"would_you_like_to_receive_notifications": "您想在收到款项时得到通知吗?",
"notifications_subtitle": "进账付款和交易确认",
"no_and_dont_ask": "不,别再问我了。",
"permission_denied_message": "您已拒绝向您发送通知。如果您希望接收通知,请在设备设置中启用。"
},
"transactions": {
"cancel_explain": "我们将用向您付款且费用更高的交易替代该交易。这实际上取消了当前的交易。这就是所谓的费用替换Replace by Fee。",
"cancel_no": "此交易不可替换。",
"cancel_title": "取消此交易RBF",
"transaction_loading_error": "加载交易时出现问题。请稍后再试。",
"transaction_not_available": "交易不可用",
"confirmations_lowercase": "{confirmations}个确认",
"copy_link": "复制链接",
"expand_note": "打开备注",
@ -287,51 +338,88 @@
"details_balance_hide": "隐藏余额",
"details_balance_show": "显示余额",
"details_copy": "复制",
"details_copy_block_explorer_link": "复制区块浏览器链接",
"details_copy_note": "复印备注",
"details_copy_txid": "复制 Tx ID",
"details_from": "输入",
"details_inputs": "输入",
"details_outputs": "输出",
"date": "日期",
"details_received": "已收到",
"details_view_in_browser": "在浏览器中查看",
"details_title": "转账",
"incoming_transaction": "接受的交易",
"outgoing_transaction": "发出的交易",
"expired_transaction": "过期的交易",
"pending_transaction": "待定的交易",
"offchain": "链下",
"onchain": "链上",
"details_to": "输出",
"enable_offline_signing": "此钱包未与线下签名结合使用。您想立即启用它吗?",
"list_conf": "Conf: {number}",
"pending": "待办",
"pending_with_amount": "待定的 {amt1} ({amt2})",
"received_with_amount": "+{amt1} ({amt2})",
"eta_10m": "预计时间:大约 10 分钟",
"eta_3h": "预计时间:大约 3 小时",
"eta_1d": "预计时间:大约 1 天",
"view_wallet": "查看 {walletLabel}",
"list_title": "交易",
"transaction": "转账",
"open_url_error": "无法使用默认浏览器打开链接。请更换默认浏览器并重试。",
"rbf_explain": "我们会用手续费更高的交易来替代这笔交易,这样它的挖矿速度会更快。这就是所谓的 RBF —— 费用替代。",
"rbf_title": "碰撞费用RBF",
"status_bump": "碰撞费用",
"status_cancel": "取消交易",
"transactions_count": "交易计数",
"txid": "交易ID",
"updating": "正在更新..."
"from": "从:{counterparty}",
"to": "到:{counterparty}",
"updating": "正在更新...",
"watchOnlyWarningTitle": "安全警告",
"watchOnlyWarningDescription": "小心诈骗份子,他们经常使用 “仅供观察 ”钱包来欺骗用户。这些钱包不允许您控制或发送资金,只允许您查看余额。"
},
"wallets": {
"add_bitcoin": "比特币",
"add_bitcoin_explain": "简单而强大的比特币钱包",
"add_create": "创建",
"total_balance": "总余额",
"add_entropy": "随机熵值",
"add_entropy_reset_title": "重置熵值",
"add_entropy_reset_message": "更改钱包类型将重置当前熵值。要继续吗?",
"add_entropy": "熵值",
"add_entropy_bytes": "{bytes} 字节的熵值",
"add_entropy_generated": "产生熵的{gen}个字节",
"add_entropy_provide": "通过掷骰子提供熵",
"add_entropy_remain": "产生熵的{gen}个字节。 剩余的{rem}字节将从系统随机数生成器中获得。",
"add_import_wallet": "导入钱包",
"add_lightning": "闪电",
"add_lightning_explain": "用于即时交易的花费",
"add_lndhub": "连接到你的 LNDhub",
"add_lndhub_error": "提供的节点地址是无效的 LNDhub 节点。",
"add_lndhub_placeholder": "您的节点地址",
"add_placeholder": "我的第一个钱包",
"add_title": "添加钱包",
"add_wallet_name": "名称",
"add_wallet_type": "类型",
"add_wallet_seed_length": "助记词长度",
"add_wallet_seed_length_message": "选择您希望用于此钱包的助记词长度。",
"add_wallet_seed_length_12": "12 个单词",
"add_wallet_seed_length_24": "24 个单词",
"clipboard_bitcoin": "您的剪贴板上有一个比特币地址。您想使用它进行交易吗?",
"clipboard_lightning": "您的剪贴板上有一张闪电賬單。 您想使用它进行交易吗?",
"clear_clipboard_on_import": "导入后清除剪贴板",
"details_address": "地址",
"details_advanced": "进阶的",
"details_are_you_sure": "你确认么?",
"details_connected_to": "连接到",
"details_del_wb_err": "提供的余额与此钱包的余额不匹配。请重试。",
"details_del_wb_q": "此钱包有余额。在继续操作前,请注意如果没有该钱包的助记词,您将无法恢复资金。为避免意外删除,请输入您钱包余额中的 {balance} 聪。",
"details_delete": "删除",
"details_delete_wallet": "删除钱包",
"details_derivation_path": "推导路径",
"details_display": "在主屏幕中显示",
"details_export_backup": "导出/备份",
"details_export_history": "将历史记录导出为 CSV",
"details_master_fingerprint": "主指纹",
"details_multisig_type": "多重签名",
"details_no_cancel": "不了,请取消",
@ -347,14 +435,31 @@
"import_do_import": "导入",
"import_passphrase": "密语",
"import_passphrase_title": "密语",
"import_passphrase_message": "如果您使用密码短语,请输入。",
"import_error": "导入失败,请确认你提供的信息是有效的",
"import_explanation": "输入你的种子短语、公钥、WIF或者任何你拥有的东西。BlueWallet将尽可能猜测正确的格式并导入您的钱包",
"import_imported": "已经导入",
"import_scan_qr": "扫描或导入一个档案",
"import_success": "你的钱包已成功导入。",
"import_success_watchonly": "您的钱包已成功导入。警告:这是一个 “仅供观察” 钱包,您不能使用它进行消费。",
"import_search_accounts": "搜索账户",
"import_title": "导入",
"learn_more": "了解更多",
"import_discovery_title": "发现",
"import_discovery_subtitle": "选择一个已被发现的钱包",
"import_discovery_derivation": "使用自定义派生路径",
"import_discovery_no_wallets": "未能发现钱包。",
"import_discovery_offline": "BlueWallet 目前处于离线模式。在此模式下,它无法验证钱包的存在,因此您需要手动选择正确的钱包",
"import_derivation_found": "已发现",
"import_derivation_found_not": "未发现",
"import_derivation_loading": "加载中...",
"import_derivation_subtitle": "输入自定义派生路径,我们将尝试发现您的钱包。",
"import_derivation_title": "派生路径",
"import_derivation_unknown": "未知",
"import_wrong_path": "错误的派生路径",
"list_create_a_button": "现在添加",
"list_create_a_wallet": "添加钱包",
"list_create_a_wallet_text": "这是免费的,您可以创建\n要多少有多少。",
"list_empty_txs1": "你的交易将在这里展示",
"list_empty_txs1_lightning": "应使用闪电钱包进行日常交易。费用超便宜而且速度飞快。",
"list_empty_txs2": "从你的钱包开始。",
@ -368,19 +473,35 @@
"list_tryagain": "再试一次",
"no_ln_wallet_error": "在支付闪电账单之前,必须先添加一个闪电钱包。",
"looks_like_bip38": "这看起来像是受密码保护的私钥BIP38。",
"manage_title": "管理钱包",
"no_results_found": "未能找到任何结果。",
"please_continue_scanning": "请继续扫描。",
"select_no_bitcoin": "当前没有可用的比特币钱包。",
"select_no_bitcoin_exp": "需要一个比特币钱包来为闪电钱包充值。 请创建或导入一个。",
"select_wallet": "选择钱包",
"xpub_copiedToClipboard": "复制到粘贴板。",
"pull_to_refresh": "拉动来刷新",
"warning_do_not_disclose": "警告! 不要透露。",
"warning_do_not_disclose": "切勿分享以下信息",
"scan_import": "扫一扫此二维码,即可在其他应用程序中导入您的钱包。",
"write_down_header": "创建一个手动备份",
"write_down": "写下并妥善保存这些单词。以后用它们恢复钱包。",
"wallet_type_this": "该钱包类型为 {type}。",
"share_number": "分享 {number}",
"copy_ln_url": "复制并安全存储此网址,以便日后恢复钱包。",
"copy_ln_public": "复制并安全存储这些信息,以便日后恢复钱包。",
"add_ln_wallet_first": "您必须先添加一个闪电钱包。",
"identity_pubkey": "身份公钥",
"xpub_title": "钱包公钥"
"xpub_title": "钱包公钥",
"manage_wallets_search_placeholder": "搜索钱包、备忘录",
"more_info": "更多信息"
},
"total_balance_view": {
"title": "总余额"
"display_in_bitcoin": "以比特币单位显示",
"hide": "隐藏",
"display_in_sats": "以单位 “聪” 显示",
"display_in_fiat": "以 {currency} 显示",
"title": "总余额",
"explanation": "在概览屏幕中查看所有钱包的总余额。"
},
"multisig": {
"multisig_vault": "多重签名保管库",
@ -395,6 +516,8 @@
"header": "发送",
"share": "分享...",
"view": "查看",
"shared_key_detected": "已分享的共同签署人",
"shared_key_detected_question": "已与您分享了一名共同签署人,您想导入它吗?",
"manage_keys": "管理密钥",
"how_many_signatures_can_bluewallet_make": "BlueWallet能够生成多少签名",
"signatures_required_to_spend": "需要签名 {number}",
@ -420,14 +543,21 @@
"quorum_header": "法定人数",
"of": "的",
"wallet_type": "钱包类型",
"invalid_mnemonics": "该助记词似乎是无效的。",
"invalid_cosigner": "共同签署人数据无效",
"not_a_multisignature_xpub": "这不是来自多重签名钱包的公钥!",
"invalid_cosigner_format": "共同签署人数据无效:这不是 {format} 格式的共同签署人。",
"create_new_key": "创建新的",
"scan_or_open_file": "扫描或打开文件",
"i_have_mnemonics": "我有这个密钥的种子。",
"type_your_mnemonics": "插入种子以导入现有的保管库密钥。",
"this_is_cosigners_xpub": "这是共同签署人的扩展公钥,可以导入另一个钱包。分享它是安全的。",
"this_is_cosigners_xpub_airdrop": "如果通过 “隔空传送”共享,接收者必须在协调界面中。",
"wallet_key_created": "您的保管库密钥已创建。花点时间安全地备份您的助记符种子。",
"are_you_sure_seed_will_be_lost": "你确定吗? 如果没有备份,助记符种子将丢失。",
"forget_this_seed": "忘记此种子,而是使用公钥。",
"view_edit_cosigners": "查看或编辑共同签署人",
"this_cosigner_is_already_imported": "该共同签署人已经被导入。",
"export_signed_psbt": "导出已签名的PSBT",
"input_fp": "输入指纹",
"input_fp_explain": "跳过以使用默认值00000000",
@ -439,7 +569,9 @@
"ms_help_title1": "建议使用多个设备。",
"ms_help_1": "这个保管库将与其他BlueWallet应用程序和PSBT兼容的钱包配合使用例如Electrum、Spectre、Coldcard、Cobo Vault等。",
"ms_help_title2": "编辑密钥",
"ms_help_2": "您可以在该设备上创建所有金库密钥,然后删除或编辑它们。所有密钥都在同一设备上,其安全性与普通比特币钱包相当。",
"ms_help_title3": "保管库备份",
"ms_help_3": "在钱包选项中,您可以找到您的金库备份和仅供观察备份。该备份就像是您钱包的地图。如果您丢失了助记词,它对钱包恢复至关重要。",
"ms_help_title4": "导入保管库",
"ms_help_4": "要导入多重签名,请使用您的备份文件和导入功能。 如果只有种子和公钥,则可以在创建保管库密钥时使用单独的“导入”按钮。",
"ms_help_title5": "进阶模式",
@ -450,11 +582,20 @@
"owns": "{label}拥有{address}",
"enter_address": "输入地址",
"check_address": "检查地址",
"no_wallet_owns_address": "没有可用的钱包拥有提供的地址。"
"no_wallet_owns_address": "没有可用的钱包拥有提供的地址。",
"view_qrcode": "查看二维码"
},
"autofill_word": {
"title": "生成最后一个助记词单词",
"enter": "输入您的部分助记词",
"generate_word": "生成最后一个单词",
"error": "输入的不是 11 个 或 23 个单词的部分助记词。请重试。"
},
"cc": {
"change": "更变",
"coins_selected": "所选的币({number}",
"selected_summ": "以选定 {value} ",
"empty": "此钱包目前没有任何加密货币。",
"freeze": "冻结",
"freezeLabel": "冻结",
"freezeLabel_un": "解冻",
@ -462,23 +603,72 @@
"use_coin": "使用币",
"use_coins": "使用币",
"tip": "此功能使您可以查看、标记、冻结或选择币,以改善钱包管理。 您可以通过点击彩色圆圈选择多个币。",
"sort_asc": "上升式",
"sort_desc": "下降式",
"sort_height": "高度",
"sort_value": "价值",
"sort_label": "标签",
"sort_status": "状态"
"sort_status": "状态",
"sort_by": "排序方式"
},
"units": {
"BTC": "比特幣",
"MAX": "最大",
"sat_vbyte": "聪 / vByte",
"sats": "聪"
},
"addresses": {
"copy_private_key": "复制私钥",
"sensitive_private_key": "警告:私钥极其敏感。是否继续?",
"sign_title": "签署或验证留言",
"sign_help": "在这里,您可以根据比特币地址创建或验证加密签名。",
"sign_sign": "签署",
"sign_verify": "验证",
"sign_signature_correct": "验证成功!",
"sign_signature_incorrect": "验证失败!",
"sign_placeholder_address": "地址",
"sign_placeholder_message": "信息",
"sign_placeholder_signature": "签名",
"addresses_title": "地址",
"type_change": "改变",
"type_receive": "接收",
"type_used": "已使用过的",
"transactions": "交易"
},
"lnurl_auth": {
"register_question_part_1": "您是否想在以下网站注册一个账户",
"register_question_part_2": "使用您的闪电网络钱包?",
"register_answer": "您已在 {hostname} 成功注册了一个账户!",
"login_question_part_1": "您是否愿意登录在",
"login_question_part_2": "使用您的闪电网络钱包?",
"login_answer": "您已在 {hostname} 成功登录!",
"link_question_part_1": "您是否愿意将您的账户链接到",
"link_question_part_2": "到您的闪电网络钱包?",
"link_answer": "您的闪电网络钱包已成功链接到您在 {hostname} 的账户!",
"auth_question_part_1": "您是否愿意在以下进行身份验证",
"auth_question_part_2": "使用您的闪电网络钱包?",
"auth_answer": "您已成功在 {hostname} 上进行身份验证!",
"could_not_auth": "我们无法在 {hostname} 上验证您的身份。",
"authenticate": "身份验证"
},
"bip47": {
"payment_code": "支付代码",
"contacts": "联系人",
"purpose": "可重复使用和共享的代码 (BIP47)",
"pay_this_contact": "支付该联系人",
"rename_contact": "重命名联系人",
"copy_payment_code": "复制支付代码",
"hide_contact": "隐藏联系人",
"rename": "重命名",
"provide_name": "为该联系人提供新的名称",
"add_contact": "添加联系人",
"provide_payment_code": "提供支付代码",
"invalid_pc": "支付代码无效",
"notification_tx_unconfirmed": "通知交易尚未确认,请等待",
"failed_create_notif_tx": "创建链上交易失败",
"onchain_tx_needed": "需要在链上交易",
"notif_tx_sent": "交易通知已发送。请等待确认",
"notif_tx": "通知交易",
"not_found": "未能找到支付代码"
}
}

View file

@ -315,7 +315,6 @@
"select_wallet": "選擇錢包",
"xpub_copiedToClipboard": "複製到貼上板.",
"pull_to_refresh": "拉動來刷新",
"warning_do_not_disclose": "警告! 不要透露。",
"add_ln_wallet_first": "您必須先添加一個閃電錢包。",
"identity_pubkey": "身份公鑰",
"xpub_title": "錢包公鑰"

View file

@ -125,6 +125,13 @@
"symbol": "£",
"country": "United Kingdom (British Pound)"
},
"HKD": {
"endPointKey": "HKD",
"locale": "zh-HK",
"source": "CoinGecko",
"symbol": "HK$",
"country": "Hong Kong (Hong Kong Dollar)"
},
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",

View file

@ -17,13 +17,15 @@ import {
WalletsAddMultisigHelpComponent,
WalletsAddMultisigStep2Component,
} from './LazyLoadAddWalletStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
import { ScanQRCodeParamList } from './DetailViewStackParamList';
export type AddWalletStackParamList = {
AddWallet: undefined;
ImportWallet?: {
label?: string;
triggerImport?: boolean;
scannedData?: string;
onBarScanned?: string;
};
ImportWalletDiscovery: {
importText: string;
@ -55,6 +57,7 @@ export type AddWalletStackParamList = {
format: string;
};
WalletsAddMultisigHelp: undefined;
ScanQRCode: ScanQRCodeParamList;
};
const Stack = createNativeStackNavigator<AddWalletStackParamList>();
@ -138,6 +141,16 @@ const AddWalletStack = () => {
headerShadowVisible: false,
})(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View file

@ -33,7 +33,6 @@ import PaymentCodesListComponent from './LazyLoadPaymentCodeStack';
import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack';
import ReceiveDetailsStackRoot from './ReceiveDetailsStack';
import ScanLndInvoiceRoot from './ScanLndInvoiceStack';
import ScanQRCodeStackRoot from './ScanQRCodeStack';
import SendDetailsStack from './SendDetailsStack';
import SignVerifyStackRoot from './SignVerifyStack';
import ViewEditMultisigCosignersStackRoot from './ViewEditMultisigCosignersStack';
@ -65,6 +64,7 @@ import SelfTest from '../screen/settings/SelfTest';
import ReleaseNotes from '../screen/settings/ReleaseNotes';
import ToolsScreen from '../screen/settings/tools';
import SettingsPrivacy from '../screen/settings/SettingsPrivacy';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const DetailViewStackScreensStack = () => {
const theme = useTheme();
@ -243,27 +243,6 @@ const DetailViewStackScreensStack = () => {
component={WalletAddresses}
options={navigationStyle({ title: loc.addresses.addresses_title, statusBarStyle: 'auto' })(theme)}
/>
<DetailViewStack.Screen
name="AddWalletRoot"
component={AddWalletStack}
options={navigationStyle({ closeButtonPosition: CloseButtonPosition.Left, ...NavigationFormModalOptions })(theme)}
/>
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationFormModalOptions} />
<DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="AztecoRedeemRoot" component={AztecoRedeemStackRoot} options={NavigationDefaultOptions} />
{/* screens */}
<DetailViewStack.Screen
name="WalletExportRoot"
component={WalletExportStack}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }}
/>
<DetailViewStack.Screen
name="ExportMultisigCoordinationSetupRoot"
component={ExportMultisigCoordinationSetupStack}
options={NavigationDefaultOptions}
/>
<DetailViewStack.Screen
name="Settings"
component={Settings}
@ -341,12 +320,43 @@ const DetailViewStackScreensStack = () => {
component={SettingsPrivacy}
options={navigationStyle({ title: loc.settings.privacy })(theme)}
/>
<DetailViewStack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
<DetailViewStack.Screen
name="ViewEditMultisigCosignersRoot"
component={ViewEditMultisigCosignersStackRoot}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }}
initialParams={{ walletID: undefined, cosigners: undefined }}
/>
<DetailViewStack.Screen
name="AddWalletRoot"
component={AddWalletStack}
options={navigationStyle({ closeButtonPosition: CloseButtonPosition.Left, ...NavigationFormModalOptions })(theme)}
/>
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationFormModalOptions} />
<DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="AztecoRedeemRoot" component={AztecoRedeemStackRoot} options={NavigationDefaultOptions} />
{/* screens */}
<DetailViewStack.Screen
name="WalletExportRoot"
component={WalletExportStack}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }}
/>
<DetailViewStack.Screen
name="ExportMultisigCoordinationSetupRoot"
component={ExportMultisigCoordinationSetupStack}
options={NavigationDefaultOptions}
/>
<DetailViewStack.Screen
name="WalletXpubRoot"
component={WalletXpubStackRoot}
@ -358,15 +368,6 @@ const DetailViewStackScreensStack = () => {
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }}
/>
<DetailViewStack.Screen name="ReceiveDetailsRoot" component={ReceiveDetailsStackRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen
name="ScanQRCodeRoot"
component={ScanQRCodeStackRoot}
options={{
headerShown: false,
presentation: 'fullScreenModal',
statusBarHidden: true,
}}
/>
<DetailViewStack.Screen
name="ManageWallets"
component={ManageWallets}

View file

@ -2,10 +2,23 @@ import { LightningTransaction, Transaction, TWallet } from '../class/wallets/typ
import { ElectrumServerItem } from '../screen/settings/ElectrumSettings';
import { SendDetailsParams } from './SendDetailsStackParamList';
export type ScanQRCodeParamList = {
cameraStatusGranted?: boolean;
backdoorPressed?: boolean;
launchedBy?: string;
urTotal?: number;
urHave?: number;
backdoorText?: string;
onBarScanned?: (data: string) => void;
showFileImportButton?: boolean;
backdoorVisible?: boolean;
animatedQRCodeData?: Record<string, any>;
};
export type DetailViewStackParamList = {
UnlockWithScreen: undefined;
WalletsList: { scannedData?: string };
WalletTransactions: { isLoading?: boolean; walletID: string; walletType: string };
WalletsList: { onBarScanned?: string };
WalletTransactions: { isLoading?: boolean; walletID: string; walletType: string; onBarScanned?: string };
WalletDetails: { walletID: string };
TransactionDetails: { tx: Transaction; hash: string; walletID: string };
TransactionStatus: { hash: string; walletID?: string };
@ -19,8 +32,8 @@ export type DetailViewStackParamList = {
LNDViewInvoice: { invoice: LightningTransaction; walletID: string };
LNDViewAdditionalInvoiceInformation: { invoiceId: string };
LNDViewAdditionalInvoicePreImage: { invoiceId: string };
Broadcast: { scannedData?: string };
IsItMyAddress: { address?: string };
Broadcast: { onBarScanned?: string };
IsItMyAddress: { address?: string; onBarScanned?: string };
GenerateWord: undefined;
LnurlPay: undefined;
LnurlPaySuccess: {
@ -57,12 +70,13 @@ export type DetailViewStackParamList = {
NetworkSettings: undefined;
About: undefined;
DefaultView: undefined;
ElectrumSettings: { server?: ElectrumServerItem };
ElectrumSettings: { server?: ElectrumServerItem; onBarScanned?: string };
SettingsBlockExplorer: undefined;
EncryptStorage: undefined;
Language: undefined;
LightningSettings: {
url?: string;
onBarScanned?: string;
};
NotificationSettings: undefined;
SelfTest: undefined;
@ -85,22 +99,7 @@ export type DetailViewStackParamList = {
address: string;
};
};
ScanQRCodeRoot: {
screen: string;
params: {
isLoading: false;
cameraStatusGranted?: boolean;
backdoorPressed?: boolean;
launchedBy?: string;
urTotal?: number;
urHave?: number;
backdoorText?: string;
onDismiss?: () => void;
showFileImportButton: true;
backdoorVisible?: boolean;
animatedQRCodeData?: Record<string, any>;
};
};
ScanQRCode: ScanQRCodeParamList;
PaymentCodeList: {
paymentCode: string;
walletID: string;

View file

@ -10,6 +10,7 @@ import {
LNDViewInvoiceComponent,
SelectWalletComponent,
} from './LazyLoadLNDCreateInvoiceStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const Stack = createNativeStackNavigator();
@ -54,6 +55,16 @@ const LNDCreateInvoiceRoot = () => {
component={LNDViewAdditionalInvoicePreImageComponent}
options={navigationStyle({ title: loc.lndViewInvoice.additional_info })(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View file

@ -11,6 +11,7 @@ import {
SelectWalletComponent,
SuccessComponent,
} from './LazyLoadScanLndInvoiceStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const Stack = createNativeStackNavigator();
@ -50,6 +51,16 @@ const ScanLndInvoiceRoot = () => {
gestureEnabled: false,
})(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View file

@ -1,28 +0,0 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import navigationStyle from '../components/navigationStyle';
import { useTheme } from '../components/themes';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const Stack = createNativeStackNavigator();
const ScanQRCodeStackRoot = () => {
const theme = useTheme();
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
})(theme)}
/>
</Stack.Navigator>
);
};
export default ScanQRCodeStackRoot;

View file

@ -18,6 +18,7 @@ import {
import { SendDetailsStackParamList } from './SendDetailsStackParamList';
import HeaderRightButton from '../components/HeaderRightButton';
import { BitcoinUnit } from '../models/bitcoinUnits';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const Stack = createNativeStackNavigator<SendDetailsStackParamList>();
@ -81,6 +82,16 @@ const SendDetailsStack = () => {
component={PaymentCodesListComponent}
options={navigationStyle({ title: loc.bip47.contacts })(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View file

@ -1,6 +1,7 @@
import { Psbt } from 'bitcoinjs-lib';
import { CreateTransactionTarget, CreateTransactionUtxo, TWallet } from '../class/wallets/types';
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
import { ScanQRCodeParamList } from './DetailViewStackParamList';
export type SendDetailsParams = {
transactionMemo?: string;
@ -12,6 +13,7 @@ export type SendDetailsParams = {
address?: string;
amount?: number;
amountSats?: number;
onBarScanned?: string;
unit?: BitcoinUnit;
noRbf?: boolean;
walletID: string;
@ -84,4 +86,5 @@ export type SendDetailsStackParamList = {
PaymentCodeList: {
walletID: string;
};
ScanQRCode: ScanQRCodeParamList;
};

View file

@ -5,11 +5,15 @@ import navigationStyle from '../components/navigationStyle';
import { useTheme } from '../components/themes';
import loc from '../loc';
import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
import { ScanQRCodeParamList } from './DetailViewStackParamList';
export type ViewEditMultisigCosignersStackParamList = {
ViewEditMultisigCosigners: {
walletID: string;
onBarScanned?: string;
};
ScanQRCode: ScanQRCodeParamList;
};
const Stack = createNativeStackNavigator<ViewEditMultisigCosignersStackParamList>();
@ -27,6 +31,16 @@ const ViewEditMultisigCosignersStackRoot = () => {
title: loc.multisig.manage_keys,
})(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

2169
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -59,7 +59,7 @@
"android:clean": "cd android; ./gradlew clean ; cd .. ; npm run android",
"ios": "react-native run-ios",
"postinstall": "rn-nodeify --install buffer,events,process,stream,inherits,path,assert,crypto --hack; npm run releasenotes2json; npm run branch2json; npm run patches",
"patches": "patch -p1 < scripts/react-native-camera-kit.patch;",
"patches": "",
"test": "npm run tslint && npm run lint && npm run unit && npm run jest",
"jest": "jest tests/integration/*",
"e2e:debug-build": "detox build -c android.debug",
@ -82,9 +82,9 @@
"@ngraveio/bc-ur": "1.1.13",
"@noble/secp256k1": "1.6.3",
"@react-native-async-storage/async-storage": "2.1.0",
"@react-native-clipboard/clipboard": "1.15.0",
"@react-native-clipboard/clipboard": "1.16.0",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#4e47b33",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#14bab79",
"@react-native/gradle-plugin": "0.75.4",
"@react-native/metro-config": "0.75.4",
"@react-navigation/drawer": "6.7.2",
@ -117,7 +117,7 @@
"electrum-mnemonic": "2.0.0",
"events": "3.3.0",
"junderw-crc32c": "1.2.0",
"lottie-react-native": "7.1.0",
"lottie-react-native": "7.2.1",
"path-browserify": "1.0.1",
"payjoin-client": "1.0.1",
"process": "0.11.10",
@ -127,12 +127,12 @@
"react-native": "0.75.4",
"react-native-biometrics": "3.0.1",
"react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442",
"react-native-camera-kit": "13.0.0",
"react-native-camera-kit": "14.1.0",
"react-native-crypto": "2.2.0",
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
"react-native-device-info": "13.2.0",
"react-native-document-picker": "9.3.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
"react-native-draglist": "github:BlueWallet/react-native-draglist#a4af02f",
"react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.21.2",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
@ -142,14 +142,14 @@
"react-native-keychain": "9.1.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.3.0",
"react-native-permissions": "5.2.1",
"react-native-permissions": "5.2.2",
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
"react-native-push-notification": "8.1.1",
"react-native-qrcode-svg": "6.3.2",
"react-native-quick-actions": "0.3.13",
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.16.5",
"react-native-reanimated": "3.16.6",
"react-native-safe-area-context": "4.14.1",
"react-native-screen-capture": "github:BlueWallet/react-native-screen-capture#18cb79f",
"react-native-screens": "3.35.0",

View file

@ -24,7 +24,6 @@ import AmountInput from '../../components/AmountInput';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc, { formatBalance, formatBalancePlain, formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import * as NavigationService from '../../NavigationService';
@ -36,7 +35,7 @@ const LNDCreateInvoice = () => {
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
const { walletID, uri } = useRoute().params;
const wallet = useRef(wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN));
const { name } = useRoute();
const { params } = useRoute();
const { colors } = useTheme();
const { navigate, getParent, goBack, pop, setParams } = useNavigation();
const [unit, setUnit] = useState(wallet.current?.getPreferredBalanceUnit() || BitcoinUnit.BTC);
@ -75,6 +74,100 @@ const LNDCreateInvoice = () => {
},
});
const processLnurl = useCallback(
async data => {
setIsLoading(true);
if (!wallet.current) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.wallets.no_ln_wallet_error });
return goBack();
}
// decoding the lnurl
const url = Lnurl.getUrlFromLnurl(data);
const { query } = parse(url, true);
if (query.tag === Lnurl.TAG_LOGIN_REQUEST) {
navigate('LnurlAuth', {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
});
return;
}
// calling the url
try {
const resp = await fetch(url, { method: 'GET' });
if (resp.status >= 300) {
throw new Error('Bad response from server');
}
const reply = await resp.json();
if (reply.status === 'ERROR') {
throw new Error('Reply from server: ' + reply.reason);
}
if (reply.tag === Lnurl.TAG_PAY_REQUEST) {
// we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates
// invoices (including through lnurl-withdraw)
navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPay',
params: {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
},
});
return;
}
if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) {
throw new Error('Unsupported lnurl');
}
// amount that comes from lnurl is always in sats
let newAmount = (reply.maxWithdrawable / 1000).toString();
const sats = newAmount;
switch (unit) {
case BitcoinUnit.SATS:
// nop
break;
case BitcoinUnit.BTC:
newAmount = satoshiToBTC(newAmount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
newAmount = formatBalancePlain(newAmount, BitcoinUnit.LOCAL_CURRENCY);
AmountInput.setCachedSatoshis(newAmount, sats);
break;
}
// setting the invoice creating screen with the parameters
setLNURLParams({
k1: reply.k1,
callback: reply.callback,
fixed: reply.minWithdrawable === reply.maxWithdrawable,
min: (reply.minWithdrawable || 0) / 1000,
max: reply.maxWithdrawable / 1000,
});
setAmount(newAmount);
setDescription(reply.defaultDescription);
setIsLoading(false);
} catch (Err) {
Keyboard.dismiss();
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: Err.message });
}
},
[goBack, navigate, unit, walletID],
);
useEffect(() => {
const data = params.onBarScanned;
if (data) {
processLnurl(data);
setParams({ onBarScanned: undefined });
}
}, [params.onBarScanned, processLnurl, setParams]);
useEffect(() => {
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', _keyboardDidShow);
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', _keyboardDidHide);
@ -228,89 +321,6 @@ const LNDCreateInvoice = () => {
}
};
const processLnurl = async data => {
setIsLoading(true);
if (!wallet.current) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.wallets.no_ln_wallet_error });
return goBack();
}
// decoding the lnurl
const url = Lnurl.getUrlFromLnurl(data);
const { query } = parse(url, true);
if (query.tag === Lnurl.TAG_LOGIN_REQUEST) {
navigate('LnurlAuth', {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
});
return;
}
// calling the url
try {
const resp = await fetch(url, { method: 'GET' });
if (resp.status >= 300) {
throw new Error('Bad response from server');
}
const reply = await resp.json();
if (reply.status === 'ERROR') {
throw new Error('Reply from server: ' + reply.reason);
}
if (reply.tag === Lnurl.TAG_PAY_REQUEST) {
// we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates
// invoices (including through lnurl-withdraw)
navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPay',
params: {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
},
});
return;
}
if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) {
throw new Error('Unsupported lnurl');
}
// amount that comes from lnurl is always in sats
let newAmount = (reply.maxWithdrawable / 1000).toString();
const sats = newAmount;
switch (unit) {
case BitcoinUnit.SATS:
// nop
break;
case BitcoinUnit.BTC:
newAmount = satoshiToBTC(newAmount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
newAmount = formatBalancePlain(newAmount, BitcoinUnit.LOCAL_CURRENCY);
AmountInput.setCachedSatoshis(newAmount, sats);
break;
}
// setting the invoice creating screen with the parameters
setLNURLParams({
k1: reply.k1,
callback: reply.callback,
fixed: reply.minWithdrawable === reply.maxWithdrawable,
min: (reply.minWithdrawable || 0) / 1000,
max: reply.maxWithdrawable / 1000,
});
setAmount(newAmount);
setDescription(reply.defaultDescription);
setIsLoading(false);
} catch (Err) {
Keyboard.dismiss();
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: Err.message });
}
};
const renderCreateButton = () => {
return (
<View style={styles.createButton}>
@ -320,7 +330,9 @@ const LNDCreateInvoice = () => {
};
const navigateToScanQRCode = () => {
scanQrHelper(name, true, processLnurl);
navigate('ScanQRCode', {
showFileImportButton: true,
});
Keyboard.dismiss();
};

View file

@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { useNavigationState, useRoute } from '@react-navigation/native';
import { RouteProp, useNavigation, useNavigationState, useRoute } from '@react-navigation/native';
import { BackHandler, I18nManager, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Icon } from '@rneui/themed';
import Share from 'react-native-share';
@ -16,18 +16,28 @@ import { SuccessView } from '../send/success';
import LNDCreateInvoice from './lndCreateInvoice';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import BigNumber from 'bignumber.js';
import { LightningTransaction } from '../../class/wallets/types';
import dayjs from 'dayjs';
type LNDViewInvoiceRouteParams = {
walletID: string;
invoice: LightningTransaction;
};
const LNDViewInvoice = () => {
const { invoice, walletID } = useRoute().params;
const { invoice, walletID } = useRoute<RouteProp<{ params: LNDViewInvoiceRouteParams }, 'params'>>().params;
const { wallets, setSelectedWalletID, fetchAndSaveWalletTransactions } = useStorage();
const wallet = wallets.find(w => w.getID() === walletID);
const { colors, closeImage } = useTheme();
const { goBack, navigate, setParams, setOptions, getParent } = useExtendedNavigation();
const { goBack, navigate, setParams, setOptions } = useExtendedNavigation();
const navigation = useNavigation();
const wallet = wallets.find(w => w.getID() === walletID);
const [isLoading, setIsLoading] = useState(typeof invoice === 'string');
const [isFetchingInvoices, setIsFetchingInvoices] = useState(true);
const [invoiceStatusChanged, setInvoiceStatusChanged] = useState(false);
const [qrCodeSize, setQRCodeSize] = useState(90);
const fetchInvoiceInterval = useRef();
const [isFetchingInvoices, setIsFetchingInvoices] = useState<boolean>(true);
const [invoiceStatusChanged, setInvoiceStatusChanged] = useState<boolean>(false);
const [qrCodeSize, setQRCodeSize] = useState<number>(90);
const fetchInvoiceInterval = useRef<any>();
const isModal = useNavigationState(state => state.routeNames[0] === LNDCreateInvoice.routeName);
const stylesHook = StyleSheet.create({
@ -65,14 +75,13 @@ const LNDViewInvoice = () => {
},
gestureEnabled: false,
headerBackVisible: false,
// eslint-disable-next-line react/no-unstable-nested-components
headerRight: () => (
<TouchableOpacity
accessibilityRole="button"
style={styles.button}
onPress={() => {
getParent().pop();
// @ts-ignore: navigation
navigation?.getParent().pop();
}}
testID="NavigationCloseButton"
>
@ -91,17 +100,21 @@ const LNDViewInvoice = () => {
}, [colors, isModal]);
useEffect(() => {
if (!wallet) {
return;
}
setSelectedWalletID(walletID);
console.log('LNDViewInvoice - useEffect');
if (!invoice.ispaid) {
fetchInvoiceInterval.current = setInterval(async () => {
if (isFetchingInvoices) {
try {
// @ts-ignore - getUserInvoices is not set on TWallet
const userInvoices = await wallet.getUserInvoices(20);
// fetching only last 20 invoices
// for invoice that was created just now - that should be enough (it is basically the last one, so limit=1 would be sufficient)
// but that might not work as intended IF user creates 21 invoices, and then tries to check the status of invoice #0, it just wont be updated
const updatedUserInvoice = userInvoices.filter(filteredInvoice =>
const updatedUserInvoice = userInvoices.filter((filteredInvoice: LightningTransaction) =>
typeof invoice === 'object'
? filteredInvoice.payment_request === invoice.payment_request
: filteredInvoice.payment_request === invoice,
@ -143,7 +156,7 @@ const LNDViewInvoice = () => {
}, []);
const handleBackButton = () => {
goBack(null);
goBack();
return true;
};
@ -174,7 +187,7 @@ const LNDViewInvoice = () => {
}
}, [invoiceStatusChanged]);
const onLayout = e => {
const onLayout = (e: any) => {
const { height, width } = e.nativeEvent.layout;
setQRCodeSize(height > width ? width - 40 : e.nativeEvent.layout.width / 1.8);
};
@ -187,31 +200,40 @@ const LNDViewInvoice = () => {
</View>
);
}
if (typeof invoice === 'object') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = invoice.timestamp + invoice.expire_time;
const invoiceExpiration = invoice?.timestamp && invoice?.expire_time ? invoice.timestamp + invoice.expire_time : undefined;
if (invoice.ispaid || invoice.type === 'paid_invoice') {
let amount = 0;
if (invoice.type === 'paid_invoice' && invoice.value) {
let description;
let invoiceDate;
if (invoice.type === 'paid_invoice' && invoice?.value) {
amount = invoice.value;
} else if (invoice.type === 'user_invoice' && invoice.amt) {
amount = invoice.amt;
}
let description = invoice.description;
if (invoice.memo && invoice.memo.length > 0) {
if (invoice.description) {
description = invoice.description;
} else if (invoice.memo && invoice.memo.length > 0) {
description = invoice.memo;
}
if (invoice.timestamp) {
invoiceDate = dayjs(invoice.timestamp * (String(invoice.timestamp).length === 10 ? 1000 : 1)).format('LLL');
}
return (
<View style={styles.root}>
<SuccessView
amount={amount}
amountUnit={BitcoinUnit.SATS}
invoiceDescription={description}
shouldAnimate={invoiceStatusChanged}
fee={invoice.fee ? new BigNumber(invoice.fee).multipliedBy(-1).dividedBy(1e8).toNumber() : undefined}
shouldAnimate={false}
/>
<View style={styles.detailsRoot}>
<Text style={[styles.detailsText, stylesHook.detailsText]}>
{loc.lndViewInvoice.date_time}: {invoiceDate}
</Text>
{invoice.payment_preimage && typeof invoice.payment_preimage === 'string' ? (
<TouchableOpacity accessibilityRole="button" style={styles.detailsTouch} onPress={navigateToPreImageScreen}>
<Text style={[styles.detailsText, stylesHook.detailsText]}>{loc.send.create_details}</Text>
@ -227,7 +249,7 @@ const LNDViewInvoice = () => {
</View>
);
}
if (invoiceExpiration < now) {
if (invoiceExpiration ? invoiceExpiration < now : undefined) {
return (
<View style={[styles.root, stylesHook.root, styles.justifyContentCenter]}>
<View style={[styles.expired, stylesHook.expired]}>
@ -238,40 +260,42 @@ const LNDViewInvoice = () => {
);
}
// Invoice has not expired, nor has it been paid for.
return (
<ScrollView>
<View style={[styles.activeRoot, stylesHook.root]}>
<View style={styles.activeQrcode}>
<QRCodeComponent value={invoice.payment_request} size={qrCodeSize} />
</View>
<BlueSpacing20 />
<BlueText>
{loc.lndViewInvoice.please_pay} {invoice.amt} {loc.lndViewInvoice.sats}
</BlueText>
{'description' in invoice && invoice.description.length > 0 && (
if (invoice.payment_request) {
return (
<ScrollView>
<View style={[styles.activeRoot, stylesHook.root]}>
<View style={styles.activeQrcode}>
<QRCodeComponent value={invoice.payment_request} size={qrCodeSize} />
</View>
<BlueSpacing20 />
<BlueText>
{loc.lndViewInvoice.for} {invoice.description}
{loc.lndViewInvoice.please_pay} {invoice.amt} {loc.lndViewInvoice.sats}
</BlueText>
)}
<CopyTextToClipboard truncated text={invoice.payment_request} />
<Button onPress={handleOnSharePressed} title={loc.receive.details_share} />
<BlueSpacing20 />
<Button
style={stylesHook.additionalInfo}
onPress={handleOnViewAdditionalInformationPressed}
title={loc.lndViewInvoice.additional_info}
/>
</View>
</ScrollView>
);
{'description' in invoice && (invoice.description?.length ?? 0) > 0 && (
<BlueText>
{loc.lndViewInvoice.for} {invoice.description ?? ''}
</BlueText>
)}
<CopyTextToClipboard truncated text={invoice.payment_request} />
<Button onPress={handleOnSharePressed} title={loc.receive.details_share} />
<BlueSpacing20 />
<Button
style={stylesHook.additionalInfo}
onPress={handleOnViewAdditionalInformationPressed}
title={loc.lndViewInvoice.additional_info}
/>
</View>
</ScrollView>
);
}
}
};
return <SafeArea onLayout={onLayout}>{render()}</SafeArea>;
};
export default LNDViewInvoice;
const styles = StyleSheet.create({
root: {
flex: 1,
@ -312,5 +336,3 @@ const styles = StyleSheet.create({
marginHorizontal: 16,
},
});
export default LNDViewInvoice;

View file

@ -18,13 +18,14 @@ import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
const ScanLndInvoice = () => {
const { wallets, fetchAndSaveWalletTransactions } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { colors } = useTheme();
const route = useRoute();
const { walletID, uri, invoice } = useRoute().params;
const name = useRoute().name;
/** @type {LightningCustodianWallet} */
const [wallet, setWallet] = useState(
wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN),
@ -281,6 +282,25 @@ const ScanLndInvoice = () => {
pop();
};
const onBarScanned = useCallback(
value => {
if (!value) return;
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
navigate(...completionValue);
});
},
[navigate],
);
useEffect(() => {
const data = route.params?.onBarScanned;
if (data) {
onBarScanned(data);
setParams({ onBarScanned: undefined });
}
}, [navigate, onBarScanned, route.params?.onBarScanned, setParams]);
if (wallet === undefined || !wallet) {
return (
<View style={[styles.loadingIndicator, stylesHook.root]}>
@ -323,7 +343,6 @@ const ScanLndInvoice = () => {
isLoading={isLoading}
placeholder={loc.lnd.placeholder}
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}
launchedBy={name}
onBlur={onBlur}
keyboardType="email-address"
style={styles.addressInput}

View file

@ -166,11 +166,12 @@ const ReceiveDetails = () => {
}
}, [showConfirmedBalance]);
const isBIP47Enabled = wallet?.isBIP47Enabled();
const toolTipActions = useMemo(() => {
const action = CommonToolTipActions.PaymentsCode;
action.menuState = wallet?.isBIP47Enabled();
const action = { ...CommonToolTipActions.PaymentsCode };
action.menuState = isBIP47Enabled;
return [action];
}, [wallet]);
}, [isBIP47Enabled]);
const onPressMenuItem = useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
@ -203,10 +204,9 @@ const ReceiveDetails = () => {
useEffect(() => {
wallet?.allowBIP47() &&
wallet?.isBIP47Enabled() &&
setOptions({
headerLeft: () => (wallet?.isBIP47Enabled() ? null : HeaderLeft),
headerRight: () => (wallet?.isBIP47Enabled() ? HeaderLeft : HeaderRight),
headerLeft: () => HeaderLeft,
headerRight: () => HeaderRight,
});
}, [HeaderLeft, HeaderRight, colors.foregroundColor, setOptions, wallet]);

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useRoute, RouteProp } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import { ActivityIndicator, Keyboard, Linking, StyleSheet, TextInput, View } from 'react-native';
@ -19,11 +19,12 @@ import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useSettings } from '../../hooks/context/useSettings';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
import { navigate } from '../../NavigationService';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const BROADCAST_RESULT = Object.freeze({
none: 'Input transaction hex',
@ -35,12 +36,13 @@ const BROADCAST_RESULT = Object.freeze({
type RouteProps = RouteProp<DetailViewStackParamList, 'Broadcast'>;
const Broadcast: React.FC = () => {
const { name, params } = useRoute<RouteProps>();
const { params } = useRoute<RouteProps>();
const [tx, setTx] = useState<string | undefined>();
const [txHex, setTxHex] = useState<string | undefined>();
const { colors } = useTheme();
const [broadcastResult, setBroadcastResult] = useState<string>(BROADCAST_RESULT.none);
const { selectedBlockExplorer } = useSettings();
const { setParams } = useExtendedNavigation();
const stylesHooks = StyleSheet.create({
input: {
@ -50,13 +52,26 @@ const Broadcast: React.FC = () => {
},
});
const handleScannedData = useCallback((scannedData: string) => {
if (scannedData.indexOf('+') === -1 && scannedData.indexOf('=') === -1 && scannedData.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
return handleUpdateTxHex(scannedData);
}
try {
// should be base64 encoded PSBT
const validTx = bitcoin.Psbt.fromBase64(scannedData).extractTransaction();
return handleUpdateTxHex(validTx.toHex());
} catch (e) {}
}, []);
useEffect(() => {
const scannedData = params?.scannedData;
const scannedData = params?.onBarScanned;
if (scannedData) {
handleScannedData(scannedData);
setParams({ onBarScanned: undefined });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.scannedData]);
}, [handleScannedData, params?.onBarScanned, setParams]);
const handleUpdateTxHex = (nextValue: string) => setTxHex(nextValue.trim());
@ -88,21 +103,8 @@ const Broadcast: React.FC = () => {
}
};
const handleScannedData = (scannedData: string) => {
if (scannedData.indexOf('+') === -1 && scannedData.indexOf('=') === -1 && scannedData.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
return handleUpdateTxHex(scannedData);
}
try {
// should be base64 encoded PSBT
const validTx = bitcoin.Psbt.fromBase64(scannedData).extractTransaction();
return handleUpdateTxHex(validTx.toHex());
} catch (e) {}
};
const handleQRScan = () => {
scanQrHelper(name, true, undefined, false);
navigate('ScanQRCode');
};
let status;

View file

@ -2,23 +2,18 @@ import { useFocusEffect, useIsFocused, useNavigation, useRoute } from '@react-na
import * as bitcoin from 'bitcoinjs-lib';
import createHash from 'create-hash';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, Image, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import { CameraScreen } from 'react-native-camera-kit';
import { Icon } from '@rneui/themed';
import { launchImageLibrary } from 'react-native-image-picker';
import { Alert, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import Base43 from '../../blue_modules/base43';
import * as fs from '../../blue_modules/fs';
import { BlueURDecoder, decodeUR, extractSingleWorkload } from '../../blue_modules/ur';
import { BlueLoading, BlueSpacing40, BlueText } from '../../BlueComponents';
import { openPrivacyDesktopSettings } from '../../class/camera';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import RNQRGenerator from 'rn-qr-generator';
import CameraScreen from '../../components/CameraScreen';
let decoder = false;
@ -27,39 +22,6 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#000000',
},
closeTouch: {
width: 40,
height: 40,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 16,
top: 55,
},
closeImage: {
alignSelf: 'center',
},
imagePickerTouch: {
width: 40,
height: 40,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 24,
bottom: 48,
},
filePickerTouch: {
width: 40,
height: 40,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 96,
bottom: 48,
},
openSettingsContainer: {
flex: 1,
justifyContent: 'center',
@ -71,6 +33,9 @@ const styles = StyleSheet.create({
height: 60,
backgroundColor: 'rgba(0,0,0,0.01)',
position: 'absolute',
top: 10,
left: '50%',
transform: [{ translateX: -30 }],
},
backdoorInputWrapper: { position: 'absolute', left: '5%', top: '0%', width: '90%', height: '70%', backgroundColor: 'white' },
progressWrapper: { position: 'absolute', alignSelf: 'center', alignItems: 'center', top: '50%', padding: 8, borderRadius: 8 },
@ -89,7 +54,11 @@ const ScanQRCode = () => {
const { setIsDrawerShouldHide } = useSettings();
const navigation = useNavigation();
const route = useRoute();
const { launchedBy, onBarScanned, onDismiss, showFileImportButton } = route.params;
const navigationState = navigation.getState();
const previousRoute = navigationState.routes[navigationState.routes.length - 2];
const defaultLaunchedBy = previousRoute ? previousRoute.name : undefined;
const { launchedBy = defaultLaunchedBy, onBarScanned, showFileImportButton } = route.params || {};
const scannedCache = {};
const { colors } = useTheme();
const isFocused = useIsFocused();
@ -139,13 +108,11 @@ const ScanQRCode = () => {
const data = decoder.toString();
decoder = false; // nullify for future use (?)
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge });
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
}
onBarScanned && onBarScanned({ data });
} else {
setUrTotal(100);
setUrHave(Math.floor(decoder.estimatedPercentComplete() * 100));
@ -192,13 +159,11 @@ const ScanQRCode = () => {
data = Buffer.from(payload, 'hex').toString();
}
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge });
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
}
onBarScanned && onBarScanned({ data });
} else {
setAnimatedQRCodeData(animatedQRCodeData);
}
@ -259,13 +224,12 @@ const ScanQRCode = () => {
bitcoin.Psbt.fromHex(hex); // if it doesnt throw - all good
const data = Buffer.from(hex, 'hex').toString('base64');
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge });
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
}
onBarScanned && onBarScanned({ data });
return;
} catch (_) {}
@ -273,13 +237,12 @@ const ScanQRCode = () => {
setIsLoading(true);
try {
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: { scannedData: ret.data }, merge });
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: ret.data }, merge });
} else {
onBarScanned && onBarScanned(ret.data);
}
onBarScanned && onBarScanned(ret.data);
} catch (e) {
console.log(e);
}
@ -294,59 +257,19 @@ const ScanQRCode = () => {
setIsLoading(false);
};
const showImagePicker = () => {
const onShowImagePickerButtonPress = () => {
if (!isLoading) {
setIsLoading(true);
launchImageLibrary(
{
title: null,
mediaType: 'photo',
takePhotoButtonTitle: null,
maxHeight: 800,
maxWidth: 600,
selectionLimit: 1,
},
response => {
if (response.didCancel) {
setIsLoading(false);
} else {
const asset = response.assets[0];
if (asset.uri) {
RNQRGenerator.detect({
uri: decodeURI(asset.uri.toString()),
})
.then(result => {
if (result) {
onBarCodeRead({ data: result.values[0] });
}
})
.catch(error => {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
})
.finally(() => {
setIsLoading(false);
});
} else {
setIsLoading(false);
}
}
},
);
fs.showImagePickerAndReadImage()
.then(data => {
if (data) onBarCodeRead({ data });
})
.finally(() => setIsLoading(false));
}
};
const dismiss = () => {
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: {}, merge });
} else {
navigation.goBack();
}
if (onDismiss) onDismiss();
navigation.goBack();
};
const render = isLoading ? (
@ -358,38 +281,21 @@ const ScanQRCode = () => {
<BlueText>{loc.send.permission_camera_message}</BlueText>
<BlueSpacing40 />
<Button title={loc.send.open_settings} onPress={openPrivacyDesktopSettings} />
<BlueSpacing40 />
<Button title={loc._.cancel} onPress={dismiss} />
</View>
) : isFocused ? (
<CameraScreen
scanBarcode
torchOffImage={require('../../img/flash-off.png')}
torchOnImage={require('../../img/flash-on.png')}
cameraFlipImage={require('../../img/camera-rotate-solid.png')}
onReadCode={event => onBarCodeRead({ data: event?.nativeEvent?.codeStringValue })}
showFrame={false}
showFilePickerButton={showFileImportButton}
showImagePickerButton={true}
onFilePickerButtonPress={showFilePicker}
onImagePickerButtonPress={onShowImagePickerButtonPress}
onCancelButtonPress={dismiss}
/>
) : null}
<TouchableOpacity accessibilityRole="button" accessibilityLabel={loc._.close} style={styles.closeTouch} onPress={dismiss}>
<Image style={styles.closeImage} source={require('../../img/close-white.png')} />
</TouchableOpacity>
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_image}
style={styles.imagePickerTouch}
onPress={showImagePicker}
>
<Icon name="image" type="font-awesome" color="#ffffff" />
</TouchableOpacity>
{showFileImportButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_file}
style={styles.filePickerTouch}
onPress={showFilePicker}
>
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
</TouchableOpacity>
)}
{urTotal > 0 && (
<View style={[styles.progressWrapper, stylesHook.progressWrapper]} testID="UrProgressBar">
<BlueText>{loc.wallets.please_continue_scanning}</BlueText>

View file

@ -1,5 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { RouteProp, StackActions, useFocusEffect, useRoute } from '@react-navigation/native';
import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -39,7 +39,6 @@ import Button from '../../components/Button';
import CoinsSelected from '../../components/CoinsSelected';
import InputAccessoryAllFunds, { InputAccessoryAllFundsAccessoryViewID } from '../../components/InputAccessoryAllFunds';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
@ -56,7 +55,7 @@ import { useKeyboard } from '../../hooks/useKeyboard';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import ActionSheet from '../ActionSheet';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { CommonToolTipActions, ToolTipAction } from '../../typings/CommonToolTipActions';
import { Action } from '../../components/types';
interface IPaymentDestinations {
@ -64,6 +63,7 @@ interface IPaymentDestinations {
amountSats?: number | string;
amount?: string | number | 'MAX';
key: string; // random id to look up this record
unit: BitcoinUnit;
}
interface IFee {
@ -78,6 +78,7 @@ type RouteProps = RouteProp<SendDetailsStackParamList, 'SendDetails'>;
const SendDetails = () => {
const { wallets, setSelectedWalletID, sleep, txMetadata, saveToDisk } = useStorage();
const navigation = useExtendedNavigation<NavigationProps>();
const selectedDataProcessor = useRef<ToolTipAction | undefined>();
const setParams = navigation.setParams;
const route = useRoute<RouteProps>();
const name = route.name;
@ -92,7 +93,6 @@ const SendDetails = () => {
const scrollView = useRef<FlatList<any>>(null);
const scrollIndex = useRef(0);
const { colors } = useTheme();
const popAction = StackActions.pop(1);
// state
const [width, setWidth] = useState(Dimensions.get('window').width);
@ -100,8 +100,9 @@ const SendDetails = () => {
const [wallet, setWallet] = useState<TWallet | null>(null);
const feeModalRef = useRef<BottomModalHandle>(null);
const { isVisible } = useKeyboard();
const [addresses, setAddresses] = useState<IPaymentDestinations[]>([]);
const [units, setUnits] = useState<BitcoinUnit[]>([]);
const [addresses, setAddresses] = useState<IPaymentDestinations[]>([
{ address: '', key: String(Math.random()), unit: amountUnit } as IPaymentDestinations,
]);
const [networkTransactionFees, setNetworkTransactionFees] = useState(new NetworkTransactionFee(3, 2, 1));
const [networkTransactionFeesIsLoading, setNetworkTransactionFeesIsLoading] = useState(false);
const [customFee, setCustomFee] = useState<string | null>(null);
@ -144,9 +145,9 @@ const SendDetails = () => {
try {
const { address, amount, memo, payjoinUrl: pjUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(routeParams.uri);
setUnits(u => {
u[scrollIndex.current] = BitcoinUnit.BTC; // also resetting current unit to BTC
return [...u];
setAddresses(addrs => {
addrs[scrollIndex.current].unit = BitcoinUnit.BTC;
return [...addrs];
});
setAddresses(addrs => {
@ -178,15 +179,12 @@ const SendDetails = () => {
if (currentAddress && currentAddress.address && routeParams.address) {
currentAddress.address = routeParams.address;
value[scrollIndex.current] = currentAddress;
value[scrollIndex.current].unit = unit;
return [...value];
} else {
return [...value, { address: routeParams.address, key: String(Math.random()), amount, amountSats }];
}
});
setUnits(u => {
u[scrollIndex.current] = unit;
return [...u];
});
} else if (routeParams.addRecipientParams) {
const index = addresses.length === 0 ? 0 : scrollIndex.current;
const { address, amount } = routeParams.addRecipientParams;
@ -451,9 +449,9 @@ const SendDetails = () => {
addrs[scrollIndex.current].amountSats = new BigNumber(options?.amount ?? 0).multipliedBy(100000000).toNumber();
return [...addrs];
});
setUnits(u => {
u[scrollIndex.current] = BitcoinUnit.BTC; // also resetting current unit to BTC
return [...u];
setAddresses(addrs => {
addrs[scrollIndex.current].unit = BitcoinUnit.BTC;
return [...addrs];
});
setParams({ transactionMemo: options.label || '', amountUnit: BitcoinUnit.BTC, payjoinUrl: options.pj || '' }); // there used to be `options.message` here as well. bug?
// RN Bug: contentOffset gets reset to 0 when state changes. Remove code once this bug is resolved.
@ -666,34 +664,35 @@ const SendDetails = () => {
return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' });
}
const data = await scanQrHelper(route.name, true);
importQrTransactionOnBarScanned(data);
navigateToQRCodeScanner();
};
const importQrTransactionOnBarScanned = (ret: any) => {
navigation.getParent()?.getParent()?.dispatch(popAction);
if (!wallet) return;
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
const importQrTransactionOnBarScanned = useCallback(
(ret: any) => {
if (!wallet) return;
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
// we construct PSBT object and pass to next screen
// so user can do smth with it:
const psbt = bitcoin.Psbt.fromBase64(ret.data);
// we construct PSBT object and pass to next screen
// so user can do smth with it:
const psbt = bitcoin.Psbt.fromBase64(ret.data);
navigation.navigate('PsbtWithHardwareWallet', {
memo: transactionMemo,
walletID: wallet.getID(),
psbt,
});
setIsLoading(false);
}
};
navigation.navigate('PsbtWithHardwareWallet', {
memo: transactionMemo,
walletID: wallet.getID(),
psbt,
});
setIsLoading(false);
}
},
[navigation, transactionMemo, wallet],
);
/**
* watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
@ -777,53 +776,126 @@ const SendDetails = () => {
});
};
const _importTransactionMultisig = async (base64arg: string | false) => {
try {
const base64 = base64arg || (await fs.openSignedTransaction());
if (!base64) return;
const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid
const _importTransactionMultisig = useCallback(
async (base64arg: string | false) => {
try {
const base64 = base64arg || (await fs.openSignedTransaction());
if (!base64) return;
const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid
if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) {
setIsLoading(true);
await sleep(100);
(wallet as MultisigHDWallet).cosignPsbt(psbt);
setIsLoading(false);
await sleep(100);
}
if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) {
setIsLoading(true);
await sleep(100);
(wallet as MultisigHDWallet).cosignPsbt(psbt);
setIsLoading(false);
await sleep(100);
}
if (wallet) {
navigation.navigate('PsbtMultisig', {
memo: transactionMemo,
psbtBase64: psbt.toBase64(),
walletID: wallet.getID(),
});
if (wallet) {
navigation.navigate('PsbtMultisig', {
memo: transactionMemo,
psbtBase64: psbt.toBase64(),
walletID: wallet.getID(),
});
}
} catch (error: any) {
presentAlert({ title: loc.send.problem_with_psbt, message: error.message });
}
} catch (error: any) {
presentAlert({ title: loc.send.problem_with_psbt, message: error.message });
}
setIsLoading(false);
};
setIsLoading(false);
},
[navigation, sleep, transactionMemo, wallet],
);
const importTransactionMultisig = () => {
return _importTransactionMultisig(false);
};
const onBarScanned = (ret: any) => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
return _importTransactionMultisig(ret.data);
}
};
const onBarScanned = useCallback(
(ret: any) => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
return _importTransactionMultisig(ret.data);
}
},
[_importTransactionMultisig],
);
const importTransactionMultisigScanQr = async () => {
const data = await scanQrHelper(route.name, true);
onBarScanned(data);
const handlePsbtSign = useCallback(
async (psbtBase64: string) => {
let tx;
let psbt;
try {
psbt = bitcoin.Psbt.fromBase64(psbtBase64);
tx = (wallet as MultisigHDWallet).cosignPsbt(psbt).tx;
} catch (e: any) {
presentAlert({ title: loc.errors.error, message: e.message });
return;
} finally {
setIsLoading(false);
}
if (!tx || !wallet) return setIsLoading(false);
// we need to remove change address from recipients, so that Confirm screen show more accurate info
const changeAddresses: string[] = [];
// @ts-ignore hacky
for (let c = 0; c < wallet.next_free_change_address_index + wallet.gap_limit; c++) {
// @ts-ignore hacky
changeAddresses.push(wallet._getInternalAddressByIndex(c));
}
const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(String(address)));
navigation.navigate('CreateTransaction', {
fee: new BigNumber(psbt.getFee()).dividedBy(100000000).toNumber(),
feeSatoshi: psbt.getFee(),
wallet,
tx: tx.toHex(),
recipients,
satoshiPerByte: psbt.getFeeRate(),
showAnimatedQr: true,
psbt,
});
},
[navigation, wallet],
);
useEffect(() => {
const data = routeParams.onBarScanned;
if (data) {
if (selectedDataProcessor.current) {
if (
selectedDataProcessor.current === CommonToolTipActions.ImportTransactionQR ||
selectedDataProcessor.current === CommonToolTipActions.CoSignTransaction ||
selectedDataProcessor.current === CommonToolTipActions.SignPSBT
) {
if (selectedDataProcessor.current === CommonToolTipActions.ImportTransactionQR) {
importQrTransactionOnBarScanned(data);
} else if (
selectedDataProcessor.current === CommonToolTipActions.CoSignTransaction ||
selectedDataProcessor.current === CommonToolTipActions.SignPSBT
) {
handlePsbtSign(data);
} else {
onBarScanned(data);
}
} else {
console.log('Unknown selectedDataProcessor:', selectedDataProcessor.current);
}
}
setParams({ onBarScanned: undefined });
}
}, [handlePsbtSign, importQrTransactionOnBarScanned, onBarScanned, routeParams.onBarScanned, setParams]);
const navigateToQRCodeScanner = () => {
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
};
const handleAddRecipient = () => {
@ -892,48 +964,6 @@ const SendDetails = () => {
navigation.navigate('PaymentCodeList', { walletID: wallet.getID() });
};
const handlePsbtSign = async () => {
setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 100)); // sleep for animations
const scannedData = await scanQrHelper(name, true, undefined);
if (!scannedData) return setIsLoading(false);
let tx;
let psbt;
try {
psbt = bitcoin.Psbt.fromBase64(scannedData);
tx = (wallet as MultisigHDWallet).cosignPsbt(psbt).tx;
} catch (e: any) {
presentAlert({ title: loc.errors.error, message: e.message });
return;
} finally {
setIsLoading(false);
}
if (!tx || !wallet) return setIsLoading(false);
// we need to remove change address from recipients, so that Confirm screen show more accurate info
const changeAddresses: string[] = [];
// @ts-ignore hacky
for (let c = 0; c < wallet.next_free_change_address_index + wallet.gap_limit; c++) {
// @ts-ignore hacky
changeAddresses.push(wallet._getInternalAddressByIndex(c));
}
const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(String(address)));
navigation.navigate('CreateTransaction', {
fee: new BigNumber(psbt.getFee()).dividedBy(100000000).toNumber(),
feeSatoshi: psbt.getFee(),
wallet,
tx: tx.toHex(),
recipients,
satoshiPerByte: psbt.getFeeRate(),
showAnimatedQr: true,
psbt,
});
};
// Header Right Button
const headerRightOnPress = (id: string) => {
@ -942,7 +972,8 @@ const SendDetails = () => {
} else if (id === CommonToolTipActions.RemoveRecipient.id) {
handleRemoveRecipient();
} else if (id === CommonToolTipActions.SignPSBT.id) {
handlePsbtSign();
selectedDataProcessor.current = CommonToolTipActions.SignPSBT;
navigateToQRCodeScanner();
} else if (id === CommonToolTipActions.SendMax.id) {
onUseAllPressed();
} else if (id === CommonToolTipActions.AllowRBF.id) {
@ -950,11 +981,13 @@ const SendDetails = () => {
} else if (id === CommonToolTipActions.ImportTransaction.id) {
importTransaction();
} else if (id === CommonToolTipActions.ImportTransactionQR.id) {
selectedDataProcessor.current = CommonToolTipActions.ImportTransactionQR;
importQrTransaction();
} else if (id === CommonToolTipActions.ImportTransactionMultsig.id) {
importTransactionMultisig();
} else if (id === CommonToolTipActions.CoSignTransaction.id) {
importTransactionMultisigScanQr();
selectedDataProcessor.current = CommonToolTipActions.CoSignTransaction;
navigateToQRCodeScanner();
} else if (id === CommonToolTipActions.CoinControl.id) {
handleCoinControl();
} else if (id === CommonToolTipActions.InsertContact.id) {
@ -971,7 +1004,10 @@ const SendDetails = () => {
const recipientActions: Action[] = [
CommonToolTipActions.AddRecipient,
CommonToolTipActions.RemoveRecipient,
{
...CommonToolTipActions.RemoveRecipient,
hidden: addresses.length <= 1,
},
{
...CommonToolTipActions.RemoveAllRecipients,
hidden: !(addresses.length > 1),
@ -1073,9 +1109,9 @@ const SendDetails = () => {
addrs[scrollIndex.current].amountSats = BitcoinUnit.MAX;
return [...addrs];
});
setUnits(u => {
u[scrollIndex.current] = BitcoinUnit.BTC;
return [...u];
setAddresses(addrs => {
addrs[scrollIndex.current].unit = BitcoinUnit.BTC;
return [...addrs];
});
}
});
@ -1206,15 +1242,15 @@ const SendDetails = () => {
addrs[index] = addr;
return [...addrs];
});
setUnits(u => {
u[index] = unit;
return [...u];
setAddresses(addrs => {
addrs[index].unit = unit;
return [...addrs];
});
}}
onChangeText={(text: string) => {
setAddresses(addrs => {
item.amount = text;
switch (units[index] || amountUnit) {
switch (item.unit || amountUnit) {
case BitcoinUnit.BTC:
item.amountSats = btcToSatoshi(item.amount);
break;
@ -1230,7 +1266,7 @@ const SendDetails = () => {
return [...addrs];
});
}}
unit={units[index] || amountUnit}
unit={item.unit || amountUnit}
editable={isEditable}
disabled={!isEditable}
inputAccessoryViewID={InputAccessoryAllFundsAccessoryViewID}
@ -1341,7 +1377,7 @@ const SendDetails = () => {
feeRate={feeRate}
setCustomFee={setCustomFee}
setFeePrecalc={setFeePrecalc}
feeUnit={units[scrollIndex.current]}
feeUnit={addresses[scrollIndex.current]?.unit ?? BitcoinUnit.BTC}
/>
</View>
<DismissKeyboardInputAccessory />

View file

@ -1,6 +1,6 @@
import { useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, ScrollView, StyleSheet, View } from 'react-native';
import { BlueSpacing20 } from '../../BlueComponents';
@ -10,15 +10,14 @@ import SafeArea from '../../components/SafeArea';
import SaveFileButton from '../../components/SaveFileButton';
import { SquareButton } from '../../components/SquareButton';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
const PsbtMultisigQRCode = () => {
const { navigate } = useNavigation();
const { colors } = useTheme();
const openScannerButton = useRef();
const { psbtBase64, isShowOpenScanner } = useRoute().params;
const { name } = useRoute();
const { params } = useRoute();
const { psbtBase64, isShowOpenScanner } = params;
const [isLoading, setIsLoading] = useState(false);
const dynamicQRCode = useRef();
const isFocused = useIsFocused();
@ -45,23 +44,34 @@ const PsbtMultisigQRCode = () => {
}
}, [isFocused]);
const onBarScanned = ret => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
presentAlert({ message: loc.wallets.import_error });
} else {
// psbt base64?
navigate({ name: 'PsbtMultisig', params: { receivedPSBTBase64: ret.data }, merge: true });
}
};
const onBarScanned = useCallback(
ret => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
presentAlert({ message: loc.wallets.import_error });
} else {
// psbt base64?
navigate({ name: 'PsbtMultisig', params: { receivedPSBTBase64: ret.data }, merge: true });
}
},
[navigate],
);
const openScanner = async () => {
const scanned = await scanQrHelper(name, true);
onBarScanned({ data: scanned });
useEffect(() => {
const data = params.onBarScanned;
if (data) {
onBarScanned({ data });
}
}, [onBarScanned, params.onBarScanned]);
const openScanner = () => {
navigate('ScanQRCode', {
showFileImportButton: true,
});
};
const saveFileButtonBeforeOnPress = () => {

View file

@ -1,7 +1,7 @@
import Clipboard from '@react-native-clipboard/clipboard';
import { useIsFocused, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Linking, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
@ -14,7 +14,6 @@ import { DynamicQRCode } from '../../components/DynamicQRCode';
import SaveFileButton from '../../components/SaveFileButton';
import { SecondButton } from '../../components/SecondButton';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useBiometrics, unlockWithBiometrics } from '../../hooks/useBiometrics';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
@ -62,34 +61,40 @@ const PsbtWithHardwareWallet = () => {
},
});
const _combinePSBT = receivedPSBT => {
return wallet.combinePsbt(psbt, receivedPSBT);
};
const _combinePSBT = useCallback(
receivedPSBT => {
return wallet.combinePsbt(psbt, receivedPSBT);
},
[psbt, wallet],
);
const onBarScanned = ret => {
if (ret && !ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
}
if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
setTxHex(ret.data);
return;
}
try {
const Tx = _combinePSBT(ret.data);
setTxHex(Tx.toHex());
if (launchedBy) {
// we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves)
// most likely for LN channel opening
navigation.navigate({ name: launchedBy, params: { psbt }, merge: true });
// ^^^ we just use `psbt` variable sinse it was finalized in the above _combinePSBT()
// (passed by reference)
const onBarScanned = useCallback(
ret => {
if (ret && !ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
}
} catch (Err) {
presentAlert({ message: Err.message });
}
};
if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
setTxHex(ret.data);
return;
}
try {
const Tx = _combinePSBT(ret.data);
setTxHex(Tx.toHex());
if (launchedBy) {
// we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves)
// most likely for LN channel opening
navigation.navigate({ name: launchedBy, params: { psbt }, merge: true });
// ^^^ we just use `psbt` variable sinse it was finalized in the above _combinePSBT()
// (passed by reference)
}
} catch (Err) {
presentAlert({ message: Err.message });
}
},
[_combinePSBT, launchedBy, navigation, psbt],
);
useEffect(() => {
if (isFocused) {
@ -217,11 +222,18 @@ const PsbtWithHardwareWallet = () => {
}
};
const openScanner = async () => {
const data = await scanQrHelper(route.name, true);
useEffect(() => {
const data = route.params.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
}, [navigation, onBarScanned, route.params.onBarScanned]);
const openScanner = async () => {
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
};
if (txHex) return _renderBroadcastHex();

View file

@ -103,7 +103,7 @@ export const SuccessView = ({ amount, amountUnit, fee, invoiceDescription, shoul
</View>
{fee > 0 && (
<Text style={styles.feeText}>
{loc.send.create_fee}: {new BigNumber(fee).toFixed()} {loc.units[BitcoinUnit.BTC]}
{loc.send.create_fee}: {new BigNumber(fee).toFixed(8)} {loc.units[BitcoinUnit.BTC]}
</Text>
)}
<Text numberOfLines={0} style={styles.feeText}>

View file

@ -6,7 +6,6 @@ import { BlueCard, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComp
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import {
DoneAndDismissKeyboardInputAccessory,
@ -22,27 +21,30 @@ import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { Divider } from '@rneui/themed';
import { Header } from '../../components/Header';
import AddressInput from '../../components/AddressInput';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { GROUP_IO_BLUEWALLET } from '../../blue_modules/currency';
import { Action } from '../../components/types';
import ListItem, { PressableWrapper } from '../../components/ListItem';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { useSettings } from '../../hooks/context/useSettings';
import { suggestedServers, hardcodedPeers, presentResetToDefaultsAlert } from '../../blue_modules/BlueElectrum';
type RouteProps = RouteProp<DetailViewStackParamList, 'ElectrumSettings'>;
export interface ElectrumServerItem {
host: string;
port?: number;
sslPort?: number;
tcp?: number;
ssl?: number;
}
const SET_PREFERRED_PREFIX = 'set_preferred_';
const ElectrumSettings: React.FC = () => {
const { colors } = useTheme();
const { server } = useRoute<RouteProps>().params;
const { setOptions } = useExtendedNavigation();
const params = useRoute<RouteProps>().params;
const { server } = params;
const navigation = useExtendedNavigation();
const [isLoading, setIsLoading] = useState(true);
const [serverHistory, setServerHistory] = useState<ElectrumServerItem[]>([]);
const [serverHistory, setServerHistory] = useState<Set<ElectrumServerItem>>(new Set());
const [config, setConfig] = useState<{ connected?: number; host?: string; port?: string }>({});
const [host, setHost] = useState<string>('');
const [port, setPort] = useState<number | undefined>();
@ -79,36 +81,56 @@ const ElectrumSettings: React.FC = () => {
},
});
useEffect(() => {
let configInterval: NodeJS.Timeout | null = null;
const fetchData = async () => {
const savedHost = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_HOST);
const savedPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_TCP_PORT);
const savedSslPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SSL_PORT);
const serverHistoryStr = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SERVER_HISTORY);
const configIntervalRef = React.useRef<NodeJS.Timeout | null>(null);
const parsedServerHistory: ElectrumServerItem[] = serverHistoryStr ? JSON.parse(serverHistoryStr) : [];
const fetchData = React.useCallback(async () => {
console.log('Fetching data...');
const preferredServer = await BlueElectrum.getPreferredServer();
const savedHost = preferredServer?.host;
const savedPort = preferredServer?.tcp;
const savedSslPort = preferredServer?.ssl;
const serverHistoryStr = (await DefaultPreference.get(BlueElectrum.ELECTRUM_SERVER_HISTORY)) as string;
setHost(savedHost || '');
setPort(savedPort ? Number(savedPort) : undefined);
setSslPort(savedSslPort ? Number(savedSslPort) : undefined);
setServerHistory(parsedServerHistory);
console.log('Preferred server:', preferredServer);
console.log('Server history string:', serverHistoryStr);
const parsedServerHistory: ElectrumServerItem[] = serverHistoryStr ? JSON.parse(serverHistoryStr) : [];
const filteredServerHistory = new Set(
parsedServerHistory.filter(
v =>
v.host &&
(v.tcp || v.ssl) &&
!suggestedServers.some(suggested => suggested.host === v.host && suggested.tcp === v.tcp && suggested.ssl === v.ssl) &&
!hardcodedPeers.some(peer => peer.host === v.host),
),
);
console.log('Filtered server history:', filteredServerHistory);
setHost(savedHost || '');
setPort(savedPort ? Number(savedPort) : undefined);
setSslPort(savedSslPort ? Number(savedSslPort) : undefined);
setServerHistory(filteredServerHistory);
setConfig(await BlueElectrum.getConfig());
configIntervalRef.current = setInterval(async () => {
setConfig(await BlueElectrum.getConfig());
configInterval = setInterval(async () => {
setConfig(await BlueElectrum.getConfig());
}, 500);
}, 500);
setIsLoading(false);
};
fetchData();
setIsLoading(false);
return () => {
if (configInterval) clearInterval(configInterval);
if (configIntervalRef.current) clearInterval(configIntervalRef.current);
};
}, []);
useEffect(() => {
fetchData();
return () => {
if (configIntervalRef.current) clearInterval(configIntervalRef.current);
};
}, [fetchData]);
useEffect(() => {
if (server) {
triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy);
@ -130,147 +152,213 @@ const ElectrumSettings: React.FC = () => {
}
}, [server]);
const clearHistory = useCallback(async () => {
setIsLoading(true);
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify([]));
setServerHistory([]);
setIsLoading(false);
}, []);
const save = useCallback(
async (v?: ElectrumServerItem) => {
Keyboard.dismiss();
setIsLoading(true);
const serverExists = useCallback(
(value: ElectrumServerItem) => {
return serverHistory.some(s => `${s.host}:${s.port}:${s.sslPort}` === `${value.host}:${value.port}:${value.sslPort}`);
},
[serverHistory],
);
try {
const serverHost = v?.host || host;
const serverPort = v?.tcp || port?.toString() || '';
const serverSslPort = v?.ssl || sslPort?.toString() || '';
const save = useCallback(async () => {
Keyboard.dismiss();
setIsLoading(true);
if (serverHost && (serverPort || serverSslPort)) {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, serverHost);
await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, serverPort);
await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, serverSslPort);
try {
if (!host && !port && !sslPort) {
await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_HOST);
await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_TCP_PORT);
await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_SSL_PORT);
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.clear(BlueElectrum.ELECTRUM_HOST);
await DefaultPreference.clear(BlueElectrum.ELECTRUM_TCP_PORT);
await DefaultPreference.clear(BlueElectrum.ELECTRUM_SSL_PORT);
} else {
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_HOST, host);
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_TCP_PORT, port?.toString() || '');
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SSL_PORT, sslPort?.toString() || '');
if (!serverExists({ host, port, sslPort })) {
const newServerHistory = [...serverHistory, { host, port, sslPort }];
await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(newServerHistory));
setServerHistory(newServerHistory);
const serverExistsInHistory = Array.from(serverHistory).some(
s => s.host === serverHost && s.tcp === Number(serverPort) && s.ssl === Number(serverSslPort),
);
if (!serverExistsInHistory && (serverPort || serverSslPort) && !hardcodedPeers.some(peer => peer.host === serverHost)) {
const newServerHistory = new Set(serverHistory);
newServerHistory.add({ host: serverHost, tcp: Number(serverPort), ssl: Number(serverSslPort) });
await DefaultPreference.set(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(Array.from(newServerHistory)));
setServerHistory(newServerHistory);
}
}
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, host);
await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, port?.toString() || '');
await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, sslPort?.toString() || '');
}
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
presentAlert({ message: loc.settings.electrum_saved });
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: (error as Error).message });
}
setIsLoading(false);
}, [host, port, sslPort, serverExists, serverHistory]);
const resetToDefault = useCallback(() => {
Alert.alert(loc.settings.electrum_reset, loc.settings.electrum_reset_to_default, [
{
text: loc._.cancel,
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{
text: loc._.ok,
style: 'destructive',
onPress: async () => {
setHost('');
setPort(undefined);
setSslPort(undefined);
await save();
},
},
]);
}, [save]);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
presentAlert({ message: loc.settings.electrum_saved });
fetchData();
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: (error as Error).message });
} finally {
setIsLoading(false);
}
},
[host, port, sslPort, fetchData, serverHistory],
);
const selectServer = useCallback(
(value: string) => {
const parsedServer = JSON.parse(value) as ElectrumServerItem;
setHost(parsedServer.host);
setPort(parsedServer.port);
setSslPort(parsedServer.sslPort);
save();
setPort(parsedServer.tcp);
setSslPort(parsedServer.ssl);
save(parsedServer);
},
[save],
);
const clearHistoryAlert = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy);
Alert.alert(loc.settings.electrum_clear_alert_title, loc.settings.electrum_clear_alert_message, [
{ text: loc.settings.electrum_clear_alert_cancel, onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{ text: loc.settings.electrum_clear_alert_ok, onPress: () => clearHistory() },
]);
}, [clearHistory]);
const presentSelectServerAlert = useCallback(
(value: ElectrumServerItem) => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy);
Alert.alert(
loc.settings.electrum_preferred_server,
loc.formatString(loc.settings.set_as_preferred_electrum, { host: value.host, port: String(value.ssl ?? value.tcp) }),
[
{
text: loc._.ok,
onPress: () => {
selectServer(JSON.stringify(value));
},
style: 'default',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
],
{ cancelable: false },
);
},
[selectServer],
);
const onPressMenuItem = useCallback(
(id: string) => {
switch (id) {
case CommonToolTipActions.ResetToDefault.id:
resetToDefault();
break;
case CommonToolTipActions.ClearHistory.id:
clearHistoryAlert();
break;
default:
try {
selectServer(id);
} catch (error) {
console.warn('Unknown menu item selected:', id);
}
break;
if (id.startsWith(SET_PREFERRED_PREFIX)) {
const rawServer = JSON.parse(id.replace(SET_PREFERRED_PREFIX, ''));
presentSelectServerAlert(rawServer);
} else {
switch (id) {
case CommonToolTipActions.ResetToDefault.id:
presentResetToDefaultsAlert().then(reset => {
if (reset) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
presentAlert({ message: loc.settings.electrum_saved });
fetchData();
}
});
break;
default:
try {
selectServer(id);
} catch (error) {
console.warn('Unknown menu item selected:', id);
}
break;
}
}
},
[clearHistoryAlert, resetToDefault, selectServer],
[presentSelectServerAlert, fetchData, selectServer],
);
const toolTipActions = useMemo(() => {
const actions: Action[] = [CommonToolTipActions.ResetToDefault];
type TCreateServerActionParameters = {
value: ElectrumServerItem;
seenHosts: Set<string>;
isPreferred?: boolean;
isConnectedTo?: boolean;
isSuggested?: boolean;
};
const createServerAction = useCallback(
({ value, seenHosts, isPreferred = false, isConnectedTo = false, isSuggested = false }: TCreateServerActionParameters) => {
const hostKey = `${value.host}:${value.ssl ?? value.tcp}`;
if (seenHosts.has(hostKey)) return null;
if (serverHistory.length > 0) {
const serverSubactions: Action[] = serverHistory.map(value => ({
id: JSON.stringify(value),
text: `${value.host}`,
subtitle: `${value.port || value.sslPort}`,
menuState: `${host}:${port}:${sslPort}` === `${value.host}:${value.port}:${value.sslPort}`,
disabled: isLoading || (host === value.host && (port === value.port || sslPort === value.sslPort)),
}));
seenHosts.add(hostKey);
return {
id: `${SET_PREFERRED_PREFIX}${JSON.stringify(value)}`,
text: Platform.OS === 'android' ? `${value.host}:${value.ssl ?? value.tcp}` : value.host,
icon: isPreferred ? { iconValue: Platform.OS === 'ios' ? 'star.fill' : 'star_off' } : undefined,
menuState: isConnectedTo,
disabled: isPreferred,
subtitle: value.ssl ? `${loc._.ssl_port}: ${value.ssl}` : `${loc._.port}: ${value.tcp}`,
subactions:
isSuggested || isPreferred
? []
: [
...(host === value.host && (port === value.tcp || sslPort === value.ssl)
? []
: [
{
id: `${SET_PREFERRED_PREFIX}${JSON.stringify(value)}`,
text: loc.settings.set_as_preferred,
subtitle: value.ssl ? `${loc._.ssl_port}: ${value.ssl}` : `${loc._.port}: ${value.tcp}`,
},
]),
],
} as Action;
},
[host, port, sslPort],
);
const generateToolTipActions = useCallback(() => {
const actions: Action[] = [];
const seenHosts = new Set<string>();
const suggestedServersAction: Action = {
id: 'suggested_servers',
text: loc._.suggested,
displayInline: true,
subtitle: loc.settings.electrum_suggested_description,
subactions: suggestedServers
.map(value =>
createServerAction({
value,
seenHosts,
isPreferred: host === value.host && (port === value.tcp || sslPort === value.ssl),
isConnectedTo: config?.host === value.host && (config.port === value.tcp || config.port === value.ssl),
isSuggested: true,
}),
)
.filter((action): action is Action => action !== null),
};
actions.push(suggestedServersAction);
if (serverHistory.size > 0) {
const serverSubactions: Action[] = Array.from(serverHistory)
.map(value =>
createServerAction({
value,
seenHosts,
isPreferred: host === value.host && (port === value.tcp || sslPort === value.ssl),
isConnectedTo: config?.host === value.host && (config.port === value.tcp || config.port === value.ssl),
isSuggested: false,
}),
)
.filter((action): action is Action => action !== null);
actions.push({
id: 'server_history',
text: loc.settings.electrum_history,
subactions: [CommonToolTipActions.ClearHistory, ...serverSubactions],
displayInline: serverHistory.size <= 5 && serverHistory.size > 0,
subactions: serverSubactions,
hidden: serverHistory.size === 0,
});
}
const resetToDefaults = { ...CommonToolTipActions.ResetToDefault };
resetToDefaults.hidden = !host;
actions.push(resetToDefaults);
return actions;
}, [host, isLoading, port, serverHistory, sslPort]);
}, [config?.host, config.port, createServerAction, host, port, serverHistory, sslPort]);
const HeaderRight = useMemo(
() => <HeaderMenuButton actions={toolTipActions} onPressMenuItem={onPressMenuItem} />,
[onPressMenuItem, toolTipActions],
() => <HeaderMenuButton actions={generateToolTipActions()} onPressMenuItem={onPressMenuItem} />,
[onPressMenuItem, generateToolTipActions],
);
useEffect(() => {
setOptions({
navigation.setOptions({
headerRight: isElectrumDisabled ? null : () => HeaderRight,
});
}, [HeaderRight, isElectrumDisabled, setOptions]);
}, [HeaderRight, isElectrumDisabled, navigation]);
const checkServer = async () => {
setIsLoading(true);
@ -302,12 +390,17 @@ const ElectrumSettings: React.FC = () => {
};
const importScan = async () => {
const scanned = await scanQrHelper('ElectrumSettings', true);
if (scanned) {
onBarScanned(scanned);
}
navigation.navigate('ScanQRCode');
};
useEffect(() => {
const data = params.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
}, [navigation, params.onBarScanned]);
const onSSLPortChange = (value: boolean) => {
Keyboard.dismiss();
if (value) {
@ -331,6 +424,8 @@ const ElectrumSettings: React.FC = () => {
}
};
const preferredServerIsEmpty = !host || (!port && !sslPort);
const renderElectrumSettings = () => {
return (
<>
@ -411,13 +506,19 @@ const ElectrumSettings: React.FC = () => {
testID="SSLPortInput"
value={sslPort !== undefined}
onValueChange={onSSLPortChange}
disabled={host?.endsWith('.onion') ?? false}
disabled={host?.endsWith('.onion') || isLoading || host === '' || (port === undefined && sslPort === undefined)}
/>
</View>
</BlueCard>
<BlueCard>
<BlueSpacing20 />
<Button showActivityIndicator={isLoading} disabled={isLoading} testID="Save" onPress={save} title={loc.settings.save} />
<Button
showActivityIndicator={isLoading}
disabled={isLoading || preferredServerIsEmpty}
testID="Save"
onPress={save}
title={loc.settings.save}
/>
</BlueCard>
{Platform.select({

View file

@ -4,7 +4,6 @@ import { Keyboard, StyleSheet, TextInput, View, ScrollView, TouchableOpacity, Te
import { BlueButtonLink, BlueCard, BlueSpacing10, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { TWallet } from '../../class/wallets/types';
@ -15,6 +14,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
import { Divider } from '@rneui/themed';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import presentAlert from '../../components/Alert';
import { navigate } from '../../NavigationService';
type RouteProps = RouteProp<DetailViewStackParamList, 'IsItMyAddress'>;
type NavigationProp = NativeStackNavigationProp<DetailViewStackParamList, 'IsItMyAddress'>;
@ -109,11 +109,16 @@ const IsItMyAddress: React.FC = () => {
};
const importScan = async () => {
const data = await scanQrHelper(route.name, true, undefined, true);
navigate('ScanQRCode');
};
useEffect(() => {
const data = route.params?.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
};
}, [navigation, route.name, route.params?.onBarScanned]);
const viewQRCode = () => {
if (!resultCleanAddress) return;

View file

@ -9,11 +9,12 @@ import { LightningCustodianWallet } from '../../class/wallets/lightning-custodia
import presentAlert, { AlertType } from '../../components/Alert';
import { Button } from '../../components/Button';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { GROUP_IO_BLUEWALLET } from '../../blue_modules/currency';
import { clearLNDHub, getLNDHub, setLNDHub } from '../../helpers/lndHub';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const styles = StyleSheet.create({
uri: {
@ -38,21 +39,14 @@ const styles = StyleSheet.create({
},
});
type LightingSettingsRouteProps = RouteProp<
{
params?: {
url?: string;
};
},
'params'
>;
type LightingSettingsRouteProps = RouteProp<DetailViewStackParamList, 'LightningSettings'>;
const LightningSettings: React.FC = () => {
const params = useRoute<LightingSettingsRouteProps>().params;
const [isLoading, setIsLoading] = useState(true);
const [URI, setURI] = useState<string>();
const { colors } = useTheme();
const route = useRoute();
const { navigate, setParams } = useExtendedNavigation();
const styleHook = StyleSheet.create({
uri: {
borderColor: colors.formBorder,
@ -112,7 +106,6 @@ const LightningSettings: React.FC = () => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
if (URI) {
const normalizedURI = new URL(URI.replace(/([^:]\/)\/+/g, '$1')).toString();
await LightningCustodianWallet.isValidNodeAddress(normalizedURI);
await setLNDHub(normalizedURI);
@ -131,13 +124,19 @@ const LightningSettings: React.FC = () => {
}, [URI]);
const importScan = () => {
scanQrHelper(route.name).then(data => {
if (data) {
setLndhubURI(data);
}
navigate('ScanQRCode', {
showFileImportButton: true,
});
};
useEffect(() => {
const data = params?.onBarScanned;
if (data) {
setLndhubURI(data);
setParams({ onBarScanned: undefined });
}
}, [params?.onBarScanned, setParams]);
return (
<ScrollView automaticallyAdjustContentInsets contentInsetAdjustmentBehavior="automatic">
<BlueCard>

View file

@ -11,7 +11,6 @@ import {
} from '../../components/DoneAndDismissKeyboardInputAccessory';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useSettings } from '../../hooks/context/useSettings';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useKeyboard } from '../../hooks/useKeyboard';
@ -30,7 +29,6 @@ const ImportWallet = () => {
const route = useRoute<RouteProps>();
const label = route?.params?.label ?? '';
const triggerImport = route?.params?.triggerImport ?? false;
const scannedData = route?.params?.scannedData ?? '';
const [importText, setImportText] = useState<string>(label);
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState<boolean>(false);
const [, setSpeedBackdoor] = useState<number>(0);
@ -108,11 +106,18 @@ const ImportWallet = () => {
);
const importScan = useCallback(async () => {
const data = await scanQrHelper(route.name, true);
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation]);
useEffect(() => {
const data = route.params?.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
}, [route.name, onBarScanned]);
}, [route.name, onBarScanned, route.params?.onBarScanned, navigation]);
const speedBackdoorTap = () => {
setSpeedBackdoor(v => {
@ -162,12 +167,6 @@ const ImportWallet = () => {
if (triggerImport) handleImport();
}, [triggerImport, handleImport]);
useEffect(() => {
if (scannedData) {
onBarScanned(scannedData);
}
}, [scannedData, onBarScanned]);
// Adding the ToolTipMenu to the header
useEffect(() => {
navigation.setOptions({

View file

@ -1,13 +1,16 @@
import React, { useEffect, useLayoutEffect, useReducer, useCallback, useMemo, useRef } from 'react';
import { StyleSheet, TouchableOpacity, Image, Text, Alert, I18nManager, Animated, LayoutAnimation } from 'react-native';
import React, { useEffect, useLayoutEffect, useReducer, useCallback, useMemo, useRef, useState, lazy, Suspense } from 'react';
import {
NestableScrollContainer,
ScaleDecorator,
OpacityDecorator,
NestableDraggableFlatList,
RenderItem,
// @ts-expect-error: react-native-draggable-flatlist is not typed
} from 'react-native-draggable-flatlist';
StyleSheet,
TouchableOpacity,
Image,
Text,
Alert,
I18nManager,
Animated,
LayoutAnimation,
FlatList,
ActivityIndicator,
} from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
@ -23,8 +26,10 @@ import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import presentAlert from '../../components/Alert';
import prompt from '../../helpers/prompt';
import HeaderRightButton from '../../components/HeaderRightButton';
import { ManageWalletsListItem } from '../../components/ManageWalletsListItem';
import { useSettings } from '../../hooks/context/useSettings';
import DragList, { DragListRenderItemInfo } from 'react-native-draglist';
const ManageWalletsListItem = lazy(() => import('../../components/ManageWalletsListItem'));
enum ItemType {
WalletSection = 'wallet',
@ -206,21 +211,24 @@ const ManageWallets: React.FC = () => {
color: colors.foregroundColor,
},
};
const [data, setData] = useState(state.tempOrder);
const listRef = useRef<FlatList<Item> | null>(null);
useEffect(() => {
dispatch({
type: SET_INITIAL_ORDER,
payload: { wallets: walletsRef.current, txMetadata },
});
setData(state.tempOrder);
}, [state.tempOrder]);
useEffect(() => {
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } });
}, [txMetadata]);
useEffect(() => {
if (debouncedSearchQuery) {
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
} else {
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } });
dispatch({ type: SET_TEMP_ORDER, payload: state.order });
}
}, [debouncedSearchQuery, txMetadata]);
}, [debouncedSearchQuery, state.order]);
const handleClose = useCallback(() => {
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
@ -244,6 +252,7 @@ const ManageWallets: React.FC = () => {
dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false });
}
}, [goBack, setWalletsWithNewOrder, state.searchQuery, state.isSearchFocused, state.tempOrder, navigation]);
const hasUnsavedChanges = useMemo(() => {
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data));
}, [state.tempOrder]);
@ -319,6 +328,14 @@ const ManageWallets: React.FC = () => {
}, [hasUnsavedChanges, navigation, setIsDrawerShouldHide]),
);
// Ensure the listener is re-added every time there are unsaved changes
useEffect(() => {
if (beforeRemoveListenerRef.current) {
navigation.removeListener('beforeRemove', beforeRemoveListenerRef.current);
navigation.addListener('beforeRemove', beforeRemoveListenerRef.current);
}
}, [hasUnsavedChanges, navigation]);
const renderHighlightedText = useCallback(
(text: string, query: string) => {
const parts = text.split(new RegExp(`(${query})`, 'gi'));
@ -425,60 +442,46 @@ const ManageWallets: React.FC = () => {
},
[goBack, navigate],
);
const renderWalletItem = useCallback(
({ item, drag, isActive }: RenderItem<Item>) => (
<ScaleDecorator drag={drag} activeScale={1.1}>
<OpacityDecorator activeOpacity={0.5}>
<ManageWalletsListItem
item={item}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
drag={drag}
state={state}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
handleDeleteWallet={handleDeleteWallet}
handleToggleHideBalance={handleToggleHideBalance}
/>
</OpacityDecorator>
</ScaleDecorator>
),
const renderItem = useCallback(
(info: DragListRenderItemInfo<Item>) => {
const { item, onDragStart, isActive } = info;
return (
<ManageWalletsListItem
item={item}
onPressIn={undefined}
onPressOut={undefined}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
state={state}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
handleDeleteWallet={handleDeleteWallet}
handleToggleHideBalance={handleToggleHideBalance}
isActive={isActive}
drag={onDragStart}
/>
);
},
[state, navigateToWallet, renderHighlightedText, handleDeleteWallet, handleToggleHideBalance],
);
const renderPlaceholder = useCallback(
({ item, drag, isActive }: RenderItem<Item>) => (
<ManageWalletsListItem
item={item}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
state={state}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
isPlaceHolder
handleDeleteWallet={handleDeleteWallet}
handleToggleHideBalance={handleToggleHideBalance}
/>
),
[handleDeleteWallet, handleToggleHideBalance, navigateToWallet, renderHighlightedText, state],
);
const onReordered = useCallback(
(fromIndex: number, toIndex: number) => {
const copy = [...state.order];
const removed = copy.splice(fromIndex, 1);
copy.splice(toIndex, 0, removed[0]);
const onChangeOrder = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
}, []);
const onDragBegin = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.Selection);
}, []);
const onRelease = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
}, []);
const onDragEnd = useCallback(
({ data }: { data: Item[] }) => {
const updatedWallets = data.filter((item): item is WalletItem => item.type === ItemType.WalletSection).map(item => item.data);
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: updatedWallets, txMetadata: state.txMetadata } });
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
dispatch({ type: SET_TEMP_ORDER, payload: copy });
dispatch({
type: SET_INITIAL_ORDER,
payload: {
wallets: copy.filter(item => item.type === ItemType.WalletSection).map(item => item.data as TWallet),
txMetadata: state.txMetadata,
},
});
},
[state.txMetadata],
[state.order, state.txMetadata],
);
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
@ -498,45 +501,29 @@ const ManageWallets: React.FC = () => {
}, [state.searchQuery, state.wallets.length, state.txMetadata, stylesHook.noResultsText]);
return (
<GestureHandlerRootView style={[{ backgroundColor: colors.background }, styles.root]}>
<NestableScrollContainer contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets scrollEnabled>
{renderHeader}
<NestableDraggableFlatList
data={state.tempOrder.filter((item): item is WalletItem => item.type === ItemType.WalletSection)}
extraData={state.tempOrder}
keyExtractor={keyExtractor}
renderItem={renderWalletItem}
onChangeOrder={onChangeOrder}
onDragBegin={onDragBegin}
onPlaceholderIndexChange={onChangeOrder}
onRelease={onRelease}
delayLongPress={150}
useNativeDriver={true}
dragItemOverflow
autoscrollThreshold={1}
renderPlaceholder={renderPlaceholder}
autoscrollSpeed={0.5}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
onDragEnd={onDragEnd}
containerStyle={styles.root}
/>
<NestableDraggableFlatList
data={state.tempOrder.filter((item): item is TransactionItem => item.type === ItemType.TransactionSection)}
keyExtractor={keyExtractor}
renderItem={renderWalletItem}
dragItemOverflow
containerStyle={styles.root}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
useNativeDriver={true}
/>
</NestableScrollContainer>
</GestureHandlerRootView>
<Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}>
<GestureHandlerRootView style={[{ backgroundColor: colors.background }, styles.root]}>
<>
{renderHeader}
<DragList
automaticallyAdjustContentInsets
automaticallyAdjustKeyboardInsets
automaticallyAdjustsScrollIndicatorInsets
contentInsetAdjustmentBehavior="automatic"
data={data}
containerStyle={[{ backgroundColor: colors.background }, styles.root]}
keyExtractor={keyExtractor}
onReordered={onReordered}
renderItem={renderItem}
ref={listRef}
/>
</>
</GestureHandlerRootView>
</Suspense>
);
};
export default ManageWallets;
export default React.memo(ManageWallets);
const styles = StyleSheet.create({
root: {

View file

@ -1,5 +1,5 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useFocusEffect, useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CommonActions, RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import {
ActivityIndicator,
Alert,
@ -38,7 +38,6 @@ import QRCodeComponent from '../../components/QRCodeComponent';
import SquareEnumeratedWords, { SquareEnumeratedWordsContentAlign } from '../../components/SquareEnumeratedWords';
import { useTheme } from '../../components/themes';
import prompt from '../../helpers/prompt';
import { scanQrHelper } from '../../helpers/scan-qr';
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { disallowScreenshot } from 'react-native-screen-capture';
@ -48,6 +47,13 @@ import { useStorage } from '../../hooks/context/useStorage';
import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useSettings } from '../../hooks/context/useSettings';
import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { navigationRef } from '../../NavigationService';
import SafeArea from '../../components/SafeArea';
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
const ViewEditMultisigCosigners: React.FC = () => {
const hasLoaded = useRef(false);
@ -55,10 +61,10 @@ const ViewEditMultisigCosigners: React.FC = () => {
const { wallets, setWalletsWithNewOrder } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { isElectrumDisabled, isPrivacyBlurEnabled } = useSettings();
const { navigate, dispatch, addListener } = useExtendedNavigation();
const { navigate, dispatch, addListener, setParams } = useExtendedNavigation<NavigationProp>();
const openScannerButtonRef = useRef();
const route = useRoute();
const { walletID } = route.params as { walletID: string };
const route = useRoute<RouteParams>();
const { walletID } = route.params;
const w = useRef(wallets.find(wallet => wallet.getID() === walletID));
const tempWallet = useRef(new MultisigHDWallet());
const [wallet, setWallet] = useState<MultisigHDWallet>();
@ -73,6 +79,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR
const [exportFilename, setExportFilename] = useState('bw-cosigner.json');
const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', passphrase: '', path: '', fp: '', isLoading: false }); // string rendered in modal
const [isVaultKeyIndexDataLoading, setIsVaultKeyIndexDataLoading] = useState<number | undefined>(undefined);
const [askPassphrase, setAskPassphrase] = useState(false);
const data = useRef<any[]>();
/* discardChangesRef is only so the action sheet can be shown on mac catalyst when a
@ -183,7 +190,8 @@ const ViewEditMultisigCosigners: React.FC = () => {
setIsSaveButtonDisabled(true);
setTimeout(() => {
setWalletsWithNewOrder(newWallets);
navigate('WalletsList');
// dismiss this modal
navigationRef.dispatch(CommonActions.navigate({ name: 'WalletsList' }));
}, 500);
};
useFocusEffect(
@ -220,10 +228,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
<BottomModal
ref={mnemonicsModalRef}
backgroundColor={colors.elevated}
contentContainerStyle={styles.newKeyModalContent}
contentContainerStyle={[styles.newKeyModalContent, styles.paddingTop44]}
shareButtonOnPress={() => {
shareModalRef.current?.present();
}}
sizes={[Platform.OS === 'ios' ? 'auto' : '50%']}
header={
<View style={styles.itemKeyUnprovidedWrapper}>
<View style={[styles.vaultKeyCircleSuccess, stylesHook.vaultKeyCircleSuccess]}>
@ -268,6 +277,24 @@ const ViewEditMultisigCosigners: React.FC = () => {
);
};
const resetModalData = () => {
setVaultKeyData({
keyIndex: 1,
xpub: '',
seed: '',
passphrase: '',
path: '',
fp: '',
isLoading: false,
});
setImportText('');
setExportString('{}');
setExportStringURv2('');
setExportFilename('');
setIsSaveButtonDisabled(false);
setAskPassphrase(false);
};
const _renderKeyItem = (el: ListRenderItemInfo<any>) => {
if (!wallet) {
// failsafe
@ -305,29 +332,34 @@ const ViewEditMultisigCosigners: React.FC = () => {
buttonType: MultipleStepsListItemButtohType.partial,
leftText,
text: loc.multisig.view,
showActivityIndicator: isVaultKeyIndexDataLoading === el.index + 1,
disabled: vaultKeyData.isLoading,
onPress: () => {
const keyIndex = el.index + 1;
const xpub = wallet.getCosigner(keyIndex);
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
setVaultKeyData({
keyIndex,
seed: '',
passphrase: '',
xpub,
fp,
path,
isLoading: false,
});
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(el.index + 1);
setTimeout(() => {
const keyIndex = el.index + 1;
const xpub = wallet.getCosigner(keyIndex);
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
setVaultKeyData({
keyIndex,
seed: '',
passphrase: '',
xpub,
fp,
path,
isLoading: false,
});
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(undefined);
}, 100);
},
}}
dashes={MultipleStepsListItemDashType.topAndBottom}
@ -356,31 +388,36 @@ const ViewEditMultisigCosigners: React.FC = () => {
leftText,
text: loc.multisig.view,
disabled: vaultKeyData.isLoading,
showActivityIndicator: isVaultKeyIndexDataLoading === el.index + 1,
buttonType: MultipleStepsListItemButtohType.partial,
onPress: () => {
const keyIndex = el.index + 1;
const seed = wallet.getCosigner(keyIndex);
const passphrase = wallet.getCosignerPassphrase(keyIndex);
setVaultKeyData({
keyIndex,
seed,
xpub: '',
fp: '',
path: '',
passphrase: passphrase ?? '',
isLoading: false,
});
mnemonicsModalRef.current?.present();
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase));
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
setIsVaultKeyIndexDataLoading(el.index + 1);
setTimeout(() => {
const keyIndex = el.index + 1;
const seed = wallet.getCosigner(keyIndex);
const passphrase = wallet.getCosignerPassphrase(keyIndex);
setVaultKeyData({
keyIndex,
seed,
xpub: '',
fp: '',
path: '',
passphrase: passphrase ?? '',
isLoading: false,
});
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase));
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(undefined);
}, 100);
},
}}
dashes={MultipleStepsListItemDashType.topAndBottom}
@ -435,6 +472,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
await provideMnemonicsModalRef.current?.dismiss();
await shareModalRef.current?.dismiss();
await mnemonicsModalRef.current?.dismiss();
resetModalData();
};
const handleUseMnemonicPhrase = async () => {
let passphrase;
@ -471,9 +509,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
provideMnemonicsModalRef.current?.dismiss();
setIsSaveButtonDisabled(false);
setImportText('');
setAskPassphrase(false);
resetModalData();
};
const xpubInsteadOfSeed = (index: number): Promise<void> => {
@ -495,16 +531,22 @@ const ViewEditMultisigCosigners: React.FC = () => {
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current?.dismiss();
const scanned = await scanQrHelper(route.name, true, undefined);
setImportText(String(scanned));
provideMnemonicsModalRef.current?.present();
navigate('ScanQRCode', { showFileImportButton: true });
};
useEffect(() => {
const scannedData = route.params.onBarScanned;
if (scannedData) {
setImportText(String(scannedData));
setParams({ onBarScanned: undefined });
provideMnemonicsModalRef.current?.present();
}
}, [route.params.onBarScanned, setParams]);
const hideProvideMnemonicsModal = () => {
Keyboard.dismiss();
provideMnemonicsModalRef.current?.dismiss();
setImportText('');
setAskPassphrase(false);
resetModalData();
};
const hideShareModal = () => {};
@ -569,13 +611,15 @@ const ViewEditMultisigCosigners: React.FC = () => {
backgroundColor={colors.elevated}
shareContent={{ fileName: exportFilename, fileContent: exportString }}
>
<View style={styles.alignItemsCenter}>
<Text style={[styles.headerText, stylesHook.textDestination]}>
{loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''}
</Text>
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
<BlueSpacing20 />
</View>
<SafeArea>
<View style={styles.alignItemsCenter}>
<Text style={[styles.headerText, stylesHook.textDestination]}>
{loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''}
</Text>
<BlueSpacing20 />
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
</View>
</SafeArea>
</BottomModal>
);
};
@ -632,9 +676,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
keyExtractor={(_item, index) => `${index}`}
contentContainerStyle={styles.contentContainerStyle}
/>
<BlueSpacing10 />
<BlueCard>{footer}</BlueCard>
<BlueSpacing20 />
{renderProvideMnemonicsModal()}
@ -656,6 +698,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 22,
minHeight: 350,
},
paddingTop44: { paddingTop: 44 },
multiLineTextInput: {
minHeight: 200,
},

Some files were not shown because too many files have changed in this diff Show more