Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [IOPLT-784] Request App feedback to user #6617

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
19a72c8
Add the new alert prompt to get user feedback or store review
CrisTofani Jan 20, 2025
db568e0
[IOPLT-784] Request App feedback to user
CrisTofani Jan 20, 2025
b9c9628
fix
CrisTofani Jan 20, 2025
c314d70
AppReview new feature
CrisTofani Feb 5, 2025
3665266
Merge remote-tracking branch 'origin/master' into IOPLT-784
CrisTofani Feb 5, 2025
ee0f33f
Shape feature
CrisTofani Feb 5, 2025
eda8d96
missing files
CrisTofani Feb 5, 2025
dc26358
Merge branch 'master' into IOPLT-784
CrisTofani Feb 5, 2025
f1f43dd
wip
CrisTofani Feb 5, 2025
c7946a2
missing action and reducer
CrisTofani Feb 5, 2025
6c43a15
Complete implementation
CrisTofani Feb 6, 2025
51be4e8
missing files
CrisTofani Feb 6, 2025
26a12b2
fixes snaps
CrisTofani Feb 7, 2025
1f5fde8
Adds the Playground to test the alet presentation logic
CrisTofani Feb 7, 2025
df251f0
missing files
CrisTofani Feb 7, 2025
69ffdbc
fix remote config verification
CrisTofani Feb 7, 2025
0a081e4
Merge branch 'master' into IOPLT-784
CrisTofani Feb 7, 2025
5499a00
Merge branch 'master' into IOPLT-784
CrisTofani Feb 7, 2025
a93bd9c
fix logic and add simple tests on month checker utility function
CrisTofani Feb 7, 2025
0c82160
improvements
CrisTofani Feb 7, 2025
422b10b
copy fixes
CrisTofani Feb 10, 2025
686ebf9
Copy fixes
thisisjp Feb 10, 2025
d4a6249
adds minor comment
CrisTofani Feb 10, 2025
940e021
Merge branch 'master' into IOPLT-784
CrisTofani Feb 10, 2025
b0e92fa
fix services-metadata version
CrisTofani Feb 10, 2025
05f6aad
Merge branch 'master' into IOPLT-784
CrisTofani Feb 10, 2025
8c0c407
Merge branch 'master' into IOPLT-784
CrisTofani Feb 11, 2025
144fc15
fix actions/cache
CrisTofani Feb 11, 2025
f5f61db
fix actions/cache
Feb 11, 2025
2d3c2f3
fix actions/cache
CrisTofani Feb 11, 2025
24b55f2
Merge branch 'master' into IOPLT-784
CrisTofani Feb 12, 2025
379ce2c
Merge branch 'master' into IOPLT-784
CrisTofani Feb 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ global:
noResultsTitle: No results.
tablet:
message: The IO interface is optimized for smartphones, but all the functions compatible with your device are still available.
appFeedback:
alert:
title: "Do you like IO app?"
description: "We'll use your answer to get better"
continue: "Yes, I like it"
discard: "I don't like it"
rooted:
title: Looks like the security of your device has been compromised!
body: If you press 'Continue', you agree to continue browsing at your own risk, otherwise close the app.
Expand Down
6 changes: 6 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ global:
noResultsTitle: Nessun risultato.
tablet:
message: L’interfaccia di IO è ottimizzata per smartphone, ma tutte le funzioni compatibili con il tuo dispositivo sono comunque disponibili.
appFeedback:
alert:
title: "Ti piace l'app IO?"
description: "Useremo la tua risposta per migliorare"
continue: "Sì, mi piace"
discard: "Non mi piace"
rooted:
title: Sembra che la sicurezza del tuo dispositivo sia compromessa!
body: Se premi su ‘Continua’, accetti di proseguire a tuo rischio nella navigazione, altrimenti chiudi l’app.
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-api-models.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

IO_BACKEND_VERSION=v16.7.4-RELEASE
# need to change after merge on io-services-metadata
IO_SERVICES_METADATA_VERSION=1.0.57
IO_SERVICES_METADATA_VERSION=IOPLT-927-store-review-remote-config

