diff --git a/native/account/terms-and-privacy-modal.react.js b/native/account/terms-and-privacy-modal.react.js new file mode 100644 --- /dev/null +++ b/native/account/terms-and-privacy-modal.react.js @@ -0,0 +1,190 @@ +// @flow + +import { useIsFocused } from '@react-navigation/native'; +import * as React from 'react'; +import { + ActivityIndicator, + BackHandler, + Linking, + Platform, + Text, + View, +} from 'react-native'; + +import { + policyAcknowledgment, + policyAcknowledgmentActionTypes, +} from 'lib/actions/user-actions'; +import { type PolicyType, policyTypes } from 'lib/facts/policies'; +import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; +import { + useDispatchActionPromise, + useServerCall, +} from 'lib/utils/action-utils'; +import { acknowledgePolicy } from 'lib/utils/policy-acknowledge-utlis'; + +import Button from '../components/button.react'; +import Modal from '../components/modal.react'; +import type { RootNavigationProp } from '../navigation/root-navigator.react'; +import type { NavigationRoute } from '../navigation/route-names'; +import { useSelector } from '../redux/redux-utils'; +import { useStyles } from '../themes/colors'; + +export type TermsAndPrivacyModalParams = { + +policyType: PolicyType, +}; + +type Props = { + +navigation: RootNavigationProp<'TermsAndPrivacyModal'>, + +route: NavigationRoute<'TermsAndPrivacyModal'>, +}; + +const loadingStatusSelector = createLoadingStatusSelector( + policyAcknowledgmentActionTypes, +); + +function TermsAndPrivacyModal(props: Props): React.Node { + const loadingStatus = useSelector(loadingStatusSelector); + const [acknowledgmentError, setAcknowledgmentError] = React.useState(''); + const sendAcknowledgmentRequest = useServerCall(policyAcknowledgment); + const dispatchActionPromise = useDispatchActionPromise(); + + const policyType = props.route.params.policyType; + const policyState = useSelector(store => store.userPolicies[policyType]); + const isAcknowledged = policyState?.isAcknowledged; + const isFocused = useIsFocused(); + + React.useEffect(() => { + if (isAcknowledged && isFocused) { + props.navigation.goBack(); + } + }, [isAcknowledged, props.navigation, isFocused]); + + const onAccept = React.useCallback(() => { + acknowledgePolicy( + policyTypes.tosAndPrivacyPolicy, + dispatchActionPromise, + sendAcknowledgmentRequest, + setAcknowledgmentError, + ); + }, [dispatchActionPromise, sendAcknowledgmentRequest]); + + const styles = useStyles(unboundStyles); + + const buttonContent = React.useMemo(() => { + if (loadingStatus === 'loading') { + return ( + + + + ); + } + return I accept; + }, [loadingStatus, styles.buttonText, styles.loading]); + + const onBackPress = props.navigation.isFocused; + React.useEffect(() => { + BackHandler.addEventListener('hardwareBackPress', onBackPress); + return () => { + BackHandler.removeEventListener('hardwareBackPress', onBackPress); + }; + }, [onBackPress]); + + const safeAreaEdges = ['top', 'bottom']; + return ( + + Terms of Service and Privacy Policy + + We recently updated our  + + Terms of Service + +  &  + + Privacy Policy + + + . In order to continue using Comm, we're asking you to read + through, acknowledge, and accept the updated policies. + + + + + + {acknowledgmentError} + + + ); +} + +const unboundStyles = { + modal: { + backgroundColor: 'modalForeground', + paddingBottom: 10, + paddingTop: 32, + paddingHorizontal: 32, + flex: 0, + borderColor: 'modalForegroundBorder', + }, + header: { + color: 'modalForegroundLabel', + fontSize: 20, + fontWeight: '600', + textAlign: 'center', + paddingBottom: 16, + }, + label: { + color: 'modalForegroundSecondaryLabel', + fontSize: 16, + lineHeight: 20, + textAlign: 'center', + }, + link: { + color: 'purpleLink', + fontWeight: 'bold', + }, + buttonsContainer: { + flexDirection: 'column', + marginTop: 24, + height: 72, + paddingHorizontal: 16, + }, + button: { + borderRadius: 5, + height: 48, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'purpleButton', + }, + buttonText: { + color: 'white', + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + }, + error: { + marginTop: 6, + fontStyle: 'italic', + color: 'redText', + textAlign: 'center', + }, + loading: { + paddingTop: Platform.OS === 'android' ? 0 : 6, + }, +}; + +const onTermsOfUsePressed = () => { + Linking.openURL('https://comm.app/terms'); +}; + +const onPrivacyPolicyPressed = () => { + Linking.openURL('https://comm.app/privacy'); +}; + +export default TermsAndPrivacyModal; diff --git a/native/components/modal.react.js b/native/components/modal.react.js --- a/native/components/modal.react.js +++ b/native/components/modal.react.js @@ -14,14 +14,18 @@ +containerStyle?: ViewStyle, +modalStyle?: ViewStyle, +safeAreaEdges?: $ReadOnlyArray<'top' | 'right' | 'bottom' | 'left'>, + +disableClosing?: boolean, }>; function Modal(props: Props): React.Node { const navigation = useNavigation(); const close = React.useCallback(() => { + if (props.disableClosing) { + return; + } if (navigation.isFocused()) { navigation.goBack(); } - }, [navigation]); + }, [navigation, props.disableClosing]); const styles = useStyles(unboundStyles); const { containerStyle, modalStyle, children, safeAreaEdges } = props; diff --git a/native/navigation/root-navigator.react.js b/native/navigation/root-navigator.react.js --- a/native/navigation/root-navigator.react.js +++ b/native/navigation/root-navigator.react.js @@ -18,6 +18,7 @@ import { enableScreens } from 'react-native-screens'; import LoggedOutModal from '../account/logged-out-modal.react'; +import TermsAndPrivacyModal from '../account/terms-and-privacy-modal.react'; import ThreadPickerModal from '../calendar/thread-picker-modal.react'; import ImagePasteModal from '../chat/image-paste-modal.react'; import AddUsersModal from '../chat/settings/add-users-modal.react'; @@ -43,6 +44,7 @@ SidebarListModalRouteName, type ScreenParamList, type RootParamList, + TermsAndPrivacyRouteName, } from './route-names'; enableScreens(); @@ -151,6 +153,10 @@ const modalOverlayScreenOptions = { cardOverlayEnabled: true, }; +const termsAndPrivacyModalScreenOptions = { + gestureEnabled: false, + cardOverlayEnabled: true, +}; export type RootRouterNavigationProp< ParamList: ParamListBase = ParamListBase, @@ -181,6 +187,11 @@ options={disableGesturesScreenOptions} /> +