mirror of
https://github.com/selfxyz/self.git
synced 2026-04-05 03:00:53 -04:00
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:
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
306
app/src/screens/ProofHistoryDetailScreen.tsx
Normal file
306
app/src/screens/ProofHistoryDetailScreen.tsx
Normal 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;
|
||||
379
app/src/screens/ProofHistoryScreen.tsx
Normal file
379
app/src/screens/ProofHistoryScreen.tsx
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'],
|
||||
|
||||
19
app/src/stores/databaseProvider.tsx
Normal file
19
app/src/stores/databaseProvider.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
296
app/src/stores/proofHistoryStore.ts
Normal file
296
app/src/stores/proofHistoryStore.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user