Skip to content

Commit a928ada

Browse files
committed
new price percent feature
1 parent f088cd1 commit a928ada

10 files changed

Lines changed: 1484 additions & 226 deletions

File tree

app/components/UI/Assets/PriceAlerts/Views/CreatePriceAlertView/CreatePriceAlertView.tsx

Lines changed: 491 additions & 154 deletions
Large diffs are not rendered by default.

app/components/UI/Assets/PriceAlerts/Views/ManagePriceAlertsView/ManagePriceAlertsView.tsx

Lines changed: 90 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,39 @@ import { IconName } from '../../../../../../component-library/components/Icons/I
4646
import Routes from '../../../../../../constants/navigation/Routes';
4747
import { formatPriceWithSubscriptNotation } from '../../../../Predict/utils/format';
4848
import {
49+
Alert,
4950
ManagePriceAlertsTestIds,
50-
PriceAlert,
5151
PriceAlertRouteParams,
5252
PriceAlertAnalytics,
5353
} from '../../constants';
5454
import {
55+
deleteAlertByType,
5556
fetchAlerts,
56-
deleteAlert,
57-
updateAlert,
57+
normalizeAlerts,
5858
priceAlertsQueryKey,
59+
updateAlertByType,
5960
} from '../../api';
61+
import {
62+
formatPercentAlertSubtitle,
63+
formatPercentAlertTitle,
64+
} from '../../utils';
6065
import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics';
6166
import { MetaMetricsEvents } from '../../../../../../core/Analytics';
6267

6368
const styles = StyleSheet.create({
6469
switchDisabled: { opacity: 0.5 },
6570
});
6671

