Skip to content

Commit 5a9d68b

Browse files
committed
Merge branch 'native/4588-spike-store-rating' into 'master'
Store review #4588 See merge request minds/mobile-native!2018
2 parents abfe81d + 24628f2 commit 5a9d68b

File tree

15 files changed

+309
-6
lines changed

15 files changed

+309
-6
lines changed

AppInitManager.ts

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import mindsConfigService from './src/common/services/minds-config.service';
2121
import openUrlService from '~/common/services/open-url.service';
2222
import { hasVariation, updateGrowthBookAttributes } from 'ExperimentsProvider';
2323
import checkTOS from '~/tos/checkTOS';
24+
import { storeRatingService } from 'modules/store-rating';
2425

2526
/**
2627
* App initialization manager
@@ -44,6 +45,8 @@ export class AppInitManager {
4445

4546
openUrlService.init();
4647

48+
storeRatingService.track('appSession');
49+
4750
try {
4851
logService.info('[App] init session');
4952
const token = await sessionService.init();

__tests__/common/services/image-picker.service.js

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jest.mock('react-native', () => ({
2020
},
2121
Platform: {
2222
OS: 'android',
23+
select: obj => 'android',
2324
},
2425
}));
2526

ios/Podfile.lock

+10-4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ PODS:
4040
- ReactCommon/turbomodule/core
4141
- EXSensors (11.4.0):
4242
- ExpoModulesCore
43+
- EXStoreReview (5.3.0):
44+
- ExpoModulesCore
4345
- FBLazyVector (0.69.7)
4446
- FBReactNativeSpec (0.69.7):
4547
- RCT-Folly (= 2021.06.28.00-v2)
@@ -693,6 +695,7 @@ DEPENDENCIES:
693695
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
694696
- ExpoModulesCore (from `../node_modules/expo-modules-core/ios`)
695697
- EXSensors (from `../node_modules/expo-sensors/ios`)
698+
- EXStoreReview (from `../node_modules/expo-store-review/ios`)
696699
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
697700
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
698701
- Flipper (= 0.169.0)
@@ -877,6 +880,8 @@ EXTERNAL SOURCES:
877880
:path: "../node_modules/expo-modules-core/ios"
878881
EXSensors:
879882
:path: "../node_modules/expo-sensors/ios"
883+
EXStoreReview:
884+
:path: "../node_modules/expo-store-review/ios"
880885
FBLazyVector:
881886
:path: "../node_modules/react-native/Libraries/FBLazyVector"
882887
FBReactNativeSpec:
@@ -1054,7 +1059,7 @@ SPEC CHECKSUMS:
10541059
boost: a7c83b31436843459a1961bfd74b96033dc77234
10551060
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
10561061
CodePush: ef496b6fd053012e985e3d4a0ab9f9dbf2739eac
1057-
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
1062+
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
10581063
EXApplication: e418d737a036e788510f2c4ad6c10a7d54d18586
10591064
EXAV: 596506c9bee54ad52f2f3b625cdaeb9d9f2dd6b7
10601065
EXConstants: 7c44785d41d8e959d527d23d29444277a4d1ee73
@@ -1068,6 +1073,7 @@ SPEC CHECKSUMS:
10681073
ExpoLinearGradient: 1a3af07c6dab3c612967a294836df9ae717431df
10691074
ExpoModulesCore: 5a973701f4400d70254bc836305228731c829010
10701075
EXSensors: a34fb0d416eae3ecaaab43ec64dca0854227d3bf
1076+
EXStoreReview: cbb6b2202bb6f831cd3234d9d8b995cec0eb32f2
10711077
FBLazyVector: 6b7f5692909b4300d50e7359cdefbcd09dd30faa
10721078
FBReactNativeSpec: affcf71d996f6b0c01f68883482588297b9d5e6e
10731079
Flipper: 55f10244ce1a82d4adc1cb817bc492f11e22f6ff
@@ -1081,7 +1087,7 @@ SPEC CHECKSUMS:
10811087
FlipperKit: 1868c4faed64da1c779da268417515127dbde450
10821088
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
10831089
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
1084-
glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a
1090+
glog: 476ee3e89abb49e07f822b48323c51c57124b572
10851091
GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f
10861092
GoogleMLKit: 85ffdc9641d05311c76dbba5bbf93059087be12f
10871093
GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34
@@ -1108,7 +1114,7 @@ SPEC CHECKSUMS:
11081114
Permission-PhotoLibrary: 5b34ca67279f7201ae109cef36f9806a6596002d
11091115
PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb
11101116
Protobuf: 02524ec14183fe08fb259741659e79683788158b
1111-
RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a
1117+
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
11121118
RCTRequired: 54bff6aa61efd9598ab59d2a823c382b4fe13d27
11131119
RCTSilentSwitch: bc3867bfe1d9bd19391d15ce0d27f208b6731e52
11141120
RCTSystemSetting: 5107b7350d63b3f7b42a1277d07e4e5d9df879be
@@ -1189,6 +1195,6 @@ SPEC CHECKSUMS:
11891195
Yoga: 0b84a956f7393ef1f37f3bb213c516184e4a689d
11901196
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
11911197

1192-
PODFILE CHECKSUM: cb358b8be853ffb5ace41a3401194ce0721de1b3
1198+
PODFILE CHECKSUM: b3aad2fe2c34bf0342ed93736b65129c71a3197f
11931199

11941200
COCOAPODS: 1.11.3

locales/en.json

+3
Original file line numberDiff line numberDiff line change
@@ -1824,6 +1824,9 @@
18241824
}
18251825
},
18261826
"failedTryAgain": "Sorry, failed to load. please try again",
1827+
"storeRating": {
1828+
"prompt": "Are you enjoying Minds?"
1829+
},
18271830
"codePush": {
18281831
"prompt": {
18291832
"title": "New update available",

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@
7474
"expo-av": "^12.0.4",
7575
"expo-image-manipulator": "^10.4.0",
7676
"expo-linear-gradient": "^11.4.0",
77+
"expo-linking": "^3.2.3",
7778
"expo-sensors": "~11.4.0",
79+
"expo-store-review": "^5.3.0",
7880
"i18n-js": "^3.8.0",
7981
"i18next": "^22.0.2",
8082
"jest-circus": "^27.0.6",

src/comments/v2/CommentsStore.ts

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type GroupModel from '../../groups/GroupModel';
2323
import { showNotification } from '../../../AppMessages';
2424
import i18n from '../../common/services/i18n.service';
2525
import { isNetworkError } from '../../common/services/api.service';
26+
import { storeRatingService } from 'modules/store-rating';
2627

2728
const COMMENTS_PAGE_SIZE = 12;
2829

@@ -423,6 +424,7 @@ export default class CommentsStore {
423424
this.setShowInput(false);
424425
this.embed.clearRichEmbedAction();
425426
this.attachment.clear();
427+
storeRatingService.track('comment', true);
426428

427429
if (this.entity.incrementCommentsCounter) {
428430
this.entity.incrementCommentsCounter();

src/common/BaseModel.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type UserModel from '../channel/UserModel';
1111
import type FeedStore from './stores/FeedStore';
1212
import AbstractModel from './AbstractModel';
1313
import MetadataService from './services/metadata.service';
14+
import { storeRatingService } from 'modules/store-rating';
1415

1516
/**
1617
* Base model
@@ -176,7 +177,7 @@ export default class BaseModel extends AbstractModel {
176177
* @param {string} direction
177178
*/
178179
@action
179-
async toggleVote(direction) {
180+
async toggleVote(direction: 'up' | 'down') {
180181
const voted = direction === 'up' ? this.votedUp : this.votedDown;
181182
const delta = voted ? -1 : 1;
182183

@@ -202,6 +203,11 @@ export default class BaseModel extends AbstractModel {
202203

203204
try {
204205
await vote(this.guid, direction, params);
206+
if (direction === 'up') {
207+
storeRatingService.track('upvote', true);
208+
} else {
209+
storeRatingService.track('downvote');
210+
}
205211
} catch (err) {
206212
if (!voted) {
207213
this['thumbs:' + direction + ':user_guids'] = guids.filter(function (

src/compose/createComposeStore.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import NavigationService from '../navigation/NavigationService';
2121
import MultiAttachmentStore from '~/common/stores/MultiAttachmentStore';
2222
import SupermindRequestModel from '../supermind/SupermindRequestModel';
2323
import { confirm } from '../common/components/Confirm';
24+
import { storeRatingService } from 'modules/store-rating';
2425

2526
/**
2627
* Display an error message to the user.
@@ -626,6 +627,8 @@ export default function (props) {
626627
return this.entity;
627628
}
628629

630+
storeRatingService.track('createPost', true);
631+
629632
if (this.supermindRequest) {
630633
showNotification(i18n.t('supermind.requestSubmitted'), 'success');
631634
}

src/config/Config.ts

+28
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,31 @@ export const CAPTCHA_ENABLED_ENDPOINTS = [
187187
origin: 'vote_up',
188188
},
189189
];
190+
191+
/**
192+
* used to measure the user's interaction with the app
193+
*/
194+
export const USAGE_SCORES = {
195+
viewPost: 0.1,
196+
appSession: 1,
197+
upvote: 2,
198+
downvote: -1,
199+
remind: 3,
200+
createPost: 5,
201+
comment: 5,
202+
};
203+
204+
/**
205+
* if user's usage score increased beyond this threshold, they will
206+
* get prompted to rate the app
207+
*/
208+
export const RATING_APP_SCORE_THRESHOLD = 20;
209+
210+
const APP_STORE_LINK =
211+
'itms-apps://apps.apple.com/app/id961771928?action=write-review';
212+
const PLAY_STORE_LINK = 'market://details?id=com.minds.mobile';
213+
214+
export const STORE_LINK = Platform.select({
215+
ios: APP_STORE_LINK,
216+
android: PLAY_STORE_LINK,
217+
}) as string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react';
2+
import { SafeAreaView } from 'react-native-safe-area-context';
3+
import {
4+
BottomSheetButton,
5+
pushBottomSheet,
6+
} from '../../../common/components/bottom-sheet';
7+
import i18nService from '../../../common/services/i18n.service';
8+
import { Spacer, H2 } from '../../../common/ui';
9+
import ThemedStyles from '../../../styles/ThemedStyles';
10+
11+
interface RateAppProps {
12+
onConfirm: () => void;
13+
onCancel: () => void;
14+
onLayout?: ({
15+
nativeEvent: {
16+
layout: { height },
17+
},
18+
}: any) => void;
19+
}
20+
21+
export default function RateApp({
22+
onConfirm,
23+
onCancel,
24+
onLayout,
25+
}: RateAppProps) {
26+
return (
27+
<SafeAreaView
28+
edges={['bottom']}
29+
style={ThemedStyles.style.flexContainer}
30+
onLayout={onLayout}>
31+
<Spacer horizontal="L" bottom="S">
32+
<H2 align="center" bottom="L">
33+
{i18nService.t('storeRating.prompt')}
34+
</H2>
35+
</Spacer>
36+
<BottomSheetButton
37+
action
38+
solid
39+
text={i18nService.t('yes')}
40+
onPress={onConfirm}
41+
/>
42+
<BottomSheetButton text={i18nService.t('no')} onPress={onCancel} />
43+
<Spacer bottom="L" />
44+
</SafeAreaView>
45+
);
46+
}
47+
48+
export const rateApp = (
49+
props?: Omit<RateAppProps, 'onConfirm' | 'onCancel'>,
50+
): Promise<boolean | null> => {
51+
return new Promise(resolve =>
52+
pushBottomSheet({
53+
component: (bottomSheetRef, handleContentLayout) => (
54+
<RateApp
55+
{...props}
56+
onLayout={handleContentLayout}
57+
onConfirm={() => {
58+
resolve(true);
59+
bottomSheetRef.close();
60+
}}
61+
onCancel={() => {
62+
resolve(false);
63+
bottomSheetRef.close();
64+
}}
65+
/>
66+
),
67+
onClose: () => resolve(null),
68+
}),
69+
);
70+
};

src/modules/store-rating/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import storeRatingService from './store-rating.service';
2+
3+
export { storeRatingService };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as StoreReview from 'expo-store-review';
2+
import debounce from 'lodash/debounce';
3+
import moment from 'moment';
4+
import { InteractionManager, Linking } from 'react-native';
5+
import openUrlService from '../../common/services/open-url.service';
6+
import { storages } from '../../common/services/storage/storages.service';
7+
import {
8+
RATING_APP_SCORE_THRESHOLD,
9+
STORE_LINK,
10+
USAGE_SCORES,
11+
} from '../../config/Config';
12+
import { rateApp } from './components/RateApp';
13+
14+
const SCORES_KEY = 'APP_SCORES';
15+
const LAST_PROMPTED_AT_KEY = 'STORE_RATING_LAST_PROMPTED_AT';
16+
17+
class StoreRatingService {
18+
lastPromptedAt: number | null =
19+
storages.app.getInt(LAST_PROMPTED_AT_KEY) || null;
20+
points = storages.app.getInt(SCORES_KEY) || 0;
21+
22+
constructor() {
23+
this.debouncedSetStorage = debounce(this.debouncedSetStorage, 2000);
24+
}
25+
26+
track(key: keyof typeof USAGE_SCORES, prompt = false) {
27+
this.points += USAGE_SCORES[key];
28+
29+
if (prompt && this.shouldPrompt) {
30+
return setTimeout(() => {
31+
InteractionManager.runAfterInteractions(() => {
32+
this.prompt();
33+
});
34+
}, 500);
35+
}
36+
37+
this.debouncedSetStorage(SCORES_KEY, this.points);
38+
}
39+
40+
get shouldPrompt() {
41+
if (
42+
this.lastPromptedAt &&
43+
moment().isBefore(moment(this.lastPromptedAt).add({ days: 120 }))
44+
) {
45+
return false;
46+
}
47+
48+
return this.points > RATING_APP_SCORE_THRESHOLD;
49+
}
50+
51+
async redirectToStore() {
52+
Linking.openURL(STORE_LINK);
53+
}
54+
55+
async openFeedbackForm() {
56+
openUrlService.openLinkInInAppBrowser(
57+
'https://mindsdotcom.typeform.com/app-feedback',
58+
);
59+
}
60+
61+
async prompt() {
62+
const rated = await rateApp();
63+
this.lastPromptedAt = Date.now();
64+
storages.app.setIntAsync(LAST_PROMPTED_AT_KEY, this.lastPromptedAt);
65+
66+
if (rated === null) {
67+
return null;
68+
}
69+
70+
if (rated) {
71+
this.redirectToStore();
72+
} else if (rated === false) {
73+
this.openFeedbackForm();
74+
}
75+
}
76+
77+
async promptNatively() {
78+
if (!(await StoreReview.hasAction())) {
79+
return false;
80+
}
81+
82+
if (!(await StoreReview.isAvailableAsync())) {
83+
return false;
84+
}
85+
86+
await StoreReview.requestReview();
87+
this.lastPromptedAt = Date.now();
88+
storages.app.setIntAsync(LAST_PROMPTED_AT_KEY, this.lastPromptedAt);
89+
}
90+
91+
debouncedSetStorage(key: string, value: any) {
92+
storages.app.setIntAsync(key, value);
93+
}
94+
}
95+
96+
const storeRatingService = new StoreRatingService();
97+
98+
export default storeRatingService;

src/newsfeed/NewsfeedService.ts

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type ActivityModel from './ActivityModel';
99
import type BlogModel from '../blogs/BlogModel';
1010
import UserModel from '../channel/UserModel';
1111
import GroupModel from '../groups/GroupModel';
12+
import { storeRatingService } from 'modules/store-rating';
1213

1314
export default class NewsfeedService {
1415
async _getFeed(endpoint, offset, limit) {
@@ -130,6 +131,9 @@ export async function setViewed(
130131
extra,
131132
);
132133
}
134+
135+
storeRatingService.track('viewPost');
136+
133137
return data;
134138
}
135139

0 commit comments

Comments
 (0)