Merge branch 'master' into patch-1

This commit is contained in:
MC Saeid 2020-12-13 00:53:53 +03:30 committed by GitHub
commit ca17e03c03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 837 additions and 1336 deletions

View file

@ -33,7 +33,7 @@ import * as NavigationService from './NavigationService';
import WalletGradient from './class/wallet-gradient';
import ToolTip from 'react-native-tooltip';
import { BlurView } from '@react-native-community/blur';
import ImagePicker from 'react-native-image-picker';
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
import showPopupMenu from 'react-native-popup-menu-android';
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees';
import Biometric from './class/biometrics';
@ -345,6 +345,7 @@ export class BlueWalletNavigationHeader extends Component {
<LinearGradient
colors={WalletGradient.gradientsFor(this.state.wallet.type)}
style={{ padding: 15, minHeight: 140, justifyContent: 'center' }}
{...WalletGradient.linearGradientProps(this.state.wallet.type)}
>
<Image
source={(() => {
@ -452,6 +453,34 @@ export class BlueWalletNavigationHeader extends Component {
</View>
</TouchableOpacity>
)}
{this.state.wallet.type === MultisigHDWallet.type && (
<TouchableOpacity onPress={this.manageFundsPressed}>
<View
style={{
marginTop: 14,
marginBottom: 10,
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: 9,
minHeight: 39,
alignSelf: 'flex-start',
paddingHorizontal: 12,
height: 39,
justifyContent: 'center',
alignItems: 'center',
}}
>
<Text
style={{
fontWeight: '500',
fontSize: 14,
color: '#FFFFFF',
}}
>
{loc.multisig.manage_keys}
</Text>
</View>
</TouchableOpacity>
)}
</LinearGradient>
);
}
@ -915,15 +944,13 @@ export const BlueSpacing40 = props => {
return <View {...props} style={{ height: 50 }} />;
};
export class BlueSpacingVariable extends Component {
render() {
if (isIpad) {
return <BlueSpacing40 {...this.props} />;
} else {
return <BlueSpacing {...this.props} />;
}
export const BlueSpacingVariable = props => {
if (isIpad) {
return <BlueSpacing40 {...props} />;
} else {
return <BlueSpacing {...props} />;
}
}
};
export class is {
static ipad() {
@ -1015,61 +1042,59 @@ export class BlueUseAllFundsButton extends Component {
}
}
export class BlueDismissKeyboardInputAccessory extends Component {
static InputAccessoryViewID = 'BlueDismissKeyboardInputAccessory';
export const BlueDismissKeyboardInputAccessory = () => {
const { colors } = useTheme();
BlueDismissKeyboardInputAccessory.InputAccessoryViewID = 'BlueDismissKeyboardInputAccessory';
render() {
return Platform.OS !== 'ios' ? null : (
<InputAccessoryView nativeID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}>
<View
style={{
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
height: 44,
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<BlueButtonLink title={loc.send.input_done} onPress={() => Keyboard.dismiss()} />
</View>
</InputAccessoryView>
);
}
}
export class BlueDoneAndDismissKeyboardInputAccessory extends Component {
static InputAccessoryViewID = 'BlueDoneAndDismissKeyboardInputAccessory';
onPasteTapped = async () => {
const clipboard = await Clipboard.getString();
this.props.onPasteTapped(clipboard);
};
render() {
const inputView = (
return Platform.OS !== 'ios' ? null : (
<InputAccessoryView nativeID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}>
<View
style={{
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
backgroundColor: colors.inputBackgroundColor,
height: 44,
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
maxHeight: 44,
}}
>
<BlueButtonLink title={loc.send.input_clear} onPress={this.props.onClearTapped} />
<BlueButtonLink title={loc.send.input_paste} onPress={this.onPasteTapped} />
<BlueButtonLink title={loc.send.input_done} onPress={() => Keyboard.dismiss()} />
<BlueButtonLink title={loc.send.input_done} onPress={Keyboard.dismiss} />
</View>
);
</InputAccessoryView>
);
};
if (Platform.OS === 'ios') {
return <InputAccessoryView nativeID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
} else {
return <KeyboardAvoidingView>{inputView}</KeyboardAvoidingView>;
}
export const BlueDoneAndDismissKeyboardInputAccessory = props => {
const { colors } = useTheme();
BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID = 'BlueDoneAndDismissKeyboardInputAccessory';
const onPasteTapped = async () => {
const clipboard = await Clipboard.getString();
props.onPasteTapped(clipboard);
};
const inputView = (
<View
style={{
backgroundColor: colors.inputBackgroundColor,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
maxHeight: 44,
}}
>
<BlueButtonLink title={loc.send.input_clear} onPress={props.onClearTapped} />
<BlueButtonLink title={loc.send.input_paste} onPress={onPasteTapped} />
<BlueButtonLink title={loc.send.input_done} onPress={Keyboard.dismiss} />
</View>
);
if (Platform.OS === 'ios') {
return <InputAccessoryView nativeID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
} else {
return <KeyboardAvoidingView>{inputView}</KeyboardAvoidingView>;
}
}
};
export const BlueLoading = props => {
return (
@ -1599,24 +1624,17 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
});
const isDesktop = getSystemName() === 'Mac OS X';
export class BlueAddressInput extends Component {
static propTypes = {
isLoading: PropTypes.bool,
onChangeText: PropTypes.func,
onBarScanned: PropTypes.func.isRequired,
launchedBy: PropTypes.string.isRequired,
address: PropTypes.string,
placeholder: PropTypes.string,
};
static defaultProps = {
isLoading: false,
address: '',
placeholder: loc.send.details_address,
};
choosePhoto = () => {
ImagePicker.launchImageLibrary(
export const BlueAddressInput = ({
isLoading = false,
address = '',
placeholder = loc.send.details_address,
onChangeText,
onBarScanned,
launchedBy,
}) => {
const { colors } = useTheme();
const choosePhoto = () => {
launchImageLibrary(
{
title: null,
mediaType: 'photo',
@ -1624,10 +1642,10 @@ export class BlueAddressInput extends Component {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
this.props.onBarScanned(result);
onBarScanned(result);
} else {
alert(loc.send.qr_error_no_qrcode);
}
@ -1637,8 +1655,8 @@ export class BlueAddressInput extends Component {
);
};
takePhoto = () => {
ImagePicker.launchCamera(
const takePhoto = () => {
launchCamera(
{
title: null,
mediaType: 'photo',
@ -1646,10 +1664,10 @@ export class BlueAddressInput extends Component {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
this.props.onBarScanned(result);
onBarScanned(result);
} else {
alert(loc.send.qr_error_no_qrcode);
}
@ -1661,11 +1679,11 @@ export class BlueAddressInput extends Component {
);
};
copyFromClipbard = async () => {
this.props.onBarScanned(await Clipboard.getString());
const copyFromClipbard = async () => {
onBarScanned(await Clipboard.getString());
};
showActionSheet = async () => {
const showActionSheet = async () => {
const isClipboardEmpty = (await Clipboard.getString()).trim().length === 0;
let copyFromClipboardIndex;
if (Platform.OS === 'ios') {
@ -1677,84 +1695,88 @@ export class BlueAddressInput extends Component {
ActionSheet.showActionSheetWithOptions({ options, cancelButtonIndex: 0 }, buttonIndex => {
if (buttonIndex === 1) {
this.choosePhoto();
choosePhoto();
} else if (buttonIndex === 2) {
this.takePhoto();
takePhoto();
} else if (buttonIndex === copyFromClipboardIndex) {
this.copyFromClipbard();
copyFromClipbard();
}
});
}
};
render() {
return (
<View
return (
<View
style={{
flexDirection: 'row',
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: colors.inputBackgroundColor,
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
}}
>
<TextInput
testID="AddressInput"
onChangeText={onChangeText}
placeholder={placeholder}
numberOfLines={1}
placeholderTextColor="#81868e"
value={address}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33, color: '#81868e' }}
editable={!isLoading}
onSubmitEditing={Keyboard.dismiss}
/>
<TouchableOpacity
testID="BlueAddressInputScanQrButton"
disabled={isLoading}
onPress={() => {
Keyboard.dismiss();
if (isDesktop) {
showActionSheet();
} else {
NavigationService.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
launchedBy,
onBarScanned,
},
});
}
}}
style={{
height: 36,
flexDirection: 'row',
borderColor: BlueCurrentTheme.colors.formBorder,
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
justifyContent: 'space-between',
backgroundColor: colors.scanLabel,
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
marginHorizontal: 4,
}}
>
<TextInput
testID="AddressInput"
onChangeText={text => {
this.props.onChangeText(text);
}}
placeholder={this.props.placeholder}
numberOfLines={1}
placeholderTextColor="#81868e"
value={this.props.address}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33, color: '#81868e' }}
editable={!this.props.isLoading}
onSubmitEditing={Keyboard.dismiss}
{...this.props}
/>
<TouchableOpacity
testID="BlueAddressInputScanQrButton"
disabled={this.props.isLoading}
onPress={() => {
Keyboard.dismiss();
if (isDesktop) {
this.showActionSheet();
} else {
NavigationService.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
launchedBy: this.props.launchedBy,
onBarScanned: this.props.onBarScanned,
},
});
}
}}
style={{
height: 36,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: BlueCurrentTheme.colors.scanLabel,
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
marginHorizontal: 4,
}}
>
<Image style={{}} source={require('./img/scan-white.png')} />
<Text style={{ marginLeft: 4, color: BlueCurrentTheme.colors.inverseForegroundColor }}>{loc.send.details_scan}</Text>
</TouchableOpacity>
</View>
);
}
}
<Image style={{}} source={require('./img/scan-white.png')} />
<Text style={{ marginLeft: 4, color: colors.inverseForegroundColor }}>{loc.send.details_scan}</Text>
</TouchableOpacity>
</View>
);
};
BlueAddressInput.propTypes = {
isLoading: PropTypes.bool,
onChangeText: PropTypes.func,
onBarScanned: PropTypes.func.isRequired,
launchedBy: PropTypes.string.isRequired,
address: PropTypes.string,
placeholder: PropTypes.string,
};
export class BlueReplaceFeeSuggestions extends Component {
static propTypes = {

View file

@ -346,6 +346,36 @@ const InitRoot = () => (
</InitStack.Navigator>
);
const ViewEditMultisigCosignersStack = createStackNavigator();
const ViewEditMultisigCosignersRoot = () => (
<ViewEditMultisigCosignersStack.Navigator
name="ViewEditMultisigCosignersRoot"
screenOptions={defaultStackScreenOptions}
initialRouteName="ViewEditMultisigCosigners"
>
<ViewEditMultisigCosignersStack.Screen
name="ViewEditMultisigCosigners"
component={ViewEditMultisigCosigners}
options={ViewEditMultisigCosigners.navigationOptions}
/>
</ViewEditMultisigCosignersStack.Navigator>
);
const ExportMultisigCoordinationSetupStack = createStackNavigator();
const ExportMultisigCoordinationSetupRoot = () => (
<ExportMultisigCoordinationSetupStack.Navigator
name="ExportMultisigCoordinationSetupRoot"
screenOptions={defaultStackScreenOptions}
initialRouteName="ExportMultisigCoordinationSetup"
>
<ExportMultisigCoordinationSetupStack.Screen
name="ExportMultisigCoordinationSetup"
component={ExportMultisigCoordinationSetup}
options={ExportMultisigCoordinationSetup.navigationOptions}
/>
</ExportMultisigCoordinationSetupStack.Navigator>
);
const RootStack = createStackNavigator();
const Navigation = () => (
<RootStack.Navigator mode="modal" screenOptions={defaultScreenOptions} initialRouteName="LoadingScreenRoot">
@ -363,15 +393,11 @@ const Navigation = () => (
{/* screens */}
<RootStack.Screen name="WalletExportRoot" component={WalletExportStackRoot} options={{ headerShown: false }} />
<RootStack.Screen
name="ExportMultisigCoordinationSetup"
component={ExportMultisigCoordinationSetup}
options={ExportMultisigCoordinationSetup.navigationOptions}
/>
<RootStack.Screen
name="ViewEditMultisigCosigners"
component={ViewEditMultisigCosigners}
options={ViewEditMultisigCosigners.navigationOptions}
name="ExportMultisigCoordinationSetupRoot"
component={ExportMultisigCoordinationSetupRoot}
options={{ headerShown: false }}
/>
<RootStack.Screen name="ViewEditMultisigCosignersRoot" component={ViewEditMultisigCosignersRoot} options={{ headerShown: false }} />
<RootStack.Screen name="WalletXpubRoot" component={WalletXpubStackRoot} options={{ headerShown: false }} />
<RootStack.Screen name="BuyBitcoin" component={BuyBitcoin} options={BuyBitcoin.navigationOptions} />
<RootStack.Screen name="Marketplace" component={Marketplace} options={Marketplace.navigationOptions} />

13
__mocks__/react-native-image-picker.js vendored Normal file
View file

@ -0,0 +1,13 @@
import {NativeModules} from 'react-native';
// Mock the ImagePickerManager native module to allow us to unit test the JavaScript code
NativeModules.ImagePickerManager = {
showImagePicker: jest.fn(),
launchCamera: jest.fn(),
launchImageLibrary: jest.fn(),
};
// Reset the mocks before each test
global.beforeEach(() => {
jest.resetAllMocks();
});

View file

@ -2,7 +2,7 @@
buildscript {
ext {
minSdkVersion = 18
minSdkVersion = 21
supportLibVersion = "28.0.0"
buildToolsVersion = "29.0.2"
compileSdkVersion = 29

View file

@ -119,7 +119,7 @@ const showFilePickerAndReadFile = async function () {
} else {
if (res.type === DocumentPicker.types.images || res.type.startsWith('image/')) {
return new Promise(resolve => {
const uri = Platform.OS === 'ios' ? res.uri.toString().replace('file://', '') : res.path.toString();
const uri = Platform.OS === 'ios' ? res.uri.toString().replace('file://', '') : res.uri;
LocalQRCode.decode(decodeURI(uri), (error, result) => {
if (!error) {
resolve({ data: result, uri: decodeURI(res.uri) });

View file

@ -77,7 +77,7 @@ class DeeplinkSchemaMatch {
{
screen: 'ScanLndInvoice',
params: {
secret,
walletID: wallet.getID(),
},
},
]);
@ -290,7 +290,7 @@ class DeeplinkSchemaMatch {
screen: 'ScanLndInvoice',
params: {
uri: uri.lndInvoice,
fromSecret: wallet.getSecret(),
walletID: wallet.getID(),
},
},
];

View file

@ -9,7 +9,7 @@ const createHash = require('create-hash');
*/
export default class Lnurl {
static TAG_PAY_REQUEST = 'payRequest'; // type of LNURL
static TAG_WITHDRAW_REQUEST = "withdrawRequest"; // type of LNURL
static TAG_WITHDRAW_REQUEST = 'withdrawRequest'; // type of LNURL
constructor(url, AsyncStorage) {
this._lnurl = url;
@ -31,12 +31,12 @@ export default class Lnurl {
const found = Lnurl.findlnurl(lnurlExample);
if (!found) return false;
const decoded = bech32.decode(lnurlExample, 10000);
const decoded = bech32.decode(found, 10000);
return Buffer.from(bech32.fromWords(decoded.words)).toString();
}
static isLnurl(url) {
return Lnurl.findlnurl(url) !== null
return Lnurl.findlnurl(url) !== null;
}
async fetchGet(url) {

View file

@ -72,6 +72,21 @@ export default class WalletGradient {
return gradient;
}
static linearGradientProps(type) {
let props;
switch (type) {
case MultisigHDWallet.type:
/* Example
props = { start: { x: 0, y: 0 } };
https://github.com/react-native-linear-gradient/react-native-linear-gradient
*/
break;
default:
break;
}
return props;
}
static headerColorFor(type) {
let gradient;
switch (type) {

View file

@ -171,6 +171,9 @@ export class AbstractWallet {
// It is a ColdCard Hardware Wallet
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);
}
if (parsedSecret.keystore.label) {
this.setLabel(parsedSecret.keystore.label);
}
this.secret = parsedSecret.keystore.xpub;
this.masterFingerprint = masterFingerprint;
}

View file

@ -265,8 +265,8 @@ PODS:
- React
- react-native-geolocation (2.0.2):
- React
- react-native-image-picker (2.3.4):
- React-Core
- react-native-image-picker (3.0.1):
- React
- react-native-is-catalyst (1.0.0):
- React
- react-native-randombytes (3.5.3):
@ -278,7 +278,7 @@ PODS:
- react-native-tcp-socket (3.7.1):
- CocoaAsyncSocket
- React
- react-native-webview (10.10.0):
- react-native-webview (11.0.0):
- React-Core
- react-native-widget-center (0.0.4):
- React
@ -359,7 +359,7 @@ PODS:
- React-Core
- RNDefaultPreference (1.4.3):
- React
- RNDeviceInfo (6.2.0):
- RNDeviceInfo (7.3.1):
- React-Core
- RNFS (2.16.6):
- React
@ -703,13 +703,13 @@ SPEC CHECKSUMS:
react-native-document-picker: c5752781fbc0c126c627c1549b037c139444a4cf
react-native-fingerprint-scanner: c68136ca57e3704d7bdf5faa554ea535ce15b1d0
react-native-geolocation: cbd9d6bd06bac411eed2671810f454d4908484a8
react-native-image-picker: 32d1ad2c0024ca36161ae0d5c2117e2d6c441f11
react-native-image-picker: 4efc5b7f3a780975bcc677335eb670e52d203424
react-native-is-catalyst: 52ee70e0123c82419dd4ce47dc4cc94b22467512
react-native-randombytes: 991545e6eaaf700b4ee384c291ef3d572e0b2ca8
react-native-safe-area-context: 01158a92c300895d79dee447e980672dc3fb85a6
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
react-native-tcp-socket: 96a4f104cdcc9c6621aafe92937f163d88447c5b
react-native-webview: 2e330b109bfd610e9818bf7865d1979f898960a7
react-native-webview: f0da708d7e471b60ebdbf861c114d2c5d7f7af2d
react-native-widget-center: 0f81d17beb163e7fb5848b06754d7d277fe7d99a
React-RCTActionSheet: 53ea72699698b0b47a6421cb1c8b4ab215a774aa
React-RCTAnimation: 1befece0b5183c22ae01b966f5583f42e69a83c2
@ -729,7 +729,7 @@ SPEC CHECKSUMS:
RNCMaskedView: f5c7d14d6847b7b44853f7acb6284c1da30a3459
RNCPushNotificationIOS: eaf01f848a0b872b194d31bcad94bb864299e01e
RNDefaultPreference: 21816c0a6f61a2829ccc0cef034392e9b509ee5f
RNDeviceInfo: 980848feea8d74412b16f2e3e8758c8294d63ca2
RNDeviceInfo: 57bb2806fb7bd982a1434e9f0b4e6a6ab1f6702e
RNFS: 2bd9eb49dc82fa9676382f0585b992c424cd59df
RNGestureHandler: 7a5833d0f788dbd107fbb913e09aa0c1ff333c39
RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa

View file

@ -394,6 +394,7 @@
"list_long_clipboard": "Copy from Clipboard",
"list_long_scan": "Scan QR Code",
"list_tap_here_to_buy": "Buy Bitcoin",
"no_ln_wallet_error": "Before paying a Lightning invoice, you must first add a Lightning wallet.",
"list_title": "Wallets",
"list_tryagain": "Try again",
"looks_like_bip38": "This looks like a password-protected private key (BIP38).",
@ -405,6 +406,7 @@
"xpub_copiedToClipboard": "Copied to clipboard.",
"pull_to_refresh": "Pull to Refresh",
"warning_do_not_disclose": "Warning! Do not disclose",
"add_ln_wallet_first": "You must first add a Lightning wallet.",
"xpub_title": "Wallet XPUB"
},
"multisig": {
@ -418,6 +420,8 @@
"confirm": "Confirm",
"header": "Send",
"share": "Share",
"view": "View",
"manage_keys": "Manage Keys",
"how_many_signatures_can_bluewallet_make": "How Many Signatures Can BlueWallet Make",
"scan_or_import_file": "Scan or import file",
"export_coordination_setup": "Export Coordination Setup",
@ -462,7 +466,6 @@
"input_fp_explain": "Skip to use the default one (00000000)",
"input_path": "Insert Derivation Path",
"input_path_explain": "Skip to use the default one ({default})",
"view_edit_cosigners_title": "Edit Cosigners",
"ms_help": "Help",
"ms_help_title": "How Multisig Vaults Work: Tips and Tricks",
"ms_help_text": "A wallet with multiple keys, for increased security or shared custody",
@ -480,8 +483,9 @@
"is_it_my_address": {
"title": "Is it my address?",
"owns": "{label} owns {address}",
"enter_address": "Enter Address:",
"check_address": "Check Address"
"enter_address": "Enter address",
"check_address": "Check address",
"no_wallet_owns_address": "None of the available wallets own the provided address."
},
"cc": {
"change": "Change",

View file

@ -394,9 +394,10 @@
"list_long_clipboard": "کپی از کلیپ‌بورد",
"list_long_scan": "اسکن کد QR",
"list_tap_here_to_buy": "خرید بیت‌کوین",
"no_ln_wallet_error": "قبل از پرداخت یک فاکتور لایتنینگ، ابتدا باید یک کیف پول لایتنینگ اضافه کنید.",
"list_title": "کیف پول‌ها",
"list_tryagain": "دوباره امتحان کنید",
"looks_like_bip38": "این به کلید خصوصی محافظت‌شده با گذرواژه (BIP38) شباهت دارد",
"looks_like_bip38": "این به کلید خصوصی محافظت‌شده با گذرواژه (BIP38) شباهت دارد.",
"reorder_title": "بازچینی کیف پول‌ها",
"select_no_bitcoin": "هیچ کیف پول بیت‌کوینی درحال‌حاضر دردسترس نیست.",
"select_no_bitcoin_exp": "یک کیف پول بیت‌کوین برای پرکردن کیف پول‌های لایتنینگ نیاز است. لطفاً یکی بسازید یا وارد کنید.",
@ -405,6 +406,7 @@
"xpub_copiedToClipboard": "در کلیپ‌بورد کپی شد.",
"pull_to_refresh": "برای به‌روزسانی به پایین بکشید",
"warning_do_not_disclose": "هشدار! فاش نکنید.",
"add_ln_wallet_first": "ابتدا باید یک کیف پول لایتنینگ اضافه کنید.",
"xpub_title": "کلید XPUB کیف پول"
},
"multisig": {
@ -418,6 +420,8 @@
"confirm": "تأیید",
"header": "ارسال",
"share": "اشتراک‌گذاری",
"view": "مشاهده",
"manage_keys": "مدیریت کلیدها",
"how_many_signatures_can_bluewallet_make": "امضاهایی که BlueWallet می‌تواند ایجاد کند",
"scan_or_import_file": "اسکن یا واردکردن فایل",
"export_coordination_setup": "راه‌اندازی هماهنگی صادرکردن",
@ -453,7 +457,7 @@
"this_is_cosigners_xpub": "این XPUB امضاکنندهٔ مشترک است—آماده برای واردشدن درون یک کیف پول دیگر. به‌اشتراک‌گذاری آن بی‌خطر است.",
"wallet_key_created": "کلید گاوصندوق شما ایجاد شد. لحظه‌ای درنگ کرده تا با خیال راحت از سید خود نسخهٔ پشتیبان تهیه کنید.",
"are_you_sure_seed_will_be_lost": "مطمئن هستید؟ درصورتی‌که نسخهٔ پشتیبان نداشته باشید، سید شما ازبین خواهد رفت.",
"forget_this_seed": "این سید را فراموش و به‌جای آن از XPUB استفاده کن",
"forget_this_seed": "این سید را فراموش و به‌جای آن از XPUB استفاده کن.",
"invalid_fingerprint": "اثر انگشت سید با اثر انگشت این امضاکنندهٔ مشترک مطابقت ندارد.",
"view_edit_cosigners": "مشاهده/ویرایش امضاکنندگان مشترک",
"this_cosigner_is_already_imported": "این امضاکنندهٔ مشترک قبلاً وارد شده است.",
@ -462,7 +466,6 @@
"input_fp_explain": "جهت استفاده از تنظیمات پیش‌فرض (۰۰۰۰۰۰۰۰) رد کنید",
"input_path": "مسیر اشتقاق را وارد کنید",
"input_path_explain": "جهت استفاده از تنظیمات پیش‌فرض ({default}) رد کنید",
"view_edit_cosigners_title": "ویرایش امضاکنندگان مشترک",
"ms_help": "راهنما",
"ms_help_title": "نحوهٔ کارکرد گاوصندوق‌های چندامضایی: نکات و ترفندها",
"ms_help_text": "یک کیف پول با چندین کلید، جهت امنیت بالاتر یا داشتن حساب مشترک",
@ -480,8 +483,9 @@
"is_it_my_address": {
"title": "آیا آدرس من است؟",
"owns": "آدرس {address} متعلق به «{label}» است.",
"enter_address": "آدرس را وارد کنید:",
"check_address": "بررسی آدرس"
"enter_address": "آدرس را وارد کنید",
"check_address": "بررسی آدرس",
"no_wallet_owns_address": "آدرس ارائه‌شده متعلق به هیچ‌کدام از کیف پول‌های موجود نیست."
},
"cc": {
"change": "باقی‌مانده (change)",

12
package-lock.json generated
View file

@ -18931,9 +18931,9 @@
"integrity": "sha512-sQDYwGEdxwKwXKP/8Intc81FyH33Rv8ZvOxdmPX4NM75RAIVeBc13pdabEqycAimNZoY5IDvGp4o1cTTa5gNrA=="
},
"react-native-device-info": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-7.0.2.tgz",
"integrity": "sha512-3jzEzzX0Zo5rm+fClPFp3OGUnRPjK0w4X6RCNZtuaNbwMG+Vm3BLcU69dv+UfkYMrCo5JSKV6lA2fG25qq2tuA=="
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-7.3.1.tgz",
"integrity": "sha512-RQP3etbmXsOlcaxHeHNug68nRli02S9iGC7TbaXpkvyyevIuRogfnrI71sWtqmlT91kdpYAOYKmNfRL9LOSKVw=="
},
"react-native-document-picker": {
"version": "git+https://github.com/BlueWallet/react-native-document-picker.git#3684d4fcc2bc0b47c32be39024e4796004c3e428",
@ -18996,9 +18996,9 @@
"integrity": "sha512-KTIy7lExwMtB6pOpCARyUzFj5EzYTh+A1GN/FB5Eb0LrW5C6hbb1kdj9K2/RHyZC+wyAJD1M823ZaDCU6n6cLA=="
},
"react-native-image-picker": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-2.3.4.tgz",
"integrity": "sha512-4UHu+zOyDT570r5mIbjH6h1iMrKIq/qfsKiAVUEZwncVegh0usJiEYNyJw4CEVwNeehmye/ia5sLDsa+kzIE4g=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-3.0.1.tgz",
"integrity": "sha512-jVWiKT24jxr7K8dAixlsfHURZd+eDA4WWYeZhhKeJDGITCtFPphcTuFomTMtaWYE/gJgNTEL8TiLedcfeqJ9qw=="
},
"react-native-inappbrowser-reborn": {
"version": "git+https://github.com/BlueWallet/react-native-inappbrowser.git#fa2d8e1763e46dd12a7e53081e97a0f908049103",

View file

@ -121,7 +121,7 @@
"react-native-blue-crypto": "git+https://github.com/Overtorment/react-native-blue-crypto.git",
"react-native-camera": "3.40.0",
"react-native-default-preference": "1.4.3",
"react-native-device-info": "7.0.2",
"react-native-device-info": "7.3.1",
"react-native-document-picker": "git+https://github.com/BlueWallet/react-native-document-picker.git#3684d4fcc2bc0b47c32be39024e4796004c3e428",
"react-native-elements": "2.3.2",
"react-native-fingerprint-scanner": "git+https://github.com/BlueWallet/react-native-fingerprint-scanner.git#ce644673681716335d786727bab998f7e632ab5e",
@ -129,7 +129,7 @@
"react-native-gesture-handler": "1.8.0",
"react-native-handoff": "git+https://github.com/marcosrdz/react-native-handoff.git",
"react-native-haptic-feedback": "1.11.0",
"react-native-image-picker": "2.3.4",
"react-native-image-picker": "3.0.1",
"react-native-inappbrowser-reborn": "git+https://github.com/BlueWallet/react-native-inappbrowser.git#fa2d8e1763e46dd12a7e53081e97a0f908049103",
"react-native-is-catalyst": "git+https://github.com/BlueWallet/react-native-is-catalyst.git#v1.0.0",
"react-native-level-fs": "3.0.1",

View file

@ -290,14 +290,12 @@ const styles = StyleSheet.create({
export default class Browser extends Component {
constructor(props) {
super(props);
if (!props.route.params.fromSecret) throw new Error('Invalid param');
if (!props.route.params.fromWallet) throw new Error('Invalid param');
let url;
if (props.route.params.url) url = props.route.params.url;
this.state = {
url: url || 'https://bluewallet.io/marketplace/',
fromSecret: props.route.params.fromSecret,
fromWallet: props.route.params.fromWallet,
canGoBack: false,
pageIsLoading: false,
@ -360,7 +358,7 @@ export default class Browser extends Component {
screen: 'ScanLndInvoice',
params: {
uri: json.sendPayment,
fromSecret: this.state.fromSecret,
walletID: this.state.fromWallet.getID(),
},
});
},

View file

@ -42,7 +42,7 @@ const LNDCreateInvoice = () => {
const { name } = useRoute();
const { colors } = useTheme();
const { navigate, dangerouslyGetParent, goBack, pop, setParams } = useNavigation();
const [unit, setUnit] = useState(wallet.current.getPreferredBalanceUnit());
const [unit, setUnit] = useState(wallet.current?.getPreferredBalanceUnit() || BitcoinUnit.BTC);
const [amount, setAmount] = useState();
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false);
const [isLoading, setIsLoading] = useState(true);
@ -131,6 +131,10 @@ const LNDCreateInvoice = () => {
},
});
}
} else {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert(loc.wallets.add_ln_wallet_first);
goBack();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallet]),
@ -194,7 +198,7 @@ const LNDCreateInvoice = () => {
navigate('LNDViewInvoice', {
invoice: invoiceRequest,
walletID,
walletID: wallet.current.getID(),
isModal: true,
});
} catch (Err) {
@ -206,9 +210,9 @@ const LNDCreateInvoice = () => {
const processLnurl = async data => {
setIsLoading(true);
if (!wallet) {
if (!wallet.current) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
alert(loc.wallets.no_ln_wallet_error);
return goBack();
}
@ -233,7 +237,7 @@ const LNDCreateInvoice = () => {
screen: 'LnurlPay',
params: {
lnurl: data,
fromWalletID: walletID || wallet.current.getID(),
fromWalletID: wallet.current.getID(),
},
});
return;
@ -342,7 +346,7 @@ const LNDCreateInvoice = () => {
pop();
};
if (wallet.current === undefined || !walletID) {
if (!wallet.current) {
return (
<View style={[styles.root, styleHooks.root]}>
<StatusBar barStyle="light-content" />

View file

@ -1,5 +1,5 @@
/* global alert */
import React from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import {
Text,
ActivityIndicator,
@ -11,7 +11,6 @@ import {
ScrollView,
StyleSheet,
} from 'react-native';
import PropTypes from 'prop-types';
import {
BlueButton,
SafeBlueArea,
@ -29,16 +28,351 @@ import { Icon } from 'react-native-elements';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Biometric from '../../class/biometrics';
import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BlueCurrentTheme } from '../../components/themes';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
const currency = require('../../blue_modules/currency');
const ScanLndInvoice = () => {
const { wallets, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext);
const { colors } = useTheme();
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.type === LightningCustodianWallet.type),
);
const { navigate, setParams, goBack, pop } = useNavigation();
const [isLoading, setIsLoading] = useState(false);
const [renderWalletSelectionButtonHidden, setRenderWalletSelectionButtonHidden] = useState(false);
const [destination, setDestination] = useState('');
const [unit, setUnit] = useState(wallet?.getPreferredBalanceUnit() || BitcoinUnit.SATS);
const [decoded, setDecoded] = useState();
const [amount, setAmount] = useState();
const [isAmountInitiallyEmpty, setIsAmountInitiallyEmpty] = useState();
const [expiresIn, setExpiresIn] = useState();
const stylesHook = StyleSheet.create({
walletWrapLabel: {
color: colors.buttonAlternativeTextColor,
},
walletWrapBalance: {
color: colors.buttonAlternativeTextColor,
},
walletWrapSats: {
color: colors.buttonAlternativeTextColor,
},
root: {
backgroundColor: colors.elevated,
},
});
useEffect(() => {
console.log('scanLndInvoice useEffect');
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.removeListener('keyboardDidShow', _keyboardDidShow);
Keyboard.removeListener('keyboardDidHide', _keyboardDidHide);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (walletID && wallet?.getID() !== walletID) {
setWallet(wallets.find(w => w.getID() === walletID));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletID]);
useFocusEffect(
useCallback(() => {
if (!wallet) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert(loc.wallets.no_ln_wallet_error);
goBack();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallet]),
[],
);
useEffect(() => {
if (wallet && uri) {
let data = uri;
// handling BIP21 w/BOLT11 support
const ind = data.indexOf('lightning=');
if (ind !== -1) {
data = data.substring(ind + 10).split('&')[0];
}
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data);
/**
* @type {LightningCustodianWallet}
*/
let decoded;
try {
decoded = wallet.decodeInvoice(data);
let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
expiresIn = loc.lnd.expiredLow;
} else {
expiresIn = Math.round((expiresIn - +new Date()) / (60 * 1000)) + ' min';
}
Keyboard.dismiss();
setParams({ uri: undefined, invoice: data });
setIsAmountInitiallyEmpty(decoded.num_satoshis === '0');
setDestination(data);
setIsLoading(false);
setAmount(decoded.num_satoshis);
setExpiresIn(expiresIn);
setDecoded(decoded);
} catch (Err) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
Keyboard.dismiss();
setParams({ uri: undefined });
setTimeout(() => alert(Err.message), 10);
setIsLoading(false);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uri]);
const _keyboardDidShow = () => {
setRenderWalletSelectionButtonHidden(true);
};
const _keyboardDidHide = () => {
setRenderWalletSelectionButtonHidden(false);
};
const processInvoice = data => {
if (Lnurl.isLnurl(data)) return processLnurlPay(data);
setParams({ uri: data });
};
const processLnurlPay = data => {
navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPay',
params: {
lnurl: data,
fromWalletID: walletID || wallet.getID(),
},
});
};
const pay = async () => {
if (!decoded) {
return null;
}
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return;
}
}
let amountSats = amount;
switch (unit) {
case BitcoinUnit.SATS:
amountSats = parseInt(amountSats); // nop
break;
case BitcoinUnit.BTC:
amountSats = currency.btcToSatoshi(amountSats);
break;
case BitcoinUnit.LOCAL_CURRENCY:
amountSats = currency.btcToSatoshi(currency.fiatToBTC(amountSats));
break;
}
setIsLoading(true);
const expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
setIsLoading(false);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return alert(loc.lnd.errorInvoiceExpired);
}
const currentUserInvoices = wallet.let.user_invoices_raw; // not fetching invoices, as we assume they were loaded previously
if (currentUserInvoices.some(invoice => invoice.payment_hash === decoded.payment_hash)) {
setIsLoading(false);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return alert(loc.lnd.sameWalletAsInvoiceError);
}
try {
await wallet.payInvoice(invoice, amountSats);
} catch (Err) {
console.log(Err.message);
setIsLoading(false);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return alert(Err.message);
}
navigate('Success', {
amount: amountSats,
amountUnit: BitcoinUnit.SATS,
invoiceDescription: decoded.description,
});
fetchAndSaveWalletTransactions(walletID);
};
const processTextForInvoice = text => {
if (text.toLowerCase().startsWith('lnb') || text.toLowerCase().startsWith('lightning:lnb') || Lnurl.isLnurl(text)) {
processInvoice(text);
} else {
setDecoded(undefined);
setExpiresIn(undefined);
setDestination(text);
}
};
const shouldDisablePayButton = () => {
if (!decoded) {
return true;
} else {
if (!amount) {
return true;
}
}
return !(amount > 0);
// return decoded.num_satoshis <= 0 || isLoading || isNaN(decoded.num_satoshis);
};
const naviageToSelectWallet = () => {
navigate('SelectWallet', { onWalletSelect, chainType: Chain.OFFCHAIN });
};
const renderWalletSelectionButton = () => {
if (renderWalletSelectionButtonHidden) return;
const walletLabel = wallet.getLabel();
return (
<View style={styles.walletSelectRoot}>
{!isLoading && (
<TouchableOpacity style={styles.walletSelectTouch} onPress={naviageToSelectWallet}>
<Text style={styles.walletSelectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
<Icon name="angle-right" size={18} type="font-awesome" color="#9aa0aa" />
</TouchableOpacity>
)}
<View style={styles.walletWrap}>
<TouchableOpacity style={styles.walletWrapTouch} onPress={naviageToSelectWallet}>
<Text style={[styles.walletWrapLabel, stylesHook.walletWrapLabel]}>{walletLabel}</Text>
<Text style={[styles.walletWrapBalance, stylesHook.walletWrapBalance]}>
{formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, false)}
</Text>
<Text style={[styles.walletWrapSats, stylesHook.walletWrapSats]}>{BitcoinUnit.SATS}</Text>
</TouchableOpacity>
</View>
</View>
);
};
const getFees = () => {
const min = Math.floor(decoded.num_satoshis * 0.003);
const max = Math.floor(decoded.num_satoshis * 0.01) + 1;
return `${min} sat - ${max} sat`;
};
const onWalletSelect = selectedWallet => {
setParams({ walletID: selectedWallet.getID() });
pop();
};
if (wallet === undefined || !wallet) {
return (
<View style={[styles.loadingIndicator, stylesHook.root]}>
<BlueLoading />
</View>
);
}
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={[styles.root, stylesHook.root]}>
<StatusBar barStyle="light-content" />
<View style={[styles.root, stylesHook.root]}>
<ScrollView contentContainerStyle={styles.scroll}>
<KeyboardAvoidingView enabled behavior="position" keyboardVerticalOffset={20}>
<View style={styles.scrollMargin}>
<BlueBitcoinAmount
pointerEvents={isAmountInitiallyEmpty ? 'auto' : 'none'}
isLoading={isLoading}
amount={amount}
onAmountUnitChange={setUnit}
onChangeText={setAmount}
disabled={!decoded || isLoading || decoded.num_satoshis > 0}
unit={BitcoinUnit.SATS}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
</View>
<BlueCard>
<BlueAddressInput
onChangeText={text => {
text = text.trim();
processTextForInvoice(text);
}}
onBarScanned={processInvoice}
address={destination}
isLoading={isLoading}
placeholder={loc.lnd.placeholder}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
launchedBy={name}
/>
<View style={styles.description}>
<Text numberOfLines={0} style={styles.descriptionText}>
{decoded !== undefined ? decoded.description : ''}
</Text>
</View>
{expiresIn !== undefined && (
<View>
<Text style={styles.expiresIn}>{loc.formatString(loc.lnd.expiresIn, { time: expiresIn })}</Text>
{decoded && decoded.num_satoshis > 0 && (
<Text style={styles.expiresIn}>{loc.formatString(loc.lnd.potentialFee, { fee: getFees() })}</Text>
)}
</View>
)}
<BlueCard>
{isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
<View>
<BlueButton title={loc.lnd.payButton} onPress={pay} disabled={shouldDisablePayButton()} />
</View>
)}
</BlueCard>
</BlueCard>
</KeyboardAvoidingView>
{renderWalletSelectionButton()}
</ScrollView>
</View>
<BlueDismissKeyboardInputAccessory />
</SafeBlueArea>
);
};
export default ScanLndInvoice;
ScanLndInvoice.navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: loc.send.header,
headerLeft: null,
});
const styles = StyleSheet.create({
walletSelectRoot: {
marginBottom: 16,
alignItems: 'center',
justifyContent: 'flex-end',
},
loadingIndicator: {
flex: 1,
justifyContent: 'center',
},
walletSelectTouch: {
flexDirection: 'row',
alignItems: 'center',
@ -58,18 +392,15 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
walletWrapLabel: {
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
fontSize: 14,
},
walletWrapBalance: {
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
fontSize: 14,
fontWeight: '600',
marginLeft: 4,
marginRight: 4,
},
walletWrapSats: {
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
fontSize: 11,
fontWeight: '600',
textAlignVertical: 'bottom',
@ -77,7 +408,6 @@ const styles = StyleSheet.create({
},
root: {
flex: 1,
backgroundColor: BlueCurrentTheme.colors.elevated,
},
scroll: {
flex: 1,
@ -105,371 +435,3 @@ const styles = StyleSheet.create({
top: 10,
},
});
export default class ScanLndInvoice extends React.Component {
static contextType = BlueStorageContext;
state = {
isLoading: false,
isAmountInitiallyEmpty: false,
renderWalletSelectionButtonHidden: false,
};
constructor(props, context) {
super(props);
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
if (!context.wallets.some(item => item.type === LightningCustodianWallet.type)) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
props.navigation.dangerouslyGetParent().pop();
} else {
let fromSecret;
if (props.route.params.fromSecret) fromSecret = props.route.params.fromSecret;
let fromWallet = {};
if (!fromSecret) {
const lightningWallets = context.wallets.filter(item => item.type === LightningCustodianWallet.type);
if (lightningWallets.length > 0) {
fromSecret = lightningWallets[0].getSecret();
console.warn('warning: using ln wallet index 0');
}
}
for (const w of context.wallets) {
if (w.getSecret() === fromSecret) {
fromWallet = w;
break;
}
}
this.state = {
fromWallet,
fromSecret,
unit: BitcoinUnit.SATS,
destination: '',
};
}
}
static getDerivedStateFromProps(props, state) {
if (props.route.params.uri) {
let data = props.route.params.uri;
// handling BIP21 w/BOLT11 support
const ind = data.indexOf('lightning=');
if (ind !== -1) {
data = data.substring(ind + 10).split('&')[0];
}
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data);
/**
* @type {LightningCustodianWallet}
*/
const w = state.fromWallet;
let decoded;
try {
decoded = w.decodeInvoice(data);
let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
expiresIn = loc.lnd.expiredLow;
} else {
expiresIn = Math.round((expiresIn - +new Date()) / (60 * 1000)) + ' min';
}
Keyboard.dismiss();
props.navigation.setParams({ uri: undefined });
return {
invoice: data,
decoded,
unit: state.unit,
amount: decoded.num_satoshis,
expiresIn,
destination: data,
isAmountInitiallyEmpty: decoded.num_satoshis === '0',
isLoading: false,
};
} catch (Err) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
Keyboard.dismiss();
props.navigation.setParams({ uri: undefined });
setTimeout(() => alert(Err.message), 10);
return { ...state, isLoading: false };
}
}
return state;
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow = () => {
this.setState({ renderWalletSelectionButtonHidden: true });
};
_keyboardDidHide = () => {
this.setState({ renderWalletSelectionButtonHidden: false });
};
processInvoice = data => {
if (Lnurl.isLnurl(data)) return this.processLnurlPay(data);
this.props.navigation.setParams({ uri: data });
};
processLnurlPay = data => {
this.props.navigation.navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPay',
params: {
lnurl: data,
fromWalletID: this.state.fromWallet.getID(),
},
});
};
async pay() {
if (!('decoded' in this.state)) {
return null;
}
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return;
}
}
let amountSats = this.state.amount;
switch (this.state.unit) {
case BitcoinUnit.SATS:
amountSats = parseInt(amountSats); // nop
break;
case BitcoinUnit.BTC:
amountSats = currency.btcToSatoshi(amountSats);
break;
case BitcoinUnit.LOCAL_CURRENCY:
amountSats = currency.btcToSatoshi(currency.fiatToBTC(amountSats));
break;
}
this.setState(
{
isLoading: true,
},
async () => {
const decoded = this.state.decoded;
/** @type {LightningCustodianWallet} */
const fromWallet = this.state.fromWallet;
const expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms
if (+new Date() > expiresIn) {
this.setState({ isLoading: false });
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return alert(loc.lnd.errorInvoiceExpired);
}
const currentUserInvoices = fromWallet.user_invoices_raw; // not fetching invoices, as we assume they were loaded previously
if (currentUserInvoices.some(invoice => invoice.payment_hash === decoded.payment_hash)) {
this.setState({ isLoading: false });
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return alert(loc.lnd.sameWalletAsInvoiceError);
}
try {
await fromWallet.payInvoice(this.state.invoice, amountSats);
} catch (Err) {
console.log(Err.message);
this.setState({ isLoading: false });
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return alert(Err.message);
}
this.props.navigation.navigate('Success', {
amount: amountSats,
amountUnit: BitcoinUnit.SATS,
invoiceDescription: this.state.decoded.description,
});
this.context.fetchAndSaveWalletTransactions(fromWallet.getID());
},
);
}
processTextForInvoice = text => {
if (text.toLowerCase().startsWith('lnb') || text.toLowerCase().startsWith('lightning:lnb') || Lnurl.isLnurl(text)) {
this.processInvoice(text);
} else {
this.setState({ decoded: undefined, expiresIn: undefined, destination: text });
}
};
shouldDisablePayButton = () => {
if (typeof this.state.decoded !== 'object') {
return true;
} else {
if (!this.state.amount) {
return true;
}
}
return !(this.state.amount > 0);
// return this.state.decoded.num_satoshis <= 0 || this.state.isLoading || isNaN(this.state.decoded.num_satoshis);
};
renderWalletSelectionButton = () => {
if (this.state.renderWalletSelectionButtonHidden) return;
return (
<View style={styles.walletSelectRoot}>
{!this.state.isLoading && (
<TouchableOpacity
style={styles.walletSelectTouch}
onPress={() =>
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
}
>
<Text style={styles.walletSelectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
<Icon name="angle-right" size={18} type="font-awesome" color="#9aa0aa" />
</TouchableOpacity>
)}
<View style={styles.walletWrap}>
<TouchableOpacity
style={styles.walletWrapTouch}
onPress={() =>
this.props.navigation.navigate('SelectWallet', { onWalletSelect: this.onWalletSelect, chainType: Chain.OFFCHAIN })
}
>
<Text style={styles.walletWrapLabel}>{this.state.fromWallet.getLabel()}</Text>
<Text style={styles.walletWrapBalance}>
{formatBalanceWithoutSuffix(this.state.fromWallet.getBalance(), BitcoinUnit.SATS, false)}
</Text>
<Text style={styles.walletWrapSats}>{BitcoinUnit.SATS}</Text>
</TouchableOpacity>
</View>
</View>
);
};
getFees() {
const min = Math.floor(this.state.decoded.num_satoshis * 0.003);
const max = Math.floor(this.state.decoded.num_satoshis * 0.01) + 1;
return `${min} sat - ${max} sat`;
}
onWalletSelect = wallet => {
this.setState({ fromSecret: wallet.getSecret(), fromWallet: wallet }, () => {
this.props.navigation.pop();
});
};
async componentDidMount() {
console.log('scanLndInvoice did mount');
}
render() {
if (!this.state.fromWallet) {
return <BlueLoading />;
}
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={styles.root}>
<StatusBar barStyle="light-content" />
<View style={styles.root}>
<ScrollView contentContainerStyle={styles.scroll}>
<KeyboardAvoidingView enabled behavior="position" keyboardVerticalOffset={20}>
<View style={styles.scrollMargin}>
<BlueBitcoinAmount
pointerEvents={this.state.isAmountInitiallyEmpty ? 'auto' : 'none'}
isLoading={this.state.isLoading}
amount={this.state.amount}
onAmountUnitChange={unit => this.setState({ unit })}
onChangeText={text => {
this.setState({ amount: text });
/* if (typeof this.state.decoded === 'object') {
text = parseInt(text || 0);
const decoded = this.state.decoded;
decoded.num_satoshis = text;
this.setState({ decoded: decoded });
} */
}}
disabled={
typeof this.state.decoded !== 'object' ||
this.state.isLoading ||
(this.state.decoded && this.state.decoded.num_satoshis > 0)
}
unit={BitcoinUnit.SATS}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
</View>
<BlueCard>
<BlueAddressInput
onChangeText={text => {
text = text.trim();
this.processTextForInvoice(text);
}}
onBarScanned={this.processInvoice}
address={this.state.destination}
isLoading={this.state.isLoading}
placeholder={loc.lnd.placeholder}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
launchedBy={this.props.route.name}
/>
<View style={styles.description}>
<Text numberOfLines={0} style={styles.descriptionText}>
{'decoded' in this.state && this.state.decoded !== undefined ? this.state.decoded.description : ''}
</Text>
</View>
{this.state.expiresIn !== undefined && (
<View>
<Text style={styles.expiresIn}>{loc.formatString(loc.lnd.expiresIn, { time: this.state.expiresIn })}</Text>
{this.state.decoded && this.state.decoded.num_satoshis > 0 && (
<Text style={styles.expiresIn}>{loc.formatString(loc.lnd.potentialFee, { fee: this.getFees() })}</Text>
)}
</View>
)}
<BlueCard>
{this.state.isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
<View>
<BlueButton title={loc.lnd.payButton} onPress={() => this.pay()} disabled={this.shouldDisablePayButton()} />
</View>
)}
</BlueCard>
</BlueCard>
</KeyboardAvoidingView>
{this.renderWalletSelectionButton()}
</ScrollView>
</View>
<BlueDismissKeyboardInputAccessory />
</SafeBlueArea>
);
}
}
ScanLndInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
navigate: PropTypes.func,
pop: PropTypes.func,
setParams: PropTypes.func,
dangerouslyGetParent: PropTypes.func,
}),
route: PropTypes.shape({
name: PropTypes.string,
params: PropTypes.shape({
uri: PropTypes.string,
fromSecret: PropTypes.string,
}),
}),
};
ScanLndInvoice.navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: loc.send.header,
headerLeft: null,
});

View file

@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { Image, View, TouchableOpacity, StatusBar, Platform, StyleSheet, TextInput } from 'react-native';
import { RNCamera } from 'react-native-camera';
import { Icon } from 'react-native-elements';
import ImagePicker from 'react-native-image-picker';
import { launchImageLibrary } from 'react-native-image-picker';
import { decodeUR, extractSingleWorkload } from 'bc-ur';
import { useNavigation, useRoute, useIsFocused, useTheme } from '@react-navigation/native';
import loc from '../../loc';
@ -192,7 +192,7 @@ const ScanQRCode = () => {
const showImagePicker = () => {
if (!isLoading) {
setIsLoading(true);
ImagePicker.launchImageLibrary(
launchImageLibrary(
{
title: null,
mediaType: 'photo',
@ -200,7 +200,7 @@ const ScanQRCode = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarCodeRead({ data: result });

View file

@ -7,12 +7,10 @@ import {
BlueButton,
BlueSpacing10,
BlueSpacing20,
BlueFormLabel,
BlueNavigationStyle,
BlueText,
BlueButtonLink,
} from '../../BlueComponents';
import { BlueCurrentTheme } from '../../components/themes';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
@ -33,6 +31,11 @@ const IsItMyAddress = () => {
text: {
color: colors.foregroundColor,
},
input: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
});
const handleUpdateAddress = nextValue => setAddress(nextValue.trim());
@ -46,6 +49,10 @@ const IsItMyAddress = () => {
}
}
if (_result.length === 0) {
setResult(_result.push(loc.is_it_my_address.no_wallet_owns_address));
}
setResult(_result.join('\n\n'));
};
@ -64,30 +71,38 @@ const IsItMyAddress = () => {
});
};
const clearAddressInput = () => {
setAddress('');
setResult();
};
return (
<SafeBlueArea style={[styles.blueArea, stylesHooks.blueArea]}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null} keyboardShouldPersistTaps="handled">
<View style={styles.wrapper}>
<BlueCard style={styles.mainCard}>
<View style={styles.topFormRow}>
<BlueFormLabel>{loc.is_it_my_address.enter_address}</BlueFormLabel>
<View style={[styles.input, stylesHooks.input]}>
<TextInput
style={styles.text}
maxHeight={100}
minHeight={100}
maxWidth="100%"
minWidth="100%"
multiline
editable
placeholder={loc.is_it_my_address.enter_address}
placeholderTextColor="#81868e"
value={address}
onChangeText={handleUpdateAddress}
/>
</View>
<TextInput
style={[styles.text, stylesHooks.text]}
maxHeight={100}
minHeight={100}
maxWidth="100%"
minWidth="100%"
multiline
editable
value={address}
onChangeText={handleUpdateAddress}
/>
<BlueSpacing10 />
<BlueButtonLink title={loc.wallets.import_scan_qr} onPress={importScan} />
<BlueSpacing10 />
<BlueButton title={loc.is_it_my_address.check_address} onPress={checkAddress} />
<BlueButton title={loc.send.input_clear} onPress={clearAddressInput} />
<BlueSpacing20 />
<BlueButton disabled={address.trim().length === 0} title={loc.is_it_my_address.check_address} onPress={checkAddress} />
<BlueSpacing20 />
<BlueText>{result}</BlueText>
</BlueCard>
@ -140,17 +155,16 @@ const styles = StyleSheet.create({
height: 30,
maxHeight: 30,
},
text: {
flex: 1,
borderColor: '#ebebeb',
backgroundColor: '#d2f8d6',
input: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
alignItems: 'center',
borderRadius: 4,
marginTop: 20,
color: BlueCurrentTheme.colors.foregroundColor,
fontWeight: '500',
fontSize: 14,
paddingHorizontal: 16,
paddingBottom: 16,
paddingTop: 16,
},
text: {
padding: 8,
minHeight: 33,
color: '#81868e',
},
});

View file

@ -6,7 +6,7 @@ import { DynamicQRCode } from '../../components/DynamicQRCode';
import { SquareButton } from '../../components/SquareButton';
import { getSystemName } from 'react-native-device-info';
import loc from '../../loc';
import ImagePicker from 'react-native-image-picker';
import { launchCamera } from 'react-native-image-picker';
import ScanQRCode from './ScanQRCode';
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
const bitcoin = require('bitcoinjs-lib');
@ -49,7 +49,7 @@ const PsbtMultisigQRCode = () => {
const openScanner = () => {
if (isDesktop) {
ImagePicker.launchCamera(
launchCamera(
{
title: null,
mediaType: 'photo',
@ -57,7 +57,7 @@ const PsbtMultisigQRCode = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarScanned(result);

View file

@ -13,7 +13,7 @@ import {
StyleSheet,
Alert,
} from 'react-native';
import ImagePicker from 'react-native-image-picker';
import { launchCamera } from 'react-native-image-picker';
import Clipboard from '@react-native-community/clipboard';
import {
SecondButton,
@ -244,7 +244,7 @@ const PsbtWithHardwareWallet = () => {
const openScanner = () => {
if (isDesktop) {
ImagePicker.launchCamera(
launchCamera(
{
title: null,
mediaType: 'photo',
@ -252,7 +252,7 @@ const PsbtWithHardwareWallet = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarScanned(result);

View file

@ -29,7 +29,7 @@ import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
import loc from '../../loc';
import { getSystemName } from 'react-native-device-info';
import ImagePicker from 'react-native-image-picker';
import { launchCamera } from 'react-native-image-picker';
import ScanQRCode from '../send/ScanQRCode';
import QRCode from 'react-native-qrcode-svg';
import { SquareButton } from '../../components/SquareButton';
@ -59,7 +59,6 @@ const WalletsAddMultisigStep2 = () => {
const { m, n, format } = useRoute().params;
const [cosigners, setCosigners] = useState([]); // array of cosigners user provided. if format [cosigner, fp, path]
const [isOnCreateButtonEnabled, setIsOnCreateButtonEnabled] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isMnemonicsModalVisible, setIsMnemonicsModalVisible] = useState(false);
const [isProvideMnemonicsModalVisible, setIsProvideMnemonicsModalVisible] = useState(false);
@ -186,16 +185,18 @@ const WalletsAddMultisigStep2 = () => {
w.generate().then(() => {
const cosignersCopy = [...cosigners];
cosignersCopy.push([w.getSecret(), false, false]);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
setVaultKeyData({ keyIndex: cosignersCopy.length, seed: w.getSecret(), xpub: w.getXpub(), isLoading: false });
setIsLoading(true);
setIsMnemonicsModalVisible(true);
if (cosignersCopy.length === n) setIsOnCreateButtonEnabled(true);
// filling cache
setTimeout(() => {
// filling cache
setXpubCacheForMnemonics(w.getSecret());
setFpCacheForMnemonics(w.getSecret());
setIsLoading(false);
}, 500);
});
};
@ -279,9 +280,8 @@ const WalletsAddMultisigStep2 = () => {
const cosignersCopy = [...cosigners];
cosignersCopy.push([xpub, fp, path]);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
if (cosignersCopy.length === n) setIsOnCreateButtonEnabled(true);
};
const useMnemonicPhrase = () => {
@ -299,9 +299,9 @@ const WalletsAddMultisigStep2 = () => {
const cosignersCopy = [...cosigners];
cosignersCopy.push([hd.getSecret(), false, false]);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
if (cosignersCopy.length === n) setIsOnCreateButtonEnabled(true);
setIsProvideMnemonicsModalVisible(false);
setIsLoading(false);
setImportText('');
@ -386,16 +386,15 @@ const WalletsAddMultisigStep2 = () => {
const cosignersCopy = [...cosigners];
cosignersCopy.push([cosigner.getXpub(), cosigner.getFp(), cosigner.getPath()]);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
if (cosignersCopy.length === n) setIsOnCreateButtonEnabled(true);
}
};
const scanOrOpenFile = () => {
setIsProvideMnemonicsModalVisible(false);
if (isDesktop) {
ImagePicker.launchCamera(
launchCamera(
{
title: null,
mediaType: 'photo',
@ -403,7 +402,7 @@ const WalletsAddMultisigStep2 = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarScanned(result);
@ -570,7 +569,11 @@ const WalletsAddMultisigStep2 = () => {
<BlueSpacing10 />
<View style={styles.secretContainer}>{renderSecret(vaultKeyData.seed.split(' '))}</View>
<BlueSpacing20 />
<BlueButton title={loc.send.success_done} onPress={() => setIsMnemonicsModalVisible(false)} />
{isLoading ? (
<ActivityIndicator />
) : (
<BlueButton title={loc.send.success_done} onPress={() => setIsMnemonicsModalVisible(false)} />
)}
</View>
</BottomModal>
);
@ -659,7 +662,7 @@ const WalletsAddMultisigStep2 = () => {
<BlueLoading />
) : (
<View style={styles.buttonBottom}>
<BlueButton title={loc.multisig.create} onPress={onCreate} disabled={!isOnCreateButtonEnabled} />
<BlueButton title={loc.multisig.create} onPress={onCreate} disabled={cosigners.length !== n} />
</View>
);

View file

@ -193,13 +193,19 @@ const WalletDetails = () => {
});
};
const navigateToMultisigCoordinationSetup = () => {
navigate('ExportMultisigCoordinationSetup', {
walletId: wallet.getID(),
navigate('ExportMultisigCoordinationSetupRoot', {
screen: 'ExportMultisigCoordinationSetup',
params: {
walletId: wallet.getID(),
},
});
};
const navigateToViewEditCosigners = () => {
navigate('ViewEditMultisigCosigners', {
walletId: wallet.getID(),
navigate('ViewEditMultisigCosignersRoot', {
screen: 'ViewEditMultisigCosigners',
params: {
walletId: wallet.getID(),
},
});
};
const navigateToXPub = () =>

View file

@ -12,7 +12,7 @@ import { BlueStorageContext } from '../../blue_modules/storage-context';
const styles = StyleSheet.create({
loading: {
flex: 1,
paddingTop: 20,
justifyContent: 'center',
},
root: {
flex: 1,

View file

@ -1,4 +1,4 @@
import React, { useCallback, useContext, useState } from 'react';
import React, { useCallback, useContext, useRef, useState } from 'react';
import { ActivityIndicator, InteractionManager, ScrollView, StatusBar, StyleSheet, View } from 'react-native';
import { BlueNavigationStyle, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents';
import { DynamicQRCode } from '../../components/DynamicQRCode';
@ -14,29 +14,32 @@ const ExportMultisigCoordinationSetup = () => {
const walletId = useRoute().params.walletId;
const { wallets } = useContext(BlueStorageContext);
const wallet = wallets.find(w => w.getID() === walletId);
const qrCodeContents = Buffer.from(wallet.getXpub(), 'ascii').toString('hex');
const qrCodeContents = useRef();
const [isLoading, setIsLoading] = useState(true);
const [isShareButtonTapped, setIsShareButtonTapped] = useState(false);
const { goBack } = useNavigation();
const { colors } = useTheme();
const stylesHook = {
...styles,
const stylesHook = StyleSheet.create({
loading: {
...styles.loading,
backgroundColor: colors.elevated,
},
root: {
...styles.root,
backgroundColor: colors.elevated,
},
type: { ...styles.type, color: colors.foregroundColor },
secret: { ...styles.secret, color: colors.foregroundColor },
type: { color: colors.foregroundColor },
secret: { color: colors.foregroundColor },
exportButton: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
};
});
const exportTxtFile = async () => {
await fs.writeFileAndExport(wallet.getLabel() + '.txt', wallet.getXpub());
setIsShareButtonTapped(true);
setTimeout(() => {
fs.writeFileAndExport(wallet.getLabel() + '.txt', wallet.getXpub()).finally(() => {
setIsShareButtonTapped(false);
});
}, 10);
};
useFocusEffect(
@ -51,7 +54,7 @@ const ExportMultisigCoordinationSetup = () => {
return goBack();
}
}
qrCodeContents.current = Buffer.from(wallet.getXpub(), 'ascii').toString('hex');
setIsLoading(false);
}
});
@ -63,22 +66,26 @@ const ExportMultisigCoordinationSetup = () => {
);
return isLoading ? (
<View style={stylesHook.loading}>
<View style={[styles.loading, stylesHook.loading]}>
<ActivityIndicator />
</View>
) : (
<SafeBlueArea style={stylesHook.root}>
<SafeBlueArea style={[styles.root, stylesHook.root]}>
<StatusBar barStyle="light-content" />
<ScrollView contentContainerStyle={styles.scrollViewContent}>
<View>
<BlueText style={stylesHook.type}>{wallet.getLabel()}</BlueText>
<BlueText style={[styles.type, stylesHook.type]}>{wallet.getLabel()}</BlueText>
</View>
<BlueSpacing20 />
<DynamicQRCode value={qrCodeContents} capacity={400} />
<DynamicQRCode value={qrCodeContents.current} capacity={400} />
<BlueSpacing20 />
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} onPress={exportTxtFile} title={loc.multisig.share} />
{isShareButtonTapped ? (
<ActivityIndicator />
) : (
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} onPress={exportTxtFile} title={loc.multisig.share} />
)}
<BlueSpacing20 />
<BlueText style={stylesHook.secret}>{wallet.getXpub()}</BlueText>
<BlueText style={[styles.secret, stylesHook.secret]}>{wallet.getXpub()}</BlueText>
</ScrollView>
</SafeBlueArea>
);
@ -87,7 +94,7 @@ const ExportMultisigCoordinationSetup = () => {
const styles = StyleSheet.create({
loading: {
flex: 1,
paddingTop: 20,
justifyContent: 'center',
},
root: {
flex: 1,

View file

@ -1,27 +1,18 @@
import React, { Component } from 'react';
import React from 'react';
import { WebView } from 'react-native-webview';
import { BlueNavigationStyle, SafeBlueArea } from '../../BlueComponents';
import PropTypes from 'prop-types';
import { useRoute } from '@react-navigation/native';
export default class HodlHodlWebview extends Component {
constructor(props) {
super(props);
const HodlHodlWebview = () => {
const { uri } = useRoute().params;
const uri = props.route.params.uri;
this.state = {
uri,
};
}
render() {
return (
<SafeBlueArea>
<WebView source={{ uri: this.state.uri }} incognito />
</SafeBlueArea>
);
}
}
return (
<SafeBlueArea>
<WebView source={{ uri }} incognito />
</SafeBlueArea>
);
};
HodlHodlWebview.propTypes = {
route: PropTypes.shape({
@ -31,6 +22,8 @@ HodlHodlWebview.propTypes = {
}),
};
export default HodlHodlWebview;
HodlHodlWebview.navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: '',

View file

@ -17,7 +17,7 @@ import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
import WalletImport from '../../class/wallet-import';
import Clipboard from '@react-native-community/clipboard';
import ActionSheet from '../ActionSheet';
import ImagePicker from 'react-native-image-picker';
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
import loc from '../../loc';
import { getSystemName } from 'react-native-device-info';
import RNFS from 'react-native-fs';
@ -115,7 +115,7 @@ const WalletsImport = () => {
};
const choosePhoto = () => {
ImagePicker.launchImageLibrary(
launchImageLibrary(
{
title: null,
mediaType: 'photo',
@ -123,7 +123,7 @@ const WalletsImport = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarScanned(result);
@ -137,7 +137,7 @@ const WalletsImport = () => {
};
const takePhoto = () => {
ImagePicker.launchCamera(
launchCamera(
{
title: null,
mediaType: 'photo',
@ -145,7 +145,7 @@ const WalletsImport = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarScanned(result);

View file

@ -21,7 +21,7 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { PlaceholderWallet } from '../../class';
import WalletImport from '../../class/wallet-import';
import ActionSheet from '../ActionSheet';
import ImagePicker from 'react-native-image-picker';
import { launchImageLibrary, launchCamera } from 'react-native-image-picker';
import Clipboard from '@react-native-community/clipboard';
import loc from '../../loc';
import { FContainer, FButton } from '../../components/FloatButtons';
@ -363,7 +363,7 @@ const WalletsList = () => {
};
const choosePhoto = () => {
ImagePicker.launchImageLibrary(
launchImageLibrary(
{
title: null,
mediaType: 'photo',
@ -371,7 +371,7 @@ const WalletsList = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarScanned(result);
@ -385,7 +385,7 @@ const WalletsList = () => {
};
const takePhoto = () => {
ImagePicker.launchCamera(
launchCamera(
{
title: null,
mediaType: 'photo',
@ -393,7 +393,7 @@ const WalletsList = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarScanned(result);

View file

@ -18,7 +18,7 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import ImagePicker from 'react-native-image-picker';
import { launchImageLibrary } from 'react-native-image-picker';
import Clipboard from '@react-native-community/clipboard';
import { Icon } from 'react-native-elements';
import Handoff from 'react-native-handoff';
@ -26,7 +26,7 @@ import { useRoute, useNavigation, useTheme, useFocusEffect } from '@react-naviga
import { Chain } from '../../models/bitcoinUnits';
import { BlueTransactionListItem, BlueWalletNavigationHeader, BlueAlertWalletExportReminder, BlueListItem } from '../../BlueComponents';
import WalletGradient from '../../class/wallet-gradient';
import { LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import { LightningCustodianWallet, MultisigHDWallet, WatchOnlyWallet } from '../../class';
import HandoffSettings from '../../class/handoff';
import ActionSheet from '../ActionSheet';
import loc from '../../loc';
@ -436,8 +436,7 @@ const WalletTransactions = () => {
if (!isLoading) {
setIsLoading(true);
const params = {
fromSecret: wallet.current.getSecret(),
// ScanLndInvoice actrually uses `fromSecret` so keeping it for now
walletID: wallet.current.getID(),
uri: ret.data ? ret.data : ret,
fromWallet: wallet.current,
};
@ -451,7 +450,7 @@ const WalletTransactions = () => {
};
const choosePhoto = () => {
ImagePicker.launchImageLibrary(
launchImageLibrary(
{
title: null,
mediaType: 'photo',
@ -459,7 +458,7 @@ const WalletTransactions = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
onBarCodeRead({ data: result });
@ -478,7 +477,7 @@ const WalletTransactions = () => {
const sendButtonPress = () => {
if (wallet.current.chain === Chain.OFFCHAIN) {
navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: { fromSecret: wallet.current.getSecret() } });
navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: { walletID: wallet.current.getID() } });
} else {
if (wallet.current.type === WatchOnlyWallet.type && wallet.current.isHd() && wallet.current.getSecret().startsWith('zpub')) {
if (wallet.current.useWithHardwareWalletEnabled()) {
@ -570,6 +569,15 @@ const WalletTransactions = () => {
}
};
const navigateToViewEditCosigners = () => {
navigate('ViewEditMultisigCosignersRoot', {
screen: 'ViewEditMultisigCosigners',
params: {
walletId: wallet.current.getID(),
},
});
};
return (
<View style={styles.flex}>
<StatusBar barStyle="light-content" backgroundColor={WalletGradient.headerColorFor(wallet.current.type)} />
@ -589,23 +597,27 @@ const WalletTransactions = () => {
})
}
onManageFundsPressed={() => {
if (wallet.current.getUserHasSavedExport()) {
setIsManageFundsModalVisible(true);
} else {
BlueAlertWalletExportReminder({
onSuccess: async () => {
wallet.current.setUserHasSavedExport(true);
await saveToDisk();
setIsManageFundsModalVisible(true);
},
onFailure: () =>
navigate('WalletExportRoot', {
screen: 'WalletExport',
params: {
walletID: wallet.current.getID(),
},
}),
});
if (wallet.current.type === MultisigHDWallet.type) {
navigateToViewEditCosigners();
} else if (wallet.current.type === LightningCustodianWallet.type) {
if (wallet.current.getUserHasSavedExport()) {
setIsManageFundsModalVisible(true);
} else {
BlueAlertWalletExportReminder({
onSuccess: async () => {
wallet.current.setUserHasSavedExport(true);
await saveToDisk();
setIsManageFundsModalVisible(true);
},
onFailure: () =>
navigate('WalletExportRoot', {
screen: 'WalletExport',
params: {
walletID: wallet.current.getID(),
},
}),
});
}
}
}}
/>

View file

@ -18,7 +18,7 @@ import { Icon } from 'react-native-elements';
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { getSystemName } from 'react-native-device-info';
import ImagePicker from 'react-native-image-picker';
import { launchCamera } from 'react-native-image-picker';
import {
BlueButton,
@ -41,6 +41,8 @@ import MultipleStepsListItem, {
MultipleStepsListItemDashType,
} from '../../components/MultipleStepsListItem';
import ScanQRCode from '../send/ScanQRCode';
import Privacy from '../../Privacy';
import Biometric from '../../class/biometrics';
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
const isDesktop = getSystemName() === 'Mac OS X';
@ -121,6 +123,16 @@ const ViewEditMultisigCosigners = () => {
const onSave = async () => {
setIsLoading(true);
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
setIsLoading(false);
return;
}
}
// eslint-disable-next-line prefer-const
let newWallets = wallets.filter(w => {
return w.getID() !== walletId;
@ -128,14 +140,22 @@ const ViewEditMultisigCosigners = () => {
await wallet.fetchBalance();
newWallets.push(wallet);
setWalletsWithNewOrder(newWallets);
goBack();
goBack();
goBack();
navigate('WalletsList');
};
useFocusEffect(
useCallback(() => {
setIsLoading(true);
const task = InteractionManager.runAfterInteractions(() => {
Privacy.enableBlur();
const task = InteractionManager.runAfterInteractions(async () => {
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return goBack();
}
}
if (!w.current) {
// lets create fake wallet so renderer wont throw any errors
w.current = new MultisigHDWallet();
@ -148,9 +168,11 @@ const ViewEditMultisigCosigners = () => {
setIsLoading(false);
});
return () => {
Privacy.disableBlur();
task.cancel();
};
}, []),
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletId]),
);
const hideMnemonicsModal = () => {
@ -198,6 +220,7 @@ const ViewEditMultisigCosigners = () => {
)}
<BlueSpacing20 />
<BlueButton title={loc.send.success_done} onPress={() => setIsMnemonicsModalVisible(false)} />
<BlueSpacing40 />
</View>
</BottomModal>
);
@ -231,7 +254,7 @@ const ViewEditMultisigCosigners = () => {
button={{
buttonType: MultipleStepsListItemButtohType.partial,
leftText,
text: loc.multisig.share,
text: loc.multisig.view,
disabled: vaultKeyData.isLoading,
onPress: () => {
setVaultKeyData({
@ -269,7 +292,7 @@ const ViewEditMultisigCosigners = () => {
showActivityIndicator={vaultKeyData.keyIndex === el.index + 1 && vaultKeyData.isLoading}
button={{
leftText,
text: loc.multisig.share,
text: loc.multisig.view,
disabled: vaultKeyData.isLoading,
buttonType: MultipleStepsListItemButtohType.partial,
onPress: () => {
@ -382,7 +405,7 @@ const ViewEditMultisigCosigners = () => {
const scanOrOpenFile = () => {
setIsProvideMnemonicsModalVisible(false);
if (isDesktop) {
ImagePicker.launchCamera(
launchCamera(
{
title: null,
mediaType: 'photo',
@ -390,7 +413,7 @@ const ViewEditMultisigCosigners = () => {
},
response => {
if (response.uri) {
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.uri;
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
_handleUseMnemonicPhrase(result);
@ -576,7 +599,7 @@ const styles = StyleSheet.create({
ViewEditMultisigCosigners.navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: loc.multisig.view_edit_cosigners_title,
title: loc.multisig.manage_keys,
headerLeft: null,
});

View file

@ -1,619 +0,0 @@
--- ImagePickerManager.m 1985-10-26 04:15:00.000000000 -0400
+++ ImagePickerManager.m 2020-11-15 21:01:40.000000000 -0500
@@ -1,9 +1,11 @@
#import "ImagePickerManager.h"
#import <React/RCTConvert.h>
-#import <AssetsLibrary/AssetsLibrary.h>
#import <AVFoundation/AVFoundation.h>
#import <Photos/Photos.h>
#import <React/RCTUtils.h>
+#if !TARGET_OS_MACCATALYST
+#import <AssetsLibrary/AssetsLibrary.h>
+#endif
@import MobileCoreServices;
@@ -42,25 +44,25 @@
{
self.callback = callback; // Save the callback so we can use it from the delegate methods
self.options = options;
-
+
dispatch_async(dispatch_get_main_queue(), ^{
-
+
NSString *title = [self.options valueForKey:@"title"];
if ([title isEqual:[NSNull null]] || title.length == 0) {
title = nil; // A more visually appealing UIAlertControl is displayed with a nil title rather than title = @""
}
NSString *cancelTitle = [self.options valueForKey:@"cancelButtonTitle"];
NSString *takePhotoButtonTitle = [self.options valueForKey:@"takePhotoButtonTitle"];
- NSString *chooseFromLibraryButtonTitle = [self.options valueForKey:@"chooseFromLibraryButtonTitle"];
-
+ NSString *chooseFromLibraryButtonTitle = [self.options valueForKey:@"chooseFromLibraryButtonTitle"];
+
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleActionSheet];
alertController.view.tintColor = [RCTConvert UIColor:options[@"tintColor"]];
-
+
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:cancelTitle style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
self.callback(@[@{@"didCancel": @YES}]); // Return callback for 'cancel' action (if is required)
}];
[alertController addAction:cancelAction];
-
+
if (![takePhotoButtonTitle isEqual:[NSNull null]] && takePhotoButtonTitle.length > 0) {
UIAlertAction *takePhotoAction = [UIAlertAction actionWithTitle:takePhotoButtonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[self actionHandler:action];
@@ -73,7 +75,7 @@
}];
[alertController addAction:chooseFromLibraryAction];
}
-
+
// Add custom buttons to action sheet
if ([self.options objectForKey:@"customButtons"] && [[self.options objectForKey:@"customButtons"] isKindOfClass:[NSArray class]]) {
self.customButtons = [self.options objectForKey:@"customButtons"];
@@ -85,16 +87,16 @@
[alertController addAction:customAction];
}
}
-
+
UIViewController *root = RCTPresentedViewController();
-
+
/* On iPad, UIAlertController presents a popover view rather than an action sheet like on iPhone. We must provide the location
- of the location to show the popover in this case. For simplicity, we'll just display it on the bottom center of the screen
- to mimic an action sheet */
+ of the location to show the popover in this case. For simplicity, we'll just display it on the bottom center of the screen
+ to mimic an action sheet */
alertController.popoverPresentationController.sourceView = root.view;
alertController.popoverPresentationController.sourceRect = CGRectMake(root.view.bounds.size.width / 2.0, root.view.bounds.size.height, 1.0, 1.0);
-
- if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
+
+ if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
alertController.popoverPresentationController.permittedArrowDirections = 0;
for (id subview in alertController.view.subviews) {
if ([subview isMemberOfClass:[UIView class]]) {
@@ -102,7 +104,7 @@
}
}
}
-
+
[root presentViewController:alertController animated:YES completion:nil];
});
}
@@ -119,7 +121,7 @@
return;
}
}
-
+
if ([action.title isEqualToString:[self.options valueForKey:@"takePhotoButtonTitle"]]) { // Take photo
[self launchImagePicker:RNImagePickerTargetCamera];
}
@@ -137,7 +139,7 @@
- (void)launchImagePicker:(RNImagePickerTarget)target
{
self.picker = [[UIImagePickerController alloc] init];
-
+
if (target == RNImagePickerTargetCamera) {
#if TARGET_IPHONE_SIMULATOR
self.callback(@[@{@"error": @"Camera not available on simulator"}]);
@@ -155,10 +157,10 @@
else { // RNImagePickerTargetLibrarySingleImage
self.picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
}
-
+
if ([[self.options objectForKey:@"mediaType"] isEqualToString:@"video"]
|| [[self.options objectForKey:@"mediaType"] isEqualToString:@"mixed"]) {
-
+
if ([[self.options objectForKey:@"videoQuality"] isEqualToString:@"high"]) {
self.picker.videoQuality = UIImagePickerControllerQualityTypeHigh;
}
@@ -168,7 +170,7 @@
else {
self.picker.videoQuality = UIImagePickerControllerQualityTypeMedium;
}
-
+
id durationLimit = [self.options objectForKey:@"durationLimit"];
if (durationLimit) {
self.picker.videoMaximumDuration = [durationLimit doubleValue];
@@ -182,13 +184,13 @@
} else {
self.picker.mediaTypes = @[(NSString *)kUTTypeImage];
}
-
+
if ([[self.options objectForKey:@"allowsEditing"] boolValue]) {
self.picker.allowsEditing = true;
}
self.picker.modalPresentationStyle = UIModalPresentationCurrentContext;
self.picker.delegate = self;
-
+
// Check permissions
void (^showPickerViewController)() = ^void() {
dispatch_async(dispatch_get_main_queue(), ^{
@@ -196,57 +198,57 @@
[root presentViewController:self.picker animated:YES completion:nil];
});
};
-
+
if (target == RNImagePickerTargetCamera) {
[self checkCameraPermissions:^(BOOL granted) {
if (!granted) {
self.callback(@[@{@"error": @"Camera permissions not granted"}]);
return;
}
-
+
showPickerViewController();
}];
}
else { // RNImagePickerTargetLibrarySingleImage
- if (@available(iOS 11.0, *)) {
- showPickerViewController();
- } else {
- [self checkPhotosPermissions:^(BOOL granted) {
- if (!granted) {
- self.callback(@[@{@"error": @"Photo library permissions not granted"}]);
- return;
- }
-
- showPickerViewController();
- }];
- }
+ if (@available(iOS 11.0, *)) {
+ showPickerViewController();
+ } else {
+ [self checkPhotosPermissions:^(BOOL granted) {
+ if (!granted) {
+ self.callback(@[@{@"error": @"Photo library permissions not granted"}]);
+ return;
+ }
+
+ showPickerViewController();
+ }];
+ }
}
}
- (NSString * _Nullable)originalFilenameForAsset:(PHAsset * _Nullable)asset assetType:(PHAssetResourceType)type {
if (!asset) { return nil; }
-
+
PHAssetResource *originalResource;
// Get the underlying resources for the PHAsset (PhotoKit)
NSArray<PHAssetResource *> *pickedAssetResources = [PHAssetResource assetResourcesForAsset:asset];
-
+
// Find the original resource (underlying image) for the asset, which has the desired filename
for (PHAssetResource *resource in pickedAssetResources) {
if (resource.type == type) {
originalResource = resource;
}
}
-
+
return originalResource.originalFilename;
}
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{
dispatch_block_t dismissCompletionBlock = ^{
-
- NSURL *imageURL = [info valueForKey:UIImagePickerControllerReferenceURL];
+
+ NSURL *imageURL = [info valueForKey:UIImagePickerControllerPHAsset];
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
-
+
NSString *fileName;
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
NSString *tempFileName = [[NSUUID UUID] UUIDString];
@@ -264,18 +266,18 @@
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
fileName = videoURL.lastPathComponent;
}
-
+
// We default to path to the temporary directory
NSString *path = [[NSTemporaryDirectory()stringByStandardizingPath] stringByAppendingPathComponent:fileName];
-
+
// If storage options are provided, we use the documents directory which is persisted
if ([self.options objectForKey:@"storageOptions"] && [[self.options objectForKey:@"storageOptions"] isKindOfClass:[NSDictionary class]]) {
NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"];
-
+
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
path = [documentsDirectory stringByAppendingPathComponent:fileName];
-
+
// Creates documents subdirectory, if provided
if ([storageOptions objectForKey:@"path"]) {
NSString *newPath = [documentsDirectory stringByAppendingPathComponent:[storageOptions objectForKey:@"path"]];
@@ -291,10 +293,10 @@
}
}
}
-
+
// Create the response object
self.response = [[NSMutableDictionary alloc] init];
-
+
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) { // PHOTOS
UIImage *originalImage;
if ([[self.options objectForKey:@"allowsEditing"] boolValue]) {
@@ -303,13 +305,15 @@
else {
originalImage = [info objectForKey:UIImagePickerControllerOriginalImage];
}
-
+
if (imageURL) {
PHAsset *pickedAsset;
if (@available(iOS 11.0, *)) {
- pickedAsset = [info objectForKey: UIImagePickerControllerPHAsset];
+ pickedAsset = [info objectForKey: UIImagePickerControllerPHAsset];
} else {
- pickedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil].lastObject;
+#if !TARGET_OS_MACCATALYST
+ pickedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil].lastObject;
+#endif
}
NSString *originalFilename = [self originalFilenameForAsset:pickedAsset assetType:PHAssetResourceTypePhoto];
@@ -322,9 +326,10 @@
self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:pickedAsset.creationDate];
}
}
-
+
// GIFs break when resized, so we handle them differently
if (imageURL && [[imageURL absoluteString] rangeOfString:@"ext=GIF"].location != NSNotFound) {
+#if !TARGET_OS_MACCATALYST
ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init];
[assetsLibrary assetForURL:imageURL resultBlock:^(ALAsset *asset) {
ALAssetRepresentation *rep = [asset defaultRepresentation];
@@ -333,38 +338,39 @@
NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:repSize error:nil];
NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES];
[data writeToFile:path atomically:YES];
-
+
NSMutableDictionary *gifResponse = [[NSMutableDictionary alloc] init];
[gifResponse setObject:@(originalImage.size.width) forKey:@"width"];
[gifResponse setObject:@(originalImage.size.height) forKey:@"height"];
-
+
BOOL vertical = (originalImage.size.width < originalImage.size.height) ? YES : NO;
[gifResponse setObject:@(vertical) forKey:@"isVertical"];
-
+
if (![[self.options objectForKey:@"noData"] boolValue]) {
NSString *dataString = [data base64EncodedStringWithOptions:0];
[gifResponse setObject:dataString forKey:@"data"];
}
-
+
NSURL *fileURL = [NSURL fileURLWithPath:path];
[gifResponse setObject:[fileURL absoluteString] forKey:@"uri"];
-
+
NSNumber *fileSizeValue = nil;
NSError *fileSizeError = nil;
[fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError];
if (fileSizeValue){
[gifResponse setObject:fileSizeValue forKey:@"fileSize"];
}
-
+
self.callback(@[gifResponse]);
} failureBlock:^(NSError *error) {
self.callback(@[@{@"error": error.localizedFailureReason}]);
}];
+#endif
return;
}
-
+
UIImage *editedImage = [self fixOrientation:originalImage]; // Rotate the image for upload to web
-
+
// If needed, downscale image
float maxWidth = editedImage.size.width;
float maxHeight = editedImage.size.height;
@@ -375,7 +381,7 @@
maxHeight = [[self.options valueForKey:@"maxHeight"] floatValue];
}
editedImage = [self downscaleImageIfNecessary:editedImage maxWidth:maxWidth maxHeight:maxHeight];
-
+
NSData *data;
NSString *mimeType;
if ([[self.options objectForKey:@"imageFileType"] isEqualToString:@"png"]) {
@@ -388,36 +394,37 @@
}
[self.response setObject:mimeType forKey:@"type"];
[data writeToFile:path atomically:YES];
-
+
if (![[self.options objectForKey:@"noData"] boolValue]) {
NSString *dataString = [data base64EncodedStringWithOptions:0]; // base64 encoded image string
[self.response setObject:dataString forKey:@"data"];
}
-
+
BOOL vertical = (editedImage.size.width < editedImage.size.height) ? YES : NO;
[self.response setObject:@(vertical) forKey:@"isVertical"];
NSURL *fileURL = [NSURL fileURLWithPath:path];
NSString *filePath = [fileURL absoluteString];
[self.response setObject:filePath forKey:@"uri"];
-
+
// add ref to the original image
NSString *origURL = [imageURL absoluteString];
if (origURL) {
- [self.response setObject:origURL forKey:@"origURL"];
+ [self.response setObject:origURL forKey:@"origURL"];
}
-
+
NSNumber *fileSizeValue = nil;
NSError *fileSizeError = nil;
[fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError];
if (fileSizeValue){
[self.response setObject:fileSizeValue forKey:@"fileSize"];
}
-
+
[self.response setObject:@(editedImage.size.width) forKey:@"width"];
[self.response setObject:@(editedImage.size.height) forKey:@"height"];
-
+
NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"];
if (storageOptions && [[storageOptions objectForKey:@"cameraRoll"] boolValue] == YES && self.picker.sourceType == UIImagePickerControllerSourceTypeCamera) {
+#if !TARGET_OS_MACCATALYST
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
if ([[storageOptions objectForKey:@"waitUntilSaved"] boolValue]) {
[library writeImageToSavedPhotosAlbum:originalImage.CGImage metadata:[info valueForKey:UIImagePickerControllerMediaMetadata] completionBlock:^(NSURL *assetURL, NSError *error) {
@@ -440,13 +447,15 @@
} else {
[library writeImageToSavedPhotosAlbum:originalImage.CGImage metadata:[info valueForKey:UIImagePickerControllerMediaMetadata] completionBlock:nil];
}
+#endif
}
}
else { // VIDEO
- NSURL *videoRefURL = info[UIImagePickerControllerReferenceURL];
+ NSURL *videoRefURL = info[UIImagePickerControllerPHAsset];
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
NSURL *videoDestinationURL = [NSURL fileURLWithPath:path];
-
+
+#if !TARGET_OS_MACCATALYST
if (videoRefURL) {
PHAsset *pickedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[videoRefURL] options:nil].lastObject;
NSString *originalFilename = [self originalFilenameForAsset:pickedAsset assetType:PHAssetResourceTypeVideo];
@@ -459,39 +468,41 @@
self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:pickedAsset.creationDate];
}
}
-
+#endif
+
if ([videoURL.URLByResolvingSymlinksInPath.path isEqualToString:videoDestinationURL.URLByResolvingSymlinksInPath.path] == NO) {
NSFileManager *fileManager = [NSFileManager defaultManager];
-
+
// Delete file if it already exists
if ([fileManager fileExistsAtPath:videoDestinationURL.path]) {
[fileManager removeItemAtURL:videoDestinationURL error:nil];
}
-
+
if (videoURL) { // Protect against reported crash
- NSError *error = nil;
-
- // If we have write access to the source file, move it. Otherwise use copy.
- if ([fileManager isWritableFileAtPath:[videoURL path]]) {
- [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error];
- } else {
- [fileManager copyItemAtURL:videoURL toURL:videoDestinationURL error:&error];
- }
-
- if (error) {
- self.callback(@[@{@"error": error.localizedFailureReason}]);
- return;
- }
+ NSError *error = nil;
+
+ // If we have write access to the source file, move it. Otherwise use copy.
+ if ([fileManager isWritableFileAtPath:[videoURL path]]) {
+ [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error];
+ } else {
+ [fileManager copyItemAtURL:videoURL toURL:videoDestinationURL error:&error];
+ }
+
+ if (error) {
+ self.callback(@[@{@"error": error.localizedFailureReason}]);
+ return;
+ }
}
}
-
+
[self.response setObject:videoDestinationURL.absoluteString forKey:@"uri"];
if (videoRefURL.absoluteString) {
[self.response setObject:videoRefURL.absoluteString forKey:@"origURL"];
}
-
+
NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"];
if (storageOptions && [[storageOptions objectForKey:@"cameraRoll"] boolValue] == YES && self.picker.sourceType == UIImagePickerControllerSourceTypeCamera) {
+#if !TARGET_OS_MACCATALYST
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library writeVideoAtPathToSavedPhotosAlbum:videoDestinationURL completionBlock:^(NSURL *assetURL, NSError *error) {
if (error) {
@@ -509,22 +520,23 @@
self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:capturedAsset.creationDate];
}
}
-
+
self.callback(@[self.response]);
}
}
}];
+#endif
}
}
-
+
// If storage options are provided, check the skipBackup flag
if ([self.options objectForKey:@"storageOptions"] && [[self.options objectForKey:@"storageOptions"] isKindOfClass:[NSDictionary class]]) {
NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"];
-
+
if ([[storageOptions objectForKey:@"skipBackup"] boolValue]) {
[self addSkipBackupAttributeToItemAtPath:path]; // Don't back up the file to iCloud
}
-
+
if ([[storageOptions objectForKey:@"waitUntilSaved"] boolValue] == NO ||
[[storageOptions objectForKey:@"cameraRoll"] boolValue] == NO ||
self.picker.sourceType != UIImagePickerControllerSourceTypeCamera)
@@ -536,7 +548,7 @@
self.callback(@[self.response]);
}
};
-
+
dispatch_async(dispatch_get_main_queue(), ^{
[picker dismissViewControllerAnimated:YES completion:dismissCompletionBlock];
});
@@ -595,12 +607,12 @@
- (UIImage*)downscaleImageIfNecessary:(UIImage*)image maxWidth:(float)maxWidth maxHeight:(float)maxHeight
{
UIImage* newImage = image;
-
+
// Nothing to do here
if (image.size.width <= maxWidth && image.size.height <= maxHeight) {
return newImage;
}
-
+
CGSize scaledSize = CGSizeMake(image.size.width, image.size.height);
if (maxWidth < scaledSize.width) {
scaledSize = CGSizeMake(maxWidth, (maxWidth / scaledSize.width) * scaledSize.height);
@@ -608,11 +620,11 @@
if (maxHeight < scaledSize.height) {
scaledSize = CGSizeMake((maxHeight / scaledSize.height) * scaledSize.width, maxHeight);
}
-
+
// If the pixels are floats, it causes a white line in iOS8 and probably other versions too
scaledSize.width = (int)scaledSize.width;
scaledSize.height = (int)scaledSize.height;
-
+
UIGraphicsBeginImageContext(scaledSize); // this will resize
[image drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
newImage = UIGraphicsGetImageFromCurrentImageContext();
@@ -620,7 +632,7 @@
NSLog(@"could not scale image");
}
UIGraphicsEndImageContext();
-
+
return newImage;
}
@@ -628,7 +640,7 @@
if (srcImg.imageOrientation == UIImageOrientationUp) {
return srcImg;
}
-
+
CGAffineTransform transform = CGAffineTransformIdentity;
switch (srcImg.imageOrientation) {
case UIImageOrientationDown:
@@ -636,13 +648,13 @@
transform = CGAffineTransformTranslate(transform, srcImg.size.width, srcImg.size.height);
transform = CGAffineTransformRotate(transform, M_PI);
break;
-
+
case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
transform = CGAffineTransformTranslate(transform, srcImg.size.width, 0);
transform = CGAffineTransformRotate(transform, M_PI_2);
break;
-
+
case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, 0, srcImg.size.height);
@@ -652,14 +664,14 @@
case UIImageOrientationUpMirrored:
break;
}
-
+
switch (srcImg.imageOrientation) {
case UIImageOrientationUpMirrored:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, srcImg.size.width, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
-
+
case UIImageOrientationLeftMirrored:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, srcImg.size.height, 0);
@@ -671,7 +683,7 @@
case UIImageOrientationRight:
break;
}
-
+
CGContextRef ctx = CGBitmapContextCreate(NULL, srcImg.size.width, srcImg.size.height, CGImageGetBitsPerComponent(srcImg.CGImage), 0, CGImageGetColorSpace(srcImg.CGImage), CGImageGetBitmapInfo(srcImg.CGImage));
CGContextConcatCTM(ctx, transform);
switch (srcImg.imageOrientation) {
@@ -681,12 +693,12 @@
case UIImageOrientationRightMirrored:
CGContextDrawImage(ctx, CGRectMake(0,0,srcImg.size.height,srcImg.size.width), srcImg.CGImage);
break;
-
+
default:
CGContextDrawImage(ctx, CGRectMake(0,0,srcImg.size.width,srcImg.size.height), srcImg.CGImage);
break;
}
-
+
CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
UIImage *img = [UIImage imageWithCGImage:cgimg];
CGContextRelease(ctx);
@@ -701,7 +713,7 @@
NSError *error = nil;
BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]
forKey: NSURLIsExcludedFromBackupKey error: &error];
-
+
if(!success){
NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error);
}

View file

@ -15,16 +15,12 @@ cd node_modules/react-native-camera/ios/RCT
patch RCTCameraManager.m ../../../../scripts/maccatalystpatches/RCTCameraManager.patch --no-backup-if-mismatch
cd ../RN/
patch RNCamera.m ../../../../scripts/maccatalystpatches/RNCamera.patch
echo "Applying patch for react-native-image-picker"
cd ../../../../
cd node_modules/react-native-image-picker/ios
patch ImagePickerManager.m ../../../scripts/maccatalystpatches/ImagePickerManager.patch --no-backup-if-mismatch
echo "Applying patch for Podfile"
cd ../../../
cd ../../../../
patch ios/Podfile ./scripts/maccatalystpatches/podfile.patch --no-backup-if-mismatch
echo "Applying patch for Realm podspec"
patch node_modules/realm/RealmJS.podspec ./scripts/maccatalystpatches/realm.patch --no-backup-if-mismatch
cd ios
pod update
echo ""
echo "You should now be able to compile BlueWallet using Mac Catalyst on XCode. Enable Mac under Deployment Info by using XCode. If you are running macOS Catalina, you will need to remove the iOS 14 Widgets from the project targets."
echo "You should now be able to compile BlueWallet using Mac Catalyst on XCode. Enable Mac under Deployment Info by using XCode. If you are running macOS Catalina, you will need to remove the iOS 14 Widgets from the project targets."

View file

@ -32,13 +32,6 @@ jest.mock('react-native-quick-actions', () => {
};
});
jest.mock('react-native-image-picker', () => {
return {
launchCamera: jest.fn(),
launchImageLibrary: jest.fn(),
};
});
jest.mock('react-native-default-preference', () => {
return {
setName: jest.fn(),

View file

@ -165,6 +165,22 @@ describe('unit - DeepLinkSchemaMatch', function () {
},
],
},
{
argument: {
url:
'https://lnbits.com/?lightning=LNURL1DP68GURN8GHJ7MRWVF5HGUEWVDHK6TMHD96XSERJV9MJ7CTSDYHHVVF0D3H82UNV9UM9JDENFPN5SMMK2359J5RKWVMKZ5ZVWAV4VJD63TM',
},
expected: [
'LNDCreateInvoiceRoot',
{
screen: 'LNDCreateInvoice',
params: {
uri:
'https://lnbits.com/?lightning=LNURL1DP68GURN8GHJ7MRWVF5HGUEWVDHK6TMHD96XSERJV9MJ7CTSDYHHVVF0D3H82UNV9UM9JDENFPN5SMMK2359J5RKWVMKZ5ZVWAV4VJD63TM',
},
},
],
},
];
const asyncNavigationRouteFor = async function (event) {

View file

@ -21,6 +21,12 @@ describe('LNURL', function () {
Lnurl.getUrlFromLnurl('LNURL1DP68GURN8GHJ7MRWW3UXYMM59E3XJEMNW4HZU7RE0GHKCMN4WFKZ7URP0YLH2UM9WF5KG0FHXYCNV9G9W58'),
'https://lntxbot.bigsun.xyz/lnurl/pay?userid=7116',
);
assert.strictEqual(
Lnurl.getUrlFromLnurl(
'https://lnbits.com/?lightning=LNURL1DP68GURN8GHJ7MRWVF5HGUEWVDHK6TMHD96XSERJV9MJ7CTSDYHHVVF0D3H82UNV9UM9JDENFPN5SMMK2359J5RKWVMKZ5ZVWAV4VJD63TM',
),
'https://lnbits.com/withdraw/api/v1/lnurl/6Y73HgHovThYPvs7aPLwYV',
);
assert.strictEqual(Lnurl.getUrlFromLnurl('bs'), false);
});