This commit is contained in:
noumantahir 2024-11-11 15:46:18 +05:00
parent 61a371f4ad
commit 52aa88c7c0
6 changed files with 297 additions and 334 deletions

View File

@ -1,112 +1,102 @@
import React, { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { debounce } from 'lodash';
import EStyleSheet from 'react-native-extended-stylesheet';
import Animated, {
FadeOut,
LinearTransition,
ZoomIn,
ZoomOut,
} from 'react-native-reanimated';
import Animated, { FadeOut, LinearTransition, ZoomIn, ZoomOut } from 'react-native-reanimated';
import styles from '../styles/hiveAuthModal.styles';
import { lookupAccounts } from '../../../providers/hive/dhive';
import { FormInput, MainButton } from '../..';
import HIVE_AUTH_ICON from '../../../assets/HiveAuth_logo.png';
interface AuthInputContentProps {
initUsername?: string,
handleAuthRequest: (username: string) => void
initUsername?: string;
handleAuthRequest: (username: string) => void;
}
export const AuthInputContent = ({ initUsername, handleAuthRequest }: AuthInputContentProps) => {
const intl = useIntl();
const intl = useIntl();
const [username, setUsername] = useState(initUsername || '');
const [isUsernameValid, setIsUsernameValid] = useState(false);
const [username, setUsername] = useState(initUsername || '');
const [isUsernameValid, setIsUsernameValid] = useState(false);
const debouncedCheckValidity = debounce((uname: string) => {
_checkUsernameIsValid(uname);
}, 500);
const debouncedCheckValidity = debounce((uname: string) => {
_checkUsernameIsValid(uname);
}, 500);
useEffect(() => {
if(initUsername){
setUsername(initUsername)
}
}, [initUsername])
useEffect(() => {
debouncedCheckValidity(username);
return () => debouncedCheckValidity.cancel();
}, [username]);
const _handleUsernameChange = (username: string) => {
const formattedUsername = username.trim().toLowerCase();
setUsername(formattedUsername);
};
const _checkUsernameIsValid = async (uname: string) => {
try {
const accts = await lookupAccounts(uname);
const isValid = accts.includes(uname);
setIsUsernameValid(isValid);
} catch (err) {
setIsUsernameValid(false);
}
};
const onSignInPress = () => {
if (handleAuthRequest) {
handleAuthRequest(username)
}
useEffect(() => {
if (initUsername) {
setUsername(initUsername);
}
}, [initUsername]);
return (
<Animated.View
style={{ width: '100%', justifyContent: 'center', alignItems: 'center' }}
exiting={FadeOut}
>
<Animated.View style={{ width: '90%' }} layout={LinearTransition}>
<FormInput
rightIconName="at"
leftIconName="close"
iconType="MaterialCommunityIcons"
isValid={!username || isUsernameValid}
onChange={_handleUsernameChange}
placeholder={intl.formatMessage({
id: 'login.username',
})}
isEditable
type="username"
isFirstImage
value={username}
inputStyle={styles.input}
wrapperStyle={styles.inputWrapper}
onBlur={() => _checkUsernameIsValid(username)}
/>
</Animated.View>
useEffect(() => {
debouncedCheckValidity(username);
return () => debouncedCheckValidity.cancel();
}, [username]);
{isUsernameValid && (
<Animated.View entering={ZoomIn} exiting={ZoomOut}>
<MainButton
text={intl.formatMessage({ id: 'login.signin_with_hiveauth' })}
textStyle={{color:EStyleSheet.value("$primaryBlack")}}
style={{
backgroundColor: 'transparent',
marginTop: 12,
borderWidth: 1,
borderColor: EStyleSheet.value('$iconColor'),
height: 44,
}}
onPress={onSignInPress}
source={HIVE_AUTH_ICON}
/>
</Animated.View>
)}
const _handleUsernameChange = (username: string) => {
const formattedUsername = username.trim().toLowerCase();
setUsername(formattedUsername);
};
const _checkUsernameIsValid = async (uname: string) => {
try {
const accts = await lookupAccounts(uname);
const isValid = accts.includes(uname);
setIsUsernameValid(isValid);
} catch (err) {
setIsUsernameValid(false);
}
};
const onSignInPress = () => {
if (handleAuthRequest) {
handleAuthRequest(username);
}
};
return (
<Animated.View
style={{ width: '100%', justifyContent: 'center', alignItems: 'center' }}
exiting={FadeOut}
>
<Animated.View style={{ width: '90%' }} layout={LinearTransition}>
<FormInput
rightIconName="at"
leftIconName="close"
iconType="MaterialCommunityIcons"
isValid={!username || isUsernameValid}
onChange={_handleUsernameChange}
placeholder={intl.formatMessage({
id: 'login.username',
})}
isEditable
type="username"
isFirstImage
value={username}
inputStyle={styles.input}
wrapperStyle={styles.inputWrapper}
onBlur={() => _checkUsernameIsValid(username)}
/>
</Animated.View>
{isUsernameValid && (
<Animated.View entering={ZoomIn} exiting={ZoomOut}>
<MainButton
text={intl.formatMessage({ id: 'login.signin_with_hiveauth' })}
textStyle={{ color: EStyleSheet.value('$primaryBlack') }}
style={{
backgroundColor: 'transparent',
marginTop: 12,
borderWidth: 1,
borderColor: EStyleSheet.value('$iconColor'),
height: 44,
}}
onPress={onSignInPress}
source={HIVE_AUTH_ICON}
/>
</Animated.View>
)
};
)}
</Animated.View>
);
};

View File

@ -1,4 +1,3 @@
import React from 'react';
import { Text, ActivityIndicator } from 'react-native';
@ -14,16 +13,12 @@ import styles from '../styles/hiveAuthModal.styles';
import { Icon } from '../..';
import { HiveAuthStatus } from '../hooks/useHiveAuth';
interface StatusContentProps {
status:HiveAuthStatus,
statusText:string
status: HiveAuthStatus;
statusText: string;
}
export const StatusContent = ({ status, statusText}:StatusContentProps) => {
export const StatusContent = ({ status, statusText }: StatusContentProps) => {
const _renderResultIcon = (iconName: string, colorId: string) => (
<Animated.View entering={ZoomIn.springify().duration(500)} exiting={ZoomOut}>
<Icon
@ -36,36 +31,35 @@ export const StatusContent = ({ status, statusText}:StatusContentProps) => {
</Animated.View>
);
return (
<>
{status === HiveAuthStatus.SUCCESS && _renderResultIcon('checkcircleo', '$primaryGreen')}
{status === HiveAuthStatus.ERROR && _renderResultIcon('closecircleo', '$primaryRed')}
return (
<>
{status === HiveAuthStatus.SUCCESS && _renderResultIcon('checkcircleo', '$primaryGreen')}
{status === HiveAuthStatus.ERROR && _renderResultIcon('closecircleo', '$primaryRed')}
<Animated.View
style={{ flexDirection: 'row', alignItems: 'center' }}
layout={LinearTransition}
entering={FadeInUp}
exiting={FadeOutDown}
>
{status === HiveAuthStatus.PROCESSING && (
<ActivityIndicator
style={{ marginRight: 16 }}
size="large"
color={EStyleSheet.value('$primaryBlue')}
/>
)}
<Text
style={{
color: EStyleSheet.value('$primaryDarkText'),
fontWeight: 300,
fontSize: 24,
textAlign: 'center',
}}
>
{statusText}
</Text>
</Animated.View>
</>
)
}
<Animated.View
style={{ flexDirection: 'row', alignItems: 'center' }}
layout={LinearTransition}
entering={FadeInUp}
exiting={FadeOutDown}
>
{status === HiveAuthStatus.PROCESSING && (
<ActivityIndicator
style={{ marginRight: 16 }}
size="large"
color={EStyleSheet.value('$primaryBlue')}
/>
)}
<Text
style={{
color: EStyleSheet.value('$primaryDarkText'),
fontWeight: 300,
fontSize: 24,
textAlign: 'center',
}}
>
{statusText}
</Text>
</Animated.View>
</>
);
};

View File

@ -1,7 +1,6 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { View } from 'react-native';
import { useIntl } from 'react-intl';
import { useNavigation } from '@react-navigation/native';
import ActionSheet from 'react-native-actions-sheet';
@ -15,13 +14,11 @@ import { AuthInputContent } from '../children/authInputContent';
import { StatusContent } from '../children/statusContent';
import { HiveAuthStatus, useHiveAuth } from '../hooks/useHiveAuth';
interface HiveAuthModalProps {
onClose?: () => void;
}
export const HiveAuthModal = forwardRef(({ onClose }: HiveAuthModalProps, ref) => {
const intl = useIntl();
const navigation = useNavigation();
const hiveAuth = useHiveAuth();
@ -36,7 +33,6 @@ export const HiveAuthModal = forwardRef(({ onClose }: HiveAuthModalProps, ref) =
showModal: (_username?: string) => {
setInitUsername(_username);
bottomSheetModalRef.current?.show();
},
broadcastActiveOps: (opsArray: any) => {
if (opsArray) {
@ -46,13 +42,10 @@ export const HiveAuthModal = forwardRef(({ onClose }: HiveAuthModalProps, ref) =
},
}));
const handleAuthRequest = async (username: string) => {
const success = await hiveAuth.authenticate(username);
//isLoggedInt
// isLoggedInt
if (success) {
if (isPinCodeOpen) {
navigation.navigate({
@ -67,10 +60,8 @@ export const HiveAuthModal = forwardRef(({ onClose }: HiveAuthModalProps, ref) =
});
}
}
};
const _closeModal = () => {
bottomSheetModalRef.current?.hide();
if (onClose) {
@ -78,27 +69,25 @@ export const HiveAuthModal = forwardRef(({ onClose }: HiveAuthModalProps, ref) =
}
};
const _renderContent = () => {
const _content = hiveAuth.status === HiveAuthStatus.INPUT
? <AuthInputContent initUsername={initUsername} handleAuthRequest={handleAuthRequest} />
: <StatusContent status={hiveAuth.status} statusText={hiveAuth.statusText} />
const _content =
hiveAuth.status === HiveAuthStatus.INPUT ? (
<AuthInputContent initUsername={initUsername} handleAuthRequest={handleAuthRequest} />
) : (
<StatusContent status={hiveAuth.status} statusText={hiveAuth.statusText} />
);
return (
<View style={styles.container}>
<ModalHeader
title={intl.formatMessage({id:'hiveauth.title'})}
title={intl.formatMessage({ id: 'hiveauth.title' })}
isCloseButton={true}
onClosePress={_closeModal} />
<View style={styles.content}>
{_content}
</View>
onClosePress={_closeModal}
/>
<View style={styles.content}>{_content}</View>
</View>
)
);
};
return (
@ -107,7 +96,7 @@ export const HiveAuthModal = forwardRef(({ onClose }: HiveAuthModalProps, ref) =
gestureEnabled={false}
hideUnderlay={true}
onClose={() => {
hiveAuth.reset()
hiveAuth.reset();
setInitUsername(undefined);
}}
containerStyle={styles.sheetContent}

View File

@ -1,233 +1,224 @@
import { useEffect, useState } from 'react';
import { Linking, Keyboard } from 'react-native';
import HAS from 'hive-auth-wrapper';
import { v4 as uuidv4 } from 'uuid';
import { HiveSignerMessage } from 'utils/hive-signer-helper';
import { Operation } from '@hiveio/dhive';
import assert from 'assert';
import { useIntl } from 'react-intl';
import { getDigitPinCode } from '../../../providers/hive/dhive';
import { loginWithHiveAuth } from '../../../providers/hive/auth';
import { useAppSelector, usePostLoginActions } from '../../../hooks';
import AUTH_TYPE from '../../../constants/authType';
import { decryptKey } from '../../../utils/crypto';
import { delay } from '../../../utils/editor';
import { Operation } from '@hiveio/dhive';
import assert from 'assert';
import { useIntl } from 'react-intl';
import bugsnapInstance from '../../../config/bugsnag';
const APP_META = {
name: 'Ecency',
description: 'Decentralised Social Blogging',
icon: undefined,
name: 'Ecency',
description: 'Decentralised Social Blogging',
icon: undefined,
};
const HAS_AUTH_URI = 'has://auth_req';
const HAS_SIGN_URI = 'has://sign_req'
const HAS_SIGN_URI = 'has://sign_req';
export enum HiveAuthStatus {
INPUT = 0,
PROCESSING = 1,
SUCCESS = 2,
ERROR = 3,
INPUT = 0,
PROCESSING = 1,
SUCCESS = 2,
ERROR = 3,
}
export const useHiveAuth = () => {
const intl = useIntl();
const postLoginActions = usePostLoginActions();
const intl = useIntl();
const postLoginActions = usePostLoginActions();
const pinHash = useAppSelector((state) => state.application.pin);
const currentAccount = useAppSelector((state) => state.account.currentAccount);
const pinHash = useAppSelector((state) => state.application.pin);
const currentAccount = useAppSelector((state) => state.account.currentAccount);
const [statusText, setStatusText] = useState('');
const [status, setStatus] = useState(HiveAuthStatus.INPUT);
const [statusText, setStatusText] = useState('');
const [status, setStatus] = useState(HiveAuthStatus.INPUT);
//initiate has web hook connection
useEffect(() => {
// Retrieving connection status
HAS.connect().then(() => {
const _status = HAS.status();
console.log('has status', _status);
});
}, []);
// initiate has web hook connection
useEffect(() => {
// Retrieving connection status
HAS.connect().then(() => {
const _status = HAS.status();
console.log('has status', _status);
});
}, []);
/**
* authenticates user via installed hive auth or keychain app
* compiles and set account data in redux store
* @param username user to log in
* @returns Promise<boolean> success status
*/
const authenticate = async (username: string) => {
setStatusText(intl.formatMessage({ id: 'hiveauth.initiating' }));
setStatus(HiveAuthStatus.PROCESSING);
Keyboard.dismiss();
await delay(1000); // TODO: delay to assist modal settle height before show redirecting dialog
/**
* authenticates user via installed hive auth or keychain app
* compiles and set account data in redux store
* @param username user to log in
* @returns Promise<boolean> success status
*/
const authenticate = async (username: string) => {
setStatusText(intl.formatMessage({ id: 'hiveauth.initiating' }));
setStatus(HiveAuthStatus.PROCESSING);
try {
// Create an authentication object
const auth = {
username, // required - replace "username" with your Hive account name (without the @)
expire: undefined,
key: uuidv4(),
};
Keyboard.dismiss();
await delay(1000); // TODO: delay to assist modal settle height before show redirecting dialog
// create a challenge/message to sign with the posting key
// this sign is required for enabled hive sigenr support
const timestamp = new Date().getTime() / 1000;
const messageObj: HiveSignerMessage = {
signed_message: { type: 'refresh', app: 'ecency.app' },
authors: [auth.username],
timestamp,
};
try {
// Create an authentication object
const auth = {
username, // required - replace "username" with your Hive account name (without the @)
expire: undefined,
key: uuidv4(),
};
const challenge_data = {
key_type: 'posting',
challenge: JSON.stringify(messageObj),
};
// create a challenge/message to sign with the posting key
// this sign is required for enabled hive sigenr support
const timestamp = new Date().getTime() / 1000;
const messageObj: HiveSignerMessage = {
signed_message: { type: 'refresh', app: 'ecency.app' },
authors: [auth.username],
timestamp,
};
// callback to initiate keychain authenticated request
const _cbWait = async (evt: any) => {
console.log(evt); // process auth_wait message
const challenge_data = {
key_type: 'posting',
challenge: JSON.stringify(messageObj),
};
const { username, key } = auth;
const { host } = HAS.status();
const { uuid } = evt;
// callback to initiate keychain authenticated request
const _cbWait = async (evt: any) => {
console.log(evt); // process auth_wait message
const payload = {
account: username,
uuid,
key,
host,
};
const { username, key } = auth;
const { host } = HAS.status();
const { uuid } = evt;
const encodedData = btoa(JSON.stringify(payload));
const payload = {
account: username,
uuid,
key,
host,
};
console.log(encodedData);
const encodedData = btoa(JSON.stringify(payload));
const uri = `${HAS_AUTH_URI}/${encodedData}`;
console.log(encodedData);
const uri = `${HAS_AUTH_URI}/${encodedData}`;
const _canOpenUri = await Linking.canOpenURL(uri);
if (_canOpenUri) {
setStatusText(intl.formatMessage({ id: 'hiveauth.authenticating' }));
Linking.openURL(uri);
} else {
// TOOD: prompt to install valid keychain app
setStatusText( intl.formatMessage({ id: 'hiveauth.not_installed' }));
setStatus(HiveAuthStatus.ERROR);
}
};
const authRes = await HAS.authenticate(auth, APP_META, challenge_data, _cbWait);
setStatusText(intl.formatMessage({ id: 'hiveauth.logging_in' }));
// update message with sign and encode to created hsCode
messageObj.signatures = [authRes.data.challenge.challenge];
const hsCode = btoa(JSON.stringify(messageObj));
const accountData = await loginWithHiveAuth(hsCode, auth.key, auth.expire);
postLoginActions.updateAccountsData(accountData);
setStatusText(intl.formatMessage({ id: 'hiveauth.auth_success' }));
setStatus(HiveAuthStatus.SUCCESS);
await delay(2000);
return true;
} catch (error) {
setStatusText(intl.formatMessage({ id: error.message || 'hiveauth.auth_fail' }));
setStatus(HiveAuthStatus.ERROR);
console.warn('Login failed', error);
bugsnapInstance.notify(error);
return false;
const _canOpenUri = await Linking.canOpenURL(uri);
if (_canOpenUri) {
setStatusText(intl.formatMessage({ id: 'hiveauth.authenticating' }));
Linking.openURL(uri);
} else {
// TOOD: prompt to install valid keychain app
setStatusText(intl.formatMessage({ id: 'hiveauth.not_installed' }));
setStatus(HiveAuthStatus.ERROR);
}
};
};
const authRes = await HAS.authenticate(auth, APP_META, challenge_data, _cbWait);
/**
* Broadcasts ops array using hive auth app,
* uses hive auth key from current account data
* @param opsArray Operation array to broadcast
* @returns Promise<boolean> success status
*/
const broadcast = async (opsArray: Operation[]) => {
try {
setStatusText(intl.formatMessage({ id: 'hiveauth.logging_in' }));
// update message with sign and encode to created hsCode
messageObj.signatures = [authRes.data.challenge.challenge];
const hsCode = btoa(JSON.stringify(messageObj));
assert(opsArray, intl.formatMessage({ id: 'hiveauth.missing_op_arr' }));
assert(currentAccount.local.authType === AUTH_TYPE.HIVE_AUTH, intl.formatMessage({ id: 'hiveauth.invalid_auth_type' }))
const accountData = await loginWithHiveAuth(hsCode, auth.key, auth.expire);
setStatus(HiveAuthStatus.PROCESSING);
setStatusText(intl.formatMessage({ id: 'hiveauth.initiating' }));
await delay(1000);
postLoginActions.updateAccountsData(accountData);
setStatusText(intl.formatMessage({ id: 'hiveauth.auth_success' }));
setStatus(HiveAuthStatus.SUCCESS);
const _hiveAuthObj = {
username: currentAccount.username,
expiry: currentAccount.local.hiveAuthExpiry,
key: decryptKey(currentAccount.local.hiveAuthKey, getDigitPinCode(pinHash)),
};
await delay(2000);
assert(_hiveAuthObj.key, intl.formatMessage({id:"hiveauth.decrypt_fail"}))
assert(_hiveAuthObj.expiry > new Date().getTime(), intl.formatMessage({id:'hiveauth.expired'}) )
return true;
} catch (error) {
setStatusText(intl.formatMessage({ id: error.message || 'hiveauth.auth_fail' }));
setStatus(HiveAuthStatus.ERROR);
const _cdWait = async (evt: any) => {
console.log('sign wait', evt);
console.warn('Login failed', error);
bugsnapInstance.notify(error);
return false;
}
};
const _canOpenUri = await Linking.canOpenURL(HAS_SIGN_URI);
if (_canOpenUri) {
setStatusText(intl.formatMessage({ id: 'hiveauth.requesting' }));
Linking.openURL(HAS_SIGN_URI);
} else {
throw new Error(intl.formatMessage({ id: 'hiveauth.not_installed' }));
}
};
/**
* Broadcasts ops array using hive auth app,
* uses hive auth key from current account data
* @param opsArray Operation array to broadcast
* @returns Promise<boolean> success status
*/
const broadcast = async (opsArray: Operation[]) => {
try {
assert(opsArray, intl.formatMessage({ id: 'hiveauth.missing_op_arr' }));
assert(
currentAccount.local.authType === AUTH_TYPE.HIVE_AUTH,
intl.formatMessage({ id: 'hiveauth.invalid_auth_type' }),
);
const res = await HAS.broadcast(_hiveAuthObj, 'active', opsArray, _cdWait);
setStatus(HiveAuthStatus.PROCESSING);
setStatusText(intl.formatMessage({ id: 'hiveauth.initiating' }));
await delay(1000);
if (res && res.broadcast) {
console.log('broadcast response', res);
// TODO: hive modal
// respond back to transfer screen
}
const _hiveAuthObj = {
username: currentAccount.username,
expiry: currentAccount.local.hiveAuthExpiry,
key: decryptKey(currentAccount.local.hiveAuthKey, getDigitPinCode(pinHash)),
};
setStatus(HiveAuthStatus.SUCCESS);
setStatusText(intl.formatMessage({ id: 'hiveauth.transaction_success' }));
await delay(2000);
assert(_hiveAuthObj.key, intl.formatMessage({ id: 'hiveauth.decrypt_fail' }));
assert(
_hiveAuthObj.expiry > new Date().getTime(),
intl.formatMessage({ id: 'hiveauth.expired' }),
);
return true;
const _cdWait = async (evt: any) => {
console.log('sign wait', evt);
} catch (error) {
setStatus(HiveAuthStatus.ERROR);
setStatusText(intl.formatMessage({ id: error.message || 'hiveauth.transaction_fail' }));
console.warn('Transaction failed', error);
bugsnapInstance.notify(error);
return false;
const _canOpenUri = await Linking.canOpenURL(HAS_SIGN_URI);
if (_canOpenUri) {
setStatusText(intl.formatMessage({ id: 'hiveauth.requesting' }));
Linking.openURL(HAS_SIGN_URI);
} else {
throw new Error(intl.formatMessage({ id: 'hiveauth.not_installed' }));
}
};
const res = await HAS.broadcast(_hiveAuthObj, 'active', opsArray, _cdWait);
if (res && res.broadcast) {
console.log('broadcast response', res);
// TODO: hive modal
// respond back to transfer screen
}
setStatus(HiveAuthStatus.SUCCESS);
setStatusText(intl.formatMessage({ id: 'hiveauth.transaction_success' }));
await delay(2000);
return true;
} catch (error) {
setStatus(HiveAuthStatus.ERROR);
setStatusText(intl.formatMessage({ id: error.message || 'hiveauth.transaction_fail' }));
console.warn('Transaction failed', error);
bugsnapInstance.notify(error);
return false;
}
};
const reset = () => {
setStatus(HiveAuthStatus.INPUT);
setStatusText('');
};
const reset = () => {
setStatus(HiveAuthStatus.INPUT);
setStatusText('')
}
return {
authenticate,
broadcast,
reset,
status,
statusText
}
}
return {
authenticate,
broadcast,
reset,
status,
statusText,
};
};

View File

@ -31,7 +31,7 @@ export default EStyleSheet.create({
borderTopStartRadius: 28,
marginBottom: 12,
borderBottomWidth: 1,
paddingRight:8,
paddingRight: 8,
height: 56,
} as ViewStyle,
});

View File

@ -31,7 +31,6 @@ const decryptKeyNew = (data, key) => {
return ret;
};
// stamping mechanism will help distinguish old legacy data and new encrypted data
// second purpose is to avoid necrypting empty strings
const getStampedData = (data) => {