declare -a apis=(
# Backend APIs
Expand Down
57 changes: 57 additions & 0 deletions ts/features/appReviews/hooks/useAppReviewRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Alert } from "react-native";
import { openAuthenticationSession } from "@pagopa/io-react-native-login-utils";
import { useIODispatch, useIOSelector } from "../../../store/hooks";
import {
appFeedbackEnabledSelector,
appFeedbackUriConfigSelector
} from "../../../store/reducers/backendStatus/remoteConfig";
import { requestAppReview } from "../utils/storeReview";
import I18n from "../../../i18n";
import { canAskFeedbackSelector } from "../store/selectors";
import {
appReviewNegativeFeedback,
appReviewPositiveFeedback,
TopicKeys
} from "../store/actions";

export const useAppReviewRequest = (topic: TopicKeys = "general") => {
const dispatch = useIODispatch();

const appFeedbackEnabled = useIOSelector(appFeedbackEnabledSelector);
const canAskFeedback = useIOSelector(canAskFeedbackSelector(topic));
const surveyUrl = useIOSelector(appFeedbackUriConfigSelector(topic));

return () => {
if (!canAskFeedback) {
return;
}
if (appFeedbackEnabled) {
Alert.alert(
I18n.t("appFeedback.alert.title"),
I18n.t("appFeedback.alert.description"),
[
{
text: I18n.t("appFeedback.alert.discard"),
style: "cancel",
onPress: () => {
if (surveyUrl) {
dispatch(appReviewNegativeFeedback(topic));
void openAuthenticationSession(surveyUrl, "");
}
}
},
{
text: I18n.t("appFeedback.alert.continue"),
style: "default",
onPress: () => {
requestAppReview();
dispatch(appReviewPositiveFeedback());
}
}
]
);
return;
}
requestAppReview();
};
};
22 changes: 22 additions & 0 deletions ts/features/appReviews/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ActionType, createStandardAction } from "typesafe-actions";
import { AppFeedbackUri } from "../../../../../definitions/content/AppFeedbackUri";

export type TopicKeys = keyof AppFeedbackUri;

export const appReviewPositiveFeedback = createStandardAction(
"APP_REVIEW_POSITIVE_FEEDBACK"
)();

export const appReviewNegativeFeedback = createStandardAction(
"APP_REVIEW_NEGATIVE_FEEDBACK"
)<TopicKeys>();

// Reset feedback data just for testing purposes
export const clearFeedbackDatas = createStandardAction(
"APP_REVIEW_RESET_FEEDBACK"
)();

export type AppFeedbackActions =
| ActionType<typeof appReviewPositiveFeedback>
| ActionType<typeof appReviewNegativeFeedback>
| ActionType<typeof clearFeedbackDatas>;
60 changes: 60 additions & 0 deletions ts/features/appReviews/store/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { getType } from "typesafe-actions";
import { PersistConfig, persistReducer } from "redux-persist";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Action } from "../../../../store/actions/types";
import {
appReviewNegativeFeedback,
appReviewPositiveFeedback,
clearFeedbackDatas,
TopicKeys
} from "../actions";

export type AppFeedbackState = {
positiveFeedbackDate: string;
negativeFeedbackDate:
| Record<TopicKeys, string | undefined>
| Record<string, never>;
};

export const appFeedbackInitialState: AppFeedbackState = {
positiveFeedbackDate: "",
negativeFeedbackDate: {}
};

const appFeedbackReducer = (
state: AppFeedbackState = appFeedbackInitialState,
action: Action
): AppFeedbackState => {
switch (action.type) {
case getType(clearFeedbackDatas):
return {
...state,
...appFeedbackInitialState
};
case getType(appReviewPositiveFeedback):
return {
...state,
positiveFeedbackDate: new Date().toISOString()
};
case getType(appReviewNegativeFeedback):
return {
...state,
negativeFeedbackDate: {
...state.negativeFeedbackDate,
[action.payload]: new Date().toISOString()
} as Record<TopicKeys, string | undefined>
};
default:
return state;
}
};

const persistConfig: PersistConfig = {
key: "appFeedback",
storage: AsyncStorage
};

