From 99e004ad410aefacfd2090423ef2e480ed48302e Mon Sep 17 00:00:00 2001 From: Leandro Date: Tue, 27 Feb 2024 14:28:35 +0000 Subject: [PATCH] feat(appzi): new appzi survey for limit orders (#3918) * feat: add optional appzi data: account and pendingOrderIds * feat: use new appzi survey for limit orders * refactor: rename appzi trigger fn * refactor: create const with orderType * feat: trigger appzi limit orders survey when limit orders widget is loaded * fix: do not trigger when there are no pending LIMIT orders * fix: pendingOrderIds was returning order objs instead of ids * refactor: add fn getSurveyType --- .../updaters/orders/PendingOrdersUpdater.ts | 9 +++-- .../orders/middleware/appziMiddleware.test.ts | 4 +- .../orders/middleware/appziMiddleware.ts | 40 ++++++++++++------- .../src/modules/limitOrders/index.ts | 1 + .../updaters/TriggerAppziUpdater.ts | 40 +++++++++++++++++++ .../src/pages/LimitOrders/index.tsx | 2 + libs/common-utils/src/appzi.ts | 27 +++++++++++-- 7 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/limitOrders/updaters/TriggerAppziUpdater.ts diff --git a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts index 7cd4f22316..3c6e806f5c 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts @@ -4,8 +4,8 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' import { getExplorerOrderLink, isOrderInPendingTooLong, - openNpsAppziSometimes, timeSinceInSeconds, + triggerAppziSurvey, } from '@cowprotocol/common-utils' import { EthflowData, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { Command, UiOrderType } from '@cowprotocol/types' @@ -266,16 +266,17 @@ async function _updateOrders({ function _triggerNps(pending: Order[], chainId: ChainId) { for (const order of pending) { const { openSince, id: orderId } = order + const orderType = getUiOrderType(order) // Check if there's any SWAP pending for more than `PENDING_TOO_LONG_TIME` - if (getUiOrderType(order) === UiOrderType.SWAP && isOrderInPendingTooLong(openSince)) { + if (orderType === UiOrderType.SWAP && isOrderInPendingTooLong(openSince)) { const explorerUrl = getExplorerOrderLink(chainId, orderId) // Trigger NPS display, controlled by Appzi - openNpsAppziSometimes({ + triggerAppziSurvey({ waitedTooLong: true, secondsSinceOpen: timeSinceInSeconds(openSince), explorerUrl, chainId, - orderType: getUiOrderType(order), + orderType, }) // Break the loop, don't need to show more than once break diff --git a/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.test.ts b/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.test.ts index 7e930f59ec..d76b144902 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.test.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.test.ts @@ -1,4 +1,4 @@ -import { isOrderInPendingTooLong, openNpsAppziSometimes } from '@cowprotocol/common-utils' +import { isOrderInPendingTooLong, triggerAppziSurvey } from '@cowprotocol/common-utils' import { UiOrderType } from '@cowprotocol/types' import { AnyAction, Dispatch, MiddlewareAPI } from 'redux' @@ -26,7 +26,7 @@ jest.mock('utils/orderUtils/getUiOrderType', () => { }) const isOrderInPendingTooLongMock = jest.mocked(isOrderInPendingTooLong) -const openNpsAppziSometimesMock = jest.mocked(openNpsAppziSometimes) +const openNpsAppziSometimesMock = jest.mocked(triggerAppziSurvey) const getOrderByOrderIdFromStateMock = jest.mocked(getOrderByIdFromState) const getUiOrderTypeMock = jest.mocked(getUiOrderType) diff --git a/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.ts b/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.ts index d7a6e0d50b..a0618ec4a5 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.ts @@ -1,8 +1,9 @@ import { getExplorerOrderLink, + getSurveyType, isOrderInPendingTooLong, - openNpsAppziSometimes, timeSinceInSeconds, + triggerAppziSurvey, } from '@cowprotocol/common-utils' import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { UiOrderType } from '@cowprotocol/types' @@ -30,7 +31,7 @@ export const appziMiddleware: Middleware, AppState> = (s ordersData: [{ id }], } = action.payload - _triggerNps(store, chainId, id, { traded: true }) + _triggerAppzi(store, chainId, id, { traded: true }) } else if (isBatchExpireOrderAction(action)) { // Shows NPS feedback (or attempts to) when the order expired const { @@ -38,7 +39,7 @@ export const appziMiddleware: Middleware, AppState> = (s ids: [id], } = action.payload - _triggerNps(store, chainId, id, { expired: true }) + _triggerAppzi(store, chainId, id, { expired: true }) } else if (isBatchPresignOrderAction(action)) { // For SC wallet orders, shows NPS feedback (or attempts to) only when the order was pre-signed const { @@ -50,7 +51,7 @@ export const appziMiddleware: Middleware, AppState> = (s // Only for limit orders if (uiOrderType === UiOrderType.LIMIT) { - _triggerNps(store, chainId, id, { created: true }) + _triggerAppzi(store, chainId, id, { created: true }) } } else if (isPendingOrderAction(action)) { // For EOA orders, shows NPS feedback (or attempts to) when the order is placed @@ -61,7 +62,7 @@ export const appziMiddleware: Middleware, AppState> = (s // Only for limit orders if (uiOrderType === UiOrderType.LIMIT) { - _triggerNps(store, chainId, order.id, { created: true }, order) + _triggerAppzi(store, chainId, order.id, { created: true }, order) } } else if (isBatchCancelOrderAction(action)) { const { @@ -73,18 +74,18 @@ export const appziMiddleware: Middleware, AppState> = (s // Only for limit orders if (uiOrderType === UiOrderType.LIMIT) { - _triggerNps(store, chainId, id, { cancelled: true }) + _triggerAppzi(store, chainId, id, { cancelled: true }) } } return next(action) } -function _triggerNps( +function _triggerAppzi( store: MiddlewareAPI>, chainId: ChainId, orderId: string, - npsParams: Parameters[0], + npsParams: Parameters[0], _order?: OrderActions.SerializedOrder | undefined ) { const order = _order || getOrderByIdFromState(store.getState().orders[chainId], orderId)?.order @@ -105,13 +106,22 @@ function _triggerNps( return } - openNpsAppziSometimes({ - ...npsParams, - secondsSinceOpen: timeSinceInSeconds(openSince), - explorerUrl, - chainId, - orderType: uiOrderType, - }) + triggerAppziSurvey( + { + ...npsParams, + secondsSinceOpen: timeSinceInSeconds(openSince), + explorerUrl, + chainId, + orderType: uiOrderType, + account: order?.owner, + pendingOrderIds: getPendingOrderIds(store, chainId).join(','), + }, + getSurveyType(uiOrderType) + ) +} + +function getPendingOrderIds(store: MiddlewareAPI>, chainId: ChainId) { + return Object.keys(store.getState().orders[chainId]?.pending || {}) } function getUiOrderTypeFromStore(store: MiddlewareAPI>, chainId: any, id: any) { diff --git a/apps/cowswap-frontend/src/modules/limitOrders/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/index.ts index 8a687cfa76..5702afa6f9 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/index.ts @@ -9,3 +9,4 @@ export * from './state/limitOrdersRawStateAtom' export * from './state/limitOrdersSettingsAtom' export { useIsWidgetUnlocked } from './hooks/useIsWidgetUnlocked' export * from './const/trade' +export * from './updaters/TriggerAppziUpdater' diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/TriggerAppziUpdater.ts b/apps/cowswap-frontend/src/modules/limitOrders/updaters/TriggerAppziUpdater.ts new file mode 100644 index 0000000000..62fee58394 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/TriggerAppziUpdater.ts @@ -0,0 +1,40 @@ +import { useEffect, useMemo } from 'react' + +import { triggerAppziSurvey } from '@cowprotocol/common-utils' +import { UiOrderType } from '@cowprotocol/types' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useOnlyPendingOrders } from 'legacy/state/orders/hooks' + +import { getUiOrderType } from 'utils/orderUtils/getUiOrderType' + +/** + * Updater for triggering Appzi Limit Orders survey + * + * Should trigger only when there are pending orders on load + * Not a problem if triggered more than once. Appzi controls the form display rules + */ +export function TriggerAppziLimitOrdersSurveyUpdater(): null { + const { account, chainId } = useWalletInfo() + const orders = useOnlyPendingOrders(chainId) + + const pendingOrderIds = useMemo(() => { + return orders + .reduce((acc, order) => { + if (getUiOrderType(order) === UiOrderType.LIMIT) { + acc.push(order.id) + } + + return acc + }, []) + .join(',') + }, [orders]) + + useEffect(() => { + if (account && chainId && pendingOrderIds) { + triggerAppziSurvey({ account, chainId, pendingOrderIds, openedLimitPage: true }, 'limit') + } + }, [account, chainId, pendingOrderIds]) + + return null +} diff --git a/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx b/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx index 10ddc79d52..8b77d2362a 100644 --- a/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx @@ -12,6 +12,7 @@ import { LimitOrdersWidget, QuoteObserverUpdater, SetupLimitOrderAmountsFromUrlUpdater, + TriggerAppziLimitOrdersSurveyUpdater, useIsWidgetUnlocked, } from 'modules/limitOrders' import { OrdersTableWidget } from 'modules/ordersTable' @@ -32,6 +33,7 @@ export default function LimitOrderPage() { + diff --git a/libs/common-utils/src/appzi.ts b/libs/common-utils/src/appzi.ts index 7f04539858..3df1d0178c 100644 --- a/libs/common-utils/src/appzi.ts +++ b/libs/common-utils/src/appzi.ts @@ -1,8 +1,9 @@ import ms from 'ms.macro' import ReactAppzi from 'react-appzi' import { isImTokenBrowser, majorBrowserVersion, userAgent } from './userAgent' -import { environmentName, isProdLike } from './environments' +import { isProdLike } from './environments' import { isInjectedWidget } from './isInjectedWidget' +import { UiOrderType } from '@cowprotocol/types' // Metamask IOS app uses a version from July 2019 which causes problems in appZi const OLD_CHROME_FROM_METAMASK_IOS_APP = 76 @@ -49,11 +50,14 @@ type AppziCustomSettings = { traded?: true created?: true cancelled?: true + openedLimitPage?: true // extra contextual data for statistics/debugging explorerUrl?: string env?: string chainId?: number orderType?: string + account?: string + pendingOrderIds?: string } type AppziSettings = { @@ -95,14 +99,31 @@ const TEST_NPS_DATA: AppziCustomSettings = { isTestNps: true } // Either one or the other. If both are present, PROD takes precedence const NPS_DATA = isProdLike ? PROD_NPS_DATA : TEST_NPS_DATA +// Limit orders survey trigger conditions +const LIMIT_SURVEY_DATA_TEST = { isLimitSurveyTest: true } +const LIMIT_SURVEY_DATA_PROD = { isLimitSurveyProd: true } + +const LIMIT_SURVEY_DATA = isProdLike ? LIMIT_SURVEY_DATA_PROD : LIMIT_SURVEY_DATA_TEST + +type SurveyType = 'nps' | 'limit' + /** * Opening of the modal is delegated to Appzi * It'll display only if the trigger rules are met */ -export function openNpsAppziSometimes(data?: Omit) { +export function triggerAppziSurvey( + data?: Omit, + surveyType: SurveyType = 'nps' +) { if (isInjectedWidget()) return - updateAppziSettings({ data: { env: environmentName, ...data, ...NPS_DATA } }) + const surveyData = surveyType === 'limit' ? LIMIT_SURVEY_DATA : NPS_DATA + + updateAppziSettings({ data: { ...data, ...surveyData } }) +} + +export function getSurveyType(orderType: UiOrderType | undefined): SurveyType { + return orderType === UiOrderType.LIMIT ? 'limit' : 'nps' } initialize()