diff --git a/native/account/siwe-panel.react.js b/native/account/siwe-panel.react.js index a64b5dd67..8152e3bfd 100644 --- a/native/account/siwe-panel.react.js +++ b/native/account/siwe-panel.react.js @@ -1,227 +1,228 @@ // @flow import BottomSheet from '@gorhom/bottom-sheet'; import * as React from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import WebView from 'react-native-webview'; import { getSIWENonce, getSIWENonceActionTypes, siweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { SIWEWebViewMessage, SIWEResult } from 'lib/types/siwe-types.js'; import { useServerCall, useDispatchActionPromise, type BindServerCallsParams, } from 'lib/utils/action-utils.js'; import { useKeyboardHeight } from '../keyboard/keyboard-hooks.js'; import { useSelector } from '../redux/redux-utils.js'; +import type { BottomSheetRef } from '../types/bottom-sheet.js'; import Alert from '../utils/alert.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; import { defaultLandingURLPrefix } from '../utils/url-utils.js'; const commSIWE = `${defaultLandingURLPrefix}/siwe`; const getSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const siweAuthLoadingStatusSelector = createLoadingStatusSelector(siweAuthActionTypes); type WebViewMessageEvent = { +nativeEvent: { +data: string, ... }, ... }; type Props = { +onClosed: () => mixed, +onClosing: () => mixed, +onSuccessfulWalletSignature: SIWEResult => mixed, +closing: boolean, +setLoading: boolean => mixed, +keyserverCallParamOverride?: $Shape, }; function SIWEPanel(props: Props): React.Node { const dispatchActionPromise = useDispatchActionPromise(); const getSIWENonceCall = useServerCall( getSIWENonce, props.keyserverCallParamOverride, ); 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); })(), ); const ed25519 = await getContentSigningKey(); setPrimaryIdentityPublicKey(ed25519); })(); }, [dispatchActionPromise, getSIWENonceCall]); const [isLoading, setLoading] = React.useState(true); const [walletConnectModalHeight, setWalletConnectModalHeight] = React.useState(0); const insets = useSafeAreaInsets(); const keyboardHeight = useKeyboardHeight(); const bottomInset = insets.bottom; const snapPoints = React.useMemo(() => { if (isLoading) { return [1]; } else if (walletConnectModalHeight) { const baseHeight = bottomInset + walletConnectModalHeight + keyboardHeight; if (baseHeight < 400) { return [baseHeight - 10]; } else { return [baseHeight + 5]; } } else { const baseHeight = bottomInset + keyboardHeight; return [baseHeight + 435, baseHeight + 600]; } }, [isLoading, walletConnectModalHeight, bottomInset, keyboardHeight]); - const bottomSheetRef = React.useRef(); + 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 closeBottomSheet = bottomSheetRef.current?.close; const { closing, onSuccessfulWalletSignature } = props; const handleMessage = React.useCallback( async (event: WebViewMessageEvent) => { const data: SIWEWebViewMessage = JSON.parse(event.nativeEvent.data); if (data.type === 'siwe_success') { const { address, message, signature } = data; if (address && signature) { closeBottomSheet?.(); await onSuccessfulWalletSignature({ address, message, signature }); } } else if (data.type === 'siwe_closed') { onClosing(); closeBottomSheet?.(); } else if (data.type === 'walletconnect_modal_update') { const height = data.state === 'open' ? data.height : 0; setWalletConnectModalHeight(height); } }, [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 walletConnectModalOpen = walletConnectModalHeight !== 0; const backgroundStyle = React.useMemo( () => ({ backgroundColor: walletConnectModalOpen ? '#3396ff' : '#242529', }), [walletConnectModalOpen], ); const bottomSheetHandleIndicatorStyle = React.useMemo( () => ({ backgroundColor: 'white', }), [], ); const { onClosed } = props; const onBottomSheetChange = React.useCallback( (index: number) => { 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; diff --git a/native/profile/keyserver-selection-bottom-sheet.react.js b/native/profile/keyserver-selection-bottom-sheet.react.js index 68e1aaa1f..f00a7cbad 100644 --- a/native/profile/keyserver-selection-bottom-sheet.react.js +++ b/native/profile/keyserver-selection-bottom-sheet.react.js @@ -1,243 +1,244 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { removeKeyserverActionType } from 'lib/actions/keyserver-actions.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import type { GlobalAccountUserInfo } from 'lib/types/user-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { BottomSheetContext } from '../bottom-sheet/bottom-sheet-provider.react.js'; import BottomSheet from '../bottom-sheet/bottom-sheet.react.js'; import Button from '../components/button.react.js'; import CommIcon from '../components/comm-icon.react.js'; import Pill from '../components/pill.react.js'; import StatusIndicator from '../components/status-indicator.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useColors, useStyles } from '../themes/colors.js'; +import type { BottomSheetRef } from '../types/bottom-sheet.js'; import Alert from '../utils/alert.js'; export type KeyserverSelectionBottomSheetParams = { +keyserverAdminUserInfo: GlobalAccountUserInfo, +keyserverInfo: KeyserverInfo, }; // header + paddingTop + paddingBottom + marginBottom const keyserverHeaderHeight = 84 + 16 + 16 + 24; type Props = { +navigation: RootNavigationProp<'KeyserverSelectionBottomSheet'>, +route: NavigationRoute<'KeyserverSelectionBottomSheet'>, }; function KeyserverSelectionBottomSheet(props: Props): React.Node { const { navigation, route: { params: { keyserverAdminUserInfo, keyserverInfo }, }, } = props; const { goBack } = navigation; const bottomSheetContext = React.useContext(BottomSheetContext); invariant(bottomSheetContext, 'bottomSheetContext should be set'); const { setContentHeight } = bottomSheetContext; const removeKeyserverContainerRef = React.useRef>(); - const bottomSheetRef = React.useRef(); + const bottomSheetRef = React.useRef(); const colors = useColors(); const styles = useStyles(unboundStyles); const insets = useSafeAreaInsets(); const onLayout = React.useCallback(() => { removeKeyserverContainerRef.current?.measure( (x, y, width, height, pageX, pageY) => { if ( height === null || height === undefined || pageY === null || pageY === undefined ) { return; } setContentHeight(height + keyserverHeaderHeight + insets.bottom); }, ); }, [insets.bottom, setContentHeight]); const cloudIcon = React.useMemo( () => ( ), [colors.panelForegroundLabel], ); const onPressDisconnectKeyserver = React.useCallback(() => { // TODO: update this function when we have a way to // disconnect from a keyserver Alert.alert( 'Disconnecting from a keyserver is still not ready.', 'Please come back later.', [{ text: 'OK' }], ); }, []); const dispatch = useDispatch(); const onDeleteKeyserver = React.useCallback(() => { dispatch({ type: removeKeyserverActionType, payload: { keyserverAdminUserID: keyserverAdminUserInfo.id, }, }); bottomSheetRef.current?.close(); }, [dispatch, keyserverAdminUserInfo.id]); const onPressRemoveKeyserver = React.useCallback(() => { Alert.alert( 'Delete keyserver', 'Are you sure you want to delete this keyserver from your keyserver ' + 'list? You will still remain in the associated communities.', [ { text: 'Delete', style: 'destructive', onPress: onDeleteKeyserver, }, { text: 'Cancel', style: 'cancel', }, ], ); }, [onDeleteKeyserver]); const removeKeyserver = React.useMemo(() => { if (keyserverInfo.connection.status !== 'connected') { return ( <> You may delete offline keyservers from your keyserver list. When you delete a keyserver, you will still remain in the associated communities. Any messages or content you have previously sent will remain on the keyserver’s communities after disconnecting or deleting. ); } return ( <> Disconnecting from this keyserver will remove you from its associated communities. Any messages or content you have previously sent will remain on the keyserver. ); }, [ keyserverInfo.connection.status, onPressDisconnectKeyserver, onPressRemoveKeyserver, styles.keyserverRemoveText, styles.removeButtonContainer, styles.removeButtonText, ]); return ( {keyserverInfo.urlPrefix} {removeKeyserver} ); } const unboundStyles = { container: { paddingHorizontal: 16, }, keyserverDetailsContainer: { alignItems: 'center', justifyContent: 'space-between', paddingVertical: 16, backgroundColor: 'modalAccentBackground', marginBottom: 24, borderRadius: 8, }, keyserverHeaderContainer: { flexDirection: 'row', alignItems: 'center', }, statusIndicatorContainer: { marginLeft: 8, }, keyserverURLText: { color: 'modalForegroundLabel', marginTop: 8, }, keyserverRemoveText: { color: 'modalForegroundLabel', marginBottom: 24, }, removeButtonContainer: { backgroundColor: 'vibrantRedButton', paddingVertical: 12, borderRadius: 8, alignItems: 'center', }, removeButtonText: { color: 'floatingButtonLabel', }, }; export default KeyserverSelectionBottomSheet; diff --git a/native/types/bottom-sheet.js b/native/types/bottom-sheet.js new file mode 100644 index 000000000..51c5b94b3 --- /dev/null +++ b/native/types/bottom-sheet.js @@ -0,0 +1,7 @@ +// @flow + +export type BottomSheetRef = { + +snapToIndex: number => mixed, + +close: () => mixed, + ... +};