export const appFeedbackPersistor = persistReducer<AppFeedbackState, Action>(
persistConfig,
appFeedbackReducer
);
25 changes: 25 additions & 0 deletions ts/features/appReviews/store/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createSelector } from "reselect";
import { GlobalState } from "../../../../store/reducers/types";
import { TopicKeys } from "../actions";
import { checkFourMonthPeriod } from "../../utils/date";

export const appFeedbackSelector = (state: GlobalState) =>
state.features.appFeedback;

export const appReviewPositiveFeedbackLogSelector = (state: GlobalState) =>
state.features.appFeedback.positiveFeedbackDate;

export const appReviewNegativeFeedbackLogSelector =
(topic: TopicKeys) => (state: GlobalState) =>
state.features.appFeedback.negativeFeedbackDate[topic];

export const canAskFeedbackSelector = (topic: TopicKeys = "general") =>
createSelector(
[
appReviewPositiveFeedbackLogSelector,
appReviewNegativeFeedbackLogSelector(topic)
],
(positiveFeedbackDate, negativeFeedbackDate) =>
checkFourMonthPeriod(positiveFeedbackDate) &&
checkFourMonthPeriod(negativeFeedbackDate)
);
24 changes: 24 additions & 0 deletions ts/features/appReviews/utils/__tests__/date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import MockDate from "mockdate";
import { checkFourMonthPeriod } from "../date";

describe("Date utils for app review checks", () => {
it("should return true if the date is not provided", () => {
const result = checkFourMonthPeriod();
expect(result).toBe(true);
});

it("should return true if the date is older than four months", () => {
const date = "2024-09-14T20:43:21.361Z";
const result = checkFourMonthPeriod(date);
expect(result).toBe(true);
});

it("should return false if the date is not older than four months", () => {
const date = "2024-11-14T20:43:21.361Z";
const now = "2025-03-14T20:43:21.361Z";
MockDate.set(now);
const result = checkFourMonthPeriod(date);
expect(result).toBe(false);
MockDate.reset();
});
});
11 changes: 11 additions & 0 deletions ts/features/appReviews/utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { addMonths, isPast } from "date-fns";

export const checkFourMonthPeriod = (date?: string) => {
if (!date) {
return true;
}
const logDate = new Date(date);
const expirationDate = addMonths(logDate, 4);

return !isNaN(logDate.getTime()) && isPast(expirationDate);
};
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

