Skip to content
21 changes: 2 additions & 19 deletions app/src/components/NavBar/Points.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Pressable, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, Image, Text, View, XStack, YStack, ZStack } from 'tamagui';
Expand Down Expand Up @@ -54,14 +54,12 @@ const Points: React.FC = () => {
const [isNovaSubscribed, setIsNovaSubscribed] = useState(false);
const [isEnabling, setIsEnabling] = useState(false);
const incomingPoints = useIncomingPoints();
const { amount: points } = usePoints();
const loadEvents = usePointEventStore(state => state.loadEvents);
const { hasCompletedBackupForPoints, setBackupForPointsCompleted } =
useSettingStore();
const [isBackingUp, setIsBackingUp] = useState(false);

// Ref to trigger list refresh
const listRefreshRef = useRef<(() => Promise<void>) | null>(null);

const [isContentReady, setIsContentReady] = useState(false);
const [isFocused, setIsFocused] = useState(false);

Expand Down Expand Up @@ -114,10 +112,6 @@ const Points: React.FC = () => {
useSettingStore.getState().setBackupForPointsCompleted();
selfClient.trackEvent(PointEvents.EARN_BACKUP_SUCCESS);

if (listRefreshRef.current) {
await listRefreshRef.current();
}

const callbackId = registerModalCallbacks({
onButtonPress: () => {},
onModalDismiss: () => {},
Expand Down Expand Up @@ -156,8 +150,6 @@ const Points: React.FC = () => {
loadEvents();
}, [loadEvents]);

const points = usePoints();

useEffect(() => {
const checkSubscription = async () => {
const subscribed = await isTopicSubscribed('nova');
Expand Down Expand Up @@ -188,10 +180,6 @@ const Points: React.FC = () => {
setIsNovaSubscribed(true);
selfClient.trackEvent(PointEvents.EARN_NOTIFICATION_SUCCESS);

if (listRefreshRef.current) {
await listRefreshRef.current();
}

const callbackId = registerModalCallbacks({
onButtonPress: () => {},
onModalDismiss: () => {},
Expand Down Expand Up @@ -294,10 +282,6 @@ const Points: React.FC = () => {
setBackupForPointsCompleted();
selfClient.trackEvent(PointEvents.EARN_BACKUP_SUCCESS);

if (listRefreshRef.current) {
await listRefreshRef.current();
}
Comment on lines -297 to -299
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looked like a no op


const callbackId = registerModalCallbacks({
onButtonPress: () => {},
onModalDismiss: () => {},
Expand Down Expand Up @@ -441,7 +425,6 @@ const Points: React.FC = () => {
<ZStack flex={1}>
<PointHistoryList
ListHeaderComponent={ListHeader}
onRefreshRef={listRefreshRef}
onLayout={handleContentLayout}
/>
{isContentReady && isFocused && (
Expand Down
32 changes: 11 additions & 21 deletions app/src/components/PointHistoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator,
RefreshControl,
Expand Down Expand Up @@ -37,7 +37,6 @@ export type PointHistoryListProps = {
| React.ComponentType<Record<string, unknown>>
| React.ReactElement
| null;
onRefreshRef?: React.MutableRefObject<(() => Promise<void>) | null>;
onLayout?: () => void;
};

Expand All @@ -62,28 +61,19 @@ const getIconForEventType = (type: PointEvent['type']) => {

export const PointHistoryList: React.FC<PointHistoryListProps> = ({
ListHeaderComponent,
onRefreshRef,
onLayout,
}) => {
const [refreshing, setRefreshing] = useState(false);
// Subscribe to events directly from store - component will auto-update when store changes
const pointEvents = usePointEventStore(state => state.getAllPointEvents());
const isLoading = usePointEventStore(state => state.isLoading);
// loadEvents only needs to be called once on mount. ev
const refreshPoints = usePointEventStore(state => state.refreshPoints);
const refreshIncomingPoints = usePointEventStore(
state => state.refreshIncomingPoints,
);
// loadEvents only needs to be called once on mount.
// and it is called in Points.ts

// Expose no-op refresh function to parent via ref for backward compatibility
// Component auto-updates via Zustand, so manual refresh is not needed
// Note: We don't call loadEvents() here as it could cause data loss if called
// while events are being saved (loadEvents should only run at app startup)
useEffect(() => {
if (onRefreshRef) {
onRefreshRef.current = async () => {
// No-op: component auto-updates when store changes
};
}
}, [onRefreshRef]);

const formatDate = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
Expand Down Expand Up @@ -277,13 +267,13 @@ export const PointHistoryList: React.FC<PointHistoryListProps> = ({
[],
);

// Pull-to-refresh handler - currently a no-op
// TODO: Implement refresh logic after merge
// Pull-to-refresh handler
const onRefresh = useCallback(() => {
setRefreshing(true);
// Placeholder for future refresh logic
setTimeout(() => setRefreshing(false), 500);
}, []);
Promise.all([refreshPoints(), refreshIncomingPoints()]).finally(() =>
setRefreshing(false),
);
}, [refreshPoints, refreshIncomingPoints]);

const keyExtractor = useCallback((item: PointEvent) => item.id, []);

Expand Down
69 changes: 28 additions & 41 deletions app/src/hooks/usePoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,52 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

import { useEffect, useState } from 'react';
import { useEffect } from 'react';

import { usePointEventStore } from '@/stores/pointEventStore';
import {
getIncomingPoints,
getNextSundayNoonUTC,
getPointsAddress,
getTotalPoints,
type IncomingPoints,
} from '@/utils/points';
import { getNextSundayNoonUTC, type IncomingPoints } from '@/utils/points';

/*
* Hook to fetch incoming points for the user. It refetches the incoming points when there is a new event in the point events store.
* Hook to get incoming points for the user. It shows the optimistic incoming points.
* Refreshes incoming points once on mount.
*/
export const useIncomingPoints = (): IncomingPoints | null => {
const [incomingPoints, setIncomingPoints] = useState<null | IncomingPoints>(
null,
export const useIncomingPoints = (): IncomingPoints => {
const incomingPoints = usePointEventStore(state => state.incomingPoints);
const totalOptimisticIncomingPoints = usePointEventStore(state =>
state.totalOptimisticIncomingPoints(),
);
const refreshIncomingPoints = usePointEventStore(
state => state.refreshIncomingPoints,
);
const pointEvents = usePointEventStore(state => state.events);
const pointEventsCount = pointEvents.length;

useEffect(() => {
const fetchIncomingPoints = async () => {
try {
const points = await getIncomingPoints();
setIncomingPoints(points);
} catch (error) {
console.error('Error fetching incoming points:', error);
}
};
fetchIncomingPoints();
// when we record a new point event, we want to refetch incoming points
}, [pointEventsCount]);

return incomingPoints;
// Only refresh once on mount - the store handles promise caching for concurrent calls
refreshIncomingPoints();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Empty deps: only run once on mount

return {
amount: totalOptimisticIncomingPoints,
expectedDate: incomingPoints.expectedDate,
};
};

/*
* Hook to fetch total points for the user. It refetches the total points when the next points update time is reached (each Sunday noon UTC).
*/
export const usePoints = () => {
const [truePoints, setTruePoints] = useState({
points: 0,
});
const points = usePointEventStore(state => state.points);
const nextPointsUpdate = getNextSundayNoonUTC().getTime();
const refreshPoints = usePointEventStore(state => state.refreshPoints);

useEffect(() => {
const fetchPoints = async () => {
try {
const address = await getPointsAddress();
const points = await getTotalPoints(address);
setTruePoints({ points });
} catch (error) {
console.error('Error fetching total points:', error);
}
};
fetchPoints();
refreshPoints();
// refresh when points update time changes as its the only time points can change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nextPointsUpdate]);

return truePoints.points;
return {
amount: points,
refetch: refreshPoints,
};
};
2 changes: 1 addition & 1 deletion app/src/screens/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const HomeScreen: React.FC = () => {
>({});
const [loading, setLoading] = useState(true);

const selfPoints = usePoints();
const { amount: selfPoints } = usePoints();

// Calculate card dimensions exactly like IdCardLayout does
const { width: screenWidth } = Dimensions.get('window');
Expand Down
Loading
Loading