feat: Add Disclose history (#533)

* feat: Add Disclose history

* fix: Duplicate history in list

* fix: Outdated disclosures

* Delete app/ios/Self copy-Info.plist
This commit is contained in:
Seshanth.S🐺
2025-05-02 17:23:27 +05:30
committed by GitHub
parent f5e243cf9c
commit 7e89698e74
11 changed files with 1064 additions and 1 deletions

View File

@@ -7,6 +7,7 @@ import { YStack } from 'tamagui';
import AppNavigation from './src/Navigation';
import { initSentry, wrapWithSentry } from './src/Sentry';
import { AuthProvider } from './src/stores/authProvider';
import { DatabaseProvider } from './src/stores/databaseProvider';
import { PassportProvider } from './src/stores/passportDataProvider';
initSentry();
@@ -18,7 +19,9 @@ function App(): React.JSX.Element {
<YStack f={1} h="100%" w="100%">
<AuthProvider>
<PassportProvider>
<AppNavigation />
<DatabaseProvider>
<AppNavigation />
</DatabaseProvider>
</PassportProvider>
</AuthProvider>
</YStack>

View File

@@ -92,6 +92,7 @@
"react-native-quick-crypto": "^0.7.12",
"react-native-safe-area-context": "^5.2.0",
"react-native-screens": "^4.6.0",
"react-native-sqlite-storage": "^6.0.1",
"react-native-svg": "^15.11.1",
"socket.io-client": "^4.7.5",
"tamagui": "1.110.0",
@@ -118,6 +119,7 @@
"@types/react": "^18.2.6",
"@types/react-native": "^0.73.0",
"@types/react-native-dotenv": "^0.2.0",
"@types/react-native-sqlite-storage": "^6.0.5",
"eslint": "^8.19.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",

View File

@@ -1,5 +1,7 @@
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import ProofHistoryDetailScreen from '../screens/ProofHistoryDetailScreen';
import ProofHistoryScreen from '../screens/ProofHistoryScreen';
import CloudBackupScreen from '../screens/Settings/CloudBackupScreen';
import PassportDataInfoScreen from '../screens/Settings/PassportDataInfoScreen';
import ShowRecoveryPhraseScreen from '../screens/Settings/ShowRecoveryPhraseScreen';
@@ -54,6 +56,19 @@ const settingsScreens = {
},
} as NativeStackNavigationOptions,
},
ProofHistory: {
screen: ProofHistoryScreen,
options: {
title: 'Approved Requests',
navigationBarColor: black,
},
},
ProofHistoryDetail: {
screen: ProofHistoryDetailScreen,
options: {
title: 'Approval',
},
},
};
export default settingsScreens;

View File