exports[`featuresPersistor should match snapshot 1`] = `
{
"appFeedback": {
"negativeFeedbackDate": {},
"positiveFeedbackDate": "",
},
"fci": {
"documentPreview": {
"kind": "PotNone",
Expand Down
8 changes: 7 additions & 1 deletion ts/features/common/store/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ import {
import { GlobalState } from "../../../../store/reducers/types";
import { isIOMarkdownDisabledForMessagesAndServices } from "../../../../store/reducers/backendStatus/remoteConfig";
import { isIOMarkdownEnabledLocallySelector } from "../../../../store/reducers/persistedPreferences";
import {
appFeedbackPersistor,
AppFeedbackState
} from "../../../appReviews/store/reducers";

type LoginFeaturesState = {
testLogin: TestLoginState;
Expand All @@ -88,6 +92,7 @@ export type FeaturesState = {
mixpanel: MixpanelState;
ingress: IngressScreenState;
landingBanners: LandingScreenBannerState;
appFeedback: AppFeedbackState & PersistPartial;
};

export type PersistedFeaturesState = FeaturesState & PersistPartial;
Expand All @@ -113,7 +118,8 @@ const rootReducer = combineReducers<FeaturesState, Action>({
profileSettings: profileSettingsReducerPersistor,
mixpanel: mixpanelReducer,
ingress: ingressScreenReducer,
landingBanners: landingScreenBannersReducer
landingBanners: landingScreenBannersReducer,
appFeedback: appFeedbackPersistor
});

const CURRENT_REDUX_FEATURES_STORE_VERSION = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ import { useIsFocused } from "@react-navigation/native";
import { useDispatch, useSelector } from "react-redux";
import { itwIsPendingReviewSelector } from "../store/selectors/preferences";
import { itwSetReviewPending } from "../store/actions/preferences";
import { requestAppReview } from "../../../../utils/storeReview";
import { useAppReviewRequest } from "../../../appReviews/hooks/useAppReviewRequest";

/**
* Hook to monitor isPendingReview state and request an app review if needed.
* If isPendingReview is true, request an app review and then set isPendingReview to false.
*/
export const useItwPendingReviewRequest = () => {
const requestFeedback = useAppReviewRequest("itw");
const isPendingReview = useSelector(itwIsPendingReviewSelector);
const dispatch = useDispatch();
const isFocused = useIsFocused();

useEffect(() => {
if (isPendingReview && isFocused) {
requestAppReview();
requestFeedback();
dispatch(itwSetReviewPending(false));
}
}, [isPendingReview, dispatch, isFocused]);
}, [isPendingReview, dispatch, isFocused, requestFeedback]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel";
import { getPaymentsLatestReceiptAction } from "../../receipts/store/actions";
import { usePaymentReversedInfoBottomSheet } from "../hooks/usePaymentReversedInfoBottomSheet";
import { WalletPaymentStepEnum } from "../types";
import { requestAppReview } from "../../../../utils/storeReview";
import { useAppReviewRequest } from "../../../appReviews/hooks/useAppReviewRequest";

type WalletPaymentOutcomeScreenNavigationParams = {
outcome: WalletPaymentOutcome;
Expand All @@ -57,7 +57,7 @@ type WalletPaymentOutcomeRouteProps = RouteProp<

const WalletPaymentOutcomeScreen = () => {
useAvoidHardwareBackButton();

const requestFeedback = useAppReviewRequest("payments");
const dispatch = useIODispatch();
const { params } = useRoute<WalletPaymentOutcomeRouteProps>();
const { outcome } = params;
Expand Down Expand Up @@ -148,7 +148,7 @@ const WalletPaymentOutcomeScreen = () => {
};

const handleSuccessClose = () => {
requestAppReview();
requestFeedback();
handleClose();
};

Expand Down
5 changes: 5 additions & 0 deletions ts/navigation/ProfileNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { isGestureEnabled } from "../utils/navigation";
import TrialSystemPlayground from "../screens/profile/TrialSystemPlayground";
import ProfileMainScreen from "../screens/profile/ProfileMainScreen";
import { IOMarkdownPlayground } from "../screens/profile/playgrounds/IOMarkdownPlayground";
import { AppFeedbackPlayground } from "../screens/profile/playgrounds/AppFeedbackPlayground";
import { ProfileParamsList } from "./params/ProfileParamsList";
import ROUTES from "./routes";

Expand Down Expand Up @@ -113,6 +114,10 @@ const ProfileStackNavigator = () => (
name={ROUTES.IO_MARKDOWN_PLAYGROUND}
component={IOMarkdownPlayground}
/>
<Stack.Screen
name={ROUTES.APP_FEEDBACK_PLAYGROUND}
component={AppFeedbackPlayground}
/>
<Stack.Screen
options={{
headerShown: false
Expand Down
1 change: 1 addition & 0 deletions ts/navigation/params/ProfileParamsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ export type ProfileParamsList = {
[ROUTES.IDPAY_ONBOARDING_PLAYGROUND]: undefined;
[ROUTES.IDPAY_CODE_PLAYGROUND]: undefined;
[ROUTES.IO_MARKDOWN_PLAYGROUND]: undefined;
[ROUTES.APP_FEEDBACK_PLAYGROUND]: undefined;
[ROUTES.SETTINGS_MAIN]: undefined;
};
1 change: 1 addition & 0 deletions ts/navigation/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ const ROUTES = {
LOLLIPOP_PLAYGROUND: "LOLLIPOP_PLAYGROUND",
IDPAY_CODE_PLAYGROUND: "IDPAY_CODE_PLAYGROUND",
IO_MARKDOWN_PLAYGROUND: "IO_MARKDOWN_PLAYGROUND",
APP_FEEDBACK_PLAYGROUND: "APP_FEEDBACK_PLAYGROUND",

// Preferences
INSERT_EMAIL_SCREEN: "INSERT_EMAIL_SCREEN",
Expand Down
Loading
Loading