Skip to content

Commit

Permalink
feat(ui-ux): UI for opt out anonymized data instrumentation (#4177)
Browse files Browse the repository at this point in the history
* added analytics section in settings page

* added ios prompt

* added functionality

* added DE and FR translations

* added alert for first login

* fixed typing issue

* added missing translations

* added localstorage

* added e2e test

* added chinese translations

* added missing chinese translations

* only saveTx if isAnalyticsOn is true

* fixed lint issue

* added boolean for first modal

* changed to use local storage

* cleaned up local storage

* stored first modal in local storage too

* revert additional space in main.tsx

* update e2e test

* added border

* pr comments (name change and add timeout)

* added 1000ms timeout for first modal

* changed timeout to 5 secs

* revert back to 1000ms

* switched analytics switch to use state variable instead

* switched to state variable for condition check
  • Loading branch information
nattadex authored Apr 2, 2024
1 parent bfe7b2d commit bf3cd26
Show file tree
Hide file tree
Showing 15 changed files with 408 additions and 45 deletions.
59 changes: 31 additions & 28 deletions mobile-app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { FavouritePoolpairProvider } from "@contexts/FavouritePoolpairContext";
import BigNumber from "bignumber.js";
import { EVMProvider } from "@contexts/EVMProvider";
import { CustomServiceProvider } from "@contexts/CustomServiceProvider";
import { AnalyticsProvider } from "@shared-contexts/AnalyticsProvider";

/**
* Loads
Expand Down Expand Up @@ -110,35 +111,37 @@ export default function App(): JSX.Element | null {
colorScheme={colorScheme}
logger={Logging}
>
<LanguageProvider
api={LanguagePersistence}
locale={Localization.locale}
>
<DomainProvider api={DomainPersistence}>
<CustomServiceProvider
api={ServiceProviderPersistence}
logger={Logging}
>
<DisplayBalancesProvider>
<ConnectionBoundary>
<GestureHandlerRootView
style={tailwind("flex-1")}
>
<ToastProvider
renderType={customToast}
<AnalyticsProvider>
<LanguageProvider
api={LanguagePersistence}
locale={Localization.locale}
>
<DomainProvider api={DomainPersistence}>
<CustomServiceProvider
api={ServiceProviderPersistence}
logger={Logging}
>
<DisplayBalancesProvider>
<ConnectionBoundary>
<GestureHandlerRootView
style={tailwind("flex-1")}
>
<FavouritePoolpairProvider>
<EVMProvider logger={Logging}>
<Main />
</EVMProvider>
</FavouritePoolpairProvider>
</ToastProvider>
</GestureHandlerRootView>
</ConnectionBoundary>
</DisplayBalancesProvider>
</CustomServiceProvider>
</DomainProvider>
</LanguageProvider>
<ToastProvider
renderType={customToast}
>
<FavouritePoolpairProvider>
<EVMProvider logger={Logging}>
<Main />
</EVMProvider>
</FavouritePoolpairProvider>
</ToastProvider>
</GestureHandlerRootView>
</ConnectionBoundary>
</DisplayBalancesProvider>
</CustomServiceProvider>
</DomainProvider>
</LanguageProvider>
</AnalyticsProvider>
</ThemeProvider>
</FeatureFlagProvider>
</StatsProvider>
Expand Down
21 changes: 21 additions & 0 deletions mobile-app/app/api/persistence/analytics_storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import AsyncStorage from "@react-native-async-storage/async-storage";

const STORAGE_PREFIX_KEY = "WALLET.";

export async function getStorageItem<T>(key: string): Promise<T | null> {
if (typeof window === "undefined") {
return null;
}
const prefixedKey = `${STORAGE_PREFIX_KEY}${key}`;
const value = await AsyncStorage.getItem(prefixedKey);
const currentValue = JSON.parse(value || String(null));
return currentValue === null ? undefined : currentValue;
}

export async function setStorageItem<T>(key: string, value: T) {
if (typeof window === "undefined") {
return;
}
const prefixedKey = `${STORAGE_PREFIX_KEY}${key}`;
await AsyncStorage.setItem(prefixedKey, JSON.stringify(value));
}
27 changes: 18 additions & 9 deletions mobile-app/app/components/OceanInterface/OceanInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { getReleaseChannel } from "@api/releaseChannel";
import { useAppDispatch } from "@hooks/useAppDispatch";
import { useFeatureFlagContext } from "@contexts/FeatureFlagContext";
import { useAnalytics } from "@shared-contexts/AnalyticsProvider";
import { TransactionDetail } from "./TransactionDetail";
import { TransactionError } from "./TransactionError";

Expand Down Expand Up @@ -121,6 +122,7 @@ export function OceanInterface(): JSX.Element | null {
const { getTransactionUrl } = useDeFiScanContext();
const { network } = useNetworkContext();
const { isFeatureAvailable } = useFeatureFlagContext();
const { isAnalyticsOn } = useAnalytics();
const isSaveTxEnabled = isFeatureAvailable("save_tx");

// store
Expand Down Expand Up @@ -179,11 +181,19 @@ export function OceanInterface(): JSX.Element | null {
calledTx !== tx?.tx.txId && // to ensure that api is only called once per tx
tx?.tx.txId !== undefined &&
network === EnvironmentNetwork.MainNet &&
isSaveTxEnabled // feature flag
isSaveTxEnabled && // feature flag
isAnalyticsOn
) {
saveTx(tx.tx.txId);
}
}, [tx?.tx.txId, calledTx, tx?.broadcasted, network, isSaveTxEnabled]);
}, [
tx?.tx.txId,
calledTx,
tx?.broadcasted,
network,
isSaveTxEnabled,
isAnalyticsOn,
]);

useEffect(() => {
// get evm tx id and url (if any)
Expand All @@ -199,13 +209,12 @@ export function OceanInterface(): JSX.Element | null {
};

if (tx !== undefined) {
const isTransferDomainTx = tx?.tx.vout.some(
(vout) =>
vout.script?.stack.some(
(item: any) =>
item.type === "OP_DEFI_TX" &&
item.tx?.name === "OP_DEFI_TX_TRANSFER_DOMAIN",
),
const isTransferDomainTx = tx?.tx.vout.some((vout) =>
vout.script?.stack.some(
(item: any) =>
item.type === "OP_DEFI_TX" &&
item.tx?.name === "OP_DEFI_TX_TRANSFER_DOMAIN",
),
);
if (isTransferDomainTx) {
fetchEvmTx(tx.tx.txId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ import * as SplashScreen from "expo-splash-screen";
import { useLogger } from "@shared-contexts/NativeLoggingProvider";
import { bottomTabDefaultRoutes } from "@screens/AppNavigator/constants/DefaultRoutes";
import { DomainType, useDomainContext } from "@contexts/DomainContext";
import { WalletAlert } from "@components/WalletAlert";
import { useAnalytics } from "@shared-contexts/AnalyticsProvider";
import { AddressSelectionButtonV2 } from "./components/AddressSelectionButtonV2";
import { ActionButtons } from "./components/ActionButtons";
import {
Expand Down Expand Up @@ -108,6 +110,7 @@ export function PortfolioScreen({ navigation }: Props): JSX.Element {
const client = useWhaleApiClient();
const whaleRpcClient = useWhaleRpcClient();
const { address } = useWalletContext();
const { hasAnalyticsModalBeenShown, setStorage } = useAnalytics();
const { denominationCurrency, setDenominationCurrency } =
useDenominationCurrency();

Expand Down Expand Up @@ -167,6 +170,32 @@ export function PortfolioScreen({ navigation }: Props): JSX.Element {
});
}, []);

useEffect(() => {
setTimeout(() => {
if (hasAnalyticsModalBeenShown === "false") {
WalletAlert({
title: translate(
"screens/AnalyticsScreen",
"Data is now collected to improve experience.",
),
message: translate(
"screens/AnalyticsScreen",
"As of the latest version, this wallet is now collecting non-identifiable performance-related data. You can choose to opt-out anytime from the settings page.",
),
buttons: [
{
text: translate("screens/AnalyticsScreen", "Continue"),
style: "cancel",
onPress: async () => {
setStorage("IS_ANALYTICS_MODAL_SHOWN", "true");
},
},
],
});
}
}, 1000);
}, [hasAnalyticsModalBeenShown]);

const fetchPortfolioData = (): void => {
batch(() => {
dispatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { WhitelistedAddress } from "@store/userPreferences";
import { NetworkSelectionScreen } from "@screens/AppNavigator/screens/Settings/screens/NetworkSelectionScreen";
import { CfpDfipProposalsFaq } from "@screens/AppNavigator/screens/Portfolio/screens/CfpDfipProposalsFaq";
import { DomainType } from "@contexts/DomainContext";
import { AnalyticsScreen } from "@screens/AppNavigator/screens/Settings/screens/AnalyticsScreen";
import { AboutScreen } from "./screens/AboutScreen";
import { CommunityScreen } from "./screens/CommunityScreen";
import { LanguageSelectionScreen } from "./screens/LanguageSelectionScreen";
Expand Down Expand Up @@ -106,6 +107,15 @@ export function SettingsNavigator(): JSX.Element {
}}
/>

<SettingsStack.Screen
component={AnalyticsScreen}
name="AnalyticsScreen"
options={{
headerTitle: translate("screens/AnalyticsScreen", "Analytics"),
headerBackTitleVisible: false,
}}
/>

<SettingsStack.Screen
component={ServiceProviderScreen}
name="ServiceProviderScreen"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export function SettingsScreen({ navigation }: Props): JSX.Element {
const { language } = useLanguageContext();
const languages = getAppLanguages();

const selectedLanguage = languages.find(
(languageItem) => language?.startsWith(languageItem.locale),
const selectedLanguage = languages.find((languageItem) =>
language?.startsWith(languageItem.locale),
);

const revealRecoveryWords = useCallback(() => {
Expand Down Expand Up @@ -179,6 +179,12 @@ export function SettingsScreen({ navigation }: Props): JSX.Element {
border
testID="view_recovery_words"
/>
<NavigateItemRow
testID="setting_analytics"
label="Analytics"
border
onPress={() => navigation.navigate("AnalyticsScreen")}
/>
<NavigateItemRow
label="Change passcode"
onPress={changePasscode}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
ThemedScrollViewV2,
ThemedSectionTitleV2,
ThemedTextV2,
ThemedViewV2,
} from "@components/themed";
import { WalletAlert } from "@components/WalletAlert";
import { translate } from "@translations";
import { tailwind } from "@tailwind";
import { Switch } from "@components";
import { useAnalytics } from "@shared-contexts/AnalyticsProvider";
import { useState } from "react";

export function AnalyticsScreen(): JSX.Element {
const { isAnalyticsOn, setStorage } = useAnalytics();
const [isSwitchOn, setIsSwitchOn] = useState<boolean>(
isAnalyticsOn === "true",
);

return (
<ThemedScrollViewV2
style={tailwind("flex-1")}
contentContainerStyle={tailwind("px-5 pb-16")}
testID="analytics_screen"
>
<ThemedSectionTitleV2
testID="analytics_screen_title"
text={translate("screens/AnalyticsScreen", "ANALYTICS")}
/>
<ThemedViewV2
style={tailwind(
"flex flex-row items-center justify-between rounded-lg-v2 px-5 py-3",
)}
dark={tailwind("bg-mono-dark-v2-00")}
light={tailwind("bg-mono-light-v2-00")}
>
<ThemedTextV2
light={tailwind("text-mono-light-v2-900")}
dark={tailwind("text-mono-dark-v2-900")}
style={tailwind("font-normal-v2 text-sm flex-1")}
testID="text_privacy_lock"
>
{translate("screens/AnalyticsScreen", "Allow data access")}
</ThemedTextV2>
<Switch
onValueChange={async () => {
if (isSwitchOn) {
WalletAlert({
title: translate(
"screens/AnalyticsScreen",
"Are you sure you want to restrict data access?",
),
message: translate(
"screens/AnalyticsScreen",
"Your data is always kept anonymous and is used only for improvements. Are you sure you want to restrict?",
),
buttons: [
{
text: translate("screens/AnalyticsScreen", "Cancel"),
style: "cancel",
},
{
text: translate("screens/AnalyticsScreen", "Restrict data"),
onPress: async () => {
setStorage("IS_ANALYTICS_ON", "false");
setIsSwitchOn(false);
},
style: "destructive",
},
],
});
} else {
setStorage("IS_ANALYTICS_ON", "true");
setIsSwitchOn(true);
}
}}
value={isSwitchOn}
testID="analytics_switch"
/>
</ThemedViewV2>
<ThemedTextV2
dark={tailwind("text-mono-dark-v2-500")}
light={tailwind("text-mono-light-v2-500")}
style={tailwind("px-5 pt-2 text-xs font-normal-v2")}
>
{translate(
"screens/AnalyticsScreen",
"Participate in BR Analytics to help us make DeFiChain Wallet better.",
)}
</ThemedTextV2>
</ThemedScrollViewV2>
);
}
21 changes: 21 additions & 0 deletions mobile-app/cypress/e2e/functional/wallet/settings/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,25 @@ context("Wallet - Settings", { testIsolation: false }, () => {
"rgb(217, 123, 1)",
);
});

it("should activate analytics by default (localstorage)", () => {
cy.url().should("include", "app/Settings/SettingsScreen", () => {
expect(localStorage.getItem("WALLET.IS_ANALYTICS_ON")).to.eq("true");
});
});

it("should switch and pop up should up to switch off analytics (local storage to be updated)", () => {
cy.getByTestID("setting_analytics").click();
cy.getByTestID("analytics_switch").click();
cy.on("window:alert", (message) => {
expect(message).to.equal(
"Your data is always kept anonymous and is used only for improvements. Are you sure you want to restrict?",
);
cy.contains("Restrict data").click();
});
cy.should(() => {
const analyticsValue = localStorage.getItem("WALLET.IS_ANALYTICS_ON");
expect(analyticsValue).to.eq('"false"');
});
});
});
Loading

0 comments on commit bf3cd26

Please sign in to comment.