diff --git a/README.md b/README.md index f399817d..c55fa7c9 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Readme will guide you on how to config. | Custom Fields on Signup | ✅ | | Report message | ✅ | | Theming | ✅ | -| Settings -> Review the App | ❌ | +| Settings -> Review the App | ✅ | | Settings -> Default Browser | ❌ | | Admin panel | ✅ | | Reply message from notification | ✅ | diff --git a/app/constants/links.js b/app/constants/links.js index 15c045a7..933f89da 100644 --- a/app/constants/links.js +++ b/app/constants/links.js @@ -1,3 +1,8 @@ -export const PLAY_MARKET_LINK = 'https://play.google.com/store/apps/details?id=chat.rocket.reactnative'; -export const APP_STORE_LINK = 'https://itunes.apple.com/app/rocket-chat-experimental/id1272915472?ls=1&mt=8'; +import { getBundleId, isIOS } from '../utils/deviceInfo'; + +const APP_STORE_ID = '1272915472'; + +export const PLAY_MARKET_LINK = `market://details?id=${ getBundleId }`; +export const APP_STORE_LINK = `itms-apps://itunes.apple.com/app/id${ APP_STORE_ID }`; export const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE'; +export const STORE_REVIEW_LINK = isIOS ? `${ APP_STORE_LINK }?action=write-review` : PLAY_MARKET_LINK; diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index d5f9e693..23ce62be 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -42,6 +42,7 @@ import { MENTIONS_TRACKING_TYPE_USERS } from './constants'; import CommandsPreview from './CommandsPreview'; +import { Review } from '../../utils/review'; const imagePickerConfig = { cropping: true, @@ -506,6 +507,7 @@ class MessageBox extends Component { }; try { await RocketChat.sendFileMessage(rid, fileInfo, tmid, server, user); + Review.pushPositiveEvent(); } catch (e) { log(e); } diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 97b54653..ef8b2565 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -332,6 +332,13 @@ export default { Reset_password: 'Reset password', resetting_password: 'resetting password', RESET: 'RESET', + Review_app_title: 'Are you enjoying this app?', + Review_app_desc: 'Give us 5 stars on {{store}}', + Review_app_yes: 'Sure!', + Review_app_no: 'No', + Review_app_later: 'Maybe later', + Review_app_unable_store: 'Unable to open {{store}}', + Review_this_app: 'Review this app', Roles: 'Roles', Room_actions: 'Room actions', Room_changed_announcement: 'Room announcement changed to: {{announcement}} by {{userBy}}', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 9087ed04..f438ca0c 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -302,6 +302,13 @@ export default { Reset_password: 'Resetar senha', resetting_password: 'redefinindo senha', RESET: 'RESETAR', + Review_app_title: 'Você está gostando do app?', + Review_app_desc: 'Nos dê 5 estrelas na {{store}}', + Review_app_yes: 'Claro!', + Review_app_no: 'Não', + Review_app_later: 'Talvez depois', + Review_app_unable_store: 'Não foi possível abrir {{store}}', + Review_this_app: 'Avaliar esse app', Roles: 'Papéis', Room_actions: 'Ações', Room_changed_announcement: 'O anúncio da sala foi alterado para: {{announcement}} por {{userBy}}', diff --git a/app/utils/review.js b/app/utils/review.js new file mode 100644 index 00000000..7e6da9d1 --- /dev/null +++ b/app/utils/review.js @@ -0,0 +1,96 @@ +import { Alert, Linking, AsyncStorage } from 'react-native'; + +import { isIOS } from './deviceInfo'; +import I18n from '../i18n'; +import { showErrorAlert } from './info'; +import { STORE_REVIEW_LINK } from '../constants/links'; + +const store = isIOS ? 'App Store' : 'Play Store'; + +const reviewKey = 'reviewKey'; +const reviewDelay = 2000; +const numberOfDays = 7; +const numberOfPositiveEvent = 5; + +const daysBetween = (date1, date2) => { + const one_day = 1000 * 60 * 60 * 24; + const date1_ms = date1.getTime(); + const date2_ms = date2.getTime(); + const difference_ms = date2_ms - date1_ms; + return Math.round(difference_ms / one_day); +}; + +const onCancelPress = () => { + try { + const data = JSON.stringify({ doneReview: true }); + return AsyncStorage.setItem(reviewKey, data); + } catch (e) { + // do nothing + } +}; + +export const onReviewPress = async() => { + await onCancelPress(); + try { + const supported = await Linking.canOpenURL(STORE_REVIEW_LINK); + if (supported) { + Linking.openURL(STORE_REVIEW_LINK); + } + } catch (e) { + showErrorAlert(I18n.t('Review_app_unable_store', { store })); + } +}; + +const onAskMeLaterPress = () => { + try { + const data = JSON.stringify({ lastReview: new Date().getTime() }); + return AsyncStorage.setItem(reviewKey, data); + } catch (e) { + // do nothing + } +}; + +const onReviewButton = { text: I18n.t('Review_app_yes'), onPress: onReviewPress }; +const onAskMeLaterButton = { text: I18n.t('Review_app_later'), onPress: onAskMeLaterPress }; +const onCancelButton = { text: I18n.t('Review_app_no'), onPress: onCancelPress }; + +const askReview = () => Alert.alert( + I18n.t('Review_app_title'), + I18n.t('Review_app_desc', { store }), + isIOS + ? [onReviewButton, onAskMeLaterButton, onCancelButton] + : [onAskMeLaterButton, onCancelButton, onReviewButton], + { + cancelable: true, + onDismiss: onAskMeLaterPress + } +); + +const tryReview = async() => { + const data = await AsyncStorage.getItem(reviewKey) || '{}'; + const reviewData = JSON.parse(data); + const { lastReview = 0, doneReview = false } = reviewData; + const lastReviewDate = new Date(lastReview); + + // if ask me later was pressed earlier, we can ask for review only after {{numberOfDays}} days + // if there's no review and it wasn't dismissed by the user + if (daysBetween(lastReviewDate, new Date()) >= numberOfDays && !doneReview) { + setTimeout(askReview, reviewDelay); + } +}; + +class ReviewApp { + positiveEventCount = 0; + + pushPositiveEvent = () => { + if (this.positiveEventCount >= numberOfPositiveEvent) { + return; + } + this.positiveEventCount += 1; + if (this.positiveEventCount === numberOfPositiveEvent) { + tryReview(); + } + } +} + +export const Review = new ReviewApp(); diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index e05b9a81..3b19fb9e 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -22,6 +22,7 @@ import StatusBar from '../containers/StatusBar'; import { SWITCH_TRACK_COLOR, themes } from '../constants/colors'; import { withTheme } from '../theme'; import { themedHeader } from '../utils/navigation'; +import { Review } from '../utils/review'; const styles = StyleSheet.create({ container: { @@ -201,6 +202,8 @@ class CreateChannelView extends React.Component { create({ name: channelName, users, type, readOnly, broadcast }); + + Review.pushPositiveEvent(); } removeUser = (user) => { diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 5d038b33..713da66d 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -47,6 +47,7 @@ import { handleCommandReplyLatest } from '../../commands'; import ModalNavigation from '../../lib/ModalNavigation'; +import { Review } from '../../utils/review'; const stateAttrsUpdate = [ 'joined', @@ -472,6 +473,7 @@ class RoomView extends React.Component { try { await RocketChat.setReaction(shortname, messageId); this.onReactionClose(); + Review.pushPositiveEvent(); } catch (e) { log(e); } @@ -556,6 +558,7 @@ class RoomView extends React.Component { this.list.current.update(); } this.setLastOpen(null); + Review.pushPositiveEvent(); }); }; diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js index e6601737..93c41f50 100644 --- a/app/views/SettingsView/index.js +++ b/app/views/SettingsView/index.js @@ -33,6 +33,7 @@ import { withSplit } from '../../split'; import Navigation from '../../lib/Navigation'; import { LISTENER } from '../../containers/Toast'; import EventEmitter from '../../utils/events'; +import { onReviewPress } from '../../utils/review'; const SectionSeparator = React.memo(({ theme }) => ( + +