add new home screen (#1019)

* add new home screen

* fix typing issue

* yarn nice
This commit is contained in:
turnoffthiscomputer
2025-09-09 01:42:33 +02:00
committed by GitHub
parent ff678b359a
commit 78b2341091
22 changed files with 1979 additions and 126 deletions

View File

@@ -13,9 +13,10 @@ import type { SelfApp } from '@selfxyz/common/utils/appType';
import { NavBar } from '@/components/NavBar/BaseNavBar';
import ActivityIcon from '@/images/icons/activity.svg';
import ScanIcon from '@/images/icons/qr_scan.svg';
import SettingsIcon from '@/images/icons/settings.svg';
import { useSelfAppStore } from '@/stores/selfAppStore';
import { black, neutral400, white } from '@/utils/colors';
import { black, charcoal, neutral400, slate50, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
import { buttonTap } from '@/utils/haptic';
@@ -70,44 +71,56 @@ export const HomeNavBar = (props: NativeStackHeaderProps) => {
};
return (
<NavBar.Container
backgroundColor={black}
backgroundColor={slate50}
barStyle={'light'}
padding={16}
padding={8}
justifyContent="space-between"
paddingTop={Math.max(insets.top, 15) + extraYPadding}
>
<NavBar.LeftAction
component={
<Button
size={'$3'}
unstyled
icon={
<ActivityIcon width={'24'} height={'100%'} color={neutral400} />
}
/>
<XStack alignItems="center">
<Button
size={'$3'}
unstyled
icon={<ScanIcon width={'24'} height={'100%'} color={charcoal} />}
onPress={() => {
buttonTap();
props.navigation.navigate('QRCodeViewFinder');
}}
/>
<Button
size={'$3'}
unstyled
marginTop={10}
icon={<ClipboardIcon size={24} color={charcoal} />}
onPress={handleConsumeToken}
/>
</XStack>
}
// disable icon click for now
onPress={() => {
buttonTap();
props.navigation.navigate('ProofHistory');
}}
/>
<NavBar.Title size="large" color={white}>
<NavBar.Title size="large" color={black}>
{props.options.title}
</NavBar.Title>
<NavBar.RightAction
component={
<XStack alignItems="center" gap={10}>
<ClipboardIcon
size={24}
color={neutral400}
onPress={handleConsumeToken}
<XStack alignItems="center">
<Button
size={'$3'}
unstyled
icon={
<ActivityIcon width={'24'} height={'100%'} color={charcoal} />
}
onPress={() => {
buttonTap();
props.navigation.navigate('ProofHistory');
}}
/>
<Button
size={'$3'}
unstyled
icon={
<SettingsIcon width={'24'} height={'100%'} color={neutral400} />
<SettingsIcon width={'24'} height={'100%'} color={charcoal} />
}
onPress={() => {
buttonTap();

View File

@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, Text, View, XStack } from 'tamagui';
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
import { ChevronLeft } from '@tamagui/lucide-icons';
import { NavBar } from '@/components/NavBar/BaseNavBar';
import { black, charcoal, slate50 } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
import { buttonTap } from '@/utils/haptic';
export const IdDetailsNavBar = (props: NativeStackHeaderProps) => {
const insets = useSafeAreaInsets();
const backButtonWidth = 50; // Adjusted for text
return (
<NavBar.Container
backgroundColor={slate50}
barStyle={'light'}
justifyContent="space-between"
paddingTop={Math.max(insets.top, 15) + extraYPadding}
>
<NavBar.LeftAction
component={
<Button
unstyled
marginLeft={'$3.5'}
padding={'$3'}
width={'$10'}
onPress={() => {
buttonTap();
props.navigation.goBack();
}}
>
<Text color={charcoal} fontSize={17} fontWeight="bold">
Done
</Text>
</Button>
}
/>
<NavBar.Title size="large" color={black}>
{props.options.title}
</NavBar.Title>
<NavBar.RightAction
component={
// Spacer to balance the back button and center the title
<View style={{ width: backButtonWidth }} />
}
/>
</NavBar.Container>
);
};

View File

@@ -4,4 +4,5 @@
export { DefaultNavBar } from '@/components/NavBar/DefaultNavBar';
export { HomeNavBar } from '@/components/NavBar/HomeNavBar';
export { IdDetailsNavBar } from '@/components/NavBar/IdDetailsNavBar';
export { ProgressNavBar } from '@/components/NavBar/ProgressNavBar';

View File

@@ -0,0 +1,578 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useState } from 'react';
import { Dimensions, Pressable } from 'react-native';
import { SvgXml } from 'react-native-svg';
import { Button, Image, Separator, Text, XStack, YStack } from 'tamagui';
import { useFocusEffect } from '@react-navigation/native';
import {
attributeToPosition,
attributeToPosition_ID,
formatMrz,
PassportData,
} from '@selfxyz/common/dist/esm';
import { pad } from '@selfxyz/common/dist/esm/src/utils/passports/passport';
import EPassport from '@/images/icons/epassport.svg';
import LogoGray from '@/images/logo_gray.svg';
import LogoInversed from '@/images/logo_inversed.svg';
import { usePassport } from '@/providers/passportDataProvider';
import {
black,
blue600,
green500,
red500,
slate50,
slate100,
slate300,
slate400,
slate500,
white,
} from '@/utils/colors';
import { dinot, plexMono } from '@/utils/fonts';
// Import the logo SVG as a string
const logoSvg = `<svg width="47" height="46" viewBox="0 0 47 46" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7814 13.2168C12.7814 12.7057 13.1992 12.2969 13.7214 12.2969H30.0017L42.5676 0H11.2408L0 11.0001V29.0973H12.7814V13.2104V13.2168Z" fill="white"/>
<path d="M34.2186 16.8515V32.3552C34.2186 32.8663 33.8008 33.2751 33.2786 33.2751H17.4357L4.43236 46H35.7592L47 34.9999V16.8579H34.2186V16.8515Z" fill="white"/>
<path d="M28.9703 17.6525H18.0362V28.3539H28.9703V17.6525Z" fill="#00FFB6"/>
</svg>`;
interface IdCardLayoutAttributes {
idDocument: PassportData;
selected: boolean;
hidden: boolean;
}
// This layout should be fully adaptative. I should perfectly fit in any screen size.
// the font size should adapt according to the size available to fit perfectly.
// only svg are allowed.
// each element size should be determined as % of the screen or the parent element
// the border radius should be adaptative too, as well as the padding
// this is the v0 of this component so we should only use placholders for now, no need to pass the real passport data as parameters.
const IdCardLayout: React.FC<IdCardLayoutAttributes> = ({
idDocument,
selected,
hidden,
}) => {
// Function to mask MRZ characters except '<' and spaces
const maskMrzValue = (text: string): string => {
return text.replace(/./g, 'X');
};
// Get screen dimensions for adaptive sizing
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
// Calculate adaptive sizes based on screen dimensions
// Reduce width slightly to account for horizontal margins (8px each side = 16px total)
const cardWidth = screenWidth * 0.95 - 16; // 90% of screen width minus margin space
const cardHeight = selected ? cardWidth * 0.645 : cardWidth * 0.645 * 0.3; // ID card aspect ratio (roughly 1.6:1)
const borderRadius = cardWidth * 0.04; // 4% of card width
const padding = cardWidth * 0.035; // 4% of card width
const fontSize = {
large: cardWidth * 0.045,
medium: cardWidth * 0.032,
small: cardWidth * 0.028,
xsmall: cardWidth * 0.022,
};
// Image dimensions (standard ID photo ratio)
const imageSize = {
width: cardWidth * 0.2, // 25% of card width
height: cardWidth * 0.29, // ID photo aspect ratio
};
// Shared left offset for content that should align with the start of the attributes block
const contentLeftOffset = imageSize.width + padding;
return (
// Container wrapper to handle shadow space properly
<YStack
width="100%" // Add space for horizontal margins
alignItems="center"
justifyContent="center"
>
<YStack
width={cardWidth}
height={cardHeight}
backgroundColor={white}
borderRadius={borderRadius}
borderWidth={0.75}
borderColor={'#E0E0E0'}
padding={padding}
// Improved shadow configuration for better visibility and containment
shadowColor={black}
shadowOffset={{ width: 0, height: 2 }}
shadowOpacity={0.1}
shadowRadius={4}
elevation={4}
// Add margins to provide space for shadow bleeding
marginBottom={8}
justifyContent="center"
>
{/* Header Section */}
<XStack>
<XStack alignItems="center">
<EPassport
width={fontSize.large * 3}
height={fontSize.large * 3 * 0.617}
/>
<YStack marginLeft={imageSize.width - fontSize.large * 3}>
<Text
fontWeight="bold"
fontFamily={dinot}
fontSize={fontSize.large * 1.4}
color="black"
>
{idDocument.documentCategory === 'passport'
? 'Passport'
: 'ID Card'}
</Text>
<Text
fontSize={fontSize.small}
color={'#9193A2'}
fontFamily={dinot}
>
Verified{' '}
{idDocument.documentCategory === 'passport'
? 'Biometric Passport'
: ' Biometric ID Card'}
</Text>
</YStack>
</XStack>
<XStack flex={1} justifyContent="flex-end">
{idDocument.mock && (
<YStack
marginTop={padding / 4}
borderWidth={1}
borderColor={slate300}
borderRadius={100}
paddingHorizontal={padding / 2}
alignSelf="flex-start"
backgroundColor={slate100}
paddingVertical={padding / 8}
>
<Text
fontSize={fontSize.xsmall}
color={slate400}
fontFamily={dinot}
letterSpacing={fontSize.xsmall * 0.15}
>
DEVELOPER
</Text>
</YStack>
)}
</XStack>
</XStack>
{selected && (
<Separator
backgroundColor={'#E0E0E0'}
height={1}
width={cardWidth - 1}
marginLeft={-padding}
marginTop={padding}
/>
)}
{/* Main Content Section */}
{selected && (
<XStack height="60%" paddingVertical={padding}>
{/* Person Image */}
<YStack
width={imageSize.width}
height={imageSize.height}
backgroundColor="#F5F5F5"
borderRadius={borderRadius * 0.5}
justifyContent="center"
alignItems="center"
marginRight={padding}
opacity={hidden ? 0.3 : 1}
>
<SvgXml
xml={logoSvg}
width={imageSize.width * 0.6}
height={imageSize.height * 0.6}
/>
</YStack>
{/* ID Attributes */}
<YStack
flex={1}
justifyContent="space-between"
height={imageSize.height}
>
<XStack flex={1} gap={padding * 0.3}>
<YStack flex={1}>
<IdAttribute
name="TYPE"
value={
idDocument.documentCategory === 'passport'
? 'PASSPORT'
: 'ID CARD'
}
maskValue={
idDocument.documentCategory === 'passport'
? 'PASSPORT'
: 'ID CARD'
}
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="CODE"
value={idDocument.mock ? 'SELF DEV' : 'SELF ID'}
maskValue={idDocument.mock ? 'SELF DEV' : 'SELF ID'}
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="DOC NO."
value={
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).passNoSlice
}
maskValue="XX-XXXXXXX"
hidden={hidden}
/>
</YStack>
</XStack>
<XStack flex={1} gap={padding * 0.3}>
<YStack flex={1}>
<IdAttribute
name="SURNAME"
value={getNameAndSurname(
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).nameSlice,
).surname.join(' ')}
maskValue="XXXXXXXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="NAME"
value={getNameAndSurname(
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).nameSlice,
).names.join(' ')}
maskValue="XXXXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="SEX"
value={
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).sexSlice
}
maskValue="X"
hidden={hidden}
/>
</YStack>
</XStack>
<XStack flex={1} gap={padding * 0.3}>
<YStack flex={1}>
<IdAttribute
name="NATIONALITY"
value={
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).nationalitySlice
}
maskValue="XXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="DOB"
value={formatDateFromYYMMDD(
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).dobSlice,
)}
maskValue="XX/XX/XXXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1}>
<IdAttribute
name="EXPIRY DATE"
value={formatDateFromYYMMDD(
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).expiryDateSlice,
true,
)}
maskValue="XX/XX/XXXX"
hidden={hidden}
/>
</YStack>
</XStack>
<XStack flex={1} gap={padding * 0.3}>
<YStack flex={1}>
<IdAttribute
name="AUTHORITY"
value={
getPassportAttributes(
idDocument.mrz,
idDocument.documentCategory,
).issuingStateSlice
}
maskValue="XXX"
hidden={hidden}
/>
</YStack>
<YStack flex={1} />
<YStack flex={1} />
</XStack>
</YStack>
</XStack>
)}
{/* Footer Section - MRZ */}
{selected && (
<XStack
alignItems="center"
backgroundColor={slate100}
borderRadius={borderRadius / 3}
paddingHorizontal={padding / 2}
paddingVertical={padding / 4}
>
{/* Fixed-width spacer to align MRZ content with the attributes block */}
<XStack width={contentLeftOffset} alignItems="center">
<LogoGray width={fontSize.large} height={fontSize.large} />
</XStack>
<YStack marginLeft={-padding / 2}>
{idDocument.documentCategory === 'passport' ? (
// Passport: 2 lines, 88 chars total (44 chars each)
<>
<Text
fontSize={fontSize.xsmall}
letterSpacing={fontSize.xsmall * 0.1}
fontFamily={plexMono}
color={slate400}
>
{hidden
? maskMrzValue(idDocument.mrz.slice(0, 44))
: idDocument.mrz.slice(0, 44)}
</Text>
<Text
fontSize={fontSize.xsmall}
letterSpacing={fontSize.xsmall * 0.1}
fontFamily={plexMono}
color={slate400}
>
{hidden
? maskMrzValue(idDocument.mrz.slice(44, 88))
: idDocument.mrz.slice(44, 88)}
</Text>
</>
) : (
// ID Card: 3 lines, 90 chars total (30 chars each)
<>
<Text
fontSize={fontSize.xsmall}
letterSpacing={fontSize.xsmall * 0.44}
fontFamily={plexMono}
color={slate400}
>
{hidden
? maskMrzValue(idDocument.mrz.slice(0, 30))
: idDocument.mrz.slice(0, 30)}
</Text>
<Text
fontSize={fontSize.xsmall}
letterSpacing={fontSize.xsmall * 0.44}
fontFamily={plexMono}
color={slate400}
>
{hidden
? maskMrzValue(idDocument.mrz.slice(30, 60))
: idDocument.mrz.slice(30, 60)}
</Text>
<Text
fontSize={fontSize.xsmall}
letterSpacing={fontSize.xsmall * 0.44}
fontFamily={plexMono}
color={slate400}
>
{hidden
? maskMrzValue(idDocument.mrz.slice(60, 90))
: idDocument.mrz.slice(60, 90)}
</Text>
</>
)}
</YStack>
</XStack>
)}
</YStack>
</YStack>
);
};
// Interface for IdAttribute props
interface IdAttributeProps {
name: string;
value: string;
maskValue: string;
hidden?: boolean;
}
// This layout should be fully adaptative. I should perfectly fit in any screen size.
// the font size should adapt according to the size available to fit perfectly.
// only svg are allowed.
// each element size should be determined as % of the screen or the parent element
const IdAttribute: React.FC<IdAttributeProps> = ({
name,
value,
maskValue,
hidden = false,
}) => {
const { width: screenWidth } = Dimensions.get('window');
const fontSize = {
label: screenWidth * 0.024,
value: screenWidth * 0.02,
};
const displayValue = hidden ? maskValue : value;
return (
<YStack>
<Text
fontWeight="bold"
fontSize={fontSize.label}
color={slate500}
fontFamily={dinot}
>
{name}
</Text>
<Text fontSize={fontSize.value} color={slate400} fontFamily={dinot}>
{displayValue}
</Text>
</YStack>
);
};
export default IdCardLayout;
function getPassportAttributes(mrz: string, documentCategory: string) {
const isPassportType = documentCategory === 'passport';
const attributePositions = isPassportType
? attributeToPosition
: attributeToPosition_ID;
const nameSlice = mrz.slice(
attributePositions.name[0],
attributePositions.name[1],
);
const dobSlice = mrz.slice(
attributePositions.date_of_birth[0],
attributePositions.date_of_birth[1] + 1,
);
const yobSlice = mrz.slice(
attributePositions.date_of_birth[0],
attributePositions.date_of_birth[0] + 1,
);
const issuingStateSlice = mrz.slice(
attributePositions.issuing_state[0],
attributePositions.issuing_state[1] + 1,
);
const nationalitySlice = mrz.slice(
attributePositions.nationality[0],
attributePositions.nationality[1] + 1,
);
const passNoSlice = mrz.slice(
attributePositions.passport_number[0],
attributePositions.passport_number[1] + 1,
);
const sexSlice = mrz.slice(
attributePositions.gender[0],
attributePositions.gender[1] + 1,
);
const expiryDateSlice = mrz.slice(
attributePositions.expiry_date[0],
attributePositions.expiry_date[1] + 1,
);
return {
nameSlice,
dobSlice,
yobSlice,
issuingStateSlice,
nationalitySlice,
passNoSlice,
sexSlice,
expiryDateSlice,
isPassportType,
};
}
function getNameAndSurname(nameSlice: string) {
// Split by double << to separate surname from names
const parts = nameSlice.split('<<');
if (parts.length < 2) {
return { surname: [], names: [] };
}
// First part is surname, second part contains names separated by single <
const surname = parts[0].replace(/</g, '').trim();
const namesString = parts[1];
// Split names by single < and filter out empty strings
const names = namesString.split('<').filter(name => name.length > 0);
return {
surname: surname ? [surname] : [],
names: names[0] ? [names[0]] : [],
};
}
function formatDateFromYYMMDD(
dateString: string,
isExpiry: boolean = false,
): string {
if (dateString.length !== 6) {
return dateString;
}
const yy = parseInt(dateString.substring(0, 2), 10);
const mm = dateString.substring(2, 4);
const dd = dateString.substring(4, 6);
const currentYear = new Date().getFullYear();
const century = Math.floor(currentYear / 100) * 100;
let year = century + yy;
if (isExpiry) {
// For expiry: if year is in the past, assume next century
if (year < currentYear) {
year += 100;
}
} else {
// For birth: if year is in the future, assume previous century
if (year > currentYear) {
year -= 100;
}
}
return `${dd}/${mm}/${year}`;
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M2.45956 14C1.64971 14 1.03731 13.7925 0.622389 13.3776C0.207463 12.9677 0 12.3628 0 11.5629V2.43706C0 1.63221 0.207463 1.02482 0.622389 0.61489C1.03731 0.204963 1.64971 0 2.45956 0H11.5404C12.3503 0 12.9627 0.204963 13.3776 0.61489C13.7925 1.02482 14 1.63221 14 2.43706V11.5629C14 12.3628 13.7925 12.9677 13.3776 13.3776C12.9627 13.7925 12.3503 14 11.5404 14H2.45956ZM6.1864 10.6856C6.32637 10.6856 6.45385 10.6531 6.56883 10.5881C6.68881 10.5231 6.79379 10.4256 6.88377 10.2957L10.3931 4.82164C10.4431 4.74165 10.4881 4.65667 10.5281 4.56668C10.5681 4.4717 10.5881 4.38172 10.5881 4.29673C10.5881 4.10177 10.5131 3.9443 10.3631 3.82432C10.2182 3.70434 10.0532 3.64435 9.86824 3.64435C9.62328 3.64435 9.42082 3.77433 9.26085 4.03428L6.1564 8.99839L4.71666 7.16872C4.61668 7.04374 4.51919 6.95626 4.42421 6.90627C4.32923 6.85628 4.22175 6.83128 4.10177 6.83128C3.9118 6.83128 3.74933 6.90127 3.61435 7.04124C3.48438 7.17622 3.41939 7.33869 3.41939 7.52866C3.41939 7.62364 3.43689 7.71612 3.47188 7.80611C3.50687 7.89609 3.55686 7.98357 3.62185 8.06856L5.45903 10.3032C5.56901 10.4381 5.68149 10.5356 5.79646 10.5956C5.91144 10.6556 6.04142 10.6856 6.1864 10.6856Z" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="117" height="72" viewBox="0 0 210 297"><path d="M-111.5625 24.75V136.125H23.539306A82.5 82.5 0 0 1 105 66 82.5 82.5 0 0 1 186.5 136.125H321.5625V24.75ZM105 90.75A57.75 57.75 0 0 0 47.25 148.5 57.75 57.75 0 0 0 105 206.25 57.75 57.75 0 0 0 162.75 148.5 57.75 57.75 0 0 0 105 90.75Zm-216.5625 70.125V272.25h433.125V160.875H186.46068A82.5 82.5 0 0 1 105 231 82.5 82.5 0 0 1 23.5 160.875Z" stroke-width="81.90428162"/></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -0,0 +1,12 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8732_23377)">
<path d="M7.50218 5.75586H7.5001C6.53705 5.75586 5.75635 6.53656 5.75635 7.49961V7.50169C5.75635 8.46474 6.53705 9.24544 7.5001 9.24544H7.50218C8.46523 9.24544 9.24593 8.46474 9.24593 7.50169V7.49961C9.24593 6.53656 8.46523 5.75586 7.50218 5.75586Z" fill="#64748B"/>
<path d="M4.07917 5.88542C4.07917 4.85 4.91875 4.01042 5.95417 4.01042H9.575L13.5854 0H3.5875L0 3.5875V9.48958H4.07917V5.88333V5.88542Z" fill="#94A3B8"/>
<path d="M10.9208 5.49609V8.97734C10.9208 10.0128 10.0812 10.8523 9.0458 10.8523H5.56455L1.41455 15.0023H11.4125L15 11.4148V5.49818H10.9208V5.49609Z" fill="#94A3B8"/>
</g>
<defs>
<clipPath id="clip0_8732_23377">
<rect width="15" height="15" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 839 B

View File

@@ -5,7 +5,7 @@
import { lazy } from 'react';
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import { HomeNavBar } from '@/components/NavBar';
import { HomeNavBar, IdDetailsNavBar } from '@/components/NavBar';
const DisclaimerScreen = lazy(() => import('@/screens/home/DisclaimerScreen'));
const HomeScreen = lazy(() => import('@/screens/home/HomeScreen'));
@@ -15,6 +15,7 @@ const ProofHistoryDetailScreen = lazy(
const ProofHistoryScreen = lazy(
() => import('@/screens/home/ProofHistoryScreen'),
);
const IdDetailsScreen = lazy(() => import('@/screens/home/IdDetailsScreen'));
const homeScreens = {
Disclaimer: {
screen: DisclaimerScreen,
@@ -44,6 +45,14 @@ const homeScreens = {
title: 'Approval',
},
},
IdDetails: {
screen: IdDetailsScreen,
options: {
title: '',
header: IdDetailsNavBar, // Use custom header
headerBackVisible: false, // Hide default back button
},
},
};
export default homeScreens;

View File

@@ -23,6 +23,7 @@ import proveScreens from '@/navigation/prove';
import recoveryScreens from '@/navigation/recovery';
import settingsScreens from '@/navigation/settings';
import systemScreens from '@/navigation/system';
import type { ProofHistory } from '@/stores/proof-types';
import analytics from '@/utils/analytics';
import { setupUniversalLinkListenerInNavigation } from '@/utils/deeplinks';

View File

@@ -2,30 +2,32 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Pressable } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, styled, YStack } from 'tamagui';
import { Button, ScrollView, styled, Text, YStack } from 'tamagui';
import {
useFocusEffect,
useNavigation,
usePreventRemove,
} from '@react-navigation/native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { PassportData } from '@selfxyz/common/dist/esm';
import { DocumentCatalog } from '@selfxyz/common/dist/esm/src/utils/types';
import { DocumentMetadata, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { pressedStyle } from '@/components/buttons/pressedStyle';
import IdCardLayout from '@/components/homeScreen/idCard';
import { BodyText } from '@/components/typography/BodyText';
import { Caption } from '@/components/typography/Caption';
import { useAppUpdates } from '@/hooks/useAppUpdates';
import useConnectionModal from '@/hooks/useConnectionModal';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import SelfCard from '@/images/card-style-1.svg';
import ScanIcon from '@/images/icons/qr_scan.svg';
import WarnIcon from '@/images/icons/warning.svg';
import { usePassport } from '@/providers/passportDataProvider';
import { useSettingStore } from '@/stores/settingStore';
import { amber500, black, neutral700, slate800, white } from '@/utils/colors';
import useUserStore from '@/stores/userStore';
import { neutral700, slate50, slate800, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
const ScanButton = styled(Button, {
@@ -43,9 +45,43 @@ const HomeScreen: React.FC = () => {
const selfClient = useSelfClient();
useConnectionModal();
const navigation = useNavigation();
const { getAllDocuments } = usePassport();
const { setIdDetailsDocumentId } = useUserStore();
const { getAllDocuments, loadDocumentCatalog, setSelectedDocument } =
usePassport();
const [isNewVersionAvailable, showAppUpdateModal, isModalDismissed] =
useAppUpdates();
const [documentCatalog, setDocumentCatalog] = useState<DocumentCatalog>({
documents: [],
});
const [allDocuments, setAllDocuments] = useState<
Record<string, { data: PassportData; metadata: DocumentMetadata }>
>({});
const [loading, setLoading] = useState(true);
const loadDocuments = useCallback(async () => {
setLoading(true);
try {
const catalog = await loadDocumentCatalog();
const docs = await getAllDocuments();
setDocumentCatalog(catalog);
setAllDocuments(docs);
if (catalog.documents.length === 0) {
navigation.navigate('Launch' as never);
}
} catch (error) {
console.warn('Failed to load documents:', error);
navigation.navigate('Launch' as never);
}
setLoading(false);
}, [loadDocumentCatalog, getAllDocuments, navigation]);
useFocusEffect(
useCallback(() => {
loadDocuments();
}, [loadDocuments]),
);
useFocusEffect(() => {
if (isNewVersionAvailable && !isModalDismissed) {
@@ -53,22 +89,12 @@ const HomeScreen: React.FC = () => {
}
});
useFocusEffect(
useCallback(() => {
async function checkDocs() {
try {
const docs = await getAllDocuments();
if (Object.keys(docs).length === 0) {
navigation.navigate('Launch' as never);
}
} catch {
// ignore errors
}
}
checkDocs();
}, [getAllDocuments, navigation]),
);
const handleDocumentSelection = async (documentId: string) => {
await setSelectedDocument(documentId);
// Reload catalog to update selected state
const updatedCatalog = await loadDocumentCatalog();
setDocumentCatalog(updatedCatalog);
};
const goToQRCodeViewFinder = useHapticNavigation('QRCodeViewFinder');
const onScanButtonPress = useCallback(() => {
@@ -82,40 +108,64 @@ const HomeScreen: React.FC = () => {
// Prevents back navigation
usePreventRemove(true, () => {});
const { bottom } = useSafeAreaInsets();
if (loading) {
return (
<YStack
backgroundColor={slate50}
flex={1}
paddingHorizontal={20}
paddingBottom={bottom + extraYPadding}
justifyContent="center"
alignItems="center"
>
<Text>Loading documents...</Text>
</YStack>
);
}
return (
<YStack
backgroundColor={black}
gap={20}
justifyContent="space-between"
backgroundColor={'#F8FAFC'}
flex={1}
paddingHorizontal={20}
alignItems="center"
paddingBottom={bottom + extraYPadding}
>
<YStack alignItems="center" gap={20} justifyContent="flex-start">
<SelfCard width="100%" />
<Caption color={amber500} opacity={0.3} textTransform="uppercase">
Only visible to you
</Caption>
<PrivacyNote />
</YStack>
<YStack alignItems="center" gap={20} justifyContent="flex-end">
<ScanButton
onPress={onScanButtonPress}
hitSlop={100}
pressStyle={pressStyle}
>
<ScanIcon color={amber500} />
</ScanButton>
<Caption
onPress={onScanButtonPress}
color={amber500}
textTransform="uppercase"
backgroundColor={black}
pressStyle={{ backgroundColor: 'transparent' }}
>
Prove your SELF
</Caption>
</YStack>
<ScrollView
showsVerticalScrollIndicator={false}
flex={1}
contentContainerStyle={{
gap: 15,
paddingVertical: 20,
paddingHorizontal: 15, // Add horizontal padding for shadow space
paddingBottom: 35, // Add extra bottom padding for shadow
}}
>
{documentCatalog.documents.map((metadata: DocumentMetadata) => {
const documentData = allDocuments[metadata.id];
const isSelected = documentCatalog.selectedDocumentId === metadata.id;
if (!documentData) {
return null;
}
return (
<Pressable
key={metadata.id}
onPress={() => {
setIdDetailsDocumentId(metadata.id);
navigation.navigate('IdDetails');
}}
>
<IdCardLayout
idDocument={documentData.data}
selected={isSelected}
hidden={true}
/>
</Pressable>
);
})}
</ScrollView>
</YStack>
);
};

View File

@@ -0,0 +1,173 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useEffect, useState } from 'react';
import { ScrollView } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, Text, XStack, YStack, ZStack } from 'tamagui';
import { BlurView } from '@react-native-community/blur';
import { useNavigation, useRoute } from '@react-navigation/native';
import { DocumentCatalog } from '@selfxyz/common/dist/esm/src/utils/types';
import { PassportData } from '@selfxyz/common/types';
import IdCardLayout from '@/components/homeScreen/idCard';
import { usePassport } from '@/providers/passportDataProvider';
import ProofHistoryList from '@/screens/home/ProofHistoryList';
import useUserStore from '@/stores/userStore';
import {
black,
slate50,
slate100,
slate300,
slate500,
white,
} from '@/utils/colors';
const IdDetailsScreen: React.FC = () => {
const { idDetailsDocumentId } = useUserStore();
const documentId = idDetailsDocumentId;
const { getAllDocuments, loadDocumentCatalog, setSelectedDocument } =
usePassport();
const [document, setDocument] = useState<PassportData | null>(null);
const [documentCatalog, setDocumentCatalog] = useState<DocumentCatalog>({
documents: [],
});
const [isHidden, setIsHidden] = useState(true);
const navigation = useNavigation();
const { bottom } = useSafeAreaInsets();
useEffect(() => {
const loadDocumentAndCatalog = async () => {
const allDocs = await getAllDocuments();
const catalog = await loadDocumentCatalog();
const docEntry = Object.entries(allDocs).find(
([id]) => id === documentId,
);
setDocument(docEntry ? docEntry[1].data : null);
setDocumentCatalog(catalog);
};
loadDocumentAndCatalog();
}, [documentId, getAllDocuments, loadDocumentCatalog]);
const isConnected = documentCatalog.selectedDocumentId === documentId;
const handleConnectId = async () => {
if (!isConnected) {
await setSelectedDocument(documentId!);
const updatedCatalog = await loadDocumentCatalog();
setDocumentCatalog(updatedCatalog);
}
};
if (!documentId) {
return (
<YStack
flex={1}
backgroundColor={slate50}
justifyContent="center"
alignItems="center"
padding={20}
>
<Text>No document selected</Text>
</YStack>
);
}
if (!document) {
return (
<YStack
flex={1}
backgroundColor={slate50}
justifyContent="center"
alignItems="center"
padding={20}
>
<Text>Loading...</Text>
</YStack>
);
}
const ListHeader = (
<YStack padding={20}>
<IdCardLayout idDocument={document} selected={true} hidden={isHidden} />
<XStack marginTop={'$3'} justifyContent="flex-start" gap={'$4'}>
<Button
onPress={() => setIsHidden(!isHidden)}
backgroundColor={white}
color={'#2463EB'}
borderColor={slate300}
borderWidth={1}
borderRadius={5}
flex={1}
height={'$5'}
fontSize={16}
fontWeight="bold"
>
{isHidden ? 'View ID Data' : 'Hide ID Data'}
</Button>
<Button
onPress={() => navigation.navigate('ManageDocuments' as never)}
backgroundColor={'#2463EB'}
color={white}
borderColor={'#2463EB'}
borderWidth={1}
borderRadius={5}
flex={1}
height={'$5'}
fontSize={16}
fontWeight="bold"
>
Manage ID
</Button>
</XStack>
</YStack>
);
return (
<YStack flex={1} backgroundColor={slate50}>
{ListHeader}
<ZStack flex={1}>
<ProofHistoryList documentId={documentId} />
<BlurView
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 100,
}}
blurType="light"
blurAmount={4}
reducedTransparencyFallbackColor={slate50}
pointerEvents="none"
/>
<YStack position="absolute" bottom={bottom + 20} left={20} right={20}>
<Button
backgroundColor={isConnected ? slate100 : white}
color={isConnected ? slate500 : '#2463EB'}
borderColor={isConnected ? slate300 : slate100}
borderWidth={1}
borderRadius={'$3'}
height={'$5'}
fontSize={17}
elevation={4}
shadowColor={black}
shadowOffset={{ width: 0, height: 2 }}
shadowOpacity={0.1}
shadowRadius={4}
fontWeight="bold"
opacity={isConnected ? 0.8 : 1}
disabled={isConnected}
onPress={handleConnectId}
>
{isConnected ? 'ID Connected' : 'Connect ID'}
</Button>
</YStack>
</ZStack>
</YStack>
);
};
export default IdDetailsScreen;

View File

@@ -0,0 +1,420 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
RefreshControl,
SectionList,
StyleSheet,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Card, Image, Text, View, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import { CheckSquare2, Wallet, XCircle } from '@tamagui/lucide-icons';
import { BodyText } from '@/components/typography/BodyText';
import type { ProofHistory } from '@/stores/proof-types';
import { ProofStatus } from '@/stores/proof-types';
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
import {
black,
blue100,
blue600,
red500,
slate50,
slate200,
slate300,
slate400,
slate500,
white,
} from '@/utils/colors';
import { dinot, plexMono } from '@/utils/fonts';
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',
};
interface ProofHistoryListProps {
documentId: string;
}
export const ProofHistoryList: React.FC<ProofHistoryListProps> = ({
documentId,
}) => {
const {
proofHistory,
isLoading,
loadMoreHistory,
resetHistory,
initDatabase,
hasMore,
} = useProofHistoryStore();
const [refreshing, setRefreshing] = useState(false);
const navigation = useNavigation();
const { bottom } = useSafeAreaInsets();
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 filteredProofs = proofHistory.filter(
proof => proof.documentId === documentId,
);
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>();
filteredProofs.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) =>
new Date(groups[b][0].timestamp).getMonth() -
new Date(groups[a][0].timestamp).getMonth(),
)
.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, documentId, getTimePeriod]);
const renderItem = useCallback(
({
item,
index,
section,
}: {
item: ProofHistory;
index: number;
section: Section;
}) => {
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;
const borderRadiusSize = 16;
const isFirstItem = index === 0;
const isLastItem = index === section.data.length - 1;
return (
<View paddingHorizontal={5}>
<YStack gap={8}>
<Card
borderTopLeftRadius={isFirstItem ? borderRadiusSize : 0}
borderTopRightRadius={isFirstItem ? borderRadiusSize : 0}
borderBottomLeftRadius={isLastItem ? borderRadiusSize : 0}
borderBottomRightRadius={isLastItem ? borderRadiusSize : 0}
borderBottomWidth={1}
borderColor={slate200}
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
fontFamily={plexMono}
color={slate400}
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>
)}
{item.status === ProofStatus.FAILURE ? (
<XStack
paddingVertical={2}
paddingHorizontal={8}
borderRadius={4}
alignItems="center"
marginLeft={4}
>
<Text
color={red500}
fontSize={14}
fontWeight="600"
marginRight={4}
>
FAIL
</Text>
<XCircle color={red500} height={14} width={14} />
</XStack>
) : (
<XStack
backgroundColor={blue100}
paddingVertical={2}
paddingHorizontal={8}
borderRadius={4}
alignItems="center"
marginLeft={4}
>
<Text
color={blue600}
fontFamily={dinot}
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;
}
},
[navigation],
);
const renderSectionHeader = useCallback(
({ section }: { section: Section }) => {
return (
<View
paddingHorizontal={20}
backgroundColor={slate50}
marginTop={20}
marginBottom={12}
gap={12}
>
<Text
color={slate500}
fontSize={15}
fontWeight="500"
letterSpacing={0.6}
fontFamily={dinot}
>
{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 for this ID.</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 (
<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}
style={{ marginHorizontal: 15 }}
/>
);
};
const styles = StyleSheet.create({
listContent: {
paddingBottom: 100, // Add space for the floating Connect ID button
},
emptyList: {
flexGrow: 1,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
footerContainer: {
alignItems: 'center',
},
});
export default ProofHistoryList;

View File

@@ -31,7 +31,10 @@ import Disclosures from '@/components/Disclosures';
import { BodyText } from '@/components/typography/BodyText';
import { Caption } from '@/components/typography/Caption';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { setDefaultDocumentTypeIfNeeded } from '@/providers/passportDataProvider';
import {
setDefaultDocumentTypeIfNeeded,
usePassport,
} from '@/providers/passportDataProvider';
import { ProofStatus } from '@/stores/proof-types';
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
import { useSelfAppStore } from '@/stores/selfAppStore';
@@ -63,21 +66,29 @@ const ProveScreen: React.FC = () => {
const isReadyToProve = currentState === 'ready_to_prove';
const { addProofHistory } = useProofHistoryStore();
const { loadDocumentCatalog } = usePassport();
useEffect(() => {
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),
});
}
}, [addProofHistory, provingStore.uuid, selectedApp]);
const addHistory = async () => {
if (provingStore.uuid && selectedApp) {
const catalog = await loadDocumentCatalog();
const selectedDocumentId = catalog.selectedDocumentId;
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),
documentId: selectedDocumentId || '', // Fallback to empty if none selected
});
}
};
addHistory();
}, [addProofHistory, provingStore.uuid, selectedApp, loadDocumentCatalog]);
useEffect(() => {
if (isContentShorterThanScrollView) {

View File

@@ -97,7 +97,8 @@ export const database: ProofDB = {
errorReason TEXT,
timestamp INTEGER NOT NULL,
disclosures TEXT NOT NULL,
logoBase64 TEXT
logoBase64 TEXT,
documentId TEXT NOT NULL
)
`);
@@ -109,28 +110,61 @@ export const database: ProofDB = {
const db = await openDatabase();
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,
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
proof.appName,
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,
proof.disclosures,
proof.logoBase64 || null,
proof.userId,
proof.userIdType,
proof.sessionId,
],
);
return {
id: insertResult.insertId.toString(),
timestamp,
rowsAffected: insertResult.rowsAffected,
};
rowsAffected: insertResult.rowsAffected,
};
} 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
proof.appName,
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 {
throw error;
}
}
},
async updateProofStatus(
status: ProofStatus,
@@ -147,3 +181,10 @@ export const database: ProofDB = {
);
},
};
async function addDocumentIdColumn() {
const db = await openDatabase();
await db.executeSql(
`ALTER TABLE ${TABLE_NAME} ADD COLUMN documentId TEXT NOT NULL DEFAULT ''`,
);
}

View File

@@ -42,6 +42,7 @@ export interface ProofHistory {
timestamp: number;
disclosures: string;
logoBase64?: string;
documentId: string;
}
export enum ProofStatus {

View File

@@ -183,6 +183,7 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => {
logoBase64: row.logoBase64,
userId: row.userId,
userIdType: row.userIdType,
documentId: row.documentId,
});
}

View File

@@ -18,8 +18,10 @@ interface UserState {
deepLinkNationality?: IdDocInput['nationality'];
deepLinkBirthDate?: string;
deepLinkGender?: string;
idDetailsDocumentId?: string;
update: (patch: Partial<UserState>) => void;
deleteMrzFields: () => void;
setIdDetailsDocumentId: (documentId: string) => void;
setDeepLinkUserDetails: (details: {
name?: string;
surname?: string;
@@ -41,6 +43,7 @@ const useUserStore = create<UserState>((set, _get) => ({
deepLinkNationality: undefined,
deepLinkBirthDate: undefined,
deepLinkGender: undefined,
idDetailsDocumentId: undefined,
update: patch => {
set(state => ({ ...state, ...patch }));
@@ -64,6 +67,9 @@ const useUserStore = create<UserState>((set, _get) => ({
deepLinkGender: details.gender,
}),
setIdDetailsDocumentId: (documentId: string) =>
set({ idDetailsDocumentId: documentId }),
clearDeepLinkUserDetails: () =>
set({
deepLinkName: undefined,

View File

@@ -12,6 +12,8 @@ export const blue700 = '#1D4ED8';
// OLD
export const borderColor = '#343434';
export const charcoal = '#485469';
export const cyan300 = '#67E8F9';
export const emerald500 = '#10B981';
@@ -28,7 +30,7 @@ export const separatorColor = '#E0E0E0';
export const sky500 = '#0EA5E9';
export const slate100 = '#F1F5F9';
export const slate100 = '#F8FAFC';
export const slate200 = '#E2E8F0';

View File

@@ -2,6 +2,9 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Platform } from 'react-native';
export const advercase = 'Advercase-Regular';
export const dinot = 'DINOT-Medium';
export const plexMono = 'IBM Plex Mono';
export const plexMono =
Platform.OS === 'ios' ? 'IBM Plex Mono' : 'IBMPlexMono-Regular';