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 }) => (
+
+