@@ -0,0 +1,306 @@
import { CheckSquare2, Info, Wallet } from '@tamagui/lucide-icons';
import React from 'react';
import { useMemo } from 'react';
import { ScrollView, StatusBar, StyleSheet } from 'react-native';
import { Card, Image, Text, XStack, YStack } from 'tamagui';
import { ProofHistory, ProofStatus } from '../stores/proofHistoryStore';
import {
black,
blue100,
blue600,
blue700,
emerald500,
red500,
slate100,
slate200,
slate400,
slate700,
white,
zinc400,
} from '../utils/colors';
type ProofHistoryDetailScreenProps = {
route: {
params: {
data: ProofHistory;
};
};
};
enum DisclosureType {
NAME = 'name',
OFAC = 'ofac',
AGE = 'age',
ISSUING_STATE = 'issuing_state',
PASSPORT_NUMBER = 'passport_number',
NATIONALITY = 'nationality',
DATE_OF_BIRTH = 'date_of_birth',
GENDER = 'gender',
EXPIRY_DATE = 'expiry_date',
EXCLUDED_COUNTRIES = 'excludedCountries',
MINIMUM_AGE = 'minimumAge',
}
const ProofHistoryDetailScreen: React.FC<ProofHistoryDetailScreenProps> = ({
route,
}) => {
const { data } = route.params;
const disclosures = useMemo(() => {
const parsedDisclosures = JSON.parse(data.disclosures);
const result: string[] = [];
Object.entries(parsedDisclosures).forEach(([key, value]) => {
if (key == DisclosureType.MINIMUM_AGE && value) {
result.push(`Age is over ${value}`);
}
if (key == DisclosureType.NAME && value) {
result.push(`Disclosed Name to ${data.appName}`);
}
if (key == DisclosureType.OFAC && value) {
result.push(`Not on the OFAC list`);
}
if (key == DisclosureType.AGE && value) {
result.push(`Disclosed Age to ${data.appName}`);
}
if (key == DisclosureType.ISSUING_STATE && value) {
result.push(`Disclosed Issuing State to ${data.appName}`);
}
if (key == DisclosureType.PASSPORT_NUMBER && value) {
result.push(`Disclosed Passport Number to ${data.appName}`);
}
if (key == DisclosureType.NATIONALITY && value) {
result.push(`Disclosed Nationality to ${data.appName}`);
}
if (key == DisclosureType.DATE_OF_BIRTH && value) {
result.push(`Disclosed Date of Birth to ${data.appName}`);
}
if (key == DisclosureType.GENDER && value) {
result.push(`Disclosed Gender to ${data.appName}`);
}
if (key == DisclosureType.EXPIRY_DATE && value) {
result.push(`Disclosed Expiry Date to ${data.appName}`);
}
if (key == DisclosureType.EXCLUDED_COUNTRIES) {
if (value && Array.isArray(value) && value.length > 0) {
result.push(`Disclosed - Not from excluded countries`);
}
}
});
return result;
}, [data.disclosures]);
const formattedDate = useMemo(() => {
return new Date(data.timestamp).toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}, [data.timestamp]);
const proofStatus = useMemo(() => {
if (data.status == 'success') {
return 'PROOF GRANTED';
} else if (data.status == ProofStatus.PENDING) {
return 'PROOF PENDING';
} else {
return 'PROOF FAILED';
}
}, [data.status]);
const logoSource = useMemo(() => {
if (!data.logoBase64) {
return null;
}
if (
data.logoBase64.startsWith('http://') ||
data.logoBase64.startsWith('https://')
) {
return { uri: data.logoBase64 };
}
const base64String = data.logoBase64.startsWith('data:image')
? data.logoBase64
: `data:image/png;base64,${data.logoBase64}`;
return { uri: base64String };
}, [data.logoBase64]);
const isEthereumAddress = useMemo(() => {
return (
/^0x[a-fA-F0-9]+$/.test(data.userId) &&
(data.endpointType == 'staging_celo' || data.endpointType == 'celo') &&
data.userIdType == 'hex'
);
}, [data.userId, data.endpointType, data.userIdType]);
return (
<YStack flex={1} backgroundColor={white}>
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
<YStack flex={1} padding={20}>
<YStack
backgroundColor={black}
borderBottomLeftRadius={0}
borderBottomRightRadius={0}
borderTopLeftRadius={10}
borderTopRightRadius={10}
paddingBottom={20}
>
<YStack alignItems="center" gap={12} marginTop={40}>
{logoSource && (
<Image
source={logoSource}
width={60}
height={60}
borderRadius={16}
objectFit="contain"
/>
)}
<Text color={white} fontSize={32} fontWeight="600">
{data.appName}
</Text>
<Text color={zinc400} fontSize={16}>
{data.appName}
</Text>
</YStack>
<YStack alignItems="center" paddingHorizontal={20} marginTop={20}>
<Text
color={zinc400}
fontSize={16}
textAlign="center"
fontWeight="500"
>
{data.appName} was granted access to the following information
from your verified Passport.
</Text>
</YStack>
</YStack>
<YStack
backgroundColor={blue100}
paddingVertical={12}
paddingHorizontal={20}
>
<XStack alignItems="center" gap={8}>
<CheckSquare2 color={blue600} size={12} />
<Text color={blue600} fontSize={12} fontWeight="500">
{proofStatus}
</Text>
<Text color={blue600} fontSize={12} marginLeft="auto">
{formattedDate}
</Text>
</XStack>
</YStack>
<Card
backgroundColor={slate100}
elevation={1}
padding={20}
gap={20}
borderTopLeftRadius={0}
borderTopRightRadius={0}
borderBottomLeftRadius={10}
borderBottomRightRadius={10}
>
<YStack gap={10}>
<YStack
backgroundColor={isEthereumAddress ? blue600 : white}
paddingTop={12}
paddingBottom={12}
paddingLeft={10}
paddingRight={6}
borderRadius={4}
style={
isEthereumAddress
? styles.connectedWalletContainer
: styles.walletContainer
}
>
<XStack
backgroundColor={isEthereumAddress ? blue700 : slate100}
paddingVertical={4}
paddingHorizontal={6}
borderRadius={4}
alignItems="center"
gap={8}
>
<Wallet
color={isEthereumAddress ? white : zinc400}
size={12}
/>
<Text
color={isEthereumAddress ? white : zinc400}
fontSize={12}
fontWeight="500"
>
{isEthereumAddress
? 'CONNECTED WALLET ADDRESS'
: 'NO CONNECTED WALLET'}
</Text>
{isEthereumAddress && (
<Text
color={white}
fontSize={12}
marginLeft="auto"
fontWeight="500"
ellipsizeMode="tail"
>
{data.userId.slice(0, 2)}...{data.userId.slice(-4)}
</Text>
)}
</XStack>
</YStack>
<YStack gap={16}>
{disclosures.map((disclosure, index) => (
<XStack
key={index}
backgroundColor={slate100}
paddingVertical={16}
paddingHorizontal={20}
borderRadius={12}
alignItems="center"
>
<YStack
backgroundColor={
data.status == ProofStatus.SUCCESS ? emerald500 : red500
}
width={8}
height={8}
borderRadius={4}
marginRight={12}
/>
<Text
color={slate700}
fontSize={12}
flex={1}
fontWeight="500"
letterSpacing={0.4}
>
{disclosure}
</Text>
<Info color={blue600} size={16} />
</XStack>
))}
</YStack>
</YStack>
</Card>
</YStack>
</ScrollView>
</YStack>
);
};
const styles = StyleSheet.create({
walletContainer: {
shadowColor: blue600,
},
connectedWalletContainer: {
shadowColor: blue700,
},
});
export default ProofHistoryDetailScreen;

