Merge branch 'master' into receietab

This commit is contained in:
Marcos Rodriguez Velez 2024-06-10 09:34:23 -04:00
commit 6ae5c4c50f
No known key found for this signature in database
GPG key ID: 6030B2F48CCE86D7
113 changed files with 1363 additions and 1337 deletions

View file

@ -1,8 +1,6 @@
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
import AsyncStorage from '@react-native-async-storage/async-storage';
import Clipboard from '@react-native-clipboard/clipboard';
import PropTypes from 'prop-types';
import React, { Component, forwardRef } from 'react';
import React, { forwardRef } from 'react';
import {
ActivityIndicator,
Dimensions,
@ -18,9 +16,8 @@ import {
} from 'react-native';
import { Icon, Text } from 'react-native-elements';
import { BlueCurrentTheme, useTheme } from './components/themes';
import loc, { formatStringAddTwoWhiteSpaces } from './loc';
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees';
import { useTheme } from './components/themes';
import loc from './loc';
const { height, width } = Dimensions.get('window');
const aspectRatio = height / width;
@ -203,165 +200,6 @@ export const BlueLoading = props => {
);
};
export class BlueReplaceFeeSuggestions extends Component {
static propTypes = {
onFeeSelected: PropTypes.func.isRequired,
transactionMinimum: PropTypes.number.isRequired,
};
static defaultProps = {
transactionMinimum: 1,
};
state = {
customFeeValue: '1',
};
async componentDidMount() {
try {
const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey));
if (cachedNetworkTransactionFees && 'fastestFee' in cachedNetworkTransactionFees) {
this.setState({ networkFees: cachedNetworkTransactionFees }, () => this.onFeeSelected(NetworkTransactionFeeType.FAST));
}
} catch (_) {}
const networkFees = await NetworkTransactionFees.recommendedFees();
this.setState({ networkFees }, () => this.onFeeSelected(NetworkTransactionFeeType.FAST));
}
onFeeSelected = selectedFeeType => {
if (selectedFeeType !== NetworkTransactionFeeType.CUSTOM) {
Keyboard.dismiss();
}
if (selectedFeeType === NetworkTransactionFeeType.FAST) {
this.props.onFeeSelected(this.state.networkFees.fastestFee);
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.fastestFee));
} else if (selectedFeeType === NetworkTransactionFeeType.MEDIUM) {
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.mediumFee));
} else if (selectedFeeType === NetworkTransactionFeeType.SLOW) {
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.slowFee));
} else if (selectedFeeType === NetworkTransactionFeeType.CUSTOM) {
this.props.onFeeSelected(Number(this.state.customFeeValue));
}
};
onCustomFeeTextChange = customFee => {
const customFeeValue = customFee.replace(/[^0-9]/g, '');
this.setState({ customFeeValue, selectedFeeType: NetworkTransactionFeeType.CUSTOM }, () => {
this.onFeeSelected(NetworkTransactionFeeType.CUSTOM);
});
};
render() {
const { networkFees, selectedFeeType } = this.state;
return (
<View>
{networkFees &&
[
{
label: loc.send.fee_fast,
time: loc.send.fee_10m,
type: NetworkTransactionFeeType.FAST,
rate: networkFees.fastestFee,
active: selectedFeeType === NetworkTransactionFeeType.FAST,
},
{
label: formatStringAddTwoWhiteSpaces(loc.send.fee_medium),
time: loc.send.fee_3h,
type: NetworkTransactionFeeType.MEDIUM,
rate: networkFees.mediumFee,
active: selectedFeeType === NetworkTransactionFeeType.MEDIUM,
},
{
label: loc.send.fee_slow,
time: loc.send.fee_1d,
type: NetworkTransactionFeeType.SLOW,
rate: networkFees.slowFee,
active: selectedFeeType === NetworkTransactionFeeType.SLOW,
},
].map(({ label, type, time, rate, active }, index) => (
<TouchableOpacity
accessibilityRole="button"
key={label}
onPress={() => this.onFeeSelected(type)}
style={[
{ paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 },
active && { borderRadius: 8, backgroundColor: BlueCurrentTheme.colors.incomingBackgroundColor },
]}
>
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ fontSize: 22, color: BlueCurrentTheme.colors.successColor, fontWeight: '600' }}>{label}</Text>
<View
style={{
backgroundColor: BlueCurrentTheme.colors.successColor,
borderRadius: 5,
paddingHorizontal: 6,
paddingVertical: 3,
}}
>
<Text style={{ color: BlueCurrentTheme.colors.background }}>~{time}</Text>
</View>
</View>
<View style={{ justifyContent: 'flex-end', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ color: BlueCurrentTheme.colors.successColor }}>{rate} sat/byte</Text>
</View>
</TouchableOpacity>
))}
<TouchableOpacity
accessibilityRole="button"
onPress={() => this.customTextInput.focus()}
style={[
{ paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 },
selectedFeeType === NetworkTransactionFeeType.CUSTOM && {
borderRadius: 8,
backgroundColor: BlueCurrentTheme.colors.incomingBackgroundColor,
},
]}
>
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ fontSize: 22, color: BlueCurrentTheme.colors.successColor, fontWeight: '600' }}>
{formatStringAddTwoWhiteSpaces(loc.send.fee_custom)}
</Text>
</View>
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center', marginTop: 5 }}>
<TextInput
onChangeText={this.onCustomFeeTextChange}
keyboardType="numeric"
value={this.state.customFeeValue}
ref={ref => (this.customTextInput = ref)}
maxLength={9}
style={{
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderBottomWidth: 0.5,
borderColor: BlueCurrentTheme.colors.formBorder,
borderRadius: 4,
borderWidth: 1.0,
color: '#81868e',
flex: 1,
marginRight: 10,
minHeight: 33,
paddingRight: 5,
paddingLeft: 5,
}}
onFocus={() => this.onCustomFeeTextChange(this.state.customFeeValue)}
defaultValue={this.props.transactionMinimum}
placeholder={loc.send.fee_satvbyte}
placeholderTextColor="#81868e"
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<Text style={{ color: BlueCurrentTheme.colors.successColor }}>sat/byte</Text>
</View>
</TouchableOpacity>
<BlueText style={{ color: BlueCurrentTheme.colors.alternativeTextColor }}>
{loc.formatString(loc.send.fee_replace_minvb, { min: this.props.transactionMinimum })}
</BlueText>
</View>
);
}
}
export function BlueBigCheckmark({ style = {} }) {
const defaultStyles = {
backgroundColor: '#ccddf9',

View file

@ -28,3 +28,9 @@ export function popToTop() {
navigationRef.current?.dispatch(StackActions.popToTop());
}
}
export function pop() {
if (navigationRef.isReady()) {
navigationRef.current?.dispatch(StackActions.pop());
}
}

View file

@ -109,6 +109,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.4.0'
}
apply plugin: 'com.google.gms.google-services' // Google Services plugin

View file

@ -14,8 +14,6 @@ import android.view.View;
import android.widget.RemoteViews;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatDelegate;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -153,20 +151,6 @@ public class BitcoinPriceWidget extends AppWidgetProvider {
String currentTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date());
views.setTextViewText(R.id.last_updated, "Last Updated");
views.setTextViewText(R.id.last_updated_time, currentTime);
if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) {
views.setTextColor(R.id.last_updated, context.getResources().getColor(android.R.color.white));
views.setTextColor(R.id.last_updated_time, context.getResources().getColor(android.R.color.white));
views.setTextColor(R.id.price_value, context.getResources().getColor(android.R.color.white));
views.setTextColor(R.id.previous_price, context.getResources().getColor(android.R.color.white));
views.setInt(R.id.widget_layout, "setBackgroundColor", context.getResources().getColor(android.R.color.black));
} else {
views.setTextColor(R.id.last_updated, context.getResources().getColor(android.R.color.black));
views.setTextColor(R.id.last_updated_time, context.getResources().getColor(android.R.color.black));
views.setTextColor(R.id.price_value, context.getResources().getColor(android.R.color.black));
views.setTextColor(R.id.previous_price, context.getResources().getColor(android.R.color.black));
views.setInt(R.id.widget_layout, "setBackgroundColor", context.getResources().getColor(android.R.color.white));
}
} else {
String errorMessage = "Network Error";
Log.e(TAG, errorMessage);

View file

@ -0,0 +1,26 @@
package io.bluewallet.bluewallet;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.ReactApplicationContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CustomSegmentControlPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
CustomSegmentedControlManager.registerIfNecessary();
List<ViewManager> viewManagers = new ArrayList<>();
viewManagers.add(new CustomSegmentedControlManager());
return viewManagers;
}
}

View file

