diff --git a/lib/types/siwe-types.js b/lib/types/siwe-types.js index fca3bc598..24edd6671 100644 --- a/lib/types/siwe-types.js +++ b/lib/types/siwe-types.js @@ -1,117 +1,123 @@ // @flow import type { LogInExtraInfo } from './account-types.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import { type DeviceTokenUpdateRequest, type PlatformDetails, } from './device-types.js'; import { type CalendarQuery } from './entry-types.js'; export type SIWENonceResponse = { +nonce: string, }; export type SIWEAuthRequest = { +message: string, +signature: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, }; export type SIWEAuthServerCall = { +message: string, +signature: string, ...LogInExtraInfo, }; export type SIWESocialProof = { +siweMessage: string, +siweMessageSignature: string, }; // This is a message that the rendered webpage (landing/siwe.react.js) uses to // communicate back to the React Native WebView that is rendering it // (native/account/siwe-panel.react.js) export type SIWEWebViewMessage = | { +type: 'siwe_success', +address: string, +message: string, +signature: string, } | { +type: 'siwe_closed', } | { +type: 'walletconnect_modal_update', +state: 'open' | 'closed', }; export type SIWEMessage = { // RFC 4501 dns authority that is requesting the signing. +domain: string, // Ethereum address performing the signing conformant to capitalization // encoded checksum specified in EIP-55 where applicable. +address: string, // Human-readable ASCII assertion that the user will sign, and it must not // contain `\n`. +statement?: string, // RFC 3986 URI referring to the resource that is the subject of the signing // (as in the __subject__ of a claim). +uri: string, // Current version of the message. +version: string, // EIP-155 Chain ID to which the session is bound, and the network where // Contract Accounts must be resolved. +chainId: number, // Randomized token used to prevent replay attacks, at least 8 alphanumeric // characters. +nonce: string, // ISO 8601 datetime string of the current time. +issuedAt: string, // ISO 8601 datetime string that, if present, indicates when the signed // authentication message is no longer valid. +expirationTime?: string, // ISO 8601 datetime string that, if present, indicates when the signed // authentication message will become valid. +notBefore?: string, // System-specific identifier that may be used to uniquely refer to the // sign-in request. +requestId?: string, // List of information or references to information the user wishes to have // resolved as part of authentication by the relying party. They are // expressed as RFC 3986 URIs separated by `\n- `. +resources?: $ReadOnlyArray, // @deprecated // Signature of the message signed by the wallet. // // This field will be removed in future releases, an additional parameter // was added to the validate function were the signature goes to validate // the message. +signature?: string, // @deprecated // Type of sign message to be generated. // // This field will be removed in future releases and will rely on the // message version. +type?: 'Personal signature', +validate: (signature: string, provider?: any) => Promise, +toMessage: () => string, }; + +export type SIWEResult = { + +address: string, + +message: string, + +signature: string, +}; diff --git a/native/account/fullscreen-siwe-panel.react.js b/native/account/fullscreen-siwe-panel.react.js index a2debac1a..c262abc51 100644 --- a/native/account/fullscreen-siwe-panel.react.js +++ b/native/account/fullscreen-siwe-panel.react.js @@ -1,38 +1,54 @@ // @flow import * as React from 'react'; -import { ActivityIndicator, View } from 'react-native'; +import { Alert, ActivityIndicator, View } from 'react-native'; +import { useSIWEServerCall } from './siwe-hooks.js'; import SIWEPanel from './siwe-panel.react.js'; type Props = { +onClose: () => mixed, +closing: boolean, }; function FullscreenSIWEPanel(props: Props): React.Node { const [loading, setLoading] = React.useState(true); const activity = loading ? : null; const activityContainer = React.useMemo( () => ({ flex: 1, }), [], ); - const { onClose, closing } = props; + const { onClose } = props; + const siweServerCallParams = React.useMemo(() => { + const onServerCallFailure = () => { + Alert.alert( + 'Unknown error', + 'Uhh... try again?', + [{ text: 'OK', onPress: onClose }], + { cancelable: false }, + ); + }; + return { onFailure: onServerCallFailure }; + }, [onClose]); + const siweServerCall = useSIWEServerCall(siweServerCallParams); + + const { closing } = props; return ( <> {activity} ); } export default FullscreenSIWEPanel; diff --git a/native/account/registration/connect-ethereum.react.js b/native/account/registration/connect-ethereum.react.js index c0de067d7..b07ed3d7f 100644 --- a/native/account/registration/connect-ethereum.react.js +++ b/native/account/registration/connect-ethereum.react.js @@ -1,181 +1,183 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; import type { CoolOrNerdMode } from './registration-types.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; import EthereumLogoDark from '../../vectors/ethereum-logo-dark.react.js'; import SIWEPanel from '../siwe-panel.react.js'; export type ConnectEthereumParams = { +userSelections: { +coolOrNerdMode: CoolOrNerdMode, +keyserverUsername: string, }, }; type PanelState = 'closed' | 'opening' | 'open' | 'closing'; type Props = { +navigation: RegistrationNavigationProp<'ConnectEthereum'>, +route: NavigationRoute<'ConnectEthereum'>, }; function ConnectEthereum(props: Props): React.Node { const isNerdMode = props.route.params.userSelections.coolOrNerdMode === 'nerd'; const styles = useStyles(unboundStyles); let body; if (!isNerdMode) { body = ( Connecting your Ethereum wallet allows you to use your ENS name and avatar in the app. You'll also be able to log in with your wallet instead of a password. ); } else { body = ( <> Connecting your Ethereum wallet has three benefits: {'1. '} Your peers will be able to cryptographically verify that your Comm account is associated with your Ethereum wallet. {'2. '} You'll be able to use your ENS name and avatar in the app. {'3. '} You can choose to skip setting a password, and to log in with your Ethereum wallet instead. ); } const [panelState, setPanelState] = React.useState('closed'); const openPanel = React.useCallback(() => { setPanelState('opening'); }, []); const onPanelClosed = React.useCallback(() => { setPanelState('closed'); }, []); const onPanelClosing = React.useCallback(() => { setPanelState('closing'); }, []); const siwePanelSetLoading = React.useCallback( (loading: boolean) => { if (panelState === 'closing' || panelState === 'closed') { return; } setPanelState(loading ? 'opening' : 'open'); }, [panelState], ); + const onSkip = React.useCallback(() => {}, []); + const onSuccessfulWalletSignature = React.useCallback(() => {}, []); + let siwePanel; if (panelState !== 'closed') { siwePanel = ( ); } - const onSkip = React.useCallback(() => {}, []); - return ( <> Do you want to connect an Ethereum Wallet to your account? {body} {siwePanel} ); } const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, ethereumLogoContainer: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', }, list: { paddingBottom: 16, }, listItem: { flexDirection: 'row', }, listItemNumber: { fontWeight: 'bold', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', }, listItemContent: { flexShrink: 1, fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', }, }; export default ConnectEthereum; diff --git a/native/account/siwe-hooks.js b/native/account/siwe-hooks.js new file mode 100644 index 000000000..172a20ae3 --- /dev/null +++ b/native/account/siwe-hooks.js @@ -0,0 +1,71 @@ +// @flow + +import * as React from 'react'; + +import { siweAuth, siweAuthActionTypes } from 'lib/actions/siwe-actions.js'; +import type { LogInStartingPayload } from 'lib/types/account-types.js'; +import { + useServerCall, + useDispatchActionPromise, +} from 'lib/utils/action-utils.js'; + +import { NavContext } from '../navigation/navigation-context.js'; +import { useSelector } from '../redux/redux-utils.js'; +import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; + +type SIWEServerCallParams = { + +message: string, + +signature: string, + ... +}; +type UseSIWEServerCallParams = { + +onFailure: () => mixed, +}; +function useSIWEServerCall( + params: UseSIWEServerCallParams, +): SIWEServerCallParams => Promise { + const { onFailure } = params; + + const siweAuthCall = useServerCall(siweAuth); + + const callSIWE = React.useCallback( + async (message, signature, extraInfo) => { + try { + return await siweAuthCall({ + message, + signature, + ...extraInfo, + }); + } catch (e) { + onFailure(); + throw e; + } + }, + [onFailure, siweAuthCall], + ); + + const navContext = React.useContext(NavContext); + const logInExtraInfo = useSelector(state => + nativeLogInExtraInfoSelector({ + redux: state, + navContext, + }), + ); + + const dispatchActionPromise = useDispatchActionPromise(); + return React.useCallback( + async ({ message, signature }) => { + const extraInfo = await logInExtraInfo(); + + dispatchActionPromise( + siweAuthActionTypes, + callSIWE(message, signature, extraInfo), + undefined, + ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), + ); + }, + [logInExtraInfo, dispatchActionPromise, callSIWE], + ); +} + +export { useSIWEServerCall }; diff --git a/native/account/siwe-panel.react.js b/native/account/siwe-panel.react.js index 05458b9ec..16bc82452 100644 --- a/native/account/siwe-panel.react.js +++ b/native/account/siwe-panel.react.js @@ -1,258 +1,212 @@ // @flow import BottomSheet from '@gorhom/bottom-sheet'; import * as React from 'react'; import { Alert } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import WebView from 'react-native-webview'; import { getSIWENonce, getSIWENonceActionTypes, - siweAuth, siweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; -import type { LogInStartingPayload } from 'lib/types/account-types.js'; -import type { SIWEWebViewMessage } from 'lib/types/siwe-types.js'; +import type { SIWEWebViewMessage, SIWEResult } from 'lib/types/siwe-types.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import { commCoreModule } from '../native-modules.js'; -import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; -import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import { defaultLandingURLPrefix } from '../utils/url-utils.js'; const commSIWE = `${defaultLandingURLPrefix}/siwe`; const getSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const siweAuthLoadingStatusSelector = createLoadingStatusSelector(siweAuthActionTypes); type Props = { +onClosed: () => mixed, +onClosing: () => mixed, + +onSuccessfulWalletSignature: SIWEResult => mixed, +closing: boolean, +setLoading: boolean => mixed, }; function SIWEPanel(props: Props): React.Node { - const navContext = React.useContext(NavContext); const dispatchActionPromise = useDispatchActionPromise(); const getSIWENonceCall = useServerCall(getSIWENonce); - const siweAuthCall = useServerCall(siweAuth); - - const logInExtraInfo = useSelector(state => - nativeLogInExtraInfoSelector({ - redux: state, - navContext, - }), - ); const getSIWENonceCallFailed = useSelector( state => getSIWENonceLoadingStatusSelector(state) === 'error', ); const { onClosing } = props; React.useEffect(() => { if (getSIWENonceCallFailed) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: onClosing }], { cancelable: false }, ); } }, [getSIWENonceCallFailed, onClosing]); const siweAuthCallLoading = useSelector( state => siweAuthLoadingStatusSelector(state) === 'loading', ); const [nonce, setNonce] = React.useState(null); const [primaryIdentityPublicKey, setPrimaryIdentityPublicKey] = React.useState(null); React.useEffect(() => { (async () => { dispatchActionPromise( getSIWENonceActionTypes, (async () => { const response = await getSIWENonceCall(); setNonce(response); })(), ); await commCoreModule.initializeCryptoAccount(); const { primaryIdentityPublicKeys: { ed25519 }, } = await commCoreModule.getUserPublicKey(); setPrimaryIdentityPublicKey(ed25519); })(); }, [dispatchActionPromise, getSIWENonceCall]); const [isLoading, setLoading] = React.useState(true); const [isWalletConnectModalOpen, setWalletConnectModalOpen] = React.useState(false); const insets = useSafeAreaInsets(); const bottomInset = insets.bottom; const snapPoints = React.useMemo(() => { if (isLoading) { return [1]; } else if (isWalletConnectModalOpen) { return [bottomInset + 600]; } else { return [bottomInset + 435, bottomInset + 600]; } }, [isLoading, isWalletConnectModalOpen, bottomInset]); const bottomSheetRef = React.useRef(); const snapToIndex = bottomSheetRef.current?.snapToIndex; React.useEffect(() => { // When the snapPoints change, always reset to the first one // Without this, when we close the WalletConnect modal we don't resize snapToIndex?.(0); }, [snapToIndex, snapPoints]); - const callSIWE = React.useCallback( - async (message, signature, extraInfo) => { - try { - return await siweAuthCall({ - message, - signature, - ...extraInfo, - }); - } catch (e) { - Alert.alert( - 'Unknown error', - 'Uhh... try again?', - [{ text: 'OK', onPress: onClosing }], - { cancelable: false }, - ); - throw e; - } - }, - [onClosing, siweAuthCall], - ); - - const handleSIWE = React.useCallback( - async ({ message, signature }) => { - const extraInfo = await logInExtraInfo(); - - dispatchActionPromise( - siweAuthActionTypes, - callSIWE(message, signature, extraInfo), - undefined, - ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), - ); - }, - [logInExtraInfo, dispatchActionPromise, callSIWE], - ); const closeBottomSheet = bottomSheetRef.current?.close; - const { closing } = props; + const { closing, onSuccessfulWalletSignature } = props; const disableOnClose = React.useRef(false); const handleMessage = React.useCallback( async event => { const data: SIWEWebViewMessage = JSON.parse(event.nativeEvent.data); if (data.type === 'siwe_success') { const { address, message, signature } = data; if (address && signature) { disableOnClose.current = true; closeBottomSheet?.(); - await handleSIWE({ message, signature }); + await onSuccessfulWalletSignature({ address, message, signature }); } } else if (data.type === 'siwe_closed') { onClosing(); closeBottomSheet?.(); } else if (data.type === 'walletconnect_modal_update') { setWalletConnectModalOpen(data.state === 'open'); } }, - [handleSIWE, onClosing, closeBottomSheet], + [onSuccessfulWalletSignature, onClosing, closeBottomSheet], ); const prevClosingRef = React.useRef(); React.useEffect(() => { if (closing && !prevClosingRef.current) { closeBottomSheet?.(); } prevClosingRef.current = closing; }, [closing, closeBottomSheet]); const source = React.useMemo( () => ({ uri: commSIWE, headers: { 'siwe-nonce': nonce, 'siwe-primary-identity-public-key': primaryIdentityPublicKey, }, }), [nonce, primaryIdentityPublicKey], ); const onWebViewLoaded = React.useCallback(() => { setLoading(false); }, []); const backgroundStyle = React.useMemo( () => ({ backgroundColor: '#242529', }), [], ); const bottomSheetHandleIndicatorStyle = React.useMemo( () => ({ backgroundColor: 'white', }), [], ); const { onClosed } = props; const onBottomSheetChange = React.useCallback( (index: number) => { if (disableOnClose.current) { disableOnClose.current = false; return; } if (index === -1) { onClosed(); } }, [onClosed], ); let bottomSheet; if (nonce && primaryIdentityPublicKey) { bottomSheet = ( ); } const setLoadingProp = props.setLoading; const loading = !getSIWENonceCallFailed && (isLoading || siweAuthCallLoading); React.useEffect(() => { setLoadingProp(loading); }, [setLoadingProp, loading]); return bottomSheet; } export default SIWEPanel;