Skip to content

Commit 67eb0d4

Browse files
authored
App/use whitelisted addresses for points (#1408)
* feat: enhance PointHistoryList and ProveScreen with disclosure event handling - Updated PointHistoryList to include loading of disclosure events on refresh. - Enhanced ProveScreen to set user's points address if not already defined. - Added endpoint field to ProofHistory and database schema for better tracking. - Introduced utility functions for fetching whitelisted disclosure addresses and managing disclosure events. * fix: update navigation flow in PointsNavBar and GratificationScreen - Changed navigation action in PointsNavBar from `goBack` to `navigate('Home')` for a more direct user experience. - Updated GratificationScreen to navigate to 'Points' instead of going back, enhancing the flow after user interactions. - Replaced the ArrowLeft icon with a new X icon for better visual consistency.
1 parent 87a81a5 commit 67eb0d4

File tree

11 files changed

+215
-38
lines changed

11 files changed

+215
-38
lines changed

app/src/components/NavBar/PointsNavBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const PointsNavBar = (props: NativeStackHeaderProps) => {
3131
color={black}
3232
onPress={() => {
3333
buttonTap();
34-
props.navigation.goBack();
34+
props.navigation.navigate('Home');
3535
}}
3636
/>
3737
<View flex={1} alignItems="center" justifyContent="center">

app/src/components/PointHistoryList.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ export const PointHistoryList: React.FC<PointHistoryListProps> = ({
6868
}) => {
6969
const selfClient = useSelfClient();
7070
const [refreshing, setRefreshing] = useState(false);
71-
// Subscribe to events directly from store - component will auto-update when store changes
7271
const pointEvents = usePointEventStore(state => state.getAllPointEvents());
7372
const isLoading = usePointEventStore(state => state.isLoading);
7473
const refreshPoints = usePointEventStore(state => state.refreshPoints);
7574
const refreshIncomingPoints = usePointEventStore(
7675
state => state.refreshIncomingPoints,
7776
);
78-
// loadEvents only needs to be called once on mount.
79-
// and it is called in Points.ts
77+
const loadDisclosureEvents = usePointEventStore(
78+
state => state.loadDisclosureEvents,
79+
);
8080

8181
const formatDate = (timestamp: number) => {
8282
return new Date(timestamp).toLocaleTimeString([], {
@@ -271,14 +271,15 @@ export const PointHistoryList: React.FC<PointHistoryListProps> = ({
271271
[],
272272
);
273273

274-
// Pull-to-refresh handler
275274
const onRefresh = useCallback(() => {
276275
selfClient.trackEvent(PointEvents.REFRESH_HISTORY);
277276
setRefreshing(true);
278-
Promise.all([refreshPoints(), refreshIncomingPoints()]).finally(() =>
279-
setRefreshing(false),
280-
);
281-
}, [selfClient, refreshPoints, refreshIncomingPoints]);
277+
Promise.all([
278+
refreshPoints(),
279+
refreshIncomingPoints(),
280+
loadDisclosureEvents(),
281+
]).finally(() => setRefreshing(false));
282+
}, [selfClient, refreshPoints, refreshIncomingPoints, loadDisclosureEvents]);
282283

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

app/src/screens/app/GratificationScreen.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
1414
import { Text, View, YStack } from 'tamagui';
1515
import { useNavigation, useRoute } from '@react-navigation/native';
1616
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
17+
import { X } from '@tamagui/lucide-icons';
1718

1819
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
1920
import youWinAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/youWin.json';
2021
import { PrimaryButton } from '@selfxyz/mobile-sdk-alpha/components';
2122

2223
import GratificationBg from '@/images/gratification_bg.svg';
23-
import ArrowLeft from '@/images/icons/arrow_left.svg';
2424
import LogoWhite from '@/images/icons/logo_white.svg';
2525
import type { RootStackParamList } from '@/navigation';
2626
import { black, slate700, white } from '@/utils/colors';
@@ -46,7 +46,7 @@ const GratificationScreen: React.FC = () => {
4646
};
4747

4848
const handleBackPress = () => {
49-
navigation.goBack();
49+
navigation.navigate('Points' as never);
5050
};
5151

5252
const handleAnimationFinish = useCallback(() => {
@@ -129,7 +129,7 @@ const GratificationScreen: React.FC = () => {
129129
alignItems="center"
130130
justifyContent="center"
131131
>
132-
<ArrowLeft width={24} height={24} />
132+
<X width={24} height={24} />
133133
</View>
134134
</Pressable>
135135
</View>

app/src/screens/verification/ProofRequestStatusScreen.tsx

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
88
import { Linking, StyleSheet, View } from 'react-native';
99
import { SystemBars } from 'react-native-edge-to-edge';
1010
import { ScrollView, Spinner } from 'tamagui';
11-
import { useIsFocused } from '@react-navigation/native';
11+
import { useIsFocused, useNavigation } from '@react-navigation/native';
12+
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
1213

1314
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
1415
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
@@ -25,6 +26,7 @@ import failAnimation from '@/assets/animations/proof_failed.json';
2526
import succesAnimation from '@/assets/animations/proof_success.json';
2627
import useHapticNavigation from '@/hooks/useHapticNavigation';
2728
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
29+
import type { RootStackParamList } from '@/navigation';
2830
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
2931
import { ProofStatus } from '@/stores/proofTypes';
3032
import { black, white } from '@/utils/colors';
@@ -33,6 +35,7 @@ import {
3335
notificationError,
3436
notificationSuccess,
3537
} from '@/utils/haptic';
38+
import { getWhiteListedDisclosureAddresses } from '@/utils/points/utils';
3639

3740
const SuccessScreen: React.FC = () => {
3841
const selfClient = useSelfClient();
@@ -41,6 +44,8 @@ const SuccessScreen: React.FC = () => {
4144
const selfApp = useSelfAppStore(state => state.selfApp);
4245
const appName = selfApp?.appName;
4346
const goHome = useHapticNavigation('Home');
47+
const navigation =
48+
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
4449

4550
const { updateProofStatus } = useProofHistoryStore();
4651

@@ -55,15 +60,28 @@ const SuccessScreen: React.FC = () => {
5560
useState<LottieViewProps['source']>(loadingAnimation);
5661
const [countdown, setCountdown] = useState<number | null>(null);
5762
const [countdownStarted, setCountdownStarted] = useState(false);
63+
const [whitelistedPoints, setWhitelistedPoints] = useState<number | null>(
64+
null,
65+
);
5866
const timerRef = useRef<NodeJS.Timeout | null>(null);
5967

60-
const onOkPress = useCallback(() => {
68+
const onOkPress = useCallback(async () => {
6169
buttonTap();
62-
goHome();
63-
setTimeout(() => {
64-
selfClient.getSelfAppState().cleanSelfApp();
65-
}, 2000); // Wait 2 seconds to user coming back to the home screen. If we don't wait the appname will change and user will see it.
66-
}, [goHome, selfClient]);
70+
71+
if (whitelistedPoints !== null) {
72+
navigation.navigate('Gratification', {
73+
points: whitelistedPoints,
74+
});
75+
setTimeout(() => {
76+
selfClient.getSelfAppState().cleanSelfApp();
77+
}, 2000);
78+
} else {
79+
goHome();
80+
setTimeout(() => {
81+
selfClient.getSelfAppState().cleanSelfApp();
82+
}, 2000);
83+
}
84+
}, [whitelistedPoints, navigation, goHome, selfClient]);
6785

6886
function cancelDeeplinkCallbackRedirect() {
6987
setCountdown(null);
@@ -88,7 +106,28 @@ const SuccessScreen: React.FC = () => {
88106
sessionId,
89107
appName,
90108
});
91-
// Start countdown for redirect (only if we are on this screen and haven't started yet)
109+
110+
if (selfApp?.endpoint && whitelistedPoints === null) {
111+
const checkWhitelist = async () => {
112+
try {
113+
const whitelistedContracts =
114+
await getWhiteListedDisclosureAddresses();
115+
const endpoint = selfApp.endpoint.toLowerCase();
116+
const whitelistedContract = whitelistedContracts.find(
117+
c => c.contract_address.toLowerCase() === endpoint,
118+
);
119+
120+
if (whitelistedContract) {
121+
setWhitelistedPoints(whitelistedContract.points_per_disclosure);
122+
}
123+
} catch (error) {
124+
console.error('Error checking whitelist:', error);
125+
}
126+
};
127+
128+
checkWhitelist();
129+
}
130+
92131
if (isFocused && !countdownStarted && selfApp?.deeplinkCallback) {
93132
if (selfApp?.deeplinkCallback) {
94133
try {
@@ -133,7 +172,9 @@ const SuccessScreen: React.FC = () => {
133172
reason,
134173
updateProofStatus,
135174
selfApp?.deeplinkCallback,
175+
selfApp?.endpoint,
136176
countdownStarted,
177+
whitelistedPoints,
137178
]);
138179

139180
useEffect(() => {

app/src/screens/verification/ProveScreen.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { ProofStatus } from '@/stores/proofTypes';
4444
import { black, slate300, white } from '@/utils/colors';
4545
import { formatUserId } from '@/utils/formatUserId';
4646
import { buttonTap } from '@/utils/haptic';
47+
import { getPointsAddress } from '@/utils/points/utils';
4748

4849
const ProveScreen: React.FC = () => {
4950
const selfClient = useSelfClient();
@@ -83,11 +84,12 @@ const ProveScreen: React.FC = () => {
8384
sessionId: provingStore.uuid!,
8485
userId: selectedApp.userId,
8586
userIdType: selectedApp.userIdType,
87+
endpoint: selectedApp.endpoint,
8688
endpointType: selectedApp.endpointType,
8789
status: ProofStatus.PENDING,
8890
logoBase64: selectedApp.logoBase64,
8991
disclosures: JSON.stringify(selectedApp.disclosures),
90-
documentId: selectedDocumentId || '', // Fallback to empty if none selected
92+
documentId: selectedDocumentId || '',
9193
});
9294
}
9395
};
@@ -115,6 +117,29 @@ const ProveScreen: React.FC = () => {
115117
selectedAppRef.current = selectedApp;
116118
}, [selectedApp, isFocused, provingStore, selfClient]);
117119

120+
// Enhance selfApp with user's points address if not already set
121+
useEffect(() => {
122+
console.log('useEffect selectedApp', selectedApp);
123+
if (!selectedApp || selectedApp.selfDefinedData) {
124+
return;
125+
}
126+
127+
const enhanceApp = async () => {
128+
const address = await getPointsAddress();
129+
130+
// Only update if still the same session
131+
if (selectedAppRef.current?.sessionId === selectedApp.sessionId) {
132+
console.log('enhancing app with points address', address);
133+
selfClient.getSelfAppState().setSelfApp({
134+
...selectedApp,
135+
selfDefinedData: address.toLowerCase(),
136+
});
137+
}
138+
};
139+
140+
enhanceApp();
141+
}, [selectedApp, selfClient]);
142+
118143
const disclosureOptions = useMemo(() => {
119144
return (selectedApp?.disclosures as SelfAppDisclosureConfig) || [];
120145
}, [selectedApp?.disclosures]);

app/src/stores/database.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const database: ProofDB = {
8787
sessionId TEXT NOT NULL UNIQUE,
8888
userId TEXT NOT NULL,
8989
userIdType TEXT NOT NULL,
90+
endpoint TEXT,
9091
endpointType TEXT NOT NULL,
9192
status TEXT NOT NULL,
9293
errorCode TEXT,
@@ -108,10 +109,11 @@ export const database: ProofDB = {
108109

109110
try {
110111
const [insertResult] = await db.executeSql(
111-
`INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
112-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
112+
`INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpoint, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
113+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
113114
[
114115
proof.appName,
116+
proof.endpoint || null,
115117
proof.endpointType,
116118
proof.status,
117119
proof.errorCode || null,
@@ -133,12 +135,40 @@ export const database: ProofDB = {
133135
} catch (error) {
134136
if ((error as Error).message.includes('no column named documentId')) {
135137
await addDocumentIdColumn();
136-
// Then retry the insert (copy the executeSql call here)
137138
const [insertResult] = await db.executeSql(
138-
`INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
139-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
139+
`INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpoint, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
140+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
140141
[
141142
proof.appName,
143+
proof.endpoint || null,
144+
proof.endpointType,
145+
proof.status,
146+
proof.errorCode || null,
147+
proof.errorReason || null,
148+
timestamp,
149+
proof.disclosures,
150+
proof.logoBase64 || null,
151+
proof.userId,
152+
proof.userIdType,
153+
proof.sessionId,
154+
proof.documentId,
155+
],
156+
);
157+
return {
158+
id: insertResult.insertId.toString(),
159+
timestamp,
160+
rowsAffected: insertResult.rowsAffected,
161+
};
162+
} else if (
163+
(error as Error).message.includes('no column named endpoint')
164+
) {
165+
await addEndpointColumn();
166+
const [insertResult] = await db.executeSql(
167+
`INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpoint, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
168+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
169+
[
170+
proof.appName,
171+
proof.endpoint || null,
142172
proof.endpointType,
143173
proof.status,
144174
proof.errorCode || null,
@@ -184,3 +214,8 @@ async function addDocumentIdColumn() {
184214
`ALTER TABLE ${TABLE_NAME} ADD COLUMN documentId TEXT NOT NULL DEFAULT ''`,
185215
);
186216
}
217+
218+
async function addEndpointColumn() {
219+
const db = await openDatabase();
220+
await db.executeSql(`ALTER TABLE ${TABLE_NAME} ADD COLUMN endpoint TEXT`);
221+
}

app/src/stores/pointEventStore.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface PointEventState {
2222
events: PointEvent[];
2323
isLoading: boolean;
2424
loadEvents: () => Promise<void>;
25+
loadDisclosureEvents: () => Promise<void>;
2526
addEvent: (
2627
title: string,
2728
type: PointEventType,
@@ -38,7 +39,6 @@ interface PointEventState {
3839
lastUpdated: number | null;
3940
promise: Promise<IncomingPoints | null> | null;
4041
};
41-
// these are the real points that are on chain. each sunday noon UTC they get updated based on incoming points
4242
points: number;
4343
refreshPoints: () => Promise<void>;
4444
fetchIncomingPoints: () => Promise<IncomingPoints | null>;
@@ -82,13 +82,11 @@ export const usePointEventStore = create<PointEventState>()((set, get) => ({
8282
if (stored) {
8383
try {
8484
const parsed = JSON.parse(stored);
85-
// Validate that parsed data is an array
8685
if (!Array.isArray(parsed)) {
8786
console.error('Invalid stored events format, expected array');
8887
set({ events: [], isLoading: false });
8988
return;
9089
}
91-
// Validate each event has required fields
9290
const events: PointEvent[] = parsed.filter((event: unknown) => {
9391
if (
9492
typeof event === 'object' &&
@@ -103,12 +101,9 @@ export const usePointEventStore = create<PointEventState>()((set, get) => ({
103101
return false;
104102
}) as PointEvent[];
105103
set({ events, isLoading: false });
106-
// Resume polling for any pending events that were interrupted by app restart
107-
// (New events are polled immediately in recordEvents.ts when created)
108104
get()
109105
.getUnprocessedEvents()
110106
.forEach(event => {
111-
// Use event.id as job_id (id is the job_id)
112107
pollEventProcessingStatus(event.id).then(result => {
113108
if (result === 'completed') {
114109
get().markEventAsProcessed(event.id);
@@ -119,19 +114,36 @@ export const usePointEventStore = create<PointEventState>()((set, get) => ({
119114
});
120115
} catch (parseError) {
121116
console.error('Error parsing stored events:', parseError);
122-
// Clear corrupted data
123117
await AsyncStorage.removeItem(STORAGE_KEY);
124118
set({ events: [], isLoading: false });
125119
}
126120
} else {
127121
set({ isLoading: false });
128122
}
123+
await get().loadDisclosureEvents();
129124
} catch (error) {
130125
console.error('Error loading point events:', error);
131126
set({ isLoading: false });
132127
}
133128
},
134129

130+
loadDisclosureEvents: async () => {
131+
try {
132+
const { getDisclosurePointEvents } = await import(
133+
'@/utils/points/getEvents'
134+
);
135+
const { useProofHistoryStore } = await import(
136+
'@/stores/proofHistoryStore'
137+
);
138+
await useProofHistoryStore.getState().initDatabase();
139+
const disclosureEvents = await getDisclosurePointEvents();
140+
const existingEvents = get().events.filter(e => e.type !== 'disclosure');
141+
set({ events: [...existingEvents, ...disclosureEvents] });
142+
} catch (error) {
143+
console.error('Error loading disclosure events:', error);
144+
}
145+
},
146+
135147
fetchIncomingPoints: async () => {
136148
if (get().incomingPoints.promise) {
137149
return await get().incomingPoints.promise;

0 commit comments

Comments
 (0)