@ -0,0 +1,100 @@
package io.bluewallet.bluewallet;
import android.content.Context;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.material.tabs.TabLayout;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class CustomSegmentedControlManager extends SimpleViewManager<CustomSegmentedControlManager.CustomSegmentedControlView> {
public static final String REACT_CLASS = "CustomSegmentedControl";
private static boolean isRegistered = false;
public static class CustomSegmentedControlView extends LinearLayout {
private TabLayout tabLayout;
private RCTEventEmitter eventEmitter;
private int viewId;
public CustomSegmentedControlView(Context context) {
super(context);
setOrientation(LinearLayout.HORIZONTAL);
tabLayout = new TabLayout(context);
addView(tabLayout, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
WritableMap event = Arguments.createMap();
event.putInt("selectedIndex", tab.getPosition());
if (eventEmitter != null) {
eventEmitter.receiveEvent(viewId, "topChange", event);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {}
});
}
public void setValues(ReadableArray values) {
tabLayout.removeAllTabs();
for (int i = 0; i < values.size(); i++) {
tabLayout.addTab(tabLayout.newTab().setText(values.getString(i)));
}
}
public void setSelectedIndex(int selectedIndex) {
if (selectedIndex >= 0 && selectedIndex < tabLayout.getTabCount()) {
TabLayout.Tab tab = tabLayout.getTabAt(selectedIndex);
if (tab != null) {
tab.select();
}
}
}
public void setEventEmitter(RCTEventEmitter eventEmitter, int viewId) {
this.eventEmitter = eventEmitter;
this.viewId = viewId;
}
}
@NonNull
@Override
public String getName() {
return REACT_CLASS;
}
@NonNull
@Override
protected CustomSegmentedControlView createViewInstance(@NonNull ThemedReactContext reactContext) {
CustomSegmentedControlView view = new CustomSegmentedControlView(reactContext);
view.setEventEmitter(reactContext.getJSModule(RCTEventEmitter.class), view.getId());
return view;
}
@ReactProp(name = "values")
public void setValues(CustomSegmentedControlView view, ReadableArray values) {
view.setValues(values);
}
@ReactProp(name = "selectedIndex")
public void setSelectedIndex(CustomSegmentedControlView view, int selectedIndex) {
view.setSelectedIndex(selectedIndex);
}
public static void registerIfNecessary() {
if (!isRegistered) {
isRegistered = true;
// Registration logic if necessary
}
}
}

View file

@ -31,7 +31,8 @@ public class MainApplication extends Application implements ReactApplication {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new CustomSegmentControlPackage());
CustomSegmentedControlManager.registerIfNecessary();
return packages;
}
@ -40,15 +41,15 @@ public class MainApplication extends Application implements ReactApplication {
return "index";
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
@Override
@ -62,19 +63,19 @@ public class MainApplication extends Application implements ReactApplication {
I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
sharedI18nUtilInstance.allowRTL(getApplicationContext(), true);
SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
SharedPreferences sharedPref = getApplicationContext().getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
SharedPreferences sharedPref = getApplicationContext().getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE);
// Retrieve the "donottrack" value. Default to "0" if not found.
String isDoNotTrackEnabled = sharedPref.getString("donottrack", "0");
// Retrieve the "donottrack" value. Default to "0" if not found.
String isDoNotTrackEnabled = sharedPref.getString("donottrack", "0");
// Check if do not track is not enabled and initialize Bugsnag if so
if (!isDoNotTrackEnabled.equals("1")) {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this);
}
// Check if do not track is not enabled and initialize Bugsnag if so
if (!isDoNotTrackEnabled.equals("1")) {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this);
}
}
}

View file

@ -5,7 +5,7 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:background="@android:color/white">
android:background="@color/widget_background">
<LinearLayout
android:layout_width="match_parent"
@ -15,21 +15,21 @@
<TextView
android:id="@+id/last_updated"
style="@style/WidgetTextSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/last_updated_time"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
android:textColor="@android:color/black"
android:layout_marginEnd="8dp"
android:layout_marginTop="2dp"/>
</LinearLayout>
@ -42,12 +42,12 @@
<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:textColor="@android:color/black"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
@ -67,11 +67,11 @@
<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:textColor="@android:color/black"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout>

View file

@ -1,4 +1,7 @@
<resources>
<color name="background_color">#000000</color> <!-- Dark background -->
<color name="background_color">#000000</color>
<color name="widget_background">#1B1B1F</color>
<color name="text_primary">#FFFFFF</color>
<color name="text_secondary">#D3D3D3</color>
</resources>

View file

@ -0,0 +1,9 @@
<resources>
<style name="WidgetTextPrimary">
<item name="android:textColor">@color/text_primary</item>
</style>
<style name="WidgetTextSecondary">
<item name="android:textColor">@color/text_secondary</item>
</style>
</resources>

View file

@ -1,4 +1,7 @@
<resources>
<color name="white">#FFF</color>
<color name="background_color">#FFFFFF</color> <!-- Light background -->
<color name="background_color">#FFFFFF</color>
<color name="widget_background">#FFFFFF</color>
<color name="text_primary">#0C234F</color>
<color name="text_secondary">#D3D3D3</color>
</resources>

View file

@ -1,9 +1,14 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style>
<style name="WidgetTextPrimary">
<item name="android:textColor">@color/text_primary</item>
</style>
</resources>
<style name="WidgetTextSecondary">
<item name="android:textColor">@color/text_secondary</item>
</style>
</resources>

View file

