Skip to content

Commit f454081

Browse files
Merge pull request #475 from reown-com/chore/wallet-impression
chore: wallet impression event
2 parents 2878d5f + 1adc141 commit f454081

File tree

7 files changed

+213
-40
lines changed

7 files changed

+213
-40
lines changed

packages/appkit/src/AppKit.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ export class AppKit {
214214
SendController.resetState();
215215
OnRampController.resetState();
216216
WcController.resetState();
217+
EventsController.resetState();
217218

218219
if (ConnectionsController.state.activeNamespace === undefined) {
219220
ConnectionsController.setActiveNamespace(
@@ -308,6 +309,7 @@ export class AppKit {
308309

309310
RouterUtil.checkOnRampBack();
310311
RouterUtil.checkSocialLoginBack();
312+
EventsController.sendWalletImpressions();
311313
}
312314

313315
back() {

packages/appkit/src/partials/w3m-all-wallets-list/components/WalletList.tsx

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FlatList, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
1+
import { FlatList, StyleSheet, type StyleProp, type ViewStyle, type ViewToken } from 'react-native';
22
import { WalletItem } from './WalletItem';
33
import {
44
CardSelectHeight,
@@ -7,8 +7,9 @@ import {
77
CardSelectLoader,
88
CardSelectWidth
99
} from '@reown/appkit-ui-react-native';
10-
import { ApiController } from '@reown/appkit-core-react-native';
10+
import { ApiController, EventsController } from '@reown/appkit-core-react-native';
1111
import type { WcWallet } from '@reown/appkit-common-react-native';
12+
import { useCallback, useRef } from 'react';
1213

1314
const imageHeaders = ApiController._getApiHeaders();
1415

@@ -25,6 +26,7 @@ interface Props {
2526
loadingItems?: number;
2627
style?: StyleProp<ViewStyle>;
2728
testIDKey?: string;
29+
searchQuery?: string;
2830
}
2931

3032
export function WalletList({
@@ -35,15 +37,77 @@ export function WalletList({
3537
isLoading = false,
3638
loadingItems = 20,
3739
testIDKey,
38-
style
40+
style,
41+
searchQuery
3942
}: Props) {
4043
const { padding, maxHeight } = useCustomDimensions();
44+
const viewedWalletsRef = useRef<Set<string>>(new Set());
4145

4246
// Create loading data if isLoading is true
4347
const displayData = isLoading
4448
? Array.from({ length: loadingItems }, (_, index) => ({ id: `loading-${index}` }) as WcWallet)
4549
: data;
4650

51+
const keyExtractor = useCallback(
52+
(item: WcWallet, index: number) => item?.id ?? `item-${index}`,
53+
[]
54+
);
55+
56+
const getItemLayout = useCallback((_: any, index: number) => {
57+
return {
58+
length: ITEM_HEIGHT_WITH_GAP,
59+
offset: ITEM_HEIGHT_WITH_GAP * index,
60+
index
61+
};
62+
}, []);
63+
64+
const renderItem = useCallback(
65+
({ item, index }: { item: WcWallet; index: number }) => {
66+
if (isLoading) {
67+
return <CardSelectLoader style={styles.itemContainer} />;
68+
}
69+
70+
return (
71+
<WalletItem
72+
item={item}
73+
imageHeaders={imageHeaders}
74+
displayIndex={index}
75+
onItemPress={onItemPress}
76+
style={styles.itemContainer}
77+
testID={testIDKey ? `${testIDKey}-${item?.id}` : undefined}
78+
/>
79+
);
80+
},
81+
[isLoading, onItemPress, testIDKey]
82+
);
83+
84+
const onViewableItemsChanged = useCallback(
85+
({ viewableItems }: { viewableItems: ViewToken[] }) => {
86+
if (isLoading) return;
87+
88+
viewableItems.forEach(({ item }, index) => {
89+
const wallet = item as WcWallet;
90+
if (wallet?.id && !viewedWalletsRef.current.has(wallet.id)) {
91+
viewedWalletsRef.current.add(wallet.id);
92+
const isInstalled = !!ApiController.state.installed.find(w => w?.id === item?.id);
93+
EventsController.trackWalletImpression({
94+
wallet,
95+
view: 'AllWallets',
96+
displayIndex: index,
97+
query: searchQuery,
98+
installed: isInstalled
99+
});
100+
}
101+
});
102+
},
103+
[isLoading, searchQuery]
104+
);
105+
106+
const viewabilityConfig = useRef({
107+
itemVisiblePercentThreshold: 50, // Item is considered visible when 50% is visible
108+
minimumViewTime: 100 // Must be visible for at least 100ms
109+
}).current;
110+
47111
return (
48112
<FlatList
49113
fadingEdgeLength={20}
@@ -52,34 +116,18 @@ export function WalletList({
52116
data={displayData}
53117
style={[styles.list, { height: maxHeight }, style]}
54118
columnWrapperStyle={styles.columnWrapperStyle}
55-
renderItem={({ item, index }) => {
56-
if (isLoading) {
57-
return <CardSelectLoader style={styles.itemContainer} />;
58-
}
59-
60-
return (
61-
<WalletItem
62-
item={item}
63-
imageHeaders={imageHeaders}
64-
displayIndex={index}
65-
onItemPress={onItemPress}
66-
style={styles.itemContainer}
67-
testID={testIDKey ? `${testIDKey}-${item?.id}` : undefined}
68-
/>
69-
);
70-
}}
119+
renderItem={renderItem}
71120
contentContainerStyle={[styles.contentContainer, { paddingHorizontal: padding }]}
72121
initialNumToRender={32}
73122
maxToRenderPerBatch={12}
74123
windowSize={10}
75124
onEndReached={onEndReached}
76125
onEndReachedThreshold={onEndReachedThreshold}
77-
keyExtractor={(item, index) => item?.id ?? `item-${index}`}
78-
getItemLayout={(_, index) => ({
79-
length: ITEM_HEIGHT_WITH_GAP,
80-
offset: ITEM_HEIGHT_WITH_GAP * index,
81-
index
82-
})}
126+
keyExtractor={keyExtractor}
127+
removeClippedSubviews={true}
128+
getItemLayout={getItemLayout}
129+
onViewableItemsChanged={onViewableItemsChanged}
130+
viewabilityConfig={viewabilityConfig}
83131
/>
84132
);
85133
}

packages/appkit/src/partials/w3m-all-wallets-list/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function AllWalletsList({ onItemPress }: AllWalletsListProps) {
116116
<WalletList
117117
data={walletList}
118118
onEndReached={fetchNextPage}
119-
onEndReachedThreshold={2}
119+
onEndReachedThreshold={0.5}
120120
onItemPress={onItemPress}
121121
/>
122122
);

packages/appkit/src/partials/w3m-all-wallets-search/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,12 @@ export function AllWalletsSearch({ searchQuery, onItemPress }: AllWalletsSearchP
8484
);
8585
}
8686

87-
return <WalletList onItemPress={onItemPress} data={results} testIDKey="wallet-search-item" />;
87+
return (
88+
<WalletList
89+
onItemPress={onItemPress}
90+
searchQuery={searchQuery}
91+
data={results}
92+
testIDKey="wallet-search-item"
93+
/>
94+
);
8895
}

packages/appkit/src/views/w3m-connect-view/components/all-wallet-list.tsx

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import {
44
ApiController,
55
AssetController,
66
AssetUtil,
7+
EventsController,
78
OptionsController,
89
WcController,
910
type WcControllerState
1011
} from '@reown/appkit-core-react-native';
1112
import { type WcWallet } from '@reown/appkit-common-react-native';
1213
import { ListItemLoader, ListWallet } from '@reown/appkit-ui-react-native';
1314
import { UiUtil } from '../../../utils/UiUtil';
15+
import { useEffect, useMemo, useRef } from 'react';
1416

1517
interface Props {
1618
itemStyle: StyleProp<ViewStyle>;
@@ -24,18 +26,45 @@ export function AllWalletList({ itemStyle, onWalletPress }: Props) {
2426
const { walletImages } = useSnapshot(AssetController.state);
2527
const imageHeaders = ApiController._getApiHeaders();
2628

27-
const combinedWallets = [
28-
...(recentWallets?.slice(0, 1) ?? []),
29-
...installed,
30-
...featured,
31-
...recommended,
32-
...(customWallets ?? [])
33-
];
29+
// Track which wallets have been tracked to prevent duplicates
30+
const trackedWalletsRef = useRef<Set<string>>(new Set());
3431

35-
// Deduplicate by wallet ID
36-
const list = Array.from(
37-
new Map(combinedWallets.map(wallet => [wallet.id, wallet])).values()
38-
).slice(0, UiUtil.TOTAL_VISIBLE_WALLETS);
32+
const list = useMemo(() => {
33+
const combinedWallets = [
34+
...(recentWallets?.slice(0, 1) ?? []),
35+
...installed,
36+
...featured,
37+
...recommended,
38+
...(customWallets ?? [])
39+
];
40+
41+
// Deduplicate by wallet ID
42+
return Array.from(new Map(combinedWallets.map(wallet => [wallet.id, wallet])).values()).slice(
43+
0,
44+
UiUtil.TOTAL_VISIBLE_WALLETS
45+
);
46+
}, [recentWallets, installed, featured, recommended, customWallets]);
47+
48+
// Track impressions once when the list stabilizes
49+
useEffect(() => {
50+
if (!prefetchLoading && list.length > 0) {
51+
list.forEach((wallet, index) => {
52+
if (!trackedWalletsRef.current.has(wallet.id)) {
53+
trackedWalletsRef.current.add(wallet.id);
54+
const isInstalled = !!ApiController.state.installed.find(
55+
installedWallet => installedWallet.id === wallet.id
56+
);
57+
EventsController.trackWalletImpression({
58+
wallet,
59+
view: 'Connect',
60+
displayIndex: index,
61+
// eslint-disable-next-line valtio/state-snapshot-rule
62+
installed: isInstalled
63+
});
64+
}
65+
});
66+
}
67+
}, [prefetchLoading, list]);
3968

4069
if (!list?.length) {
4170
return null;

packages/common/src/types/api/events.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,23 @@ import type {
1010
import type { Features } from '../ui';
1111
import type { Metadata } from '../wallet';
1212

13+
export type WalletImpressionItem = {
14+
name: string;
15+
walletRank: number | undefined;
16+
explorerId: string;
17+
view: 'Connect' | 'AllWallets';
18+
displayIndex?: number;
19+
query?: string;
20+
certified?: boolean;
21+
installed?: boolean;
22+
};
23+
1324
export type EventName =
1425
| 'MODAL_LOADED'
1526
| 'MODAL_OPEN'
1627
| 'MODAL_CLOSE'
1728
| 'INITIALIZE'
29+
| 'WALLET_IMPRESSION'
1830
| 'CLICK_ALL_WALLETS'
1931
| 'CLICK_NETWORKS'
2032
| 'SWITCH_NETWORK'
@@ -442,4 +454,9 @@ export type Event =
442454
properties?: {
443455
message?: string;
444456
};
457+
}
458+
| {
459+
type: 'track';
460+
event: 'WALLET_IMPRESSION';
461+
items: Array<WalletImpressionItem>;
445462
};

0 commit comments

Comments
 (0)