mirror of
https://github.com/selfxyz/self.git
synced 2026-04-05 03:00:53 -04:00
add new home screen (#1019)
* add new home screen * fix typing issue * yarn nice
This commit is contained in:
committed by
GitHub
parent
ff678b359a
commit
78b2341091
@@ -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();
|
||||
|
||||
56
app/src/components/NavBar/IdDetailsNavBar.tsx
Normal file
56
app/src/components/NavBar/IdDetailsNavBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
578
app/src/components/homeScreen/idCard.tsx
Normal file
578
app/src/components/homeScreen/idCard.tsx
Normal 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}`;
|
||||
}
|
||||
3
app/src/images/icons/checkmark_gray.svg
Normal file
3
app/src/images/icons/checkmark_gray.svg
Normal 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 |
1
app/src/images/icons/epassport.svg
Normal file
1
app/src/images/icons/epassport.svg
Normal 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 |
12
app/src/images/logo_gray.svg
Normal file
12
app/src/images/logo_gray.svg
Normal 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 |
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
173
app/src/screens/home/IdDetailsScreen.tsx
Normal file
173
app/src/screens/home/IdDetailsScreen.tsx
Normal 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;
|
||||
420
app/src/screens/home/ProofHistoryList.tsx
Normal file
420
app/src/screens/home/ProofHistoryList.tsx
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ''`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface ProofHistory {
|
||||
timestamp: number;
|
||||
disclosures: string;
|
||||
logoBase64?: string;
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
export enum ProofStatus {
|
||||
|
||||
@@ -183,6 +183,7 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => {
|
||||
logoBase64: row.logoBase64,
|
||||
userId: row.userId,
|
||||
userIdType: row.userIdType,
|
||||
documentId: row.documentId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user