View File

@@ -0,0 +1,379 @@
import { useNavigation } from '@react-navigation/native';
import { CheckSquare2, Wallet } from '@tamagui/lucide-icons';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
RefreshControl,
SectionList,
StyleSheet,
} from 'react-native';
import { Card, Image, SelectIcon, Text, View, XStack, YStack } from 'tamagui';
import { EndpointType } from '../../../common/src/utils/appType';
import { BodyText } from '../components/typography/BodyText';
import { Caption } from '../components/typography/Caption';
import { useProofHistoryStore } from '../stores/proofHistoryStore';
import { ProofHistory } from '../stores/proofHistoryStore';
import {
black,
blue100,
blue600,
sky500,
slate50,
slate300,
slate500,
white,
} from '../utils/colors';
type Section = {
title: string;
data: ProofHistory[];
};
const TIME_PERIODS = {
TODAY: 'TODAY',
THIS_WEEK: 'THIS WEEK',
THIS_MONTH: 'THIS MONTH',
MONTH_NAME: (date: Date): string => {
return date.toLocaleString('default', { month: 'long' }).toUpperCase();
},
OLDER: 'OLDER',
};
const ProofHistoryScreen: React.FC = () => {
const {
proofHistory,
isLoading,
loadMoreHistory,
resetHistory,
initDatabase,
hasMore,
} = useProofHistoryStore();
const [refreshing, setRefreshing] = useState(false);
const navigation = useNavigation();
useEffect(() => {
initDatabase();
}, [initDatabase]);
useEffect(() => {
if (!isLoading && refreshing) {
setRefreshing(false);
}
}, [isLoading, refreshing]);
const formatDate = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
};
const getTimePeriod = useCallback((timestamp: number): string => {
const now = new Date();
const proofDate = new Date(timestamp);
const startOfToday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
);
const startOfThisWeek = new Date(startOfToday);
startOfThisWeek.setDate(startOfToday.getDate() - startOfToday.getDay());
const startOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
if (proofDate >= startOfToday) {
return TIME_PERIODS.TODAY;
} else if (proofDate >= startOfThisWeek) {
return TIME_PERIODS.THIS_WEEK;
} else if (proofDate >= startOfThisMonth) {
return TIME_PERIODS.THIS_MONTH;
} else if (proofDate >= startOfLastMonth) {
return TIME_PERIODS.MONTH_NAME(proofDate);
} else {
return TIME_PERIODS.OLDER;
}
}, []);
const groupedProofs = useMemo(() => {
const groups: Record<string, ProofHistory[]> = {};
[
TIME_PERIODS.TODAY,
TIME_PERIODS.THIS_WEEK,
TIME_PERIODS.THIS_MONTH,
TIME_PERIODS.OLDER,
].forEach(period => {
groups[period] = [];
});
const monthGroups = new Set<string>();
proofHistory.forEach(proof => {
const period = getTimePeriod(proof.timestamp);
if (
period !== TIME_PERIODS.TODAY &&
period !== TIME_PERIODS.THIS_WEEK &&
period !== TIME_PERIODS.THIS_MONTH &&
period !== TIME_PERIODS.OLDER
) {
monthGroups.add(period);
if (!groups[period]) {
groups[period] = [];
}
}
groups[period].push(proof);
});
const sections: Section[] = [];
[
TIME_PERIODS.TODAY,
TIME_PERIODS.THIS_WEEK,
TIME_PERIODS.THIS_MONTH,
].forEach(period => {
if (groups[period] && groups[period].length > 0) {
sections.push({
title: period,
data: groups[period],
});
}
});
Array.from(monthGroups)
.sort((a, b) => {
const monthA = new Date(groups[a][0].timestamp).getMonth();
const monthB = new Date(groups[b][0].timestamp).getMonth();
return monthB - monthA;
})
.forEach(month => {
sections.push({
title: month,
data: groups[month],
});
});
if (groups[TIME_PERIODS.OLDER] && groups[TIME_PERIODS.OLDER].length > 0) {
sections.push({
title: TIME_PERIODS.OLDER,
data: groups[TIME_PERIODS.OLDER],
});
}
return sections;
}, [proofHistory, getTimePeriod]);
const renderItem = useCallback(({ item }: { item: ProofHistory }) => {
try {
const disclosures = JSON.parse(item.disclosures);
const logoSource = item.logoBase64
? {
uri:
item.logoBase64.startsWith('data:') ||
item.logoBase64.startsWith('http')
? item.logoBase64
: `data:image/png;base64,${item.logoBase64}`,
}
: null;
const disclosureCount = Object.values(disclosures).filter(
value => value,
).length;
return (
<View
marginHorizontal={16}
// marginVertical={8}
borderRadius={12}
>
<YStack gap={8}>
<Card
bordered
padded
backgroundColor={white}
onPress={() =>
navigation.navigate('ProofHistoryDetail', { data: item })
}
>
<XStack alignItems="center">
{logoSource && (
<Image
source={logoSource}
width={46}
height={46}
marginRight={12}
borderRadius={3}
gap={10}
objectFit="contain"
/>
)}
<YStack flex={1}>
<BodyText fontSize={20} color={black} fontWeight="500">
{item.appName}
</BodyText>
<BodyText color={slate300} gap={2} fontSize={14}>
{formatDate(item.timestamp)}
</BodyText>
</YStack>
{(item.endpointType == 'staging_celo' ||
item.endpointType == 'celo') && (
<XStack
backgroundColor={blue100}
paddingVertical={2}
paddingHorizontal={8}
borderRadius={4}
alignItems="center"
>
<Wallet color={blue600} height={14} width={14} />
</XStack>
)}
<XStack
backgroundColor={blue100}
paddingVertical={2}
paddingHorizontal={8}
borderRadius={4}
alignItems="center"
marginLeft={4}
>
<Text
color={blue600}
fontSize={14}
fontWeight="600"
marginRight={4}
>
{disclosureCount}
</Text>
<CheckSquare2 color={blue600} height={14} width={14} />
</XStack>
</XStack>
</Card>
</YStack>
</View>
);
} catch (e) {
console.error('Error rendering item:', e, item);
return null;
}
}, []);
const renderSectionHeader = useCallback(
({ section }: { section: Section }) => {
return (
<View
paddingHorizontal={16}
paddingVertical={20}
backgroundColor={white}
marginTop={8}
gap={12}
>
<Text
color={slate500}
fontSize={15}
fontWeight="500"
letterSpacing={4}
>
{section.title.toUpperCase()}
</Text>
</View>
);
},
[],
);
const onRefresh = useCallback(() => {
setRefreshing(true);
resetHistory();
loadMoreHistory();
}, [resetHistory, loadMoreHistory]);
const keyExtractor = useCallback((item: ProofHistory) => item.sessionId, []);
const handleEndReached = useCallback(() => {
if (!isLoading && hasMore) {
loadMoreHistory();
}
}, [isLoading, hasMore, loadMoreHistory]);
const renderEmptyComponent = useCallback(() => {
if (isLoading) {
return (
<View style={styles.emptyContainer}>
<ActivityIndicator size="large" color={slate300} />
<Text color={slate300} marginTop={16}>
Loading history...
</Text>
</View>
);
}
return (
<View style={styles.emptyContainer}>
<Text color={slate300}>No proof history available</Text>
</View>
);
}, [isLoading]);
const renderFooter = useCallback(() => {
if (!isLoading || refreshing) return null;
return (
<View style={styles.footerContainer}>
<ActivityIndicator size="small" color={slate300} />
</View>
);
}, [isLoading, refreshing]);
return (
<View flex={1} backgroundColor={slate50}>
<SectionList
sections={groupedProofs}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={keyExtractor}
onEndReached={handleEndReached}
onEndReachedThreshold={0.5}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
contentContainerStyle={[
styles.listContent,
groupedProofs.length === 0 && styles.emptyList,
]}
showsVerticalScrollIndicator={false}
stickySectionHeadersEnabled={false}
ListEmptyComponent={renderEmptyComponent}
ListFooterComponent={renderFooter}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={10}
removeClippedSubviews={true}
/>
</View>
);
};
const styles = StyleSheet.create({
listContent: {
paddingBottom: 16,
},
emptyList: {
flexGrow: 1,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 48,
},
footerContainer: {
paddingVertical: 16,
alignItems: 'center',
},
});
export default ProofHistoryScreen;

