mirror of
https://github.com/selfxyz/self.git
synced 2026-01-10 23:27:56 -05:00
update layout
This commit is contained in:
@@ -4,12 +4,16 @@
|
||||
|
||||
import { Pressable } from 'react-native';
|
||||
import { Separator, Text, View, XStack, YStack } from 'tamagui';
|
||||
import { Check, Circle } from '@tamagui/lucide-icons';
|
||||
import { Check } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
black,
|
||||
green500,
|
||||
green600,
|
||||
iosSeparator,
|
||||
slate200,
|
||||
slate300,
|
||||
slate500,
|
||||
slate400,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
@@ -24,10 +28,6 @@ export interface IDSelectorItemProps {
|
||||
|
||||
export type IDSelectorState = 'active' | 'verified' | 'expired' | 'mock';
|
||||
|
||||
const green500 = '#22C55E';
|
||||
const red500 = '#EF4444';
|
||||
const orange500 = '#F97316';
|
||||
|
||||
function getSubtitleText(state: IDSelectorState): string {
|
||||
switch (state) {
|
||||
case 'active':
|
||||
@@ -37,20 +37,20 @@ function getSubtitleText(state: IDSelectorState): string {
|
||||
case 'expired':
|
||||
return 'Expired';
|
||||
case 'mock':
|
||||
return 'Developer ID';
|
||||
return 'Testing document';
|
||||
}
|
||||
}
|
||||
|
||||
function getSubtitleColor(state: IDSelectorState): string {
|
||||
switch (state) {
|
||||
case 'active':
|
||||
return green500;
|
||||
return green600;
|
||||
case 'verified':
|
||||
return slate500;
|
||||
return slate400;
|
||||
case 'expired':
|
||||
return red500;
|
||||
return slate400;
|
||||
case 'mock':
|
||||
return orange500;
|
||||
return slate400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,10 @@ export const IDSelectorItem: React.FC<IDSelectorItemProps> = ({
|
||||
const isActive = state === 'active';
|
||||
const subtitleText = getSubtitleText(state);
|
||||
const subtitleColor = getSubtitleColor(state);
|
||||
const textColor = isDisabled ? slate500 : black;
|
||||
const textColor = isDisabled ? slate400 : black;
|
||||
|
||||
// Determine circle color based on state
|
||||
const circleColor = isDisabled ? slate200 : slate300;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -76,34 +79,38 @@ export const IDSelectorItem: React.FC<IDSelectorItemProps> = ({
|
||||
testID={testID}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical={16}
|
||||
paddingHorizontal={8}
|
||||
paddingVertical={6}
|
||||
paddingHorizontal={0}
|
||||
alignItems="center"
|
||||
gap={12}
|
||||
gap={13}
|
||||
opacity={isDisabled ? 0.6 : 1}
|
||||
>
|
||||
{/* Radio button indicator */}
|
||||
<View
|
||||
width={24}
|
||||
width={29}
|
||||
height={24}
|
||||
borderRadius={12}
|
||||
borderWidth={isActive ? 0 : 2}
|
||||
borderColor={slate300}
|
||||
backgroundColor={isActive ? green500 : 'transparent'}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{isActive && <Check size={16} color="white" strokeWidth={3} />}
|
||||
{!isActive && !isDisabled && (
|
||||
<Circle size={20} color={slate300} strokeWidth={0} />
|
||||
)}
|
||||
<View
|
||||
width={24}
|
||||
height={24}
|
||||
borderRadius={12}
|
||||
borderWidth={isActive ? 0 : 2}
|
||||
borderColor={circleColor}
|
||||
backgroundColor={isActive ? green500 : 'transparent'}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{isActive && <Check size={16} color="white" strokeWidth={3} />}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Document info */}
|
||||
<YStack flex={1} gap={2}>
|
||||
<YStack flex={1} gap={2} paddingVertical={8} paddingBottom={9}>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={16}
|
||||
fontSize={18}
|
||||
fontWeight="500"
|
||||
color={textColor}
|
||||
>
|
||||
@@ -115,7 +122,7 @@ export const IDSelectorItem: React.FC<IDSelectorItemProps> = ({
|
||||
</YStack>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
{!isLastItem && <Separator borderColor={slate300} />}
|
||||
{!isLastItem && <Separator borderColor={iosSeparator} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Button, ScrollView, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { X } from '@tamagui/lucide-icons';
|
||||
import { Button, ScrollView, Sheet, Text, View, XStack, YStack } from 'tamagui';
|
||||
|
||||
import {
|
||||
black,
|
||||
blue600,
|
||||
slate300,
|
||||
slate500,
|
||||
slate200,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
@@ -75,69 +73,85 @@ export const IDSelectorSheet: React.FC<IDSelectorSheetProps> = ({
|
||||
borderTopRightRadius="$9"
|
||||
testID={testID}
|
||||
>
|
||||
<YStack padding="$4" flex={1}>
|
||||
<YStack padding={20} paddingTop={30} flex={1}>
|
||||
{/* Header */}
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
<Text
|
||||
fontSize={20}
|
||||
fontFamily={dinot}
|
||||
fontWeight="500"
|
||||
color={black}
|
||||
marginBottom={32}
|
||||
>
|
||||
<Text
|
||||
fontSize={20}
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
color={black}
|
||||
>
|
||||
Select an ID
|
||||
</Text>
|
||||
<XStack
|
||||
onPress={onDismiss}
|
||||
padding="$2"
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
testID={`${testID}-close-button`}
|
||||
>
|
||||
<X color={slate500} size={24} />
|
||||
</XStack>
|
||||
</XStack>
|
||||
Select an ID
|
||||
</Text>
|
||||
|
||||
{/* Document List */}
|
||||
<ScrollView
|
||||
{/* Document List Container with border radius */}
|
||||
<View
|
||||
flex={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID={`${testID}-list`}
|
||||
backgroundColor={white}
|
||||
borderRadius={10}
|
||||
overflow="hidden"
|
||||
marginBottom={32}
|
||||
>
|
||||
{documents.map((doc, index) => {
|
||||
const isSelected = doc.id === selectedId;
|
||||
// Don't override to 'active' if the document is in a disabled state
|
||||
const itemState: IDSelectorState =
|
||||
isSelected && !isDisabledState(doc.state)
|
||||
? 'active'
|
||||
: doc.state;
|
||||
<ScrollView
|
||||
flex={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID={`${testID}-list`}
|
||||
>
|
||||
{documents.map((doc, index) => {
|
||||
const isSelected = doc.id === selectedId;
|
||||
// Don't override to 'active' if the document is in a disabled state
|
||||
const itemState: IDSelectorState =
|
||||
isSelected && !isDisabledState(doc.state)
|
||||
? 'active'
|
||||
: doc.state;
|
||||
|
||||
return (
|
||||
<IDSelectorItem
|
||||
key={doc.id}
|
||||
documentName={doc.name}
|
||||
state={itemState}
|
||||
onPress={() => onSelect(doc.id)}
|
||||
isLastItem={index === documents.length - 1}
|
||||
testID={`${testID}-item-${doc.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
return (
|
||||
<IDSelectorItem
|
||||
key={doc.id}
|
||||
documentName={doc.name}
|
||||
state={itemState}
|
||||
onPress={() => onSelect(doc.id)}
|
||||
isLastItem={index === documents.length - 1}
|
||||
testID={`${testID}-item-${doc.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Footer Button */}
|
||||
<XStack marginTop="$4" paddingBottom={bottomPadding}>
|
||||
{/* Footer Buttons */}
|
||||
<XStack gap={10} paddingBottom={bottomPadding}>
|
||||
<Button
|
||||
flex={1}
|
||||
backgroundColor={canApprove ? blue600 : slate300}
|
||||
backgroundColor={white}
|
||||
borderWidth={1}
|
||||
borderColor={slate200}
|
||||
borderRadius={4}
|
||||
height={48}
|
||||
onPress={onDismiss}
|
||||
testID={`${testID}-dismiss-button`}
|
||||
pressStyle={{ opacity: 0.7 }}
|
||||
>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={18}
|
||||
fontWeight="500"
|
||||
color={black}
|
||||
>
|
||||
Dismiss
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
flex={1}
|
||||
backgroundColor={blue600}
|
||||
borderRadius={4}
|
||||
height={48}
|
||||
onPress={onApprove}
|
||||
disabled={!canApprove}
|
||||
opacity={canApprove ? 1 : 0.5}
|
||||
testID={`${testID}-select-button`}
|
||||
testID={`${testID}-approve-button`}
|
||||
pressStyle={{ opacity: 0.7 }}
|
||||
>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
@@ -145,7 +159,7 @@ export const IDSelectorSheet: React.FC<IDSelectorSheetProps> = ({
|
||||
fontWeight="500"
|
||||
color={white}
|
||||
>
|
||||
Select
|
||||
Approve
|
||||
</Text>
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
@@ -84,13 +84,19 @@ export const LeftAction: React.FC<LeftActionProps> = ({
|
||||
return <View {...props}>{children}</View>;
|
||||
};
|
||||
|
||||
const NavBarTitle: React.FC<NavBarTitleProps> = ({ children, color, ...props }) => {
|
||||
const NavBarTitle: React.FC<NavBarTitleProps> = ({
|
||||
children,
|
||||
color,
|
||||
...props
|
||||
}) => {
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return typeof children === 'string' ? (
|
||||
<Title color={color} {...props}>{children}</Title>
|
||||
<Title color={color} {...props}>
|
||||
{children}
|
||||
</Title>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ export const DefaultNavBar = (props: NativeStackHeaderProps) => {
|
||||
const headerStyle = (options.headerStyle || {}) as ViewStyle;
|
||||
const insets = useSafeAreaInsets();
|
||||
const headerTitleStyle = (options.headerTitleStyle || {}) as TextStyle;
|
||||
|
||||
|
||||
return (
|
||||
<NavBar.Container
|
||||
gap={14}
|
||||
@@ -28,8 +28,7 @@ export const DefaultNavBar = (props: NativeStackHeaderProps) => {
|
||||
paddingBottom={20}
|
||||
backgroundColor={headerStyle.backgroundColor as string}
|
||||
barStyle={
|
||||
options.headerTintColor === white ||
|
||||
headerTitleStyle?.color === white
|
||||
options.headerTintColor === white || headerTitleStyle?.color === white
|
||||
? 'light'
|
||||
: 'dark'
|
||||
}
|
||||
@@ -44,7 +43,7 @@ export const DefaultNavBar = (props: NativeStackHeaderProps) => {
|
||||
}}
|
||||
color={options.headerTintColor as string}
|
||||
/>
|
||||
<NavBar.Title
|
||||
<NavBar.Title
|
||||
color={headerTitleStyle.color as string}
|
||||
style={headerTitleStyle}
|
||||
>
|
||||
|
||||
@@ -70,7 +70,10 @@ export const BottomActionBar: React.FC<BottomActionBarProps> = ({
|
||||
{selectedDocumentName}
|
||||
</Text>
|
||||
<View marginLeft={8}>
|
||||
<ChevronUpDownIcon size={20} color={proofRequestColors.slate400} />
|
||||
<ChevronUpDownIcon
|
||||
size={20}
|
||||
color={proofRequestColors.slate400}
|
||||
/>
|
||||
</View>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Pressable } from 'react-native';
|
||||
import { View, XStack, Text } from 'tamagui';
|
||||
import { Text, View, XStack } from 'tamagui';
|
||||
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export const WalletAddressModal: React.FC<WalletAddressModalProps> = ({
|
||||
const handleCopy = useCallback(() => {
|
||||
Clipboard.setString(address);
|
||||
setCopied(true);
|
||||
|
||||
|
||||
// Reset copied state and close after a brief delay
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
|
||||
@@ -10,44 +10,6 @@ export interface IconProps {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filled circle icon (checkmark/bullet point)
|
||||
*/
|
||||
export const FilledCircleIcon: React.FC<IconProps> = ({
|
||||
size = 18,
|
||||
color = '#10B981',
|
||||
}) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Circle cx="12" cy="12" r="10" fill={color} />
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Info circle icon
|
||||
*/
|
||||
export const InfoCircleIcon: React.FC<IconProps> = ({
|
||||
size = 20,
|
||||
color = '#3B82F6',
|
||||
}) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
<Path
|
||||
d="M12 16V12M12 8H12.01"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Chevron up/down icon (dropdown)
|
||||
*/
|
||||
@@ -67,17 +29,17 @@ export const ChevronUpDownIcon: React.FC<IconProps> = ({
|
||||
);
|
||||
|
||||
/**
|
||||
* Wallet icon
|
||||
* Copy icon
|
||||
*/
|
||||
export const WalletIcon: React.FC<IconProps> = ({
|
||||
export const CopyIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
color = '#FFFFFF',
|
||||
}) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Rect
|
||||
x="3"
|
||||
y="6"
|
||||
width="18"
|
||||
x="9"
|
||||
y="9"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="2"
|
||||
stroke={color}
|
||||
@@ -85,12 +47,12 @@ export const WalletIcon: React.FC<IconProps> = ({
|
||||
fill="none"
|
||||
/>
|
||||
<Path
|
||||
d="M3 10H21M7 6V4C7 3.44772 7.44772 3 8 3H16C16.5523 3 17 3.44772 17 4V6"
|
||||
d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Circle cx="17" cy="13" r="1.5" fill={color} />
|
||||
</Svg>
|
||||
);
|
||||
|
||||
@@ -128,25 +90,28 @@ export const DocumentIcon: React.FC<IconProps> = ({
|
||||
);
|
||||
|
||||
/**
|
||||
* Copy icon
|
||||
* Filled circle icon (checkmark/bullet point)
|
||||
*/
|
||||
export const CopyIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
color = '#FFFFFF',
|
||||
export const FilledCircleIcon: React.FC<IconProps> = ({
|
||||
size = 18,
|
||||
color = '#10B981',
|
||||
}) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Rect
|
||||
x="9"
|
||||
y="9"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="2"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
<Circle cx="12" cy="12" r="10" fill={color} />
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Info circle icon
|
||||
*/
|
||||
export const InfoCircleIcon: React.FC<IconProps> = ({
|
||||
size = 20,
|
||||
color = '#3B82F6',
|
||||
}) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Circle cx="12" cy="12" r="10" stroke={color} strokeWidth="2" fill="none" />
|
||||
<Path
|
||||
d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5"
|
||||
d="M12 16V12M12 8H12.01"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
@@ -154,3 +119,31 @@ export const CopyIcon: React.FC<IconProps> = ({
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Wallet icon
|
||||
*/
|
||||
export const WalletIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
color = '#FFFFFF',
|
||||
}) => (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Rect
|
||||
x="3"
|
||||
y="6"
|
||||
width="18"
|
||||
height="13"
|
||||
rx="2"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
<Path
|
||||
d="M3 10H21M7 6V4C7 3.44772 7.44772 3 8 3H16C16.5523 3 17 3.44772 17 4V6"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<Circle cx="17" cy="13" r="1.5" fill={color} />
|
||||
</Svg>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,10 @@ export type { BottomActionBarProps } from '@/components/proof-request/BottomActi
|
||||
// Metadata bar
|
||||
export type { ConnectedWalletBadgeProps } from '@/components/proof-request/ConnectedWalletBadge';
|
||||
|
||||
export type { WalletAddressModalProps } from '@/components/proof-request/WalletAddressModal';
|
||||
|
||||
export type { DisclosureItemProps } from '@/components/proof-request/DisclosureItem';
|
||||
|
||||
export type { IconProps } from '@/components/proof-request/icons';
|
||||
|
||||
// Header section
|
||||
export type { ProofMetadataBarProps } from '@/components/proof-request/ProofMetadataBar';
|
||||
|
||||
@@ -24,30 +24,12 @@ export type { ProofMetadataBarProps } from '@/components/proof-request/ProofMeta
|
||||
export type { ProofRequestCardProps } from '@/components/proof-request/ProofRequestCard';
|
||||
export type { ProofRequestHeaderProps } from '@/components/proof-request/ProofRequestHeader';
|
||||
|
||||
export type { WalletAddressModalProps } from '@/components/proof-request/WalletAddressModal';
|
||||
|
||||
// Icons
|
||||
export { BottomActionBar } from '@/components/proof-request/BottomActionBar';
|
||||
|
||||
// Bottom action bar
|
||||
export {
|
||||
ConnectedWalletBadge,
|
||||
truncateAddress,
|
||||
} from '@/components/proof-request/ConnectedWalletBadge';
|
||||
|
||||
export { DisclosureItem } from '@/components/proof-request/DisclosureItem';
|
||||
|
||||
// Connected wallet badge
|
||||
export {
|
||||
ProofMetadataBar,
|
||||
formatTimestamp,
|
||||
} from '@/components/proof-request/ProofMetadataBar';
|
||||
|
||||
// Disclosure item
|
||||
export { ProofRequestCard } from '@/components/proof-request/ProofRequestCard';
|
||||
|
||||
export { ProofRequestHeader } from '@/components/proof-request/ProofRequestHeader';
|
||||
|
||||
export { WalletAddressModal } from '@/components/proof-request/WalletAddressModal';
|
||||
|
||||
// Icons
|
||||
export {
|
||||
ChevronUpDownIcon,
|
||||
CopyIcon,
|
||||
@@ -57,7 +39,25 @@ export {
|
||||
WalletIcon,
|
||||
} from '@/components/proof-request/icons';
|
||||
|
||||
export type { IconProps } from '@/components/proof-request/icons';
|
||||
export {
|
||||
ConnectedWalletBadge,
|
||||
truncateAddress,
|
||||
} from '@/components/proof-request/ConnectedWalletBadge';
|
||||
|
||||
// Connected wallet badge
|
||||
export { DisclosureItem } from '@/components/proof-request/DisclosureItem';
|
||||
|
||||
// Disclosure item
|
||||
export {
|
||||
ProofMetadataBar,
|
||||
formatTimestamp,
|
||||
} from '@/components/proof-request/ProofMetadataBar';
|
||||
|
||||
export { ProofRequestCard } from '@/components/proof-request/ProofRequestCard';
|
||||
|
||||
export { ProofRequestHeader } from '@/components/proof-request/ProofRequestHeader';
|
||||
|
||||
export { WalletAddressModal } from '@/components/proof-request/WalletAddressModal';
|
||||
|
||||
// Design tokens
|
||||
export {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
|
||||
import { ActivityIndicator } from 'react-native';
|
||||
import { Text, View } from 'tamagui';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
@@ -11,13 +12,10 @@ import {
|
||||
isDocumentValidForProving,
|
||||
pickBestDocumentToSelect,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
black,
|
||||
blue600,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { blue600 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import { proofRequestColors } from '@/components/proof-request';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
@@ -145,58 +143,62 @@ const ProvingScreenRouter: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View
|
||||
flex={1}
|
||||
backgroundColor={proofRequestColors.white}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
testID="proving-router-container"
|
||||
>
|
||||
{error ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<View alignItems="center" gap={16}>
|
||||
<Text
|
||||
style={styles.retryText}
|
||||
fontFamily={dinot}
|
||||
fontSize={16}
|
||||
color={proofRequestColors.slate500}
|
||||
textAlign="center"
|
||||
testID="proving-router-error"
|
||||
>
|
||||
{error}
|
||||
</Text>
|
||||
<View
|
||||
paddingHorizontal={24}
|
||||
paddingVertical={12}
|
||||
borderRadius={8}
|
||||
borderWidth={1}
|
||||
borderColor={proofRequestColors.slate200}
|
||||
onPress={() => {
|
||||
hasRoutedRef.current = false;
|
||||
loadAndRoute();
|
||||
}}
|
||||
pressStyle={{ opacity: 0.7 }}
|
||||
testID="proving-router-retry"
|
||||
>
|
||||
Tap to retry
|
||||
</Text>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={16}
|
||||
color={proofRequestColors.slate500}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ActivityIndicator color={blue600} size="large" />
|
||||
<Text style={styles.loadingText}>Loading documents...</Text>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={16}
|
||||
color={proofRequestColors.slate500}
|
||||
marginTop={16}
|
||||
testID="proving-router-loading"
|
||||
>
|
||||
Loading documents...
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: black,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 16,
|
||||
color: white,
|
||||
fontFamily: dinot,
|
||||
},
|
||||
errorContainer: {
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 16,
|
||||
color: white,
|
||||
fontFamily: dinot,
|
||||
textAlign: 'center',
|
||||
},
|
||||
retryText: {
|
||||
fontSize: 14,
|
||||
color: blue600,
|
||||
fontFamily: dinot,
|
||||
},
|
||||
});
|
||||
|
||||
export { ProvingScreenRouter };
|
||||
|
||||
@@ -178,9 +178,18 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
const { getByTestId } = render(<DocumentSelectorForProvingScreen />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-list')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-item-doc-1')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-item-doc-2')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-action-bar')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Open the sheet
|
||||
fireEvent.press(
|
||||
getByTestId('document-selector-action-bar-document-selector'),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-sheet-list')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-sheet-item-doc-1')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-sheet-item-doc-2')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,12 +212,23 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
const { getByTestId } = render(<DocumentSelectorForProvingScreen />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-continue').props.disabled).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
getByTestId('document-selector-action-bar-approve').props.disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-continue'));
|
||||
// Open sheet and approve
|
||||
fireEvent.press(
|
||||
getByTestId('document-selector-action-bar-document-selector'),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
getByTestId('document-selector-sheet-approve-button'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-sheet-approve-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetSelectedDocument).toHaveBeenCalledWith('doc-1');
|
||||
@@ -243,12 +263,23 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
const { getByTestId } = render(<DocumentSelectorForProvingScreen />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-continue').props.disabled).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
getByTestId('document-selector-action-bar-approve').props.disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-continue'));
|
||||
// Open sheet and approve
|
||||
fireEvent.press(
|
||||
getByTestId('document-selector-action-bar-document-selector'),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
getByTestId('document-selector-sheet-approve-button'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-sheet-approve-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetSelectedDocument).toHaveBeenCalledWith('doc-2');
|
||||
@@ -282,18 +313,27 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
const { getByTestId } = render(<DocumentSelectorForProvingScreen />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-item-doc-2')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-action-bar')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-item-doc-2'));
|
||||
fireEvent.press(getByTestId('document-selector-continue'));
|
||||
// Open sheet
|
||||
fireEvent.press(
|
||||
getByTestId('document-selector-action-bar-document-selector'),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-sheet-item-doc-2')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-sheet-item-doc-2'));
|
||||
fireEvent.press(getByTestId('document-selector-sheet-approve-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetSelectedDocument).toHaveBeenCalledWith('doc-1');
|
||||
});
|
||||
});
|
||||
|
||||
it('continue button is disabled when only expired documents exist', async () => {
|
||||
it('approve button is disabled when only expired documents exist', async () => {
|
||||
const expiredPassport = createMetadata({
|
||||
id: 'doc-1',
|
||||
documentType: 'us',
|
||||
@@ -321,9 +361,9 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
const { getByTestId } = render(<DocumentSelectorForProvingScreen />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-continue').props.disabled).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
getByTestId('document-selector-action-bar-approve').props.disabled,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -347,13 +387,13 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Unregistered documents should be selectable
|
||||
expect(getByTestId('document-selector-continue').props.disabled).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
getByTestId('document-selector-action-bar-approve').props.disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('continue button is enabled when valid document selected', async () => {
|
||||
it('approve button is enabled when valid document selected', async () => {
|
||||
const validPassport = createMetadata({
|
||||
id: 'doc-1',
|
||||
documentType: 'us',
|
||||
@@ -372,9 +412,9 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
const { getByTestId } = render(<DocumentSelectorForProvingScreen />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-continue').props.disabled).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
getByTestId('document-selector-action-bar-approve').props.disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -406,18 +446,27 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
const { getByTestId } = render(<DocumentSelectorForProvingScreen />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-item-doc-2')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-action-bar')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-item-doc-2'));
|
||||
fireEvent.press(getByTestId('document-selector-continue'));
|
||||
// Open sheet
|
||||
fireEvent.press(
|
||||
getByTestId('document-selector-action-bar-document-selector'),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-sheet-item-doc-2')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-sheet-item-doc-2'));
|
||||
fireEvent.press(getByTestId('document-selector-sheet-approve-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetSelectedDocument).toHaveBeenCalledWith('doc-2');
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking Continue navigates to the Prove screen', async () => {
|
||||
it('clicking Approve navigates to the Prove screen', async () => {
|
||||
const passport = createMetadata({
|
||||
id: 'doc-1',
|
||||
documentType: 'us',
|
||||
@@ -436,12 +485,23 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
const { getByTestId } = render(<DocumentSelectorForProvingScreen />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-continue').props.disabled).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
getByTestId('document-selector-action-bar-approve').props.disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-continue'));
|
||||
// Open sheet and approve
|
||||
fireEvent.press(
|
||||
getByTestId('document-selector-action-bar-document-selector'),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
getByTestId('document-selector-sheet-approve-button'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-sheet-approve-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('Prove');
|
||||
@@ -502,14 +562,13 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('document-selector-error')).toBeNull();
|
||||
expect(getByTestId('document-selector-list')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-item-doc-1')).toBeTruthy();
|
||||
expect(getByTestId('document-selector-action-bar')).toBeTruthy();
|
||||
});
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('shows an error when Continue fails to select the document', async () => {
|
||||
it('shows an error when Approve fails to select the document', async () => {
|
||||
const passport = createMetadata({
|
||||
id: 'doc-1',
|
||||
documentType: 'us',
|
||||
@@ -535,12 +594,23 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-continue').props.disabled).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
getByTestId('document-selector-action-bar-approve').props.disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-continue'));
|
||||
// Open sheet and approve
|
||||
fireEvent.press(
|
||||
getByTestId('document-selector-action-bar-document-selector'),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
getByTestId('document-selector-sheet-approve-button'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.press(getByTestId('document-selector-sheet-approve-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-error')).toBeTruthy();
|
||||
@@ -553,4 +623,48 @@ describe('DocumentSelectorForProvingScreen', () => {
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('clicking Dismiss button closes the sheet without selecting', async () => {
|
||||
const passport = createMetadata({
|
||||
id: 'doc-1',
|
||||
documentType: 'us',
|
||||
isRegistered: true,
|
||||
});
|
||||
const catalog: DocumentCatalog = {
|
||||
documents: [passport],
|
||||
selectedDocumentId: 'doc-1',
|
||||
};
|
||||
|
||||
mockLoadDocumentCatalog.mockResolvedValue(catalog);
|
||||
mockGetAllDocuments.mockResolvedValue(
|
||||
createAllDocuments([createDocumentEntry(passport)]),
|
||||
);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<DocumentSelectorForProvingScreen />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('document-selector-action-bar')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Open sheet
|
||||
fireEvent.press(
|
||||
getByTestId('document-selector-action-bar-document-selector'),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
getByTestId('document-selector-sheet-dismiss-button'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click dismiss
|
||||
fireEvent.press(getByTestId('document-selector-sheet-dismiss-button'));
|
||||
|
||||
// Sheet should close (implementation detail - the sheet component handles this)
|
||||
// Document selection should not have been called
|
||||
expect(mockSetSelectedDocument).not.toHaveBeenCalled();
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,9 @@ export const cyan300 = '#67E8F9';
|
||||
export const emerald500 = '#10B981';
|
||||
|
||||
export const green500 = '#22C55E';
|
||||
export const green600 = '#16A34A';
|
||||
|
||||
export const iosSeparator = 'rgba(60,60,67,0.36)';
|
||||
|
||||
export const neutral400 = '#A3A3A3';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user