Skip to content

Commit

Permalink
Merge pull request #2713 from ecency/sa/hive-uri-support
Browse files Browse the repository at this point in the history
hive uri qr codes support
  • Loading branch information
feruzm authored Aug 31, 2023
2 parents 0c530f2 + f80699f commit 6a3b2fe
Show file tree
Hide file tree
Showing 15 changed files with 377 additions and 28 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"dns.js": "^1.0.1",
"domain-browser": "^1.1.1",
"events": "^1.0.0",
"hive-uri": "^0.2.4",
"hivesigner": "^3.2.7",
"https-browserify": "~0.0.0",
"intl": "^1.2.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ActionModalData {
headerImage?: Source;
onClosed: () => void;
headerContent?: React.ReactNode;
bodyContent?: React.ReactNode;
}

const ActionModalContainer = ({ navigation }) => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/actionModal/view/actionModalView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const ActionModalView = ({ onClose, data }: ActionModalViewProps, ref) => {
return null;
}

const { title, body, buttons, headerImage, para, headerContent } = data;
const { title, body, buttons, headerImage, para, headerContent, bodyContent } = data;

const _renderContent = (
<View style={styles.container}>
Expand All @@ -49,6 +49,7 @@ const ActionModalView = ({ onClose, data }: ActionModalViewProps, ref) => {

<View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text>
{bodyContent && bodyContent}
{!!body && (
<>
<Text style={styles.bodyText}>{body}</Text>
Expand Down
2 changes: 2 additions & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import BeneficiarySelectionContent from './beneficiarySelectionContent/beneficia
import TransferAccountSelector from './transferAccountSelector/transferAccountSelector';
import TransferAmountInputSection from './transferAmountInputSection/transferAmountInputSection';
import TextBoxWithCopy from './textBoxWithCopy/textBoxWithCopy';
import WebViewModal from './webViewModal/webViewModal';

// Basic UI Elements
import {
Expand Down Expand Up @@ -251,4 +252,5 @@ export {
TransferAccountSelector,
TransferAmountInputSection,
TextBoxWithCopy,
WebViewModal,
};
45 changes: 45 additions & 0 deletions src/components/qrModal/qrModalStyles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { TextStyle, ViewStyle } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
import getWindowDimensions from '../../utils/getWindowDimensions';
const { width: SCREEN_WIDTH } = getWindowDimensions();

export default EStyleSheet.create({
sheetContent: {
Expand Down Expand Up @@ -36,4 +39,46 @@ export default EStyleSheet.create({
alignItems: 'center',
},
activityIndicator: {},
transactionBodyContainer: {
borderWidth: 1,
borderColor: '$borderColor',
borderRadius: 8,
// padding: 8,
marginVertical: 10,
width: SCREEN_WIDTH - 64,
} as ViewStyle,
transactionRow: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 4,
} as ViewStyle,
transactionHeadingContainer: {
borderBottomWidth: 1,
borderColor: '$borderColor',
height: 36,
paddingHorizontal: 8,
justifyContent: 'center',
} as ViewStyle,
transactionHeading: {
color: '$primaryBlack',
fontSize: 18,
fontWeight: '700',
textTransform: 'capitalize',
} as TextStyle,
transactionItemsContainer: {
padding: 8,
} as ViewStyle,
transactionItem1: {
color: '$primaryBlack',
fontSize: 16,
fontWeight: '700',
flex: 1,
textTransform: 'capitalize',
} as TextStyle,
transactionItem2: {
color: '$primaryBlack',
fontSize: 16,
fontWeight: '400',
flex: 1,
} as TextStyle,
});
188 changes: 162 additions & 26 deletions src/components/qrModal/qrModalView.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
import React, { useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Alert, PermissionsAndroid, Platform, Text, View } from 'react-native';
import { ActivityIndicator, Alert, PermissionsAndroid, Platform, View, Text } from 'react-native';
import ActionSheet from 'react-native-actions-sheet';
import EStyleSheet from 'react-native-extended-stylesheet';
import QRCodeScanner from 'react-native-qrcode-scanner';
import { useIntl } from 'react-intl';
import { check, request, PERMISSIONS, RESULTS, openSettings } from 'react-native-permissions';
import styles from './qrModalStyles';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { toggleQRModal } from '../../redux/actions/uiAction';
import {
showActionModal,
showWebViewModal,
toastNotification,
toggleQRModal,
} from '../../redux/actions/uiAction';
import { deepLinkParser } from '../../utils/deepLinkParser';
import RootNavigation from '../../navigation/rootNavigation';
import getWindowDimensions from '../../utils/getWindowDimensions';
import { isHiveUri } from '../../utils/hive-uri';
import { handleHiveUriOperation } from '../../providers/hive/dhive';
import bugsnagInstance from '../../config/bugsnag';
import { get, isArray } from 'lodash';
import showLoginAlert from '../../utils/showLoginAlert';
import authType from '../../constants/authType';
import { delay } from '../../utils/editor';
import ROUTES from '../../../src/constants/routeNames';

export interface QRModalProps {}

const hiveuri = require('hive-uri');
const screenHeight = getWindowDimensions().height;
interface QRModalProps {}

export const QRModal = ({}: QRModalProps) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const isVisibleQRModal = useAppSelector((state) => state.ui.isVisibleQRModal);
const currentAccount = useAppSelector((state) => state.account.currentAccount);
const pinCode = useAppSelector((state) => state.application.pin);
const isPinCodeOpen = useAppSelector((state) => state.application.isPinCodeOpen);
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);

const [isScannerActive, setIsScannerActive] = useState(true);
const [isProcessing, setIsProcessing] = useState(false);
Expand All @@ -29,9 +46,9 @@ export const QRModal = ({}: QRModalProps) => {
useEffect(() => {
if (isVisibleQRModal) {
requestCameraPermission();
sheetModalRef.current.show();
sheetModalRef?.current?.show();
} else {
sheetModalRef.current.hide();
sheetModalRef?.current?.hide();
}
}, [isVisibleQRModal]);

Expand Down Expand Up @@ -97,42 +114,161 @@ export const QRModal = ({}: QRModalProps) => {

const onSuccess = (e) => {
setIsScannerActive(false);
_handleDeepLink(e.data);
if (isHiveUri(e.data)) {
_handleHiveUri(e.data);
} else {
_handleDeepLink(e.data);
}
};

const _handleDeepLink = async (url) => {
setIsProcessing(true);
const deepLinkData = await deepLinkParser(url, currentAccount);
const { name, params, key } = deepLinkData || {};
setIsProcessing(false);
if (name && params && key) {
const _handleHiveUri = async (uri: string) => {
try {
setIsScannerActive(false);
_onClose();
RootNavigation.navigate(deepLinkData);
} else {
if (!isLoggedIn) {
showLoginAlert({ intl });
return;
}
if (isPinCodeOpen) {
RootNavigation.navigate({
name: ROUTES.SCREENS.PINCODE,
params: {
callback: () => _handleHiveUriTransaction(uri),
},
});
} else {
_handleHiveUriTransaction(uri);
}
} catch (err) {
_showInvalidAlert();
}
};

const _handleHiveUriTransaction = async (uri: string) => {
if (get(currentAccount, 'local.authType') === authType.STEEM_CONNECT) {
await delay(500); // NOTE: it's required to avoid modal mis fire
dispatch(
showWebViewModal({
uri: uri,
}),
);
return;
}

const parsed = hiveuri.decode(uri);
// resolve the decoded tx and params to a signable tx
let { tx, signer } = hiveuri.resolveTransaction(parsed.tx, parsed.params, {
signers: currentAccount.name,
preferred_signer: currentAccount.name,
});
const operations = get(tx, 'operations', []);
if (!_checkOpsArray(operations)) {
Alert.alert(
intl.formatMessage({ id: 'qr.unsupported_alert_title' }),
intl.formatMessage({ id: 'qr.unsupported_alert_desc' }),
[
intl.formatMessage({
id: 'qr.multi_array_ops_alert',
}),
intl.formatMessage({
id: 'qr.multi_array_ops_aler_desct',
}),
);
return;
}
dispatch(
showActionModal({
title: intl.formatMessage({
id: 'qr.confirmTransaction',
}),
bodyContent: _checkOpsArray(operations) ? _renderActionModalBody(operations[0]) : null,
buttons: [
{
text: 'Close',
onPress: () => {
_onClose();
},
text: intl.formatMessage({
id: 'qr.cancel',
}),
onPress: () => {},
style: 'cancel',
},
{
text: 'Rescan',
text: intl.formatMessage({
id: 'qr.approve',
}),
onPress: () => {
setIsScannerActive(true);
scannerRef.current?.reactivate();
handleHiveUriOperation(currentAccount, pinCode, uri)
.then(() => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.successful' })));
})
.catch((err) => {
bugsnagInstance.notify(err);
dispatch(toastNotification(intl.formatMessage({ id: 'alert.key_warning' })));
});
},
},
],
);
}),
);
};

const _handleDeepLink = async (url) => {
setIsProcessing(true);
const deepLinkData = await deepLinkParser(url);
const { name, params, key } = deepLinkData || {};
setIsProcessing(false);
if (name && params && key) {
setIsScannerActive(false);
_onClose();
RootNavigation.navigate(deepLinkData);
} else {
_showInvalidAlert();
}
};

// check operation array is valid and is a single operation array
const _checkOpsArray = (ops) => {
return ops && isArray(ops) && ops.length === 1 && isArray(ops[0]) && ops[0].length === 2;
};

const _renderTransactionInfoRow = (item: any) => (
<View style={styles.transactionRow}>
<Text numberOfLines={1} style={styles.transactionItem1}>
{item[0]}
</Text>
<Text numberOfLines={1} style={styles.transactionItem2}>
{item[1]}
</Text>
</View>
);
const _renderActionModalBody = (operations: any) => (
<View style={styles.transactionBodyContainer}>
<View style={styles.transactionHeadingContainer}>
<Text style={styles.transactionHeading}>{operations[0]}</Text>
</View>
<View style={styles.transactionItemsContainer}>
{Object.entries(operations[1]).map((item) => _renderTransactionInfoRow(item))}
</View>
</View>
);
const _showInvalidAlert = () => {
Alert.alert(
intl.formatMessage({ id: 'qr.unsupported_alert_title' }),
intl.formatMessage({ id: 'qr.unsupported_alert_desc' }),
[
{
text: 'Close',
onPress: () => {
_onClose();
},
style: 'cancel',
},
{
text: 'Rescan',
onPress: () => {
setIsScannerActive(true);
scannerRef.current?.reactivate();
},
},
],
);
};

return (
<ActionSheet
ref={sheetModalRef}
Expand Down
50 changes: 50 additions & 0 deletions src/components/webViewModal/webViewModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { hideWebViewModal } from '../../redux/actions/uiAction';
import WebView from 'react-native-webview';
import { hsOptions } from '../../constants/hsOptions';
import { Modal } from '..';
import styles from './webViewModalStyles';

interface QRModalProps {}
interface WebViewModalData {
uri: string;
}

export const WebViewModal = ({}: QRModalProps) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const isVisibleWebViewModal = useAppSelector((state) => state.ui.isVisibleWebViewModal);
const webViewModalData: WebViewModalData = useAppSelector((state) => state.ui.webViewModalData);

const [hiveSignerModal, setHiveSignerModal] = useState(false);

useEffect(() => {
if (isVisibleWebViewModal) {
setHiveSignerModal(true);
} else {
setHiveSignerModal(false);
}
}, [isVisibleWebViewModal]);

const _onClose = () => {
dispatch(hideWebViewModal());
};

return (
<Modal
isOpen={hiveSignerModal}
isFullScreen
isCloseButton
handleOnModalClose={_onClose}
title={intl.formatMessage({ id: 'qr.confirmTransaction' })}
>
{webViewModalData && (
<WebView source={{ uri: `${hsOptions.base_url}${webViewModalData?.uri?.substring(7)}` }} />
)}
</Modal>
);
};

export default WebViewModal;
Loading

0 comments on commit 6a3b2fe

Please sign in to comment.