diff --git a/src/account/SupportContact.test.tsx b/src/account/SupportContact.test.tsx index 9db3bd0524f..0dd97aa293b 100644 --- a/src/account/SupportContact.test.tsx +++ b/src/account/SupportContact.test.tsx @@ -81,7 +81,7 @@ describe('Contact', () => { expect(Mailer.mail).toBeCalledWith( expect.objectContaining({ isHTML: true, - body: 'Test Message

{"version":"0.0.1","buildNumber":"1","apiLevel":-1,"os":"android","country":"US","region":null,"deviceId":"someDeviceId","deviceBrand":"someBrand","deviceModel":"someModel","address":"0x0000000000000000000000000000000000007e57","sessionId":"","numberVerifiedCentralized":false,"network":"alfajores"}

Support logs are attached...', + body: 'Test Message

{"version":"0.0.1","buildNumber":"1","apiLevel":-1,"os":"android","country":"US","region":null,"deviceId":"someDeviceId","deviceBrand":"someBrand","deviceModel":"someModel","address":"0x0000000000000000000000000000000000007e57","sessionId":"","numberVerifiedCentralized":false,"hooksPreviewEnabled":false,"network":"alfajores"}

Support logs are attached...', recipients: [CELO_SUPPORT_EMAIL_ADDRESS], subject: i18n.t('supportEmailSubject', { appName: APP_NAME, user: '+1415555XXXX' }), attachments: logAttachments, @@ -134,6 +134,7 @@ describe('Contact', () => { deviceBrand: 'someBrand', deviceId: 'someDeviceId', deviceModel: 'someModel', + hooksPreviewEnabled: false, network: 'alfajores', numberVerifiedCentralized: false, os: 'android', diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index 6186f62e4ba..83718cdbda1 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -20,6 +20,7 @@ import { navigateBack } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' import { userLocationDataSelector } from 'src/networkInfo/selectors' +import { hooksPreviewApiUrlSelector } from 'src/positions/selectors' import { getFeatureGate } from 'src/statsig' import { StatsigFeatureGates } from 'src/statsig/types' import colors from 'src/styles/colors' @@ -71,6 +72,7 @@ function SupportContact({ route }: Props) { const sessionId = useSelector(sessionIdSelector) const numberVerifiedCentralized = useSelector(numberVerifiedCentrallySelector) const { countryCodeAlpha2: country, region } = useSelector(userLocationDataSelector) + const hooksPreviewApiUrl = useSelector(hooksPreviewApiUrlSelector) const dispatch = useDispatch() const prefilledText = route.params?.prefilledText @@ -100,6 +102,7 @@ function SupportContact({ route }: Props) { address: currentAccount, sessionId, numberVerifiedCentralized, + hooksPreviewEnabled: !!hooksPreviewApiUrl, network: DEFAULT_TESTNET, } const userId = e164PhoneNumber ? anonymizedPhone(e164PhoneNumber) : t('unknown') diff --git a/src/analytics/Events.tsx b/src/analytics/Events.tsx index 2f3d5febf27..d505e29e546 100644 --- a/src/analytics/Events.tsx +++ b/src/analytics/Events.tsx @@ -606,6 +606,14 @@ export enum NftEvents { nft_image_load = 'nft_image_load', // When an NFT attempted to load contains error boolean for success or failure } +export enum BuilderHooksEvents { + hooks_enable_preview_propose = 'hooks_enable_preview_propose', // When a user scans a QR code or opens a deep link to enable hooks preview + hooks_enable_preview_cancel = 'hooks_enable_preview_cancel', // When a user cancels the hooks preview flow + hooks_enable_preview_confirm = 'hooks_enable_preview_confirm', // When a user confirms enabling hooks preview + hooks_enable_preview_error = 'hooks_enable_preview_error', // When a user encounters an error enabling hooks preview + hooks_disable_preview = 'hooks_disable_preview', // When a user disables hooks preview +} + export type AnalyticsEventType = | AppEvents | HomeEvents @@ -636,3 +644,4 @@ export type AnalyticsEventType = | CeloNewsEvents | TokenBottomSheetEvents | AssetsEvents + | BuilderHooksEvents diff --git a/src/analytics/Properties.tsx b/src/analytics/Properties.tsx index c9d23c1733d..fbc24e69843 100644 --- a/src/analytics/Properties.tsx +++ b/src/analytics/Properties.tsx @@ -10,6 +10,7 @@ import { AppEvents, AssetsEvents, AuthenticationEvents, + BuilderHooksEvents, CeloExchangeEvents, CeloNewsEvents, CICOEvents, @@ -44,6 +45,7 @@ import { import { BackQuizProgress, DappRequestOrigin, + HooksEnablePreviewOrigin, ScrollDirection, SendOrigin, WalletConnectPairingOrigin, @@ -1298,6 +1300,18 @@ interface NftsEventsProperties { } } +interface BuilderHooksProperties { + [BuilderHooksEvents.hooks_enable_preview_propose]: { + origin: HooksEnablePreviewOrigin + } + [BuilderHooksEvents.hooks_enable_preview_confirm]: undefined + [BuilderHooksEvents.hooks_enable_preview_cancel]: undefined + [BuilderHooksEvents.hooks_enable_preview_error]: { + error: string + } + [BuilderHooksEvents.hooks_disable_preview]: undefined +} + export type AnalyticsPropertiesList = AppEventsProperties & HomeEventsProperties & SettingsEventsProperties & @@ -1330,4 +1344,5 @@ export type AnalyticsPropertiesList = AppEventsProperties & QrScreenProperties & TokenBottomSheetEventsProperties & AssetsEventsProperties & - NftsEventsProperties + NftsEventsProperties & + BuilderHooksProperties diff --git a/src/analytics/ValoraAnalytics.test.ts b/src/analytics/ValoraAnalytics.test.ts index bcd6f562b3c..3947236544d 100644 --- a/src/analytics/ValoraAnalytics.test.ts +++ b/src/analytics/ValoraAnalytics.test.ts @@ -129,6 +129,7 @@ const defaultSuperProperties = { sHasCompletedBackup: false, sHasVerifiedNumber: false, sHasVerifiedNumberCPV: true, + sHooksPreviewEnabled: false, sLanguage: 'es-419', sLocalCurrencyCode: 'PHP', sNetWorthUsd: 43.910872728527195, diff --git a/src/analytics/selectors.test.ts b/src/analytics/selectors.test.ts index 789185c52a2..4cceb087de4 100644 --- a/src/analytics/selectors.test.ts +++ b/src/analytics/selectors.test.ts @@ -245,6 +245,7 @@ describe('getCurrentUserTraits', () => { hasCompletedBackup: false, hasVerifiedNumber: false, hasVerifiedNumberCPV: true, + hooksPreviewEnabled: false, language: 'es-419', localCurrencyCode: 'PHP', netWorthUsd: 5764.949123945, diff --git a/src/analytics/selectors.ts b/src/analytics/selectors.ts index 28d6b339341..b2da33a626c 100644 --- a/src/analytics/selectors.ts +++ b/src/analytics/selectors.ts @@ -12,6 +12,7 @@ import { getLocalCurrencyCode } from 'src/localCurrency/selectors' import { userLocationDataSelector } from 'src/networkInfo/selectors' import { getPositionBalanceUsd } from 'src/positions/getPositionBalanceUsd' import { + hooksPreviewApiUrlSelector, positionsByBalanceUsdSelector, totalPositionsBalanceUsdSelector, } from 'src/positions/selectors' @@ -25,8 +26,8 @@ const tokensSelector = createSelector( ) const positionsAnalyticsSelector = createSelector( - [positionsByBalanceUsdSelector, totalPositionsBalanceUsdSelector], - (positionsByUsdBalance, totalPositionsBalanceUsd) => { + [positionsByBalanceUsdSelector, totalPositionsBalanceUsdSelector, hooksPreviewApiUrlSelector], + (positionsByUsdBalance, totalPositionsBalanceUsd, hooksPreviewApiUrl) => { const appsByBalanceUsd: Record = {} for (const position of positionsByUsdBalance) { const appId = position.appId @@ -61,6 +62,7 @@ const positionsAnalyticsSelector = createSelector( .slice(0, 10) .map(([appId, balanceUsd]) => `${appId}:${balanceUsd.toFixed(2)}`) .join(','), + hooksPreviewEnabled: !!hooksPreviewApiUrl, } } ) @@ -93,6 +95,7 @@ export const getCurrentUserTraits = createSelector( topTenPositions, positionsAppsCount, positionsTopTenApps, + hooksPreviewEnabled, }, localCurrencyCode, { numberVerifiedDecentralized, numberVerifiedCentralized }, @@ -152,6 +155,7 @@ export const getCurrentUserTraits = createSelector( topTenPositions, positionsAppsCount, positionsTopTenApps, + hooksPreviewEnabled, localCurrencyCode, hasVerifiedNumber: numberVerifiedDecentralized, hasVerifiedNumberCPV: numberVerifiedCentralized, diff --git a/src/analytics/types.ts b/src/analytics/types.ts index cdc5a905be6..71d4dd4d0ae 100644 --- a/src/analytics/types.ts +++ b/src/analytics/types.ts @@ -26,3 +26,9 @@ export enum DappRequestOrigin { InAppWebView = 'in_app_web_view', External = 'external', } + +// Origin of Hooks enable preview +export enum HooksEnablePreviewOrigin { + Scan = 'scan', + Deeplink = 'deeplink', +} diff --git a/src/app/saga.test.ts b/src/app/saga.test.ts index 912dce85fe4..b028dd3c190 100644 --- a/src/app/saga.test.ts +++ b/src/app/saga.test.ts @@ -8,7 +8,7 @@ import { EffectProviders, StaticProvider } from 'redux-saga-test-plan/providers' import { call, select } from 'redux-saga/effects' import { e164NumberSelector } from 'src/account/selectors' import { AppEvents, InviteEvents } from 'src/analytics/Events' -import { WalletConnectPairingOrigin } from 'src/analytics/types' +import { HooksEnablePreviewOrigin, WalletConnectPairingOrigin } from 'src/analytics/types' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { appLock, @@ -211,7 +211,10 @@ describe('handleDeepLink', () => { .provide([[select(allowHooksPreviewSelector), true]]) .run() - expect(handleEnableHooksPreviewDeepLink).toHaveBeenCalledWith(deepLink) + expect(handleEnableHooksPreviewDeepLink).toHaveBeenCalledWith( + deepLink, + HooksEnablePreviewOrigin.Deeplink + ) }) }) diff --git a/src/app/saga.ts b/src/app/saga.ts index ec32f60823d..7be9f401813 100644 --- a/src/app/saga.ts +++ b/src/app/saga.ts @@ -23,6 +23,7 @@ import { } from 'redux-saga/effects' import { e164NumberSelector } from 'src/account/selectors' import { AppEvents, InviteEvents } from 'src/analytics/Events' +import { HooksEnablePreviewOrigin } from 'src/analytics/types' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { Actions, @@ -373,7 +374,7 @@ export function* handleDeepLink(action: OpenDeepLink) { (yield select(allowHooksPreviewSelector)) && rawParams.pathname === '/hooks/enablePreview' ) { - yield call(handleEnableHooksPreviewDeepLink, deepLink) + yield call(handleEnableHooksPreviewDeepLink, deepLink, HooksEnablePreviewOrigin.Deeplink) } } } diff --git a/src/positions/HooksPreviewModeBanner.tsx b/src/positions/HooksPreviewModeBanner.tsx index 4b9d8e06b30..c60cef7d726 100644 --- a/src/positions/HooksPreviewModeBanner.tsx +++ b/src/positions/HooksPreviewModeBanner.tsx @@ -4,6 +4,8 @@ import { StyleSheet, Text } from 'react-native' import Animated, { SlideInUp, SlideOutUp } from 'react-native-reanimated' import { SafeAreaView } from 'react-native-safe-area-context' import { useDispatch, useSelector } from 'react-redux' +import { BuilderHooksEvents } from 'src/analytics/Events' +import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import Touchable from 'src/components/Touchable' import { hooksPreviewApiUrlSelector, hooksPreviewStatusSelector } from 'src/positions/selectors' import { previewModeDisabled } from 'src/positions/slice' @@ -25,6 +27,11 @@ export default function HooksPreviewModeBanner() { const { t } = useTranslation() const status = useSelector(hooksPreviewStatusSelector) + function onPress() { + ValoraAnalytics.track(BuilderHooksEvents.hooks_disable_preview) + dispatch(previewModeDisabled()) + } + if (!hooksPreviewApiUrl) { return null } @@ -36,10 +43,7 @@ export default function HooksPreviewModeBanner() { entering={SlideInUp} exiting={SlideOutUp} > - dispatch(previewModeDisabled())} - hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }} - > + {t('hooksPreview.bannerTitle')} diff --git a/src/positions/saga.test.ts b/src/positions/saga.test.ts index ff93af48c82..c13581509ad 100644 --- a/src/positions/saga.test.ts +++ b/src/positions/saga.test.ts @@ -2,6 +2,7 @@ import { FetchMock } from 'jest-fetch-mock/types' import { Platform } from 'react-native' import { expectSaga } from 'redux-saga-test-plan' import { call, select } from 'redux-saga/effects' +import { HooksEnablePreviewOrigin } from 'src/analytics/types' import { fetchPositionsSaga, fetchShortcutsSaga, @@ -175,7 +176,7 @@ describe(handleEnableHooksPreviewDeepLink, () => { it('enables hooks preview if the deep link is valid and the user confirms', async () => { Platform.OS = 'android' - await expectSaga(handleEnableHooksPreviewDeepLink, deepLink) + await expectSaga(handleEnableHooksPreviewDeepLink, deepLink, HooksEnablePreviewOrigin.Deeplink) .provide([[call(_confirmEnableHooksPreview), true]]) .put(previewModeEnabled('http://192.168.0.42.sslip.io:18000/')) // Uses sslip.io for Android .run() @@ -183,21 +184,25 @@ describe(handleEnableHooksPreviewDeepLink, () => { it('uses the direct IP on iOS if the deep link is valid and the user confirms', async () => { Platform.OS = 'ios' - await expectSaga(handleEnableHooksPreviewDeepLink, deepLink) + await expectSaga(handleEnableHooksPreviewDeepLink, deepLink, HooksEnablePreviewOrigin.Deeplink) .provide([[call(_confirmEnableHooksPreview), true]]) .put(previewModeEnabled('http://192.168.0.42:18000')) .run() }) it('does nothing if the deep link is invalid', async () => { - await expectSaga(handleEnableHooksPreviewDeepLink, 'invalid-link') + await expectSaga( + handleEnableHooksPreviewDeepLink, + 'invalid-link', + HooksEnablePreviewOrigin.Deeplink + ) .provide([[call(_confirmEnableHooksPreview), true]]) .not.put.actionType(previewModeEnabled.type) .run() }) it("does nothing if the user doesn't confirm", async () => { - await expectSaga(handleEnableHooksPreviewDeepLink, deepLink) + await expectSaga(handleEnableHooksPreviewDeepLink, deepLink, HooksEnablePreviewOrigin.Deeplink) .provide([[call(_confirmEnableHooksPreview), false]]) .not.put.actionType(previewModeEnabled.type) .run() diff --git a/src/positions/saga.ts b/src/positions/saga.ts index 75c88352ad5..7473ab5dacc 100644 --- a/src/positions/saga.ts +++ b/src/positions/saga.ts @@ -3,6 +3,9 @@ import path from 'path' import { Alert, Platform } from 'react-native' import { call, put, select, spawn, takeLeading } from 'redux-saga/effects' import { showError } from 'src/alert/actions' +import { BuilderHooksEvents } from 'src/analytics/Events' +import { HooksEnablePreviewOrigin } from 'src/analytics/types' +import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { ErrorMessages } from 'src/app/ErrorMessages' import { DEFAULT_TESTNET } from 'src/config' import i18n from 'src/i18n' @@ -144,7 +147,11 @@ function confirmEnableHooksPreview() { // Export for testing export const _confirmEnableHooksPreview = confirmEnableHooksPreview -export function* handleEnableHooksPreviewDeepLink(deeplink: string) { +export function* handleEnableHooksPreviewDeepLink( + deeplink: string, + origin: HooksEnablePreviewOrigin +) { + ValoraAnalytics.track(BuilderHooksEvents.hooks_enable_preview_propose, { origin }) let hooksPreviewApiUrl: string | null = null try { hooksPreviewApiUrl = new URL(deeplink).searchParams.get('hooksApiUrl') @@ -161,6 +168,9 @@ export function* handleEnableHooksPreviewDeepLink(deeplink: string) { } } catch (error) { Logger.warn(TAG, 'Unable to parse hooks preview deeplink', error) + ValoraAnalytics.track(BuilderHooksEvents.hooks_enable_preview_error, { + error: error?.message || error?.toString(), + }) } if (!hooksPreviewApiUrl) { @@ -170,8 +180,11 @@ export function* handleEnableHooksPreviewDeepLink(deeplink: string) { const confirm = yield call(confirmEnableHooksPreview) if (confirm) { + ValoraAnalytics.track(BuilderHooksEvents.hooks_enable_preview_confirm) Logger.info(TAG, `Enabling hooks preview mode with API URL: ${hooksPreviewApiUrl}`) yield put(previewModeEnabled(hooksPreviewApiUrl)) + } else { + ValoraAnalytics.track(BuilderHooksEvents.hooks_enable_preview_cancel) } } diff --git a/src/qrcode/utils.test.tsx b/src/qrcode/utils.test.tsx index 9159c59534b..1040e1f09ed 100644 --- a/src/qrcode/utils.test.tsx +++ b/src/qrcode/utils.test.tsx @@ -4,6 +4,7 @@ import 'react-native' import { View } from 'react-native' import { expectSaga } from 'redux-saga-test-plan' import { select } from 'redux-saga/effects' +import { HooksEnablePreviewOrigin } from 'src/analytics/types' import { handleEnableHooksPreviewDeepLink } from 'src/positions/saga' import { allowHooksPreviewSelector } from 'src/positions/selectors' import { urlFromUriData } from 'src/qrcode/schema' @@ -63,6 +64,9 @@ describe('handleBarcode', () => { .provide([[select(allowHooksPreviewSelector), true]]) .run() - expect(handleEnableHooksPreviewDeepLink).toHaveBeenCalledWith(link) + expect(handleEnableHooksPreviewDeepLink).toHaveBeenCalledWith( + link, + HooksEnablePreviewOrigin.Scan + ) }) }) diff --git a/src/qrcode/utils.ts b/src/qrcode/utils.ts index 58f409b0421..aacad4f1899 100644 --- a/src/qrcode/utils.ts +++ b/src/qrcode/utils.ts @@ -4,7 +4,11 @@ import Share from 'react-native-share' import { call, fork, put, select } from 'redux-saga/effects' import { showError, showMessage } from 'src/alert/actions' import { SendEvents } from 'src/analytics/Events' -import { SendOrigin, WalletConnectPairingOrigin } from 'src/analytics/types' +import { + HooksEnablePreviewOrigin, + SendOrigin, + WalletConnectPairingOrigin, +} from 'src/analytics/types' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { ErrorMessages } from 'src/app/ErrorMessages' import { paymentDeepLinkHandlerSelector, phoneNumberVerifiedSelector } from 'src/app/selectors' @@ -150,7 +154,7 @@ export function* handleBarcode( (yield select(allowHooksPreviewSelector)) && barcode.data.startsWith('celo://wallet/hooks/enablePreview') ) { - yield call(handleEnableHooksPreviewDeepLink, barcode.data) + yield call(handleEnableHooksPreviewDeepLink, barcode.data, HooksEnablePreviewOrigin.Scan) return }