update layout

This commit is contained in:
Justin Hernandez
2026-01-07 22:21:34 -08:00
parent b5ef1fde68
commit 4b45e2aca1
12 changed files with 398 additions and 257 deletions

View File

@@ -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} />}
</>
);
};

View File

@@ -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>

View File

@@ -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
);

View File

@@ -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}
>

View File

@@ -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>

View File

@@ -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';

View File

@@ -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);

View File

@@ -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>
);

View File

@@ -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 {

View File

@@ -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 };

View File

@@ -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();
});
});

View File

@@ -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';