mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-26 08:55:56 +01:00
Merge branch 'master' into patch-1
This commit is contained in:
commit
ca17e03c03
35 changed files with 837 additions and 1336 deletions
|
@ -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 = {
|
||||
|
|
|
@ -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
13
__mocks__/react-native-image-picker.js
vendored
Normal 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();
|
||||
});
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
buildscript {
|
||||
ext {
|
||||
minSdkVersion = 18
|
||||
minSdkVersion = 21
|
||||
supportLibVersion = "28.0.0"
|
||||
buildToolsVersion = "29.0.2"
|
||||
compileSdkVersion = 29
|
||||
|
|
|
@ -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) });
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
10
loc/en.json
10
loc/en.json
|
@ -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",
|
||||
|
|
14
loc/fa.json
14
loc/fa.json
|
@ -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
12
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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 = () =>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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."
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue