Skip to content

Commit

Permalink
feat(appzi): new appzi survey for limit orders (cowprotocol#3918)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alfetopito authored Feb 27, 2024
1 parent 55b5e22 commit 99e004a
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -30,15 +31,15 @@ export const appziMiddleware: Middleware<Record<string, unknown>, 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 {
chainId,
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 {
Expand All @@ -50,7 +51,7 @@ export const appziMiddleware: Middleware<Record<string, unknown>, 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
Expand All @@ -61,7 +62,7 @@ export const appziMiddleware: Middleware<Record<string, unknown>, 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 {
Expand All @@ -73,18 +74,18 @@ export const appziMiddleware: Middleware<Record<string, unknown>, 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<Dispatch<AnyAction>>,
chainId: ChainId,
orderId: string,
npsParams: Parameters<typeof openNpsAppziSometimes>[0],
npsParams: Parameters<typeof triggerAppziSurvey>[0],
_order?: OrderActions.SerializedOrder | undefined
) {
const order = _order || getOrderByIdFromState(store.getState().orders[chainId], orderId)?.order
Expand All @@ -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<Dispatch<AnyAction>>, chainId: ChainId) {
return Object.keys(store.getState().orders[chainId]?.pending || {})
}

function getUiOrderTypeFromStore(store: MiddlewareAPI<Dispatch<AnyAction>>, chainId: any, id: any) {
Expand Down
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/modules/limitOrders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
@@ -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<string[]>((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
}
2 changes: 2 additions & 0 deletions apps/cowswap-frontend/src/pages/LimitOrders/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
LimitOrdersWidget,
QuoteObserverUpdater,
SetupLimitOrderAmountsFromUrlUpdater,
TriggerAppziLimitOrdersSurveyUpdater,
useIsWidgetUnlocked,
} from 'modules/limitOrders'
import { OrdersTableWidget } from 'modules/ordersTable'
Expand All @@ -32,6 +33,7 @@ export default function LimitOrderPage() {
<SetupLimitOrderAmountsFromUrlUpdater />
<InitialPriceUpdater />
<ExecutionPriceUpdater />
<TriggerAppziLimitOrdersSurveyUpdater />
<styledEl.PageWrapper isUnlocked={isUnlocked}>
<styledEl.PrimaryWrapper>
<LimitOrdersWidget />
Expand Down
27 changes: 24 additions & 3 deletions libs/common-utils/src/appzi.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<AppziCustomSettings, 'userTradedOrWaitedForLong' | 'isTestNps'>) {
export function triggerAppziSurvey(
data?: Omit<AppziCustomSettings, 'userTradedOrWaitedForLong' | 'isTestNps'>,
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()

0 comments on commit 99e004a

Please sign in to comment.