@ -159,9 +159,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
async generateFromEntropy(user: Buffer) {
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0);
const buf = Buffer.concat([user, random], 32);
this.secret = bip39.entropyToMnemonic(buf.toString('hex'));
if (user.length !== 32 && user.length !== 16) {
throw new Error('Entropy has to be 16 or 32 bytes long');
}
this.secret = bip39.entropyToMnemonic(user.toString('hex'));
}
_getExternalWIFByIndex(index: number): string | false {

View file

@ -63,18 +63,10 @@ export class LegacyWallet extends AbstractWallet {
}
async generateFromEntropy(user: Buffer): Promise<void> {
let i = 0;
do {
i += 1;
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0);
const buf = Buffer.concat([user, random], 32);
try {
this.secret = ECPair.fromPrivateKey(buf).toWIF();
return;
} catch (e) {
if (i === 5) throw e;
}
} while (true);
if (user.length !== 32) {
throw new Error('Entropy should be 32 bytes');
}
this.secret = ECPair.fromPrivateKey(user).toWIF();
}
getAddress(): string | false {

View file

@ -2,7 +2,7 @@ import 'react-native-gesture-handler'; // should be on top
import { CommonActions } from '@react-navigation/native';
import React, { lazy, Suspense, useCallback, useEffect, useRef } from 'react';
import { AppState, AppStateStatus, Linking, NativeEventEmitter, NativeModules, Platform, UIManager } from 'react-native';
import { AppState, AppStateStatus, Linking, NativeEventEmitter, NativeModules, Platform } from 'react-native';
import A from '../blue_modules/analytics';
import BlueClipboard from '../blue_modules/clipboard';
@ -31,12 +31,6 @@ const ClipboardContentType = Object.freeze({
LIGHTNING: 'LIGHTNING',
});
if (Platform.OS === 'android') {
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
const CompanionDelegates = () => {
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage();
const appState = useRef<AppStateStatus>(AppState.currentState);

View file

@ -131,7 +131,7 @@ interface FButtonProps {
last?: boolean;
disabled?: boolean;
onPress: () => void;
onLongPress: () => void;
onLongPress?: () => void;
}
export const FButton = ({ text, icon, width, first, last, ...props }: FButtonProps) => {

View file

@ -0,0 +1,210 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { View, Text, TextInput, TouchableOpacity, Keyboard, StyleSheet } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { BlueText } from '../BlueComponents';
import loc, { formatStringAddTwoWhiteSpaces } from '../loc';
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from '../models/networkTransactionFees';
import { useTheme } from './themes';
interface ReplaceFeeSuggestionsProps {
onFeeSelected: (fee: number) => void;
transactionMinimum?: number;
}
const ReplaceFeeSuggestions: React.FC<ReplaceFeeSuggestionsProps> = ({ onFeeSelected, transactionMinimum = 1 }) => {
const [networkFees, setNetworkFees] = useState<NetworkTransactionFee | null>(null);
const [selectedFeeType, setSelectedFeeType] = useState<NetworkTransactionFeeType>(NetworkTransactionFeeType.FAST);
const [customFeeValue, setCustomFeeValue] = useState<string>('1');
const customTextInput = useRef<TextInput>(null);
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
activeButton: {
backgroundColor: colors.incomingBackgroundColor,
},
buttonText: {
color: colors.successColor,
},
timeContainer: {
backgroundColor: colors.successColor,
},
timeText: {
color: colors.background,
},
rateText: {
color: colors.successColor,
},
customFeeInput: {
backgroundColor: colors.inputBackgroundColor,
borderBottomColor: colors.formBorder,
borderColor: colors.formBorder,
},
alternativeText: {
color: colors.alternativeTextColor,
},
});
const fetchNetworkFees = useCallback(async () => {
try {
const cachedNetworkTransactionFees = JSON.parse((await AsyncStorage.getItem(NetworkTransactionFee.StorageKey)) || '{}');
if (cachedNetworkTransactionFees && 'fastestFee' in cachedNetworkTransactionFees) {
setNetworkFees(cachedNetworkTransactionFees);
onFeeSelected(cachedNetworkTransactionFees.fastestFee);
setSelectedFeeType(NetworkTransactionFeeType.FAST);
}
} catch (_) {}
const fees = await NetworkTransactionFees.recommendedFees();
setNetworkFees(fees);
onFeeSelected(fees.fastestFee);
setSelectedFeeType(NetworkTransactionFeeType.FAST);
}, [onFeeSelected]);
useEffect(() => {
fetchNetworkFees();
}, [fetchNetworkFees]);
const handleFeeSelection = (feeType: NetworkTransactionFeeType) => {
if (feeType !== NetworkTransactionFeeType.CUSTOM) {
Keyboard.dismiss();
}
if (networkFees) {
let selectedFee: number;
switch (feeType) {
case NetworkTransactionFeeType.FAST:
selectedFee = networkFees.fastestFee;
break;
case NetworkTransactionFeeType.MEDIUM:
selectedFee = networkFees.mediumFee;
break;
case NetworkTransactionFeeType.SLOW:
selectedFee = networkFees.slowFee;
break;
case NetworkTransactionFeeType.CUSTOM:
selectedFee = Number(customFeeValue);
break;
}
onFeeSelected(selectedFee);
setSelectedFeeType(feeType);
}
};
const handleCustomFeeChange = (customFee: string) => {
const sanitizedFee = customFee.replace(/[^0-9]/g, '');
setCustomFeeValue(sanitizedFee);
handleFeeSelection(NetworkTransactionFeeType.CUSTOM);
};
return (
<View>
{networkFees &&
[
{
label: loc.send.fee_fast,
time: loc.send.fee_10m,
type: NetworkTransactionFeeType.FAST,
rate: networkFees.fastestFee,
active: selectedFeeType === NetworkTransactionFeeType.FAST,
},
{
label: formatStringAddTwoWhiteSpaces(loc.send.fee_medium),
time: loc.send.fee_3h,
type: NetworkTransactionFeeType.MEDIUM,
rate: networkFees.mediumFee,
active: selectedFeeType === NetworkTransactionFeeType.MEDIUM,
},
{
label: loc.send.fee_slow,
time: loc.send.fee_1d,
type: NetworkTransactionFeeType.SLOW,
rate: networkFees.slowFee,
active: selectedFeeType === NetworkTransactionFeeType.SLOW,
},
].map(({ label, type, time, rate, active }) => (
<TouchableOpacity
accessibilityRole="button"
key={label}
onPress={() => handleFeeSelection(type)}
style={[styles.button, active && stylesHook.activeButton]}
>
<View style={styles.buttonContent}>
<Text style={[styles.buttonText, stylesHook.buttonText]}>{label}</Text>
<View style={[styles.timeContainer, stylesHook.timeContainer]}>
<Text style={stylesHook.timeText}>~{time}</Text>
</View>
</View>
<View style={styles.rateContainer}>
<Text style={stylesHook.rateText}>{rate} sat/byte</Text>
</View>
</TouchableOpacity>
))}
<TouchableOpacity
accessibilityRole="button"
onPress={() => customTextInput.current?.focus()}
style={[styles.button, selectedFeeType === NetworkTransactionFeeType.CUSTOM && stylesHook.activeButton]}
>
<View style={styles.buttonContent}>
<Text style={[styles.buttonText, stylesHook.buttonText]}>{formatStringAddTwoWhiteSpaces(loc.send.fee_custom)}</Text>
</View>
<View style={[styles.buttonContent, styles.customFeeInputContainer]}>
<TextInput
onChangeText={handleCustomFeeChange}
keyboardType="numeric"
value={customFeeValue}
ref={customTextInput}
maxLength={9}
style={[styles.customFeeInput, stylesHook.customFeeInput]}
onFocus={() => handleCustomFeeChange(customFeeValue)}
placeholder={loc.send.fee_satvbyte}
placeholderTextColor="#81868e"
/>
<Text style={stylesHook.rateText}>sat/byte</Text>
</View>
</TouchableOpacity>
<BlueText style={stylesHook.alternativeText}>{loc.formatString(loc.send.fee_replace_minvb, { min: transactionMinimum })}</BlueText>
</View>
);
};
const styles = StyleSheet.create({
button: {
paddingHorizontal: 16,
paddingVertical: 8,
marginBottom: 10,
borderRadius: 8,
},
buttonContent: {
justifyContent: 'space-between',
flexDirection: 'row',
alignItems: 'center',
},
buttonText: {
fontSize: 22,
fontWeight: '600',
},
timeContainer: {
borderRadius: 5,
paddingHorizontal: 6,
paddingVertical: 3,
},
rateContainer: {
justifyContent: 'flex-end',
flexDirection: 'row',
alignItems: 'center',
},
customFeeInputContainer: {
marginTop: 5,
},
customFeeInput: {
borderBottomWidth: 0.5,
borderRadius: 4,
borderWidth: 1.0,
color: '#81868e',
flex: 1,
marginRight: 10,
minHeight: 33,
paddingRight: 5,
paddingLeft: 5,
},
});
export default ReplaceFeeSuggestions;

View file

@ -0,0 +1,44 @@
import React from 'react';
import { requireNativeComponent, View, StyleSheet, NativeSyntheticEvent } from 'react-native';
interface SegmentedControlProps {
values: string[];
selectedIndex: number;
onChange: (index: number) => void;
}
interface SegmentedControlEvent {
selectedIndex: number;
}
interface NativeSegmentedControlProps {
values: string[];
selectedIndex: number;
onChangeEvent: (event: NativeSyntheticEvent<SegmentedControlEvent>) => void;
style?: object;
}
const NativeSegmentedControl = requireNativeComponent<NativeSegmentedControlProps>('CustomSegmentedControl');
const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange }) => {
const handleChange = (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
onChange(event.nativeEvent.selectedIndex);
};
return (
<View style={styles.container}>
<NativeSegmentedControl values={values} selectedIndex={selectedIndex} style={styles.segmentedControl} onChangeEvent={handleChange} />
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
},
segmentedControl: {
height: 40,
},
});
export default SegmentedControl;

View file

