Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f43a22c
add document selector test screen
transphorm Jan 6, 2026
bbe4aa5
clean up mock docs
transphorm Jan 6, 2026
4ad10e6
update selection options
transphorm Jan 7, 2026
1e3386f
Merge branch 'dev' into justin/self-1754-proving-screen-selective-dis…
transphorm Jan 7, 2026
a63f3ad
Merge branch 'dev' into justin/self-1754-proving-screen-selective-dis…
transphorm Jan 7, 2026
d28cf5a
Add DocumentSelectorForProving screen and route proof flows through i…
transphorm Jan 8, 2026
a6d4548
remove not accepted state
transphorm Jan 8, 2026
191acca
save wip design
transphorm Jan 8, 2026
3e7d6c6
formatting
transphorm Jan 8, 2026
b5ef1fd
update design
transphorm Jan 8, 2026
4b45e2a
update layout
transphorm Jan 8, 2026
a54374a
Update proving flow tests (#1559)
transphorm Jan 8, 2026
99d200e
Refactor ProveScreen to ProofRequestCard layout and preserve scroll p…
transphorm Jan 8, 2026
edac83b
wip fix tests
transphorm Jan 8, 2026
8ea959d
fix tests
transphorm Jan 8, 2026
bacd36b
formatting
transphorm Jan 8, 2026
69e7523
agent feedback
transphorm Jan 8, 2026
fccb65c
fix tests
transphorm Jan 8, 2026
128b4fa
Merge branch 'dev' into justin/self-1754-proving-screen-selective-dis…
transphorm Jan 8, 2026
e54ba03
save wip
transphorm Jan 8, 2026
e7f8411
remove text
transphorm Jan 8, 2026
2324023
fix types
transphorm Jan 8, 2026
69c65e3
save working header update
transphorm Jan 8, 2026
0fadd1f
no transition
transphorm Jan 8, 2026
5fd9c5f
cache document load for proving flow
transphorm Jan 8, 2026
75d1c7d
save fixes
transphorm Jan 8, 2026
f8b3c65
small fixes
transphorm Jan 9, 2026
7f6d809
match disclosure text
transphorm Jan 9, 2026
cddd454
design updates
transphorm Jan 9, 2026
9128ca0
fix approve flow
transphorm Jan 9, 2026
256fcd8
fix document type flash
transphorm Jan 9, 2026
a472865
add min height so text doesn't jump
transphorm Jan 9, 2026
9c37e85
update lock
transphorm Jan 9, 2026
1cc7951
formatting
transphorm Jan 9, 2026
0d4da72
save refactor wip
transphorm Jan 9, 2026
6cd9d7a
don't enable euclid yet
transphorm Jan 9, 2026
5cf025a
fix tests
transphorm Jan 9, 2026
921c60e
fix staleness check
transphorm Jan 9, 2026
e559aca
fix select box description
transphorm Jan 9, 2026
c387940
remove id selector screen
transphorm Jan 9, 2026
e03508d
vertically center
transphorm Jan 9, 2026
97698a0
button updates
transphorm Jan 9, 2026
0953717
Remove proving document cache (#1567)
transphorm Jan 9, 2026
be9452f
Merge branch 'dev' into justin/self-1754-proving-screen-selective-dis…
transphorm Jan 9, 2026
2526ac2
formatting
transphorm Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions app/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ jest.mock('react-native', () => {
get NativeModules() {
return global.NativeModules || {};
},
useColorScheme: jest.fn(() => 'light'),
NativeEventEmitter: jest.fn().mockImplementation(nativeModule => {
return {
addListener: jest.fn(),
Expand All @@ -110,10 +111,15 @@ jest.mock('react-native', () => {
}),
PixelRatio: mockPixelRatio,
Dimensions: {
get: jest.fn(() => ({
window: { width: 375, height: 667, scale: 2 },
screen: { width: 375, height: 667, scale: 2 },
})),
get: jest.fn(dimension => {
const dimensions = {
window: { width: 375, height: 667, scale: 2, fontScale: 1 },
screen: { width: 375, height: 667, scale: 2, fontScale: 1 },
};
return dimension ? dimensions[dimension] : dimensions;
}),
addEventListener: jest.fn(() => ({ remove: jest.fn() })),
removeEventListener: jest.fn(),
},
Linking: {
getInitialURL: jest.fn().mockResolvedValue(null),
Expand All @@ -139,6 +145,7 @@ jest.mock('react-native', () => {
ScrollView: 'ScrollView',
TouchableOpacity: 'TouchableOpacity',
TouchableHighlight: 'TouchableHighlight',
Pressable: 'Pressable',
Image: 'Image',
ActivityIndicator: 'ActivityIndicator',
SafeAreaView: 'SafeAreaView',
Expand Down Expand Up @@ -273,10 +280,15 @@ jest.mock(
Version: 14,
},
Dimensions: {
get: jest.fn(() => ({
window: { width: 375, height: 667, scale: 2 },
screen: { width: 375, height: 667, scale: 2 },
})),
get: jest.fn(dimension => {
const dimensions = {
window: { width: 375, height: 667, scale: 2, fontScale: 1 },
screen: { width: 375, height: 667, scale: 2, fontScale: 1 },
};
return dimension ? dimensions[dimension] : dimensions;
}),
addEventListener: jest.fn(() => ({ remove: jest.fn() })),
removeEventListener: jest.fn(),
},
StyleSheet: {
create: jest.fn(styles => styles),
Expand Down Expand Up @@ -359,15 +371,18 @@ jest.mock(
'../packages/mobile-sdk-alpha/node_modules/react-native/Libraries/Utilities/Dimensions',
() => ({
getConstants: jest.fn(() => ({
window: { width: 375, height: 667, scale: 2 },
screen: { width: 375, height: 667, scale: 2 },
window: { width: 375, height: 667, scale: 2, fontScale: 1 },
screen: { width: 375, height: 667, scale: 2, fontScale: 1 },
})),
set: jest.fn(),
get: jest.fn(() => ({
window: { width: 375, height: 667, scale: 2 },
screen: { width: 375, height: 667, scale: 2 },
})),
addEventListener: jest.fn(),
get: jest.fn(dimension => {
const dimensions = {
window: { width: 375, height: 667, scale: 2, fontScale: 1 },
screen: { width: 375, height: 667, scale: 2, fontScale: 1 },
};
return dimension ? dimensions[dimension] : dimensions;
}),
addEventListener: jest.fn(() => ({ remove: jest.fn() })),
removeEventListener: jest.fn(),
}),
{ virtual: true },
Expand Down Expand Up @@ -550,8 +565,14 @@ jest.mock(
{ virtual: true },
);

// Mock the hooks subpath from mobile-sdk-alpha
jest.mock('@selfxyz/mobile-sdk-alpha/hooks', () => ({
useSafeBottomPadding: jest.fn((basePadding = 20) => basePadding + 50),
}));

// Mock problematic mobile-sdk-alpha components that use React Native StyleSheet
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
// Override only the specific mocks we need
NFCScannerScreen: jest.fn(() => null),
SelfClientProvider: jest.fn(({ children }) => children),
useSelfClient: jest.fn(() => {
Expand Down
75 changes: 9 additions & 66 deletions app/src/components/Disclosures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,24 @@
import React from 'react';
import { XStack, YStack } from 'tamagui';

import type { Country3LetterCode } from '@selfxyz/common/constants';
import { countryCodes } from '@selfxyz/common/constants';
import type { SelfAppDisclosureConfig } from '@selfxyz/common/utils';
import { BodyText } from '@selfxyz/mobile-sdk-alpha/components';
import { slate200, slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';

import CheckMark from '@/assets/icons/checkmark.svg';
import {
getDisclosureText,
ORDERED_DISCLOSURE_KEYS,
} from '@/utils/disclosureUtils';

interface DisclosureProps {
disclosures: SelfAppDisclosureConfig;
}

function listToString(list: string[]): string {
if (list.length === 1) {
return list[0];
} else if (list.length === 2) {
return list.join(' nor ');
}
return `${list.slice(0, -1).join(', ')} nor ${list.at(-1)}`;
}

export default function Disclosures({ disclosures }: DisclosureProps) {
// Define the order in which disclosures should appear.
const ORDERED_KEYS: Array<keyof SelfAppDisclosureConfig> = [
'issuing_state',
'name',
'passport_number',
'nationality',
'date_of_birth',
'gender',
'expiry_date',
'ofac',
'excludedCountries',
'minimumAge',
] as const;

return (
<YStack>
{ORDERED_KEYS.map(key => {
{ORDERED_DISCLOSURE_KEYS.map(key => {
const isEnabled = disclosures[key];
if (
!isEnabled ||
Expand All @@ -52,53 +31,17 @@ export default function Disclosures({ disclosures }: DisclosureProps) {
return null;
}

let text = '';
switch (key) {
case 'ofac':
text = 'I am not on the OFAC sanction list';
break;
case 'excludedCountries':
text = `I am not a citizen of the following countries: ${countriesToSentence(
(disclosures.excludedCountries as Country3LetterCode[]) || [],
)}`;
break;
case 'minimumAge':
text = `Age is over ${disclosures.minimumAge}`;
break;
case 'name':
text = 'Name';
break;
case 'passport_number':
text = 'Passport Number';
break;
case 'date_of_birth':
text = 'Date of Birth';
break;
case 'gender':
text = 'Gender';
break;
case 'expiry_date':
text = 'Passport Expiry Date';
break;
case 'issuing_state':
text = 'Issuing State';
break;
case 'nationality':
text = 'Nationality';
break;
default:
return null;
const text = getDisclosureText(key, disclosures);
if (!text) {
return null;
}

return <DisclosureItem key={key} text={text} />;
})}
</YStack>
);
}

function countriesToSentence(countries: Array<Country3LetterCode>): string {
return listToString(countries.map(country => countryCodes[country]));
}

interface DisclosureItemProps {
text: string;
}
Expand Down
132 changes: 132 additions & 0 deletions app/src/components/documents/IDSelectorItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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 { Pressable } from 'react-native';
import { Separator, Text, View, XStack, YStack } from 'tamagui';
import { Check } from '@tamagui/lucide-icons';

import {
black,
green500,
green600,
iosSeparator,
slate200,
slate300,
slate400,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';

export interface IDSelectorItemProps {
documentName: string;
state: IDSelectorState;
onPress?: () => void;
disabled?: boolean;
isLastItem?: boolean;
testID?: string;
}

export type IDSelectorState = 'active' | 'verified' | 'expired' | 'mock';

function getSubtitleText(state: IDSelectorState): string {
switch (state) {
case 'active':
return 'Currently active';
case 'verified':
return 'Verified ID';
case 'expired':
return 'Expired';
case 'mock':
return 'Testing document';
}
}

function getSubtitleColor(state: IDSelectorState): string {
switch (state) {
case 'active':
return green600;
case 'verified':
return slate400;
case 'expired':
return slate400;
case 'mock':
return slate400;
}
}

export const IDSelectorItem: React.FC<IDSelectorItemProps> = ({
documentName,
state,
onPress,
disabled,
isLastItem,
testID,
}) => {
const isDisabled = disabled || isDisabledState(state);
const isActive = state === 'active';
const subtitleText = getSubtitleText(state);
const subtitleColor = getSubtitleColor(state);
const textColor = isDisabled ? slate400 : black;

// Determine circle color based on state
const circleColor = isDisabled ? slate200 : slate300;

return (
<>
<Pressable
onPress={isDisabled ? undefined : onPress}
disabled={isDisabled}
testID={testID}
>
<XStack
paddingVertical={6}
paddingHorizontal={0}
alignItems="center"
gap={13}
opacity={isDisabled ? 0.6 : 1}
>
{/* Radio button indicator */}
<View
width={29}
height={24}
alignItems="center"
justifyContent="center"
>
<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} paddingVertical={8} paddingBottom={9}>
<Text
fontFamily={dinot}
fontSize={18}
fontWeight="500"
color={textColor}
>
{documentName}
</Text>
<Text fontFamily={dinot} fontSize={14} color={subtitleColor}>
{subtitleText}
</Text>
</YStack>
</XStack>
</Pressable>
{!isLastItem && <Separator borderColor={iosSeparator} />}
</>
);
};

export function isDisabledState(state: IDSelectorState): boolean {
return state === 'expired';
}
Loading
Loading