Skip to content

Commit

Permalink
Prompt app reviews after interaction (#5125)
Browse files Browse the repository at this point in the history
* feat: define review prompt actions

* install expo review module and adjust mmkv schema to add review tracking

* add expo-store-review module

* refactor review alert to be dynamic and accept a range of actions

* remove expo-store-review for now

* add prompt on x launches after install and on watch wallet

* fix: initialize review storage changes

* feat: add ens registration review prompt

* feat: add rest of review prompts

* Update src/screens/WalletConnectApprovalSheet.js

* Update src/utils/reviewAlert.ts

* Update src/utils/reviewAlert.ts

* fix: toggling price dispatch action issue

* Update src/utils/reviewAlert.ts

* chore: remove hiding review in settings

* chore: code review changes

* sign

* chore: code review changes

* change migration
  • Loading branch information
walmat authored Oct 20, 2023
1 parent d24226c commit 0c2ce01
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 68 deletions.
28 changes: 28 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ import { initListeners as initWalletConnectListeners } from '@/walletConnect';
import { saveFCMToken } from '@/notifications/tokens';
import branch from 'react-native-branch';
import { initializeReservoirClient } from '@/resources/reservoir/client';
import { ReviewPromptAction } from '@/storage/schema';
import { handleReviewPromptAction } from '@/utils/reviewAlert';

if (__DEV__) {
reactNativeDisableYellowBox && LogBox.ignoreAllLogs();
Expand Down Expand Up @@ -168,6 +170,17 @@ class OldApp extends Component {
* Needs to be called AFTER FCM token is loaded
*/
initWalletConnectListeners();

/**
* Launch the review prompt after the app is launched
* This is to avoid the review prompt showing up when the app is
* launched and not shown yet.
*/
InteractionManager.runAfterInteractions(() => {
setTimeout(() => {
handleReviewPromptAction(ReviewPromptAction.TimesLaunchedSinceInstall);
}, 10_000);
});
}

componentDidUpdate(prevProps) {
Expand Down Expand Up @@ -332,6 +345,21 @@ function Root() {
*/
analyticsV2.identify({});

const isReviewInitialized = ls.review.get(['initialized']);
if (!isReviewInitialized) {
ls.review.set(['hasReviewed'], false);
ls.review.set(
['actions'],
Object.values(ReviewPromptAction).map(action => ({
id: action,
numOfTimesDispatched: 0,
}))
);

ls.review.set(['timeOfLastPrompt'], 0);
ls.review.set(['initialized'], true);
}

/**
* We previously relied on the existence of a deviceId on keychain to
* determine if a user was new or not. For backwards compat, we do this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { ethereumUtils } from '@/utils';
import { useNFTListing } from '@/resources/nfts';
import { UniqueAsset } from '@/entities';
import { fetchReservoirNFTFloorPrice } from '@/resources/nfts/utils';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';

const NONE = 'None';

Expand All @@ -30,6 +32,7 @@ export default function NFTBriefTokenInfoRow({
}: {
asset: UniqueAsset;
}) {
const [hasDispatchedAction, setHasDispatchedAction] = useState(false);
const { colors } = useTheme();

const { navigate } = useNavigation();
Expand Down Expand Up @@ -73,10 +76,18 @@ export default function NFTBriefTokenInfoRow({
);

const [showFloorInEth, setShowFloorInEth] = useState(true);
const toggleFloorDisplayCurrency = useCallback(
() => setShowFloorInEth(!showFloorInEth),
[showFloorInEth, setShowFloorInEth]
);
const toggleFloorDisplayCurrency = useCallback(() => {
if (!hasDispatchedAction) {
handleReviewPromptAction(ReviewPromptAction.NftFloorPriceVisit);
setHasDispatchedAction(true);
}
setShowFloorInEth(!showFloorInEth);
}, [
showFloorInEth,
setShowFloorInEth,
hasDispatchedAction,
setHasDispatchedAction,
]);

const handlePressCollectionFloor = useCallback(() => {
navigate(Routes.EXPLAIN_SHEET, {
Expand Down
8 changes: 8 additions & 0 deletions src/hooks/useImportingWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { sanitizeSeedPhrase } from '@/utils';
import logger from '@/utils/logger';
import { deriveAccountFromWalletInput } from '@/utils/wallet';
import { logger as Logger, RainbowError } from '@/logger';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';

export default function useImportingWallet({ showImportModal = true } = {}) {
const { accountAddress } = useAccountSettings();
Expand Down Expand Up @@ -327,6 +329,12 @@ export default function useImportingWallet({ showImportModal = true } = {}) {
handleSetImporting(false);
}

setTimeout(() => {
InteractionManager.runAfterInteractions(() => {
handleReviewPromptAction(ReviewPromptAction.WatchWallet);
});
}, 1_000);

setTimeout(() => {
// If it's not read only or hardware, show the backup sheet
if (
Expand Down
33 changes: 18 additions & 15 deletions src/model/migrations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import path from 'path';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { captureException } from '@sentry/react-native';
import { findKey, isNumber, keys } from 'lodash';
import uniq from 'lodash/uniq';
Expand Down Expand Up @@ -59,7 +58,7 @@ import { resolveNameOrAddress } from '@/handlers/web3';
import { returnStringFirstEmoji } from '@/helpers/emojiHandler';
import { updateWebDataEnabled } from '@/redux/showcaseTokens';
import { ethereumUtils, profileUtils } from '@/utils';
import { REVIEW_ASKED_KEY } from '@/utils/reviewAlert';
import { review } from '@/storage';
import logger from '@/utils/logger';
import { queryClient } from '@/react-query';
import { favoritesQueryKey } from '@/resources/favorites';
Expand Down Expand Up @@ -341,7 +340,7 @@ export default async function runMigrations() {
const { selected, wallets } = store.getState().wallets;
if (!wallets) return;
const walletKeys = Object.keys(wallets);
let updatedWallets = { ...wallets };
const updatedWallets = { ...wallets };
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < walletKeys.length; i++) {
const wallet = wallets[walletKeys[i]];
Expand Down Expand Up @@ -376,7 +375,7 @@ export default async function runMigrations() {

// migrate contacts to new color index
const contacts = await getContacts();
let updatedContacts = { ...contacts };
const updatedContacts = { ...contacts };
if (!contacts) return;
const contactKeys = Object.keys(contacts);
// eslint-disable-next-line @typescript-eslint/prefer-for-of
Expand Down Expand Up @@ -412,7 +411,7 @@ export default async function runMigrations() {
try {
// migrate contacts to corresponding emoji
const contacts = await getContacts();
let updatedContacts = { ...contacts };
const updatedContacts = { ...contacts };
if (!contacts) return;
const contactKeys = Object.keys(contacts);
// eslint-disable-next-line @typescript-eslint/prefer-for-of
Expand Down Expand Up @@ -459,16 +458,20 @@ export default async function runMigrations() {
*/
const v11 = async () => {
logger.log('Start migration v11');
const reviewAsked = await AsyncStorage.getItem(REVIEW_ASKED_KEY);
const hasReviewed = review.get(['hasReviewed']);
if (hasReviewed) {
return;
}

const reviewAsked = review.get(['timeOfLastPrompt']);
const TWO_WEEKS = 14 * 24 * 60 * 60 * 1000;
const TWO_MONTHS = 2 * 30 * 24 * 60 * 60 * 1000;

if (Number(reviewAsked) > Date.now() - TWO_WEEKS) {
return;
} else {
const twoMonthsAgo = Date.now() - TWO_MONTHS;
AsyncStorage.setItem(REVIEW_ASKED_KEY, twoMonthsAgo.toString());
}

review.set(['timeOfLastPrompt'], Date.now() - TWO_MONTHS);
};

migrations.push(v11);
Expand Down Expand Up @@ -547,8 +550,8 @@ export default async function runMigrations() {
// which look like'signature_0x...'
const { wallets } = store.getState().wallets;
if (Object.keys(wallets!).length > 0) {
for (let wallet of Object.values(wallets!)) {
for (let account of (wallet as RainbowWallet).addresses) {
for (const wallet of Object.values(wallets!)) {
for (const account of (wallet as RainbowWallet).addresses) {
keysToMigrate.push(`signature_${account.address}`);
}
}
Expand Down Expand Up @@ -584,8 +587,8 @@ export default async function runMigrations() {
const { network } = store.getState().settings;
const { wallets } = store.getState().wallets;
if (!wallets) return;
for (let wallet of Object.values(wallets)) {
for (let account of (wallet as RainbowWallet).addresses) {
for (const wallet of Object.values(wallets)) {
for (const account of (wallet as RainbowWallet).addresses) {
const hiddenCoins = await getHiddenCoins(account.address, network);
const pinnedCoins = await getPinnedCoins(account.address, network);

Expand Down Expand Up @@ -647,8 +650,8 @@ export default async function runMigrations() {
const v17 = async () => {
const { wallets } = store.getState().wallets;
if (!wallets) return;
for (let wallet of Object.values(wallets)) {
for (let account of (wallet as RainbowWallet).addresses) {
for (const wallet of Object.values(wallets)) {
for (const account of (wallet as RainbowWallet).addresses) {
const pinnedCoins = JSON.parse(
mmkv.getString('pinned-coins-' + account.address) ?? '[]'
);
Expand Down
6 changes: 6 additions & 0 deletions src/redux/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getContacts, saveContacts } from '@/handlers/localstorage/contacts';
import { Network } from '@/helpers/networkTypes';
import { omitFlatten } from '@/helpers/utilities';
import { AppGetState } from '@/redux/store';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';

// -- Constants --------------------------------------- //
const CONTACTS_UPDATE = 'contacts/CONTACTS_UPDATE';
Expand Down Expand Up @@ -107,6 +109,10 @@ export const contactsAddOrUpdate = (
},
};
saveContacts(updatedContacts);

setTimeout(() => {
handleReviewPromptAction(ReviewPromptAction.AddingContact);
}, 500);
dispatch({
payload: updatedContacts,
type: CONTACTS_UPDATE,
Expand Down
8 changes: 8 additions & 0 deletions src/screens/ENSConfirmRegisterSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import { colors } from '@/styles';
import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';

export const ENSConfirmRegisterSheetHeight = 600;
export const ENSConfirmRenewSheetHeight = 560;
Expand Down Expand Up @@ -173,6 +175,12 @@ export default function ENSConfirmRegisterSheet() {
setTimeout(() => {
navigate(Routes.PROFILE_SCREEN);
}, 100);

setTimeout(() => {
InteractionManager.runAfterInteractions(() => {
handleReviewPromptAction(ReviewPromptAction.EnsNameRegistration);
});
}, 500);
});
}, [goBack, navigate]);

Expand Down
12 changes: 12 additions & 0 deletions src/screens/ExchangeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ import { useTheme } from '@/theme';
import { logger as loggr } from '@/logger';
import { getNetworkObj } from '@/networks';
import Animated from 'react-native-reanimated';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';

export const DEFAULT_SLIPPAGE_BIPS = {
[Network.mainnet]: 100,
Expand Down Expand Up @@ -718,6 +720,15 @@ export default function ExchangeModal({
});
// Tell iOS we finished running a rap (for tracking purposes)
NotificationManager?.postNotification('rapCompleted');

setTimeout(() => {
if (isBridgeSwap) {
handleReviewPromptAction(ReviewPromptAction.BridgeToL2);
} else {
handleReviewPromptAction(ReviewPromptAction.Swap);
}
}, 500);

return true;
} catch (error) {
setIsAuthorizing(false);
Expand All @@ -742,6 +753,7 @@ export default function ExchangeModal({
goBack,
inputAmount,
inputCurrency,
isBridgeSwap,
isCrosschainSwap,
isHardwareWallet,
navigate,
Expand Down
37 changes: 13 additions & 24 deletions src/screens/SettingsSheet/components/SettingsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import lang from 'i18n-js';
import React, { useCallback, useMemo } from 'react';
import { Linking, NativeModules, Share } from 'react-native';
import { Linking, Share } from 'react-native';
import {
ContextMenuButton,
MenuActionConfig,
Expand Down Expand Up @@ -35,14 +34,14 @@ import WalletTypes from '@/helpers/walletTypes';
import { useAccountSettings, useSendFeedback, useWallets } from '@/hooks';
import { Themes, useTheme } from '@/theme';
import { showActionSheetWithOptions } from '@/utils';
import { AppleReviewAddress, REVIEW_DONE_KEY } from '@/utils/reviewAlert';
import {
buildRainbowLearnUrl,
LearnUTMCampaign,
} from '@/utils/buildRainbowUrl';
import { getNetworkObj } from '@/networks';

const { RainbowRequestReview, RNReview } = NativeModules;
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import * as ls from '@/storage';
import { ReviewPromptAction } from '@/storage/schema';

const SettingsExternalURLs = {
rainbowHomepage: 'https://rainbow.me',
Expand Down Expand Up @@ -116,7 +115,6 @@ const SettingsSection = ({
onPressPrivacy,
onPressNotifications,
}: SettingsSectionProps) => {
const isReviewAvailable = false;
const { wallets, isReadOnlyWallet } = useWallets();
const {
language,
Expand All @@ -134,16 +132,9 @@ const SettingsSection = ({
const onPressReview = useCallback(async () => {
if (ios) {
onCloseModal();
RainbowRequestReview.requestReview((handled: boolean) => {
if (!handled) {
AsyncStorage.setItem(REVIEW_DONE_KEY, 'true');
Linking.openURL(AppleReviewAddress);
}
});
} else {
RNReview.show();
}
}, [onCloseModal]);
handleReviewPromptAction(ReviewPromptAction.UserPrompt);
}, []);

const onPressShare = useCallback(() => {
Share.share({
Expand Down Expand Up @@ -438,15 +429,13 @@ const SettingsSection = ({
/>
}
/>
{isReviewAvailable && (
<MenuItem
leftComponent={<MenuItem.TextIcon icon="❤️" isEmoji />}
onPress={onPressReview}
size={52}
testID="review-section"
titleComponent={<MenuItem.Title text={lang.t('settings.review')} />}
/>
)}
<MenuItem
leftComponent={<MenuItem.TextIcon icon="❤️" isEmoji />}
onPress={onPressReview}
size={52}
testID="review-section"
titleComponent={<MenuItem.Title text={lang.t('settings.review')} />}
/>
<MenuItem
leftComponent={<MenuItem.TextIcon icon={ios ? '🚧' : '🐞'} isEmoji />}
onPress={onPressDev}
Expand Down
6 changes: 6 additions & 0 deletions src/screens/WalletConnectApprovalSheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import * as lang from '@/languages';
import { ETH_ADDRESS, ETH_SYMBOL } from '@/references';
import { AssetType } from '@/entities';
import { RainbowNetworks, getNetworkObj } from '@/networks';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';

const LoadingSpinner = styled(android ? Spinner : ActivityIndicator).attrs(
({ theme: { colors } }) => ({
Expand Down Expand Up @@ -354,6 +356,10 @@ export default function WalletConnectApprovalSheet() {
handled.current = true;
goBack();
handleSuccess(true);

setTimeout(() => {
handleReviewPromptAction(ReviewPromptAction.DappConnections);
}, 500);
}, [handleSuccess, goBack]);

const handleCancel = useCallback(() => {
Expand Down
4 changes: 3 additions & 1 deletion src/storage/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MMKV } from 'react-native-mmkv';

import { Account, Device } from '@/storage/schema';
import { Account, Device, Review } from '@/storage/schema';
import { EthereumAddress } from '@/entities';
import { Network } from '@/networks/types';

Expand Down Expand Up @@ -76,3 +76,5 @@ export const device = new Storage<[], Device>({ id: 'global' });
export const account = new Storage<[EthereumAddress, Network], Account>({
id: 'account',
});

export const review = new Storage<[], Review>({ id: 'review' });
Loading

0 comments on commit 0c2ce01

Please sign in to comment.