@ -154,7 +154,7 @@ const iStyles = StyleSheet.create({
},
});
export const WalletCarouselItem = React.memo(({ item, _, onPress, handleLongPress, isSelectedWallet, customStyle }) => {
export const WalletCarouselItem = React.memo(({ item, _, onPress, handleLongPress, isSelectedWallet, customStyle, horizontal }) => {
const scaleValue = new Animated.Value(1.0);
const { colors } = useTheme();
const { walletTransactionUpdateStatus } = useStorage();
@ -197,7 +197,7 @@ export const WalletCarouselItem = React.memo(({ item, _, onPress, handleLongPres
return (
<Animated.View
style={[
isLargeScreen ? iStyles.rootLargeDevice : customStyle ?? { ...iStyles.root, width: itemWidth },
isLargeScreen || !horizontal ? iStyles.rootLargeDevice : customStyle ?? { ...iStyles.root, width: itemWidth },
{ opacity, transform: [{ scale: scaleValue }] },
]}
>
@ -287,6 +287,7 @@ const WalletsCarousel = forwardRef((props, ref) => {
index={index}
handleLongPress={handleLongPress}
onPress={onPress}
horizontal={horizontal}
/>
) : (
<NewWalletPanel onPress={onPress} />

View file

@ -1,109 +0,0 @@
import React from 'react';
import { StyleSheet, Text, View, Pressable, LayoutAnimation, Platform, UIManager, ViewStyle, TextStyle } from 'react-native';
import loc from '../../loc';
import { useTheme } from '../themes';
export const TABS = {
EXTERNAL: 'receive',
INTERNAL: 'change',
} as const;
type TabKey = keyof typeof TABS;
type TABS_VALUES = (typeof TABS)[keyof typeof TABS];
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
interface AddressTypeTabsProps {
currentTab: TABS_VALUES;
setCurrentTab: (tab: TABS_VALUES) => void;
customTabText?: { [key in TabKey]?: string };
}
const AddressTypeTabs: React.FC<AddressTypeTabsProps> = ({ currentTab, setCurrentTab, customTabText }) => {
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
activeTab: {
backgroundColor: colors.modal,
} as ViewStyle,
activeText: {
fontWeight: 'bold',
color: colors.foregroundColor,
} as TextStyle,
inactiveTab: {
fontWeight: 'normal',
color: colors.foregroundColor,
} as TextStyle,
backTabs: {
backgroundColor: colors.buttonDisabledBackgroundColor,
} as ViewStyle,
});
const tabs = Object.entries(TABS).map(([key, value]) => {
return {
key: key as TabKey,
value,
name: customTabText?.[key as TabKey] || loc.addresses[`type_${value}`],
};
});
const changeToTab = (tabKey: TabKey) => {
if (tabKey in TABS) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCurrentTab(TABS[tabKey]);
}
};
const render = () => {
const tabsButtons = tabs.map(tab => {
const isActive = tab.value === currentTab;
const tabStyle = isActive ? stylesHook.activeTab : undefined;
const textStyle = isActive ? stylesHook.activeText : stylesHook.inactiveTab;
return (
<Pressable key={tab.key} onPress={() => changeToTab(tab.key)} style={[styles.tab, tabStyle]}>
<Text style={textStyle}>{tab.name}</Text>
</Pressable>
);
});
return (
<View style={styles.container}>
<View style={[stylesHook.backTabs, styles.backTabs]}>
<View style={styles.tabs}>{tabsButtons}</View>
</View>
</View>
);
};
return render();
};
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
} as ViewStyle,
backTabs: {
padding: 4,
marginVertical: 8,
borderRadius: 8,
} as ViewStyle,
tabs: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
} as ViewStyle,
tab: {
borderRadius: 6,
paddingVertical: 8,
paddingHorizontal: 16,
justifyContent: 'center',
} as ViewStyle,
});
export { AddressTypeTabs };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 851 B

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 B

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 B

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 B

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 B

After

Width:  |  Height:  |  Size: 257 B

BIN
img/close.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 166 B

BIN
img/close@2x.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 220 B

BIN
img/close@3x.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 B

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 17 KiB

BIN
img/round-compare-arrows-24-px.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 199 B

BIN
img/round-compare-arrows-24-px@2x.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 B

After

Width:  |  Height:  |  Size: 315 B

BIN
img/round-compare-arrows-24-px@3x.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 942 B

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 B

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 B

After

Width:  |  Height:  |  Size: 347 B

BIN
img/scan.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 282 B

BIN
img/scan@2x.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 441 B

BIN
img/scan@3x.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -1,7 +1,7 @@
import './shim.js';
import React, { useEffect } from 'react';
import { AppRegistry, LogBox, Platform, UIManager } from 'react-native';
import { AppRegistry, LogBox } from 'react-native';
import App from './App';
import A from './blue_modules/analytics';
@ -14,12 +14,6 @@ if (!Error.captureStackTrace) {
LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']);
if (Platform.OS === 'android') {
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
const BlueAppComponent = () => {
useEffect(() => {
restoreSavedPreferredFiatCurrencyAndExchangeFromStorage();

View file

@ -4,3 +4,4 @@
#import "AppDelegate.h"
#import <React/RCTBridgeModule.h>
#import "React/RCTViewManager.h"

View file

@ -141,8 +141,10 @@
B450109D2C0FCD9F00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B450109E2C0FCDA000619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B450109F2C0FCDA500619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */ = {isa = PBXBuildFile; fileRef = B4549F352B82B10D002E3153 /* ci_post_clone.sh */; };
B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
B47462D02C1538D800100825 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
B47B21EC2B2128B8001F6690 /* BlueWalletUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47B21EB2B2128B8001F6690 /* BlueWalletUITests.swift */; };
B49038D92B8FBAD300A8164A /* BlueWalletUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49038D82B8FBAD300A8164A /* BlueWalletUITest.swift */; };
B4A29A2C2B55C990002A67DF /* EventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D32C5C52596CE3A008C077C /* EventEmitter.m */; };
@ -444,6 +446,8 @@
B44033FF2BCC37F800162242 /* Bundle+decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+decode.swift"; sourceTree = "<group>"; };
B440340E2BCC40A400162242 /* fiatUnits.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = fiatUnits.json; path = ../../../models/fiatUnits.json; sourceTree = "<group>"; };
B450109B2C0FCD8A00619044 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControlManager.m; sourceTree = "<group>"; };
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomSegmentedControlManager.h; sourceTree = "<group>"; };
B4549F352B82B10D002E3153 /* ci_post_clone.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = ci_post_clone.sh; sourceTree = "<group>"; };
B461B850299599F800E431AA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = BlueWallet/AppDelegate.h; sourceTree = "<group>"; };
B461B851299599F800E431AA /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = BlueWallet/AppDelegate.mm; sourceTree = "<group>"; };
@ -715,7 +719,7 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
B45010A12C1504E900619044 /* Components */,
B44033C82BCC34AC00162242 /* Shared */,
B41C2E552BB3DCB8000FE097 /* PrivacyInfo.xcprivacy */,
B4549F2E2B80FEA1002E3153 /* ci_scripts */,
@ -862,6 +866,24 @@
path = Utilities;
sourceTree = "<group>";
};
B45010A12C1504E900619044 /* Components */ = {
isa = PBXGroup;
children = (
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
B45010A82C1507F000619044 /* SegmentedControl */,
);
path = Components;
sourceTree = "<group>";
};
B45010A82C1507F000619044 /* SegmentedControl */ = {
isa = PBXGroup;
children = (
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */,
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */,
);
path = SegmentedControl;
sourceTree = "<group>";
};
B4549F2E2B80FEA1002E3153 /* ci_scripts */ = {
isa = PBXGroup;
children = (
@ -1521,6 +1543,7 @@
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */,
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */,
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
@ -1638,6 +1661,7 @@
B44033D72BCC369400162242 /* UserDefaultsExtension.swift in Sources */,
B44033F72BCC377F00162242 /* WidgetData.swift in Sources */,
B44033E12BCC36CA00162242 /* Placeholders.swift in Sources */,
B47462D02C1538D800100825 /* CustomSegmentedControlManager.m in Sources */,
B44033E72BCC36FF00162242 /* WalletData.swift in Sources */,
B44033E02BCC36C300162242 /* LatestTransaction.swift in Sources */,
B44033D92BCC369900162242 /* Colors.swift in Sources */,

View file

@ -0,0 +1,33 @@
{
"originHash" : "89509f555bc90a15b96ca0a326a69850770bdaac04a46f9cf482d81533702e3c",
"pins" : [
{
"identity" : "bugsnag-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/bugsnag/bugsnag-cocoa",
"state" : {
"revision" : "16b9145fc66e5296f16e733f6feb5d0e450574e8",
"version" : "6.28.1"
}
},
{
"identity" : "efqrcode",
"kind" : "remoteSourceControl",
"location" : "https://github.com/EFPrefix/EFQRCode.git",
"state" : {
"revision" : "2991c2f318ad9529d93b2a73a382a3f9c72c64ce",
"version" : "6.2.2"
}
},
{
"identity" : "swift_qrcodejs",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ApolloZhu/swift_qrcodejs.git",
"state" : {
"revision" : "374dc7f7b9e76c6aeb393f6a84590c6d387e1ecb",
"version" : "2.2.2"
}
}
],
"version" : 3
}

View file

@ -10,6 +10,7 @@
#import <React/RCTRootView.h>
#import <Bugsnag/Bugsnag.h>
#import "BlueWallet-Swift.h"
#import "CustomSegmentedControlManager.h"
@interface AppDelegate() <UNUserNotificationCenterDelegate>
@ -21,6 +22,7 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[CustomSegmentedControlManager registerIfNecessary];
[self clearFilesIfNeeded];
self.userDefaultsGroup = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];

View file

@ -0,0 +1,15 @@
//
// SegmentedControlManager.h
// BlueWallet
//
// Created by Marcos Rodriguez on 6/8/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface CustomSegmentedControlManager : RCTViewManager
+ (void)registerIfNecessary;
@end

View file

@ -0,0 +1,62 @@
#import "CustomSegmentedControlManager.h"
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/UIView+React.h>
@interface CustomSegmentedControl : UISegmentedControl
@property (nonatomic, copy) RCTDirectEventBlock onChangeEvent;
- (void)setValues:(NSArray<NSString *> *)values;
- (void)setSelectedIndex:(NSNumber *)selectedIndex;
@end
@implementation CustomSegmentedControl
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
}
return self;
}
- (void)setValues:(NSArray<NSString *> *)values {
[self removeAllSegments];
for (NSUInteger i = 0; i < values.count; i++) {
[self insertSegmentWithTitle:values[i] atIndex:i animated:NO];
}
}
- (void)setSelectedIndex:(NSNumber *)selectedIndex {
self.selectedSegmentIndex = selectedIndex.integerValue;
}
- (void)onChange:(UISegmentedControl *)sender {
if (self.onChangeEvent) {
self.onChangeEvent(@{@"selectedIndex": @(self.selectedSegmentIndex)});
}
}
@end
@implementation CustomSegmentedControlManager
static BOOL isRegistered = NO;
RCT_EXPORT_MODULE(CustomSegmentedControl)
- (UIView *)view {
return [CustomSegmentedControl new];
}
RCT_EXPORT_VIEW_PROPERTY(values, NSArray)
RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onChangeEvent, RCTDirectEventBlock)
+ (void)registerIfNecessary {
if (!isRegistered) {
isRegistered = YES;
// Registration logic if necessary
}
}
@end

View file

@ -474,7 +474,7 @@ PODS:
- React-Core
- RNDefaultPreference (1.4.4):
- React-Core
- RNDeviceInfo (10.14.0):
- RNDeviceInfo (11.1.0):
- React-Core
- RNFS (2.20.0):
- React-Core
@ -485,7 +485,7 @@ PODS:
- React
- RNKeychain (8.2.0):
- React-Core
- RNLocalize (3.1.0):
- RNLocalize (3.2.0):
- React-Core
- RNPermissions (4.1.5):
- React-Core
@ -830,12 +830,12 @@ SPEC CHECKSUMS:
RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 59344c19152c4b2b32283005f9737c5c64b42fba
RNDeviceInfo: b899ce37a403a4dea52b7cb85e16e49c04a5b88e
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 982741f345785f2927e7b28f67dc83679cf3bfc8
RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa
RNKeychain: bfe3d12bf4620fe488771c414530bf16e88f3678
RNLocalize: e8694475db034bf601e17bd3dfa8986565e769eb
RNLocalize: b77875884750cb6a58cd6865863fe2ba2729b72b
RNPermissions: 9fa74223844f437bc309e112994859dc47194829
RNPrivacySnapshot: 71919dde3c6a29dd332115409c2aec564afee8f4
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93

View file

@ -43,7 +43,8 @@
"entropy": {
"save": "Save",
"title": "Entropy",
"undo": "Undo"
"undo": "Undo",
"amountOfEntropy": "{bits} of {limit} bits"
},
"errors": {
"broadcast": "Broadcast failed.",
@ -387,6 +388,8 @@
"add_bitcoin": "Bitcoin",
"add_bitcoin_explain": "Simple and powerful Bitcoin wallet",
"add_create": "Create",
"add_entropy": "Entropy",
"add_entropy_bytes": "{bytes} bytes of entropy",
"add_entropy_generated": "{gen} bytes of generated entropy",
"add_entropy_provide": "Provide entropy via dice rolls",
"add_entropy_remain": "{gen} bytes of generated entropy. Remaining {rem} bytes will be obtained from the System random number generator.",
@ -400,6 +403,10 @@
"add_title": "Add Wallet",
"add_wallet_name": "Name",
"add_wallet_type": "Type",
"add_wallet_seed_length": "Seed Length",
"add_wallet_seed_length_message": "Choose the length of the seed phrase you wish to use for this wallet.",
"add_wallet_seed_length_12": "12 words",
"add_wallet_seed_length_24": "24 words",
"clipboard_bitcoin": "You have a Bitcoin address on your clipboard. Would you like to use it for a transaction?",
"clipboard_lightning": "You have a Lightning invoice on your clipboard. Would you like to use it for a transaction?",
"details_address": "Address",

View file

@ -1,11 +1,11 @@
import * as BlueElectrum from '../blue_modules/BlueElectrum';
export const NetworkTransactionFeeType = Object.freeze({
FAST: 'Fast',
MEDIUM: 'MEDIUM',
SLOW: 'SLOW',
CUSTOM: 'CUSTOM',
});
export enum NetworkTransactionFeeType {
FAST = 'Fast',
MEDIUM = 'MEDIUM',
SLOW = 'SLOW',
CUSTOM = 'CUSTOM',
}
export class NetworkTransactionFee {
static StorageKey = 'NetworkTransactionFee';

View file

@ -28,7 +28,7 @@ import WalletAddresses from '../screen/wallets/WalletAddresses';
import WalletDetails from '../screen/wallets/details';
import GenerateWord from '../screen/wallets/generateWord';
import LdkViewLogs from '../screen/wallets/ldkViewLogs';
import SelectWallet from '../screen/wallets/selectWallet';
import SelectWallet from '../screen/wallets/SelectWallet';
import WalletTransactions from '../screen/wallets/transactions';
import WalletsList from '../screen/wallets/WalletsList';
import { NavigationDefaultOptions, NavigationFormModalOptions, StatusBarLightOptions, DetailViewStack } from './index'; // Importing the navigator

View file

@ -11,7 +11,7 @@ const ImportWallet = lazy(() => import('../screen/wallets/import'));
const PleaseBackup = lazy(() => import('../screen/wallets/PleaseBackup'));
const PleaseBackupLNDHub = lazy(() => import('../screen/wallets/pleaseBackupLNDHub'));
const PleaseBackupLdk = lazy(() => import('../screen/wallets/pleaseBackupLdk'));
const ProvideEntropy = lazy(() => import('../screen/wallets/provideEntropy'));
const ProvideEntropy = lazy(() => import('../screen/wallets/ProvideEntropy'));
const WalletsAddMultisig = lazy(() => import('../screen/wallets/addMultisig'));
const WalletsAddMultisigStep2 = lazy(() => import('../screen/wallets/addMultisigStep2'));
const WalletsAddMultisigHelp = lazy(() => import('../screen/wallets/addMultisigHelp'));

View file

@ -3,7 +3,7 @@ import React, { lazy, Suspense } from 'react';
import { LazyLoadingIndicator } from './LazyLoadingIndicator';
const AztecoRedeem = lazy(() => import('../screen/receive/aztecoRedeem'));
const SelectWallet = lazy(() => import('../screen/wallets/selectWallet')); // Assuming the path is correct
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
export const AztecoRedeemComponent = () => (
<Suspense fallback={<LazyLoadingIndicator />}>

View file

@ -2,7 +2,7 @@ import React, { lazy, Suspense } from 'react';
import { LazyLoadingIndicator } from './LazyLoadingIndicator';
const SelectWallet = lazy(() => import('../screen/wallets/selectWallet'));
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
const LdkOpenChannel = lazy(() => import('../screen/lnd/ldkOpenChannel'));
const Success = lazy(() => import('../screen/send/success'));

View file

@ -3,7 +3,7 @@ import React, { lazy, Suspense } from 'react';
import { LazyLoadingIndicator } from './LazyLoadingIndicator';
const LNDCreateInvoice = lazy(() => import('../screen/lnd/lndCreateInvoice'));
const SelectWallet = lazy(() => import('../screen/wallets/selectWallet'));
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
const LNDViewInvoice = lazy(() => import('../screen/lnd/lndViewInvoice'));
const LNDViewAdditionalInvoiceInformation = lazy(() => import('../screen/lnd/lndViewAdditionalInvoiceInformation'));
const LNDViewAdditionalInvoicePreImage = lazy(() => import('../screen/lnd/lndViewAdditionalInvoicePreImage'));

View file

@ -4,7 +4,7 @@ import { LazyLoadingIndicator } from './LazyLoadingIndicator';
// Lazy loading components for the navigation stack
const ScanLndInvoice = lazy(() => import('../screen/lnd/scanLndInvoice'));
const SelectWallet = lazy(() => import('../screen/wallets/selectWallet'));
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
const Success = lazy(() => import('../screen/send/success'));
const LnurlPay = lazy(() => import('../screen/lnd/lnurlPay'));
const LnurlPaySuccess = lazy(() => import('../screen/lnd/lnurlPaySuccess'));

View file

@ -9,7 +9,7 @@ const CreateTransaction = lazy(() => import('../screen/send/create'));
const PsbtMultisig = lazy(() => import('../screen/send/psbtMultisig'));
const PsbtMultisigQRCode = lazy(() => import('../screen/send/psbtMultisigQRCode'));
const Success = lazy(() => import('../screen/send/success'));
const SelectWallet = lazy(() => import('../screen/wallets/selectWallet'));
const SelectWallet = lazy(() => import('../screen/wallets/SelectWallet'));
const CoinControl = lazy(() => import('../screen/send/coinControl'));
const PaymentCodesList = lazy(() => import('../screen/wallets/PaymentCodesList'));

View file

@ -8,7 +8,7 @@ const Settings = lazy(() => import('../screen/settings/Settings'));
const GeneralSettings = lazy(() => import('../screen/settings/GeneralSettings'));
const Licensing = lazy(() => import('../screen/settings/Licensing'));
const NetworkSettings = lazy(() => import('../screen/settings/NetworkSettings'));
const About = lazy(() => import('../screen/settings/about'));
const About = lazy(() => import('../screen/settings/About'));
const DefaultView = lazy(() => import('../screen/settings/DefaultView'));
const ElectrumSettings = lazy(() => import('../screen/settings/electrumSettings'));
const EncryptStorage = lazy(() => import('../screen/settings/EncryptStorage'));

View file

@ -4,7 +4,7 @@ import React from 'react';
import navigationStyle from '../components/navigationStyle';
import { useTheme } from '../components/themes';
import loc from '../loc';
import ReorderWallets from '../screen/wallets/reorderWallets';
import ReorderWallets from '../screen/wallets/ReorderWallets';
const Stack = createNativeStackNavigator();

View file

@ -68,7 +68,6 @@ export type SendDetailsStackParamList = {
txid?: string;
};
SelectWallet: {
onWalletSelect: (wallet: TWallet) => void;
chainType: Chain;
};
CoinControl: {

47
package-lock.json generated
View file

@ -34,7 +34,7 @@
"bip32": "3.0.1",
"bip38": "github:BlueWallet/bip38",
"bip39": "3.1.0",
"bitcoinjs-lib": "6.1.5",
"bitcoinjs-lib": "6.1.6",
"bitcoinjs-message": "2.2.0",
"bolt11": "1.4.1",
"buffer": "6.0.3",
@ -63,7 +63,7 @@
"react-native-camera-kit": "13.0.0",
"react-native-crypto": "2.2.0",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "10.14.0",
"react-native-device-info": "11.1.0",
"react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#6033c4e1b0dd0a6760b5f5a5a2c3b2e5d07f2ae4",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#ebfddc4",
"react-native-elements": "3.4.3",
@ -76,7 +76,7 @@
"react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#v1.15.3",
"react-native-keychain": "8.2.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.1.0",
"react-native-localize": "3.2.0",
"react-native-modal": "13.0.1",
"react-native-obscure": "https://github.com/BlueWallet/react-native-obscure.git#f4b83b4a261e39b1f5ed4a45ac5bcabc8a59eadb",
"react-native-permissions": "4.1.5",
@ -7858,9 +7858,10 @@
"integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow=="
},
"node_modules/bitcoinjs-lib": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz",
"integrity": "sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==",
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz",
"integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.2.0",
"bech32": "^2.0.0",
@ -7877,6 +7878,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
"license": "MIT",
"dependencies": {
"base-x": "^4.0.0"
}
@ -7885,6 +7887,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
"integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.2.0",
"bs58": "^5.0.0"
@ -19574,10 +19577,9 @@
}
},
"node_modules/react-native-device-info": {
"version": "10.14.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.14.0.tgz",
"integrity": "sha512-9NnTGfhEU4UgQtz4p6COk2Gbqly0dpSWrJtp+dw5rNAi96KtYbaNnO5yoOHDlJ1SVIzh8+hFu3WxVbnWkFU9gA==",
"license": "MIT",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-11.1.0.tgz",
"integrity": "sha512-hzXJSObJdezEz0hF7MAJ3tGeoesuQWenXXt9mrQR9Mjb8kXpZ09rqSsZ/quNpJdZpQ3rYiFa3/0GFG5KNn9PBg==",
"peerDependencies": {
"react-native": "*"
}
@ -19737,9 +19739,10 @@
}
},
"node_modules/react-native-localize": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-3.1.0.tgz",
"integrity": "sha512-A7Rrxl8vuAr5FAqtMFrM5ELLdmszohK6FoHL6qlgxx4HScyOnadoZksbvKHbn+zV5nE8kud2Z4kJM10cN120Zg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-3.2.0.tgz",
"integrity": "sha512-Wi486WdDoBcDKUNxzr2/8f64ZwkWExlIvn0lcfFhsRNP2CVXGkjLyXhr3h6jf3Utkv5EmoFKiNqcmZn9kjYDxQ==",
"license": "MIT",
"peerDependencies": {
"react": ">=18.1.0",
"react-native": ">=0.70.0",
@ -28563,9 +28566,9 @@
"integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow=="
},
"bitcoinjs-lib": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz",
"integrity": "sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==",
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz",
"integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==",
"requires": {
"@noble/hashes": "^1.2.0",
"bech32": "^2.0.0",
@ -37399,9 +37402,9 @@
"integrity": "sha512-h0vtgiSKws3UmMRJykXAVM4ne1SgfoocUcoBD19ewRpQd6wqurE0HJRQGrSxcHK5LdKE7QPSIB1VX3YGIVS8Jg=="
},
"react-native-device-info": {
"version": "10.14.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.14.0.tgz",
"integrity": "sha512-9NnTGfhEU4UgQtz4p6COk2Gbqly0dpSWrJtp+dw5rNAi96KtYbaNnO5yoOHDlJ1SVIzh8+hFu3WxVbnWkFU9gA=="
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-11.1.0.tgz",
"integrity": "sha512-hzXJSObJdezEz0hF7MAJ3tGeoesuQWenXXt9mrQR9Mjb8kXpZ09rqSsZ/quNpJdZpQ3rYiFa3/0GFG5KNn9PBg=="
},
"react-native-document-picker": {
"version": "git+ssh://git@github.com/BlueWallet/react-native-document-picker.git#6033c4e1b0dd0a6760b5f5a5a2c3b2e5d07f2ae4",
@ -37503,9 +37506,9 @@
"integrity": "sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA=="
},
"react-native-localize": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-3.1.0.tgz",
"integrity": "sha512-A7Rrxl8vuAr5FAqtMFrM5ELLdmszohK6FoHL6qlgxx4HScyOnadoZksbvKHbn+zV5nE8kud2Z4kJM10cN120Zg=="
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-3.2.0.tgz",
"integrity": "sha512-Wi486WdDoBcDKUNxzr2/8f64ZwkWExlIvn0lcfFhsRNP2CVXGkjLyXhr3h6jf3Utkv5EmoFKiNqcmZn9kjYDxQ=="
},
"react-native-modal": {
"version": "13.0.1",

View file

@ -119,7 +119,7 @@
"bip32": "3.0.1",
"bip38": "github:BlueWallet/bip38",
"bip39": "3.1.0",
"bitcoinjs-lib": "6.1.5",
"bitcoinjs-lib": "6.1.6",
"bitcoinjs-message": "2.2.0",
"bolt11": "1.4.1",
"buffer": "6.0.3",
@ -148,7 +148,7 @@
"react-native-camera-kit": "13.0.0",
"react-native-crypto": "2.2.0",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "10.14.0",
"react-native-device-info": "11.1.0",
"react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#6033c4e1b0dd0a6760b5f5a5a2c3b2e5d07f2ae4",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#ebfddc4",
"react-native-elements": "3.4.3",
@ -161,7 +161,7 @@
"react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#v1.15.3",
"react-native-keychain": "8.2.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.1.0",
"react-native-localize": "3.2.0",
"react-native-modal": "13.0.1",
"react-native-obscure": "https://github.com/BlueWallet/react-native-obscure.git#f4b83b4a261e39b1f5ed4a45ac5bcabc8a59eadb",
"react-native-permissions": "4.1.5",

View file

@ -1,343 +0,0 @@
import BIP32Factory from 'bip32';
import bip38 from 'bip38';
import * as bip39 from 'bip39';
import * as bitcoin from 'bitcoinjs-lib';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Linking, ScrollView, StyleSheet, View } from 'react-native';
import BlueCrypto from 'react-native-blue-crypto';
import wif from 'wif';
import * as BlueElectrum from '../blue_modules/BlueElectrum';
import * as encryption from '../blue_modules/encryption';
import * as fs from '../blue_modules/fs';
import ecc from '../blue_modules/noble_ecc';
import { BlueCard, BlueLoading, BlueSpacing20, BlueText } from '../BlueComponents';
import {
HDAezeedWallet,
HDSegwitBech32Wallet,
HDSegwitP2SHWallet,
LegacyWallet,
SegwitP2SHWallet,
SLIP39LegacyP2PKHWallet,
} from '../class';
import presentAlert from '../components/Alert';
import Button from '../components/Button';
import SafeArea from '../components/SafeArea';
import SaveFileButton from '../components/SaveFileButton';
import loc from '../loc';
const bip32 = BIP32Factory(ecc);
const styles = StyleSheet.create({
center: {
alignItems: 'center',
},
});
export default class SelfTest extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
};
}
onPressImportDocument = async () => {
try {
fs.showFilePickerAndReadFile().then(file => {
if (file && file.data && file.data.length > 0) {
presentAlert({ message: file.data });
} else {
presentAlert({ message: 'Error reading file' });
}
});
} catch (err) {
console.log(err);
}
};
async componentDidMount() {
let errorMessage = '';
let isOk = true;
try {
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
const uniqs = {};
const w = new SegwitP2SHWallet();
for (let c = 0; c < 1000; c++) {
await w.generate();
if (uniqs[w.getSecret()]) {
throw new Error('failed to generate unique private key');
}
uniqs[w.getSecret()] = 1;
}
} else {
// skipping RN-specific test
}
//
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
const addr4elect = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
const electrumBalance = await BlueElectrum.getBalanceByAddress(addr4elect);
if (electrumBalance.confirmed !== 51432)
throw new Error('BlueElectrum getBalanceByAddress failure, got ' + JSON.stringify(electrumBalance));
const electrumTxs = await BlueElectrum.getTransactionsByAddress(addr4elect);
if (electrumTxs.length !== 1) throw new Error('BlueElectrum getTransactionsByAddress failure, got ' + JSON.stringify(electrumTxs));
} else {
// skipping RN-specific test'
}
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
const aezeed = new HDAezeedWallet();
aezeed.setSecret(
'abstract rhythm weird food attract treat mosquito sight royal actor surround ride strike remove guilt catch filter summer mushroom protect poverty cruel chaos pattern',
);
assertStrictEqual(await aezeed.validateMnemonicAsync(), true, 'Aezeed failed');
assertStrictEqual(aezeed._getExternalAddressByIndex(0), 'bc1qdjj7lhj9lnjye7xq3dzv3r4z0cta294xy78txn', 'Aezeed failed');
} else {
// skipping RN-specific test
}
let l = new LegacyWallet();
l.setSecret('L4ccWrPMmFDZw4kzAKFqJNxgHANjdy6b7YKNXMwB4xac4FLF3Tov');
assertStrictEqual(l.getAddress(), '14YZ6iymQtBVQJk6gKnLCk49UScJK7SH4M');
let utxos = [
{
txid: 'cc44e933a094296d9fe424ad7306f16916253a3d154d52e4f1a757c18242cec4',
vout: 0,
value: 100000,
txhex:
'0200000000010161890cd52770c150da4d7d190920f43b9f88e7660c565a5a5ad141abb6de09de00000000000000008002a0860100000000001976a91426e01119d265aa980390c49eece923976c218f1588ac3e17000000000000160014c1af8c9dd85e0e55a532a952282604f820746fcd02473044022072b3f28808943c6aa588dd7a4e8f29fad7357a2814e05d6c5d767eb6b307b4e6022067bc6a8df2dbee43c87b8ce9ddd9fe678e00e0f7ae6690d5cb81eca6170c47e8012102e8fba5643e15ab70ec79528833a2c51338c1114c4eebc348a235b1a3e13ab07100000000',
},
];
let txNew = l.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, l.getAddress());
const txBitcoin = bitcoin.Transaction.fromHex(txNew.tx.toHex());
assertStrictEqual(
txNew.tx.toHex(),
'0200000001c4ce4282c157a7f1e4524d153d3a251669f10673ad24e49f6d2994a033e944cc000000006b48304502210091e58bd2021f2eeea8d39d7f7b053c9ccc52a747b60f1c3584ba33285e2d150602205b2d35a2536cbe157015e8c54a26f5fc350cc7c72b5ca80b9e548917993f652201210337c09b3cb889801638078fd4e6998218b28c92d338ea2602720a88847aedceb3ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac2e260000000000001976a91426e01119d265aa980390c49eece923976c218f1588ac00000000',
);
assertStrictEqual(txBitcoin.ins.length, 1);
assertStrictEqual(txBitcoin.outs.length, 2);
assertStrictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(txBitcoin.outs[0].script)); // to address
assertStrictEqual(l.getAddress(), bitcoin.address.fromOutputScript(txBitcoin.outs[1].script)); // change address
//
l = new SegwitP2SHWallet();
l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct');
if (l.getAddress() !== '34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53') {
throw new Error('failed to generate segwit P2SH address from WIF');
}
//
const wallet = new SegwitP2SHWallet();
wallet.setSecret('Ky1vhqYGCiCbPd8nmbUeGfwLdXB1h5aGwxHwpXrzYRfY5cTZPDo4');
assertStrictEqual(wallet.getAddress(), '3CKN8HTCews4rYJYsyub5hjAVm5g5VFdQJ');
utxos = [
{
txid: 'a56b44080cb606c0bd90e77fcd4fb34c863e68e5562e75b4386e611390eb860c',
vout: 0,
value: 300000,
},
];
txNew = wallet.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, wallet.getAddress());
const tx = bitcoin.Transaction.fromHex(txNew.tx.toHex());
assertStrictEqual(
txNew.tx.toHex(),
'020000000001010c86eb9013616e38b4752e56e5683e864cb34fcd7fe790bdc006b60c08446ba50000000017160014139dc70d73097f9d775f8a3280ba3e3435515641ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88aca73303000000000017a914749118baa93fb4b88c28909c8bf0a8202a0484f4870248304502210080545d30e3d30dff272ab11c91fd6150170b603239b48c3d56a3fa66bf240085022003762404e1b45975adc89f61ec1569fa19d6d4a8d405e060897754c489ebeade012103a5de146762f84055db3202c1316cd9008f16047f4f408c1482fdb108217eda0800000000',
);
assertStrictEqual(tx.ins.length, 1);
assertStrictEqual(tx.outs.length, 2);
assertStrictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address
assertStrictEqual(bitcoin.address.fromOutputScript(tx.outs[1].script), wallet.getAddress()); // change address
//
const data2encrypt = 'really long data string';
const crypted = encryption.encrypt(data2encrypt, 'password');
const decrypted = encryption.decrypt(crypted, 'password');
if (decrypted !== data2encrypt) {
throw new Error('encryption lib is not ok');
}
//
const mnemonic =
'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode';
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bip32.fromSeed(seed);
const path = "m/49'/0'/0'/0/0";
const child = root.derivePath(path);
const address = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wpkh({
pubkey: child.publicKey,
network: bitcoin.networks.bitcoin,
}),
network: bitcoin.networks.bitcoin,
}).address;
if (address !== '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK') {
throw new Error('bip49 is not ok');
}
//
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
const hd = new HDSegwitP2SHWallet();
const hashmap = {};
for (let c = 0; c < 1000; c++) {
await hd.generate();
const secret = hd.getSecret();
if (hashmap[secret]) {
throw new Error('Duplicate secret generated!');
}
hashmap[secret] = 1;
if (secret.split(' ').length !== 12 && secret.split(' ').length !== 24) {
throw new Error('mnemonic phrase not ok');
}
}
const hd2 = new HDSegwitP2SHWallet();
hd2.setSecret(hd.getSecret());
if (!hd2.validateMnemonic()) {
throw new Error('mnemonic phrase validation not ok');
}
//
const hd4 = new HDSegwitBech32Wallet();
hd4._xpub = 'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP';
await hd4.fetchBalance();
if (hd4.getBalance() !== 200000) throw new Error('Could not fetch HD Bech32 balance');
await hd4.fetchTransactions();
if (hd4.getTransactions().length !== 4) throw new Error('Could not fetch HD Bech32 transactions');
} else {
// skipping RN-specific test
}
// BlueCrypto test
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
const hex = await BlueCrypto.scrypt('717765727479', '4749345a22b23cf3', 64, 8, 8, 32); // using non-default parameters to speed it up (not-bip38 compliant)
if (hex.toUpperCase() !== 'F36AB2DC12377C788D61E6770126D8A01028C8F6D8FE01871CE0489A1F696A90')
throw new Error('react-native-blue-crypto is not ok');
}
// bip38 test
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
let callbackWasCalled = false;
const decryptedKey = await bip38.decryptAsync(
'6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN',
'qwerty',
() => (callbackWasCalled = true),
);
assertStrictEqual(
wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed),
'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc',
'bip38 failed',
);
// bip38 with BlueCrypto doesn't support progress callback
assertStrictEqual(callbackWasCalled, false, "bip38 doesn't use BlueCrypto");
}
// slip39 test
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
const w = new SLIP39LegacyP2PKHWallet();
w.setSecret(
'shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed\n' +
'shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking',
);
assertStrictEqual(w._getExternalAddressByIndex(0), '18pvMjy7AJbCDtv4TLYbGPbR7SzGzjqUpj', 'SLIP39 failed');
}
//
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
assertStrictEqual(await Linking.canOpenURL('https://github.com/BlueWallet/BlueWallet/'), true, 'Linking can not open https url');
} else {
// skipping RN-specific test'
}
//
assertStrictEqual(Buffer.from('00ff0f', 'hex').reverse().toString('hex'), '0fff00');
//
} catch (Err) {
errorMessage += Err;
isOk = false;
}
this.setState({
isLoading: false,
isOk,
errorMessage,
});
}
render() {
if (this.state.isLoading) {
return <BlueLoading />;
}
return (
<SafeArea>
<BlueCard>
<ScrollView>
<BlueSpacing20 />
{(() => {
if (this.state.isOk) {
return (
<View style={styles.center}>
<BlueText testID="SelfTestOk" h4>
OK
</BlueText>
<BlueSpacing20 />
<BlueText>{loc.settings.about_selftest_ok}</BlueText>
</View>
);
} else {
return (
<View style={styles.center}>
<BlueText h4 numberOfLines={0}>
{this.state.errorMessage}
</BlueText>
</View>
);
}
})()}
<BlueSpacing20 />
<SaveFileButton fileName="bluewallet-selftest.txt" fileContent={'Success on ' + new Date().toUTCString()}>
<Button title="Test Save to Storage" />
</SaveFileButton>
<BlueSpacing20 />
<Button title="Test File Import" onPress={this.onPressImportDocument} />
</ScrollView>
</BlueCard>
</SafeArea>
);
}
}
function assertStrictEqual(actual, expected, message) {
if (expected !== actual) {
if (message) throw new Error(message);
throw new Error('Assertion failed that ' + JSON.stringify(expected) + ' equals ' + JSON.stringify(actual));
}
}
SelfTest.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
}),
};