View File

@@ -14,6 +14,10 @@ import { typography } from '../../components/typography/styles';
import { Title } from '../../components/typography/Title';
import useHapticNavigation from '../../hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import {
ProofStatus,
useProofHistoryStore,
} from '../../stores/proofHistoryStore';
import { useSelfAppStore } from '../../stores/selfAppStore';
import { black, white } from '../../utils/colors';
import {
@@ -28,8 +32,12 @@ const SuccessScreen: React.FC = () => {
const appName = selfApp?.appName;
const goHome = useHapticNavigation('Home');
const { updateProofStatus } = useProofHistoryStore();
const currentState = useProvingStore(state => state.currentState);
const reason = useProvingStore(state => state.reason);
const sessionId = useProvingStore(state => state.uuid);
const errorCode = useProvingStore(state => state.error_code);
const isFocused = useIsFocused();
@@ -51,9 +59,16 @@ const SuccessScreen: React.FC = () => {
if (currentState === 'completed') {
notificationSuccess();
setAnimationSource(succesAnimation);
updateProofStatus(sessionId!, ProofStatus.SUCCESS);
} else if (currentState === 'failure' || currentState === 'error') {
notificationError();
setAnimationSource(failAnimation);
updateProofStatus(
sessionId!,
ProofStatus.FAILURE,
errorCode ?? undefined,
reason ?? undefined,
);
} else {
setAnimationSource(loadingAnimation);
}

View File

@@ -24,6 +24,10 @@ import Disclosures from '../../components/Disclosures';
import { BodyText } from '../../components/typography/BodyText';
import { Caption } from '../../components/typography/Caption';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import {
ProofStatus,
useProofHistoryStore,
} from '../../stores/proofHistoryStore';
import { useSelfAppStore } from '../../stores/selfAppStore';
import { black, slate300, white } from '../../utils/colors';
import { buttonTap } from '../../utils/haptic';
@@ -45,6 +49,23 @@ const ProveScreen: React.FC = () => {
[scrollViewContentHeight, scrollViewHeight],
);
const provingStore = useProvingStore();
const { addProofHistory } = useProofHistoryStore();
useEffect(() => {
// Only add proof history after generating a uuid
if (provingStore.uuid && selectedApp) {
addProofHistory({
appName: selectedApp.appName,
sessionId: provingStore.uuid!,
userId: selectedApp.userId,
userIdType: selectedApp.userIdType,
endpointType: selectedApp.endpointType,
status: ProofStatus.PENDING,
logoBase64: selectedApp.logoBase64,
disclosures: JSON.stringify(selectedApp.disclosures),
});
}
}, [provingStore.uuid, selectedApp]);
/**
* Whenever the relationship between content height vs. scroll view height changes,

View File

@@ -58,6 +58,7 @@ const goToStore = () => {
const routes = [
[Data, 'View passport info', 'PassportDataInfo'],
[Data, 'Proof history', 'ProofHistory'],
[Lock, 'Reveal recovery phrase', 'ShowRecoveryPhrase'],
[Cloud, 'Cloud backup', 'CloudBackupSettings'],
[Feedback, 'Send feeback', 'email_feedback'],

View File

@@ -0,0 +1,19 @@
import React, { createContext, useEffect } from 'react';
import { useProofHistoryStore } from './proofHistoryStore';
export const DatabaseContext = createContext(null);
export const DatabaseProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { initDatabase } = useProofHistoryStore();
useEffect(() => {
initDatabase();
}, [initDatabase]);
return (
<DatabaseContext.Provider value={null}>{children}</DatabaseContext.Provider>
);
};

View File

@@ -0,0 +1,296 @@
import { Platform } from 'react-native';
import SQLite from 'react-native-sqlite-storage';
import { io } from 'socket.io-client';
import { create } from 'zustand';
import { WS_DB_RELAYER } from '../../../common/src/constants/constants';
import { EndpointType } from '../../../common/src/utils/appType';
import { UserIdType } from '../../../common/src/utils/circuits/uuid';
SQLite.enablePromise(true);
export interface ProofHistory {
id: string;
appName: string;
sessionId: string;
userId: string;
userIdType: UserIdType;
endpointType: EndpointType;
status: ProofStatus;
errorCode?: string;
errorReason?: string;
timestamp: number;
disclosures: string;
logoBase64?: string;
}
export enum ProofStatus {
PENDING = 'pending',
SUCCESS = 'success',
FAILURE = 'failure',
}
interface ProofHistoryState {
proofHistory: ProofHistory[];
isLoading: boolean;
hasMore: boolean;
currentPage: number;
initDatabase: () => Promise<void>;
addProofHistory: (
proof: Omit<ProofHistory, 'id' | 'timestamp'>,
) => Promise<void>;
updateProofStatus: (
sessionId: string,
status: ProofStatus,
errorCode?: string,
errorReason?: string,
) => Promise<void>;
loadMoreHistory: () => Promise<void>;
resetHistory: () => void;
}
const PAGE_SIZE = 20;
const DB_NAME = Platform.OS === 'ios' ? 'proof_history.db' : 'proof_history.db';
const TABLE_NAME = 'proof_history';
export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => {
const syncProofHistoryStatus = async () => {
try {
set({ isLoading: true });
const db = await SQLite.openDatabase({
name: DB_NAME,
location: 'default',
});
const [pendingProofs] = await db.executeSql(`
SELECT * FROM ${TABLE_NAME} WHERE status = '${ProofStatus.PENDING}'
`);
if (pendingProofs.rows.length === 0) {
console.log('No pending proofs to sync');
return;
}
const websocket = io(WS_DB_RELAYER, {
path: '/',
transports: ['websocket'],
});
for (let i = 0; i < pendingProofs.rows.length; i++) {
const proof = pendingProofs.rows.item(i);
websocket.emit('subscribe', proof.sessionId);
}
websocket.on('status', message => {
const data =
typeof message === 'string' ? JSON.parse(message) : message;
if (data.status === 3) {
console.log('Failed to generate proof');
get().updateProofStatus(data.request_id, ProofStatus.FAILURE);
} else if (data.status === 4) {
console.log('Proof verified');
get().updateProofStatus(data.request_id, ProofStatus.SUCCESS);
} else if (data.status === 5) {
console.log('Failed to verify proof');
get().updateProofStatus(data.request_id, ProofStatus.FAILURE);
}
});
} catch (error) {
console.error('Error syncing proof status', error);
} finally {
set({ isLoading: false });
}
};
return {
proofHistory: [],
isLoading: false,
hasMore: true,
currentPage: 1,
initDatabase: async () => {
try {
const db = await SQLite.openDatabase({
name: DB_NAME,
location: 'default',
});
await db.executeSql(`
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
appName TEXT NOT NULL,
sessionId TEXT NOT NULL UNIQUE,
userId TEXT NOT NULL,
userIdType TEXT NOT NULL,
endpointType TEXT NOT NULL,
status TEXT NOT NULL,
errorCode TEXT,
errorReason TEXT,
timestamp INTEGER NOT NULL,
disclosures TEXT NOT NULL,
logoBase64 TEXT
)
`);
await db.executeSql(`
CREATE INDEX IF NOT EXISTS idx_proof_history_timestamp ON ${TABLE_NAME} (timestamp)
`);
// Load initial data
const state = get();
if (state.proofHistory.length === 0) {
await state.loadMoreHistory();
}
// Sync any pending proof statuses
await syncProofHistoryStatus();
} catch (error) {
console.error('Error initializing proof history database', error);
}
},
addProofHistory: async proof => {
try {
const db = await SQLite.openDatabase({
name: DB_NAME,
location: 'default',
});
const timestamp = Date.now();
const [insertResult] = await db.executeSql(
`INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
proof.appName,
proof.endpointType,
proof.status,
proof.errorCode || null,
proof.errorReason || null,
timestamp,
proof.disclosures,
proof.logoBase64 || null,
proof.userId,
proof.userIdType,
proof.sessionId,
],
);
if (insertResult.rowsAffected > 0 && insertResult.insertId) {
const id = insertResult.insertId.toString();
set(state => ({
proofHistory: [
{
...proof,
id,
timestamp,
disclosures: proof.disclosures,
},
...state.proofHistory,
],
}));
}
} catch (error) {
console.error('Error adding proof history', error);
}
},
updateProofStatus: async (sessionId, status, errorCode, errorReason) => {
try {
const db = await SQLite.openDatabase({
name: DB_NAME,
location: 'default',
});
await db.executeSql(
`
UPDATE ${TABLE_NAME} SET status = ?, errorCode = ?, errorReason = ? WHERE sessionId = ?
`,
[status, errorCode, errorReason, sessionId],
);
// Update the status in the state
set(state => ({
proofHistory: state.proofHistory.map(proof =>
proof.sessionId === sessionId
? { ...proof, status, errorCode, errorReason }
: proof,
),
}));
} catch (error) {
console.error('Error updating proof status', error);
}
},
loadMoreHistory: async () => {
const state = get();
if (state.isLoading || !state.hasMore) return;
set({ isLoading: true });
try {
const db = await SQLite.openDatabase({
name: DB_NAME,
location: 'default',
});
const offset = (state.currentPage - 1) * PAGE_SIZE;
const [results] = await db.executeSql(
`WITH data AS (
SELECT *, COUNT(*) OVER() as total_count
FROM ${TABLE_NAME}
ORDER BY timestamp DESC
LIMIT ? OFFSET ?
)
SELECT * FROM data`,
[PAGE_SIZE, offset],
);
const proofs: ProofHistory[] = [];
let totalCount = 0;
for (let i = 0; i < results.rows.length; i++) {
const row = results.rows.item(i);
totalCount = row.total_count; // same for all rows
proofs.push({
id: row.id.toString(),
sessionId: row.sessionId,
appName: row.appName,
endpointType: row.endpointType,
status: row.status,
errorCode: row.errorCode,
errorReason: row.errorReason,
timestamp: row.timestamp,
disclosures: row.disclosures,
logoBase64: row.logoBase64,
userId: row.userId,
userIdType: row.userIdType,
});
}
// Calculate if there are more items
const currentTotal = state.proofHistory.length + proofs.length;
const hasMore = currentTotal < totalCount;
set(state => ({
proofHistory: [...state.proofHistory, ...proofs],
currentPage: state.currentPage + 1,
hasMore,
isLoading: false,
}));
} catch (error) {
console.error('Error loading more proof history', error);
set({
isLoading: false,
});
}
},
resetHistory: () => {
set({
proofHistory: [],
currentPage: 1,
hasMore: true,
});
},
};
});

View File

@@ -12,6 +12,7 @@ export const slate500 = '#64748B';
export const slate600 = '#475569';
export const slate700 = '#334155';
export const slate800 = '#1E293B';
export const slate900 = '#0F172A';
export const sky500 = '#0EA5E9';
export const green500 = '#22C55E';
export const red500 = '#EF4444';
@@ -21,7 +22,12 @@ export const teal500 = '#5EEAD4';
export const neutral400 = '#A3A3A3';
export const neutral700 = '#404040';
export const zinc400 = '#A1A1AA';
export const blue100 = '#DBEAFE';
export const blue600 = '#2563EB';
export const blue700 = '#1D4ED8';
export const yellow500 = '#FDE047';
export const emerald500 = '#10B981';
// OLD
export const borderColor = '#343434';