72+
/** Analytics `alert_type` + `period`/`direction` properties for a given alert. */
73+
const analyticsPropsForAlert = (alert: Alert) =>
74+
alert.type === 'percent_change'
75+
? {
76+
alert_type: PriceAlertAnalytics.TYPE.PERCENT,
77+
period: alert.period,
78+
direction: alert.direction,
79+
}
80+
: { alert_type: PriceAlertAnalytics.TYPE.THRESHOLD };
81+
6782
const ManagePriceAlertsView: React.FC = () => {
6883
const tw = useTailwind();
6984
const { colors, brandColors } = useTheme();
@@ -98,10 +113,11 @@ const ManagePriceAlertsView: React.FC = () => {
98113
isError,
99114
} = useQuery({
100115
queryKey: priceAlertsQueryKey(assetId),
101-
queryFn: async (): Promise<PriceAlert[]> => {
116+
queryFn: async (): Promise<Alert[]> => {
102117
const response = await fetchAlerts(assetId);
103118
if (!response.ok) throw new Error(`HTTP ${response.status}`);
104-
return response.json();
119+
const body: unknown[] = await response.json();
120+
return normalizeAlerts(body);
105121
},
106122
retry: false,
107123
staleTime: 0,
@@ -150,16 +166,22 @@ const ManagePriceAlertsView: React.FC = () => {
150166
}, [navigation]);
151167

152168
const handleNavigateToCreate = useCallback(
153-
(editingAlert?: PriceAlert) => {
169+
(editingAlert?: Alert) => {
154170
navigation.navigate(Routes.CREATE_PRICE_ALERT, {
155171
symbol,
156172
ticker,
157173
currentPrice,
158174
currentCurrency,
159175
assetId,
160176
fromManage: true,
161-
existingThresholds: alerts.map((a) => a.threshold),
177+
existingThresholds: alerts
178+
.filter((a) => a.type === 'absolute_price')
179+
.map((a) => a.threshold),
180+
existingPercentAlerts: alerts.filter(
181+
(a) => a.type === 'percent_change',
182+
),
162183
editingAlert,
184+
initialType: editingAlert?.type,
163185
});
164186
},
165187
[
@@ -180,30 +202,27 @@ const ManagePriceAlertsView: React.FC = () => {
180202
setDeletingIds((prev) => new Set(prev).add(id));
181203

182204
const queryKey = priceAlertsQueryKey(assetId);
183-
const previous = queryClient.getQueryData<PriceAlert[]>(queryKey) ?? [];
205+
const previous = queryClient.getQueryData<Alert[]>(queryKey) ?? [];
206+
const target = previous.find((a) => a.id === id);
184207

185208
try {
186-
const response = await deleteAlert(id);
209+
if (!target) throw new Error('Alert not found');
210+
const response = await deleteAlertByType(target);
187211
if (!response.ok) throw new Error(`HTTP ${response.status}`);
188212

189-
const deleted = previous.find((a) => a.id === id);
190-
if (deleted) {
191-
trackEvent(
192-
createEventBuilder(
193-
MetaMetricsEvents.PRICE_ALERT_CREATION_INTERACTION,
194-
)
195-
.addProperties({
196-
interaction_type: PriceAlertAnalytics.INTERACTION_TYPE.DELETED,
197-
asset_id: assetId,
198-
token_symbol: displayTicker,
199-
alert_type: PriceAlertAnalytics.TYPE.THRESHOLD,
200-
alert_value: deleted.threshold,
201-
alert_recurring: deleted.recurring,
202-
alert_active: deleted.active,
203-
})
204-
.build(),
205-
);
206-
}
213+
trackEvent(
214+
createEventBuilder(MetaMetricsEvents.PRICE_ALERT_CREATION_INTERACTION)
215+
.addProperties({
216+
interaction_type: PriceAlertAnalytics.INTERACTION_TYPE.DELETED,
217+
asset_id: assetId,
218+
token_symbol: displayTicker,
219+
...analyticsPropsForAlert(target),
220+
alert_value: target.threshold,
221+
alert_recurring: target.recurring,
222+
alert_active: target.active,
223+
})
224+
.build(),
225+
);
207226

208227
const next = previous.filter((a) => a.id !== id);
209228
queryClient.setQueryData(queryKey, next);
@@ -227,8 +246,8 @@ const ManagePriceAlertsView: React.FC = () => {
227246
});
228247
const response = await fetchAlerts(assetId).catch(() => null);
229248
if (response?.ok) {
230-
const data: PriceAlert[] = await response.json().catch(() => []);
231-
queryClient.setQueryData(queryKey, data);
249+
const body: unknown[] = await response.json().catch(() => []);
250+
queryClient.setQueryData(queryKey, normalizeAlerts(body));
232251
} else {
233252
queryClient.setQueryData(queryKey, previous);
234253
}
@@ -260,37 +279,36 @@ const ManagePriceAlertsView: React.FC = () => {
260279
setTogglingIds((prev) => new Set(prev).add(id));
261280

262281
const queryKey = priceAlertsQueryKey(assetId);
263-
const previous = queryClient.getQueryData<PriceAlert[]>(queryKey) ?? [];
282+
const previous = queryClient.getQueryData<Alert[]>(queryKey) ?? [];
264283
const toggled = previous.find((a) => a.id === id);
265284
queryClient.setQueryData(
266285
queryKey,
267286
previous.map((a) => (a.id === id ? { ...a, active: newValue } : a)),
268287
);
269288

270289
try {
271-
const response = await updateAlert(id, { active: newValue });
290+
if (!toggled) throw new Error('Alert not found');
291+
const response = await updateAlertByType(toggled, {
292+
active: newValue,
293+
});
272294
if (!response.ok) throw new Error(`HTTP ${response.status}`);
273295

274-
if (toggled) {
275-
trackEvent(
276-
createEventBuilder(
277-
MetaMetricsEvents.PRICE_ALERT_CREATION_INTERACTION,
278-
)
279-
.addProperties({
280-
interaction_type: PriceAlertAnalytics.INTERACTION_TYPE.UPDATED,
281-
asset_id: assetId,
282-
token_symbol: displayTicker,
283-
alert_type: PriceAlertAnalytics.TYPE.THRESHOLD,
284-
alert_value: toggled.threshold,
285-
alert_recurring: toggled.recurring,
286-
alert_active: newValue,
287-
prev_alert_value: toggled.threshold,
288-
prev_alert_recurring: toggled.recurring,
289-
prev_alert_active: toggled.active,
290-
})
291-
.build(),
292-
);
293-
}
296+
trackEvent(
297+
createEventBuilder(MetaMetricsEvents.PRICE_ALERT_CREATION_INTERACTION)
298+
.addProperties({
299+
interaction_type: PriceAlertAnalytics.INTERACTION_TYPE.UPDATED,
300+
asset_id: assetId,
301+
token_symbol: displayTicker,
302+
...analyticsPropsForAlert(toggled),
303+
alert_value: toggled.threshold,
304+
alert_recurring: toggled.recurring,
305+
alert_active: newValue,
306+
prev_alert_value: toggled.threshold,
307+
prev_alert_recurring: toggled.recurring,
308+
prev_alert_active: toggled.active,
309+
})
310+
.build(),
311+
);
294312
} catch {
295313
toastRef?.current?.showToast({
296314
variant: ToastVariants.Icon,
@@ -323,10 +341,28 @@ const ManagePriceAlertsView: React.FC = () => {
323341
],
324342
);
325343

326-
const renderItem = ({ item }: { item: PriceAlert }) => {
344+
const renderItem = ({ item }: { item: Alert }) => {
327345
const isDeleting = deletingIds.has(item.id);
328346
const isToggling = togglingIds.has(item.id);
329347

348+
const title =
349+
item.type === 'percent_change'
350+
? formatPercentAlertTitle(item)
351+
: strings('price_alerts.reaches_threshold', {
352+
threshold: formatPriceWithSubscriptNotation(
353+
item.threshold,
354+
currentCurrency,
355+
{ maximumFractionDigits: 15 },
356+
),
357+
});
358+
359+
const subtitle =
360+
item.type === 'percent_change'
361+
? formatPercentAlertSubtitle(item)
362+
: item.recurring
363+
? strings('price_alerts.recurring')
364+
: strings('price_alerts.once_label');
365+
330366
return (
331367
<Box
332368
flexDirection={BoxFlexDirection.Row}
@@ -342,18 +378,10 @@ const ManagePriceAlertsView: React.FC = () => {
342378
testID={`${ManagePriceAlertsTestIds.ALERT_EDIT_PREFIX}-${item.id}`}
343379
>
344380
<Text variant={TextVariant.BodyMd} color={TextColor.TextDefault}>
345-
{strings('price_alerts.reaches_threshold', {
346-
threshold: formatPriceWithSubscriptNotation(
347-
item.threshold,
348-
currentCurrency,
349-
{ maximumFractionDigits: 15 },
350-
),
351-
})}
381+
{title}
352382
</Text>
353383
<Text variant={TextVariant.BodySm} color={TextColor.TextAlternative}>
354-
{item.recurring
355-
? strings('price_alerts.recurring')
356-
: strings('price_alerts.once_label')}
384+
{subtitle}
357385
</Text>
358386
</TouchableOpacity>
359387

0 commit comments

Comments
 (0)