View file

@ -664,10 +664,13 @@ const SendDetails = () => {
setIsLoading(false);
};
const onWalletSelect = (w: TWallet) => {
setWallet(w);
navigation.dispatch(popAction);
};
useEffect(() => {
const newWallet = wallets.find(w => w.getID() === routeParams.walletID);
if (newWallet) {
setWallet(newWallet);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [routeParams.walletID]);
/**
* same as `importTransaction`, but opens camera instead.
@ -1196,6 +1199,12 @@ const SendDetails = () => {
},
});
const calculateTotalAmount = () => {
const totalAmount = addresses.reduce((total, item) => total + Number(item.amountSats || 0), 0);
const totalWithFee = totalAmount + (feePrecalc.current || 0);
return totalWithFee;
};
const renderFeeSelectionModal = () => {
const nf = networkTransactionFees;
const options = [
@ -1355,12 +1364,15 @@ const SendDetails = () => {
};
const renderCreateButton = () => {
const totalWithFee = calculateTotalAmount();
const isDisabled = totalWithFee === 0 || totalWithFee > balance || balance === 0 || isLoading || addresses.length === 0;
return (
<View style={styles.createButton}>
{isLoading ? (
<ActivityIndicator />
) : (
<Button onPress={createTransaction} title={loc.send.details_next} testID="CreateTransactionButton" />
<Button onPress={createTransaction} disabled={isDisabled} title={loc.send.details_next} testID="CreateTransactionButton" />
)}
</View>
);
@ -1389,7 +1401,7 @@ const SendDetails = () => {
<TouchableOpacity
accessibilityRole="button"
style={styles.selectTouch}
onPress={() => navigation.navigate('SelectWallet', { onWalletSelect, chainType: Chain.ONCHAIN })}
onPress={() => navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN })}
>
<Text style={styles.selectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
<Icon name={I18nManager.isRTL ? 'angle-left' : 'angle-right'} size={18} type="font-awesome" color="#9aa0aa" />
@ -1399,7 +1411,7 @@ const SendDetails = () => {
<TouchableOpacity
accessibilityRole="button"
style={styles.selectTouch}
onPress={() => navigation.navigate('SelectWallet', { onWalletSelect, chainType: Chain.ONCHAIN })}
onPress={() => navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN })}
disabled={!isEditable || isLoading}
>
<Text style={[styles.selectLabel, stylesHook.selectLabel]}>{wallet?.getLabel()}</Text>

View file

@ -17,63 +17,24 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const branch = require('../../current-branch.json');
const About = () => {
const About: React.FC = () => {
const { navigate } = useExtendedNavigation();
const { colors } = useTheme();
const { width, height } = useWindowDimensions();
const { isElectrumDisabled } = useStorage();
const styles = StyleSheet.create({
copyToClipboard: {
justifyContent: 'center',
alignItems: 'center',
},
copyToClipboardText: {
fontSize: 13,
fontWeight: '400',
color: '#68bbe1',
},
center: {
justifyContent: 'center',
alignItems: 'center',
marginTop: 54,
},
logo: {
width: 102,
height: 124,
},
textFree: {
maxWidth: 260,
marginVertical: 24,
color: '#9AA0AA',
fontSize: 15,
textAlign: 'center',
fontWeight: '500',
},
const stylesHook = StyleSheet.create({
textBackup: {
maxWidth: 260,
marginBottom: 40,
color: colors.foregroundColor,
fontSize: 15,
textAlign: 'center',
fontWeight: '500',
},
buildWith: {
backgroundColor: colors.inputBackgroundColor,
padding: 16,
paddingTop: 0,
borderRadius: 8,
},
buttonLink: {
backgroundColor: colors.lightButton,
borderRadius: 12,
justifyContent: 'center',
padding: 8,
flexDirection: 'row',
},
textLink: {
color: colors.foregroundColor,
marginLeft: 8,
fontWeight: '600',
},
});
@ -104,9 +65,11 @@ const About = () => {
const handleOnTelegramPress = () => {
Linking.openURL('https://t.me/bluewallethat');
};
const handleOnGithubPress = () => {
Linking.openURL('https://github.com/BlueWallet/BlueWallet');
};
const handleOnRatePress = () => {
const options = {
AppleAppID: '1376878040',
@ -129,7 +92,7 @@ const About = () => {
<View style={styles.center}>
<Image style={styles.logo} source={require('../../img/bluebeast.png')} />
<Text style={styles.textFree}>{loc.settings.about_free}</Text>
<Text style={styles.textBackup}>{formatStringAddTwoWhiteSpaces(loc.settings.about_backup)}</Text>
<Text style={[styles.textBackup, stylesHook.textBackup]}>{formatStringAddTwoWhiteSpaces(loc.settings.about_backup)}</Text>
{((Platform.OS === 'android' && hasGmsSync()) || Platform.OS !== 'android') && (
<Button onPress={handleOnRatePress} title={loc.settings.about_review + ' ⭐🙏'} />
)}
@ -163,9 +126,8 @@ const About = () => {
title={loc.settings.about_sm_discord}
/>
<BlueCard>
<View style={styles.buildWith}>
<View style={[styles.buildWith, stylesHook.buildWith]}>
<BlueSpacing20 />
<BlueTextCentered>{loc.settings.about_awesome} 👍</BlueTextCentered>
<BlueSpacing20 />
<BlueTextCentered>React Native</BlueTextCentered>
@ -173,10 +135,9 @@ const About = () => {
<BlueTextCentered>Nodejs</BlueTextCentered>
<BlueTextCentered>Electrum server</BlueTextCentered>
<BlueSpacing20 />
<TouchableOpacity accessibilityRole="button" onPress={handleOnGithubPress} style={styles.buttonLink}>
<TouchableOpacity accessibilityRole="button" onPress={handleOnGithubPress} style={[styles.buttonLink, stylesHook.buttonLink]}>
<Icon size={22} name="github" type="font-awesome-5" color={colors.foregroundColor} />
<Text style={styles.textLink}>{formatStringAddTwoWhiteSpaces(loc.settings.about_sm_github)}</Text>
<Text style={[styles.textLink, stylesHook.textLink]}>{formatStringAddTwoWhiteSpaces(loc.settings.about_sm_github)}</Text>
</TouchableOpacity>
</View>
</BlueCard>
@ -241,24 +202,28 @@ const About = () => {
<BlueTextCentered>
{getApplicationName()} ver {getVersion()} (build {getBuildNumber() + ' ' + branch})
</BlueTextCentered>
<BlueTextCentered>{new Date(getBuildNumber() * 1000).toGMTString()}</BlueTextCentered>
<BlueTextCentered>{new Date(Number(getBuildNumber()) * 1000).toUTCString()}</BlueTextCentered>
<BlueTextCentered>{getBundleId()}</BlueTextCentered>
<BlueTextCentered>
w, h = {width}, {height}
</BlueTextCentered>
<BlueTextCentered>Unique ID: {getUniqueIdSync()}</BlueTextCentered>
<View style={styles.copyToClipboard}>
<TouchableOpacity
accessibilityRole="button"
onPress={() => {
const stringToCopy = 'userId:' + getUniqueIdSync();
A.logError('copied unique id');
Clipboard.setString(stringToCopy);
}}
>
<Text style={styles.copyToClipboardText}>{loc.transactions.details_copy}</Text>
</TouchableOpacity>
</View>
{process.env.NODE_ENV !== 'development' && (
<>
<BlueTextCentered>Unique ID: {getUniqueIdSync()}</BlueTextCentered>
<View style={styles.copyToClipboard}>
<TouchableOpacity
accessibilityRole="button"
onPress={() => {
const stringToCopy = 'userId:' + getUniqueIdSync();
A.logError('copied unique id');
Clipboard.setString(stringToCopy);
}}
>
<Text style={styles.copyToClipboardText}>{loc.transactions.details_copy}</Text>
</TouchableOpacity>
</View>
</>
)}
<BlueSpacing20 />
<BlueSpacing20 />
</ScrollView>
@ -266,3 +231,54 @@ const About = () => {
};
export default About;
const styles = StyleSheet.create({
copyToClipboard: {
justifyContent: 'center',
alignItems: 'center',
},
copyToClipboardText: {
fontSize: 13,
fontWeight: '400',
color: '#68bbe1',
},
center: {
justifyContent: 'center',
alignItems: 'center',
marginTop: 54,
},
logo: {
width: 102,
height: 124,
},
textFree: {
maxWidth: 260,
marginVertical: 24,
color: '#9AA0AA',
fontSize: 15,
textAlign: 'center',
fontWeight: '500',
},
textBackup: {
maxWidth: 260,
marginBottom: 40,
fontSize: 15,
textAlign: 'center',
fontWeight: '500',
},
buildWith: {
padding: 16,
paddingTop: 0,
borderRadius: 8,
},
buttonLink: {
borderRadius: 12,
justifyContent: 'center',
padding: 8,
flexDirection: 'row',
},
textLink: {
marginLeft: 8,
fontWeight: '600',
},
});

View file

@ -16,7 +16,7 @@ import { Text } from 'react-native-elements';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Notifications from '../../blue_modules/notifications';
import { BlueCard, BlueReplaceFeeSuggestions, BlueSpacing, BlueSpacing20, BlueText } from '../../BlueComponents';
import { BlueCard, BlueSpacing, BlueSpacing20, BlueText } from '../../BlueComponents';
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
@ -26,6 +26,7 @@ import { BlueCurrentTheme } from '../../components/themes';
import loc from '../../loc';
import { StorageContext } from '../../components/Context/StorageProvider';
import { popToTop } from '../../NavigationService';
import ReplaceFeeSuggestions from '../../components/ReplaceFeeSuggestions';
const styles = StyleSheet.create({
root: {
@ -167,7 +168,7 @@ export default class CPFP extends Component {
<BlueCard style={styles.center}>
<BlueText>{text}</BlueText>
<BlueSpacing20 />
<BlueReplaceFeeSuggestions onFeeSelected={fee => this.setState({ newFeeRate: fee })} transactionMinimum={this.state.feeRate} />
<ReplaceFeeSuggestions onFeeSelected={fee => this.setState({ newFeeRate: fee })} transactionMinimum={this.state.feeRate} />
<BlueSpacing />
<Button
disabled={this.state.newFeeRate <= this.state.feeRate}

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