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,188 @@ +// @flow + +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; + + React.useEffect(() => { + if (isAcknowledged && props.navigation.isFocused()) { + props.navigation.goBack(); + } + }, [isAcknowledged, props.navigation]); + + const onAccept = React.useCallback(() => { + acknowledgePolicy( + policyTypes.tosAndPrivacyPolicy, + dispatchActionPromise, + sendAcknowledgmentRequest, + setAcknowledgmentError, + ); + }, [dispatchActionPromise, sendAcknowledgmentRequest]); + + const styles = useStyles(unboundStyles); + + const buttonContent = React.useMemo(() => { + if (loadingStatus === 'loading') { + return ( + <View style={styles.loading}> + <ActivityIndicator size="small" color="#D3D3D3" /> + </View> + ); + } + return <Text style={styles.buttonText}>I accept</Text>; + }, [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 ( + <Modal + disableClosing={true} + modalStyle={styles.modal} + safeAreaEdges={safeAreaEdges} + > + <Text style={styles.header}>Terms of Service and Privacy Policy</Text> + <Text style={styles.label}> + <Text>We recently updated our </Text> + <Text style={styles.link} onPress={onTermsOfUsePressed}> + Terms of Service + </Text> + <Text> & </Text> + <Text style={styles.link} onPress={onPrivacyPolicyPressed}> + Privacy Policy + </Text> + <Text> + . In order to continue using Comm, we're asking you to read + through, acknowledge, and accept the updated policies. + </Text> + </Text> + + <View style={styles.buttonsContainer}> + <Button style={styles.button} onPress={onAccept}> + <Text style={styles.buttonText}>{buttonContent}</Text> + </Button> + <Text style={styles.error}>{acknowledgmentError}</Text> + </View> + </Modal> + ); +} + +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} /> <Root.Screen name={AppRouteName} component={AppNavigator} /> + <Root.Screen + name={TermsAndPrivacyRouteName} + component={TermsAndPrivacyModal} + options={termsAndPrivacyModalScreenOptions} + /> <Root.Screen name={ThreadPickerModalRouteName} component={ThreadPickerModal} diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -2,6 +2,7 @@ import type { RouteProp } from '@react-navigation/native'; +import type { TermsAndPrivacyModalParams } from '../account/terms-and-privacy-modal.react'; import type { ThreadPickerModalParams } from '../calendar/thread-picker-modal.react'; import type { ComposeSubchannelParams } from '../chat/compose-subchannel.react'; import type { ImagePasteModalParams } from '../chat/image-paste-modal.react'; @@ -67,6 +68,7 @@ 'ThreadSettingsMemberTooltipModal'; export const ThreadSettingsRouteName = 'ThreadSettings'; export const VideoPlaybackModalRouteName = 'VideoPlaybackModal'; +export const TermsAndPrivacyRouteName = 'TermsAndPrivacyModal'; export type RootParamList = { +LoggedOutModal: void, @@ -78,6 +80,7 @@ +ComposeSubchannelModal: ComposeSubchannelModalParams, +SidebarListModal: SidebarListModalParams, +ImagePasteModal: ImagePasteModalParams, + +TermsAndPrivacyModal: TermsAndPrivacyModalParams, }; export type MessageTooltipRouteNames = diff --git a/native/themes/colors.js b/native/themes/colors.js --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -73,6 +73,8 @@ panelIosHighlightUnderlay: '#EEEEEEDD', panelSecondaryForeground: '#F5F5F5', panelSecondaryForegroundBorder: '#D1D1D6', + purpleLink: '#7E57C2', + purpleButton: '#7E57C2', redButton: '#BB8888', redText: '#FF4444', spoiler: '#33332C', @@ -148,6 +150,8 @@ panelIosHighlightUnderlay: '#313035', panelSecondaryForeground: '#333333', panelSecondaryForegroundBorder: '#666666', + purpleLink: '#AE94DB', + purpleButton: '#7E57C2', redButton: '#FF4444', redText: '#FF4444', spoiler: '#33332C',