diff --git a/app/src/components/NavBar/PointsNavBar.tsx b/app/src/components/NavBar/PointsNavBar.tsx
index 5d008850a..1283d9693 100644
--- a/app/src/components/NavBar/PointsNavBar.tsx
+++ b/app/src/components/NavBar/PointsNavBar.tsx
@@ -31,7 +31,7 @@ export const PointsNavBar = (props: NativeStackHeaderProps) => {
color={black}
onPress={() => {
buttonTap();
- props.navigation.goBack();
+ props.navigation.navigate('Home');
}}
/>
diff --git a/app/src/components/PointHistoryList.tsx b/app/src/components/PointHistoryList.tsx
index 5c00bdba4..0e7c32616 100644
--- a/app/src/components/PointHistoryList.tsx
+++ b/app/src/components/PointHistoryList.tsx
@@ -68,15 +68,15 @@ export const PointHistoryList: React.FC = ({
}) => {
const selfClient = useSelfClient();
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);
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
+ const loadDisclosureEvents = usePointEventStore(
+ state => state.loadDisclosureEvents,
+ );
const formatDate = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString([], {
@@ -271,14 +271,15 @@ export const PointHistoryList: React.FC = ({
[],
);
- // Pull-to-refresh handler
const onRefresh = useCallback(() => {
selfClient.trackEvent(PointEvents.REFRESH_HISTORY);
setRefreshing(true);
- Promise.all([refreshPoints(), refreshIncomingPoints()]).finally(() =>
- setRefreshing(false),
- );
- }, [selfClient, refreshPoints, refreshIncomingPoints]);
+ Promise.all([
+ refreshPoints(),
+ refreshIncomingPoints(),
+ loadDisclosureEvents(),
+ ]).finally(() => setRefreshing(false));
+ }, [selfClient, refreshPoints, refreshIncomingPoints, loadDisclosureEvents]);
const keyExtractor = useCallback((item: PointEvent) => item.id, []);
diff --git a/app/src/screens/app/GratificationScreen.tsx b/app/src/screens/app/GratificationScreen.tsx
index 8175beed1..20f5fca07 100644
--- a/app/src/screens/app/GratificationScreen.tsx
+++ b/app/src/screens/app/GratificationScreen.tsx
@@ -14,13 +14,13 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Text, View, YStack } from 'tamagui';
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { X } from '@tamagui/lucide-icons';
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
import youWinAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/youWin.json';
import { PrimaryButton } from '@selfxyz/mobile-sdk-alpha/components';
import GratificationBg from '@/images/gratification_bg.svg';
-import ArrowLeft from '@/images/icons/arrow_left.svg';
import LogoWhite from '@/images/icons/logo_white.svg';
import type { RootStackParamList } from '@/navigation';
import { black, slate700, white } from '@/utils/colors';
@@ -46,7 +46,7 @@ const GratificationScreen: React.FC = () => {
};
const handleBackPress = () => {
- navigation.goBack();
+ navigation.navigate('Points' as never);
};
const handleAnimationFinish = useCallback(() => {
@@ -129,7 +129,7 @@ const GratificationScreen: React.FC = () => {
alignItems="center"
justifyContent="center"
>
-
+
diff --git a/app/src/screens/verification/ProofRequestStatusScreen.tsx b/app/src/screens/verification/ProofRequestStatusScreen.tsx
index 22fe04ef0..77b1fcea5 100644
--- a/app/src/screens/verification/ProofRequestStatusScreen.tsx
+++ b/app/src/screens/verification/ProofRequestStatusScreen.tsx
@@ -8,7 +8,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Linking, StyleSheet, View } from 'react-native';
import { SystemBars } from 'react-native-edge-to-edge';
import { ScrollView, Spinner } from 'tamagui';
-import { useIsFocused } from '@react-navigation/native';
+import { useIsFocused, useNavigation } from '@react-navigation/native';
+import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
@@ -25,6 +26,7 @@ import failAnimation from '@/assets/animations/proof_failed.json';
import succesAnimation from '@/assets/animations/proof_success.json';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
+import type { RootStackParamList } from '@/navigation';
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
import { ProofStatus } from '@/stores/proofTypes';
import { black, white } from '@/utils/colors';
@@ -33,6 +35,7 @@ import {
notificationError,
notificationSuccess,
} from '@/utils/haptic';
+import { getWhiteListedDisclosureAddresses } from '@/utils/points/utils';
const SuccessScreen: React.FC = () => {
const selfClient = useSelfClient();
@@ -41,6 +44,8 @@ const SuccessScreen: React.FC = () => {
const selfApp = useSelfAppStore(state => state.selfApp);
const appName = selfApp?.appName;
const goHome = useHapticNavigation('Home');
+ const navigation =
+ useNavigation>();
const { updateProofStatus } = useProofHistoryStore();
@@ -55,15 +60,28 @@ const SuccessScreen: React.FC = () => {
useState(loadingAnimation);
const [countdown, setCountdown] = useState(null);
const [countdownStarted, setCountdownStarted] = useState(false);
+ const [whitelistedPoints, setWhitelistedPoints] = useState(
+ null,
+ );
const timerRef = useRef(null);
- const onOkPress = useCallback(() => {
+ const onOkPress = useCallback(async () => {
buttonTap();
- goHome();
- setTimeout(() => {
- selfClient.getSelfAppState().cleanSelfApp();
- }, 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.
- }, [goHome, selfClient]);
+
+ if (whitelistedPoints !== null) {
+ navigation.navigate('Gratification', {
+ points: whitelistedPoints,
+ });
+ setTimeout(() => {
+ selfClient.getSelfAppState().cleanSelfApp();
+ }, 2000);
+ } else {
+ goHome();
+ setTimeout(() => {
+ selfClient.getSelfAppState().cleanSelfApp();
+ }, 2000);
+ }
+ }, [whitelistedPoints, navigation, goHome, selfClient]);
function cancelDeeplinkCallbackRedirect() {
setCountdown(null);
@@ -88,7 +106,28 @@ const SuccessScreen: React.FC = () => {
sessionId,
appName,
});
- // Start countdown for redirect (only if we are on this screen and haven't started yet)
+
+ if (selfApp?.endpoint && whitelistedPoints === null) {
+ const checkWhitelist = async () => {
+ try {
+ const whitelistedContracts =
+ await getWhiteListedDisclosureAddresses();
+ const endpoint = selfApp.endpoint.toLowerCase();
+ const whitelistedContract = whitelistedContracts.find(
+ c => c.contract_address.toLowerCase() === endpoint,
+ );
+
+ if (whitelistedContract) {
+ setWhitelistedPoints(whitelistedContract.points_per_disclosure);
+ }
+ } catch (error) {
+ console.error('Error checking whitelist:', error);
+ }
+ };
+
+ checkWhitelist();
+ }
+
if (isFocused && !countdownStarted && selfApp?.deeplinkCallback) {
if (selfApp?.deeplinkCallback) {
try {
@@ -133,7 +172,9 @@ const SuccessScreen: React.FC = () => {
reason,
updateProofStatus,
selfApp?.deeplinkCallback,
+ selfApp?.endpoint,
countdownStarted,
+ whitelistedPoints,
]);
useEffect(() => {
diff --git a/app/src/screens/verification/ProveScreen.tsx b/app/src/screens/verification/ProveScreen.tsx
index e0cd3c471..1e10f4f37 100644
--- a/app/src/screens/verification/ProveScreen.tsx
+++ b/app/src/screens/verification/ProveScreen.tsx
@@ -44,6 +44,7 @@ import { ProofStatus } from '@/stores/proofTypes';
import { black, slate300, white } from '@/utils/colors';
import { formatUserId } from '@/utils/formatUserId';
import { buttonTap } from '@/utils/haptic';
+import { getPointsAddress } from '@/utils/points/utils';
const ProveScreen: React.FC = () => {
const selfClient = useSelfClient();
@@ -83,11 +84,12 @@ const ProveScreen: React.FC = () => {
sessionId: provingStore.uuid!,
userId: selectedApp.userId,
userIdType: selectedApp.userIdType,
+ endpoint: selectedApp.endpoint,
endpointType: selectedApp.endpointType,
status: ProofStatus.PENDING,
logoBase64: selectedApp.logoBase64,
disclosures: JSON.stringify(selectedApp.disclosures),
- documentId: selectedDocumentId || '', // Fallback to empty if none selected
+ documentId: selectedDocumentId || '',
});
}
};
@@ -115,6 +117,29 @@ const ProveScreen: React.FC = () => {
selectedAppRef.current = selectedApp;
}, [selectedApp, isFocused, provingStore, selfClient]);
+ // Enhance selfApp with user's points address if not already set
+ useEffect(() => {
+ console.log('useEffect selectedApp', selectedApp);
+ if (!selectedApp || selectedApp.selfDefinedData) {
+ return;
+ }
+
+ const enhanceApp = async () => {
+ const address = await getPointsAddress();
+
+ // Only update if still the same session
+ if (selectedAppRef.current?.sessionId === selectedApp.sessionId) {
+ console.log('enhancing app with points address', address);
+ selfClient.getSelfAppState().setSelfApp({
+ ...selectedApp,
+ selfDefinedData: address.toLowerCase(),
+ });
+ }
+ };
+
+ enhanceApp();
+ }, [selectedApp, selfClient]);
+
const disclosureOptions = useMemo(() => {
return (selectedApp?.disclosures as SelfAppDisclosureConfig) || [];
}, [selectedApp?.disclosures]);
diff --git a/app/src/stores/database.ts b/app/src/stores/database.ts
index 55d94008c..717420019 100644
--- a/app/src/stores/database.ts
+++ b/app/src/stores/database.ts
@@ -87,6 +87,7 @@ export const database: ProofDB = {
sessionId TEXT NOT NULL UNIQUE,
userId TEXT NOT NULL,
userIdType TEXT NOT NULL,
+ endpoint TEXT,
endpointType TEXT NOT NULL,
status TEXT NOT NULL,
errorCode TEXT,
@@ -108,10 +109,11 @@ export const database: ProofDB = {
try {
const [insertResult] = await db.executeSql(
- `INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ `INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpoint, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
proof.appName,
+ proof.endpoint || null,
proof.endpointType,
proof.status,
proof.errorCode || null,
@@ -133,12 +135,40 @@ export const database: ProofDB = {
} catch (error) {
if ((error as Error).message.includes('no column named documentId')) {
await addDocumentIdColumn();
- // Then retry the insert (copy the executeSql call here)
const [insertResult] = await db.executeSql(
- `INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ `INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpoint, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
proof.appName,
+ proof.endpoint || null,
+ proof.endpointType,
+ proof.status,
+ proof.errorCode || null,
+ proof.errorReason || null,
+ timestamp,
+ proof.disclosures,
+ proof.logoBase64 || null,
+ proof.userId,
+ proof.userIdType,
+ proof.sessionId,
+ proof.documentId,
+ ],
+ );
+ return {
+ id: insertResult.insertId.toString(),
+ timestamp,
+ rowsAffected: insertResult.rowsAffected,
+ };
+ } else if (
+ (error as Error).message.includes('no column named endpoint')
+ ) {
+ await addEndpointColumn();
+ const [insertResult] = await db.executeSql(
+ `INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpoint, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId, documentId)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ [
+ proof.appName,
+ proof.endpoint || null,
proof.endpointType,
proof.status,
proof.errorCode || null,
@@ -184,3 +214,8 @@ async function addDocumentIdColumn() {
`ALTER TABLE ${TABLE_NAME} ADD COLUMN documentId TEXT NOT NULL DEFAULT ''`,
);
}
+
+async function addEndpointColumn() {
+ const db = await openDatabase();
+ await db.executeSql(`ALTER TABLE ${TABLE_NAME} ADD COLUMN endpoint TEXT`);
+}
diff --git a/app/src/stores/pointEventStore.ts b/app/src/stores/pointEventStore.ts
index 40616aec4..39658213b 100644
--- a/app/src/stores/pointEventStore.ts
+++ b/app/src/stores/pointEventStore.ts
@@ -22,6 +22,7 @@ interface PointEventState {
events: PointEvent[];
isLoading: boolean;
loadEvents: () => Promise;
+ loadDisclosureEvents: () => Promise;
addEvent: (
title: string,
type: PointEventType,
@@ -38,7 +39,6 @@ interface PointEventState {
lastUpdated: number | null;
promise: Promise | null;
};
- // these are the real points that are on chain. each sunday noon UTC they get updated based on incoming points
points: number;
refreshPoints: () => Promise;
fetchIncomingPoints: () => Promise;
@@ -82,13 +82,11 @@ export const usePointEventStore = create()((set, get) => ({
if (stored) {
try {
const parsed = JSON.parse(stored);
- // Validate that parsed data is an array
if (!Array.isArray(parsed)) {
console.error('Invalid stored events format, expected array');
set({ events: [], isLoading: false });
return;
}
- // Validate each event has required fields
const events: PointEvent[] = parsed.filter((event: unknown) => {
if (
typeof event === 'object' &&
@@ -103,12 +101,9 @@ export const usePointEventStore = create()((set, get) => ({
return false;
}) as PointEvent[];
set({ events, isLoading: false });
- // Resume polling for any pending events that were interrupted by app restart
- // (New events are polled immediately in recordEvents.ts when created)
get()
.getUnprocessedEvents()
.forEach(event => {
- // Use event.id as job_id (id is the job_id)
pollEventProcessingStatus(event.id).then(result => {
if (result === 'completed') {
get().markEventAsProcessed(event.id);
@@ -119,19 +114,36 @@ export const usePointEventStore = create()((set, get) => ({
});
} catch (parseError) {
console.error('Error parsing stored events:', parseError);
- // Clear corrupted data
await AsyncStorage.removeItem(STORAGE_KEY);
set({ events: [], isLoading: false });
}
} else {
set({ isLoading: false });
}
+ await get().loadDisclosureEvents();
} catch (error) {
console.error('Error loading point events:', error);
set({ isLoading: false });
}
},
+ loadDisclosureEvents: async () => {
+ try {
+ const { getDisclosurePointEvents } = await import(
+ '@/utils/points/getEvents'
+ );
+ const { useProofHistoryStore } = await import(
+ '@/stores/proofHistoryStore'
+ );
+ await useProofHistoryStore.getState().initDatabase();
+ const disclosureEvents = await getDisclosurePointEvents();
+ const existingEvents = get().events.filter(e => e.type !== 'disclosure');
+ set({ events: [...existingEvents, ...disclosureEvents] });
+ } catch (error) {
+ console.error('Error loading disclosure events:', error);
+ }
+ },
+
fetchIncomingPoints: async () => {
if (get().incomingPoints.promise) {
return await get().incomingPoints.promise;
diff --git a/app/src/stores/proofHistoryStore.ts b/app/src/stores/proofHistoryStore.ts
index 48d25116d..57465d314 100644
--- a/app/src/stores/proofHistoryStore.ts
+++ b/app/src/stores/proofHistoryStore.ts
@@ -174,6 +174,7 @@ export const useProofHistoryStore = create()((set, get) => {
id: row.id.toString(),
sessionId: row.sessionId,
appName: row.appName,
+ endpoint: row.endpoint,
endpointType: row.endpointType,
status: row.status,
errorCode: row.errorCode,
diff --git a/app/src/stores/proofTypes.ts b/app/src/stores/proofTypes.ts
index 41c61fe21..4faa3ac2b 100644
--- a/app/src/stores/proofTypes.ts
+++ b/app/src/stores/proofTypes.ts
@@ -35,6 +35,7 @@ export interface ProofHistory {
sessionId: string;
userId: string;
userIdType: UserIdType;
+ endpoint?: string;
endpointType: EndpointType;
status: ProofStatus;
errorCode?: string;
diff --git a/app/src/utils/points/getEvents.ts b/app/src/utils/points/getEvents.ts
index bcc0af372..b03c564b9 100644
--- a/app/src/utils/points/getEvents.ts
+++ b/app/src/utils/points/getEvents.ts
@@ -3,6 +3,7 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { PointEvent, PointEventType } from '@/utils/points/types';
+import { getWhiteListedDisclosureAddresses } from '@/utils/points/utils';
/**
* Shared helper to get events from store filtered by type.
@@ -35,7 +36,49 @@ export const getBackupPointEvents = async (): Promise => {
};
export const getDisclosurePointEvents = async (): Promise => {
- return getEventsByType('disclosure');
+ try {
+ const [whitelistedContracts, { useProofHistoryStore }] = await Promise.all([
+ getWhiteListedDisclosureAddresses(),
+ import('@/stores/proofHistoryStore'),
+ ]);
+
+ if (whitelistedContracts.length === 0) {
+ return [];
+ }
+
+ const whitelistedMap = new Map(
+ whitelistedContracts.map(c => [
+ c.contract_address.toLowerCase(),
+ c.points_per_disclosure,
+ ]),
+ );
+
+ const proofHistory = useProofHistoryStore.getState().proofHistory;
+ const disclosureEvents: PointEvent[] = [];
+
+ for (const proof of proofHistory) {
+ if (proof.status !== 'success' || !proof.endpoint) continue;
+
+ const endpoint = proof.endpoint.toLowerCase();
+
+ if (!whitelistedMap.has(endpoint)) continue;
+
+ const points = whitelistedMap.get(endpoint)!;
+ disclosureEvents.push({
+ id: proof.sessionId,
+ title: `${proof.appName} disclosure`,
+ type: 'disclosure',
+ timestamp: proof.timestamp,
+ points,
+ status: 'completed',
+ });
+ }
+
+ return disclosureEvents;
+ } catch (error) {
+ console.error('Error loading disclosure point events:', error);
+ return [];
+ }
};
export const getPushNotificationPointEvents = async (): Promise<
diff --git a/app/src/utils/points/utils.ts b/app/src/utils/points/utils.ts
index 33c87aeee..8ecdcf7eb 100644
--- a/app/src/utils/points/utils.ts
+++ b/app/src/utils/points/utils.ts
@@ -10,6 +10,12 @@ import { getOrGeneratePointsAddress } from '@/providers/authProvider';
import { POINTS_API_BASE_URL } from '@/utils/points/constants';
import type { IncomingPoints } from '@/utils/points/types';
+export type WhitelistedContract = {
+ contract_address: string;
+ points_per_disclosure: number;
+ num_disclosures: number;
+};
+
export const formatTimeUntilDate = (targetDate: Date): string => {
const now = new Date();
const diffMs = targetDate.getTime() - now.getTime();
@@ -103,9 +109,23 @@ export const getTotalPoints = async (address: string): Promise => {
};
export const getWhiteListedDisclosureAddresses = async (): Promise<
- string[]
+ WhitelistedContract[]
> => {
- return [];
+ try {
+ const response = await fetch(
+ `${POINTS_API_BASE_URL}/whitelisted-addresses`,
+ );
+
+ if (!response.ok) {
+ return [];
+ }
+
+ const data = await response.json();
+ return data.contracts || [];
+ } catch (error) {
+ console.error('Error fetching whitelisted addresses:', error);
+ return [];
+ }
};
export const hasUserAnIdentityDocumentRegistered =
@@ -146,7 +166,6 @@ export const hasUserDoneThePointsDisclosure = async (): Promise => {
};
export const pointsSelfApp = async () => {
- const userAddress = (await getPointsAddress())?.toLowerCase();
const endpoint = '0x829d183faaa675f8f80e8bb25fb1476cd4f7c1f0';
const builder = new SelfAppBuilder({
appName: '✨ Self Points',
@@ -158,7 +177,6 @@ export const pointsSelfApp = async () => {
disclosures: {},
logoBase64:
'https://storage.googleapis.com/self-logo-reverse/Self%20Logomark%20Reverse.png',
- selfDefinedData: userAddress,
header: '',
});