Merge pull request #1676 from selfxyz/release/staging-2026-01-30

Release to Staging - 2026-01-30
This commit is contained in:
Justin Hernandez
2026-02-03 22:03:41 -08:00
committed by GitHub
56 changed files with 4208 additions and 1206 deletions

View File

@@ -32,8 +32,12 @@ concurrency:
jobs:
build:
runs-on: ["128ram"]
timeout-minutes: 720 # 12 hours
runs-on:
- "32ram"
- "self-hosted"
- "selfxyz-org"
# GitHub-hosted runners cap at 360 min (6h); 720 applies if using self-hosted
timeout-minutes: 720
permissions:
contents: read
actions: read
@@ -165,7 +169,7 @@ jobs:
path: output/
run_id: ${{ inputs.run-id }}
- name: Build cpp circuits
- name: Prepare build scripts
run: |
chmod +x circuits/scripts/build/build_cpp.sh
chmod +x circuits/scripts/build/build_single_circuit.sh
@@ -173,47 +177,58 @@ jobs:
# Validate inputs - only one should be provided
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
if [[ "${{ inputs.circuit-type }}" != "" && "${{ inputs.circuit-name }}" != "" ]]; then
echo " Error: Cannot provide both circuit-type and circuit-name. Use only one."
echo "Error: Cannot provide both circuit-type and circuit-name. Use only one."
exit 1
fi
fi
# Check what type of build to perform
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.circuit-name }}" != "" ]]; then
# Build circuits by name
- name: Build cpp circuits (workflow_dispatch by name/type)
if: github.event_name == 'workflow_dispatch' && (inputs.circuit-name != '' || inputs.circuit-type != '')
run: |
if [[ "${{ inputs.circuit-name }}" != "" ]]; then
INPUT_CIRCUITS="${{ inputs.circuit-name }}"
INPUT_CIRCUITS=$(echo "$INPUT_CIRCUITS" | tr -d ' ')
IFS=',' read -ra CIRCUITS_ARRAY <<< "$INPUT_CIRCUITS"
echo "Building selected circuits: ${{ inputs.circuit-name }}"
echo "Building selected circuits by name: ${{ inputs.circuit-name }}"
for circuit_name in "${CIRCUITS_ARRAY[@]}"; do
echo "Building circuit: $circuit_name"
./circuits/scripts/build/build_single_circuit.sh "$circuit_name"
done
elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.circuit-type }}" != "" ]]; then
# Build circuits by type
else
INPUT_CIRCUITS="${{ inputs.circuit-type }}"
INPUT_CIRCUITS=$(echo "$INPUT_CIRCUITS" | tr -d ' ')
IFS=',' read -ra CIRCUITS_ARRAY <<< "$INPUT_CIRCUITS"
echo "Building selected circuits: ${{ inputs.circuit-type }}"
echo "Building selected circuits by type: ${{ inputs.circuit-type }}"
for circuit in "${CIRCUITS_ARRAY[@]}"; do
echo "Building circuit: $circuit"
./circuits/scripts/build/build_cpp.sh "$circuit"
done
else
# Build all circuits (default behavior)
echo "Building all circuits (default behavior)"
./circuits/scripts/build/build_cpp.sh register
./circuits/scripts/build/build_cpp.sh register_id
./circuits/scripts/build/build_cpp.sh register_aadhaar
./circuits/scripts/build/build_cpp.sh register_kyc
./circuits/scripts/build/build_cpp.sh disclose
./circuits/scripts/build/build_cpp.sh dsc
fi
- name: Build cpp circuits - register
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
run: ./circuits/scripts/build/build_cpp.sh register
- name: Build cpp circuits - register_id
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
run: ./circuits/scripts/build/build_cpp.sh register_id
- name: Build cpp circuits - register_aadhaar
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
run: ./circuits/scripts/build/build_cpp.sh register_aadhaar
- name: Build cpp circuits - register_kyc
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
run: ./circuits/scripts/build/build_cpp.sh register_kyc
- name: Build cpp circuits - disclose
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
run: ./circuits/scripts/build/build_cpp.sh disclose
- name: Build cpp circuits - dsc
if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '')
run: ./circuits/scripts/build/build_cpp.sh dsc
- name: Upload Artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:

View File

@@ -459,6 +459,37 @@ jobs:
FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath packages/mobile-sdk-demo/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule || { echo "❌ iOS build failed"; exit 1; }
echo "✅ iOS build succeeded"
- name: Build iOS Release Archive (unsigned)
run: |
echo "Building iOS Release archive (unsigned) to validate Release configuration..."
WORKSPACE_PATH="${{ env.IOS_WORKSPACE_PATH }}"
FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
xcodebuild archive \
-workspace "$WORKSPACE_PATH" \
-scheme ${{ env.IOS_PROJECT_SCHEME }} \
-configuration Release \
-archivePath packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive \
-destination "generic/platform=iOS" \
-jobs "$(sysctl -n hw.ncpu)" \
-parallelizeTargets \
-quiet \
COMPILER_INDEX_STORE_ENABLE=NO \
SWIFT_COMPILATION_MODE=wholemodule \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
AD_HOC_CODE_SIGNING_ALLOWED=NO \
|| { echo "❌ iOS Release archive build failed"; exit 1; }
echo "✅ iOS Release archive build succeeded (unsigned)"
# Verify archive was created
if [ -d "packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive" ]; then
echo "📦 Archive created at packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive"
else
echo "❌ Archive not found"
exit 1
fi
- name: Install and Test on iOS
run: |
echo "Installing app on simulator..."

View File

@@ -34,6 +34,5 @@ export const SUMSUB_TEST_TOKEN = process.env.SUMSUB_TEST_TOKEN;
export const TURNKEY_AUTH_PROXY_CONFIG_ID =
process.env.TURNKEY_AUTH_PROXY_CONFIG_ID;
export const TURNKEY_GOOGLE_CLIENT_ID = process.env.TURNKEY_GOOGLE_CLIENT_ID;
export const TURNKEY_ORGANIZATION_ID = process.env.TURNKEY_ORGANIZATION_ID;

View File

@@ -79,6 +79,8 @@ export type RootStackParamList = Omit<
| 'Home'
| 'IDPicker'
| 'IdDetails'
| 'KycSuccess'
| 'KYCVerified'
| 'RegistrationFallback'
| 'Loading'
| 'Modal'
@@ -201,7 +203,17 @@ export type RootStackParamList = Omit<
// Onboarding screens
Disclaimer: undefined;
KycSuccess: undefined;
KycSuccess:
| {
userId?: string;
}
| undefined;
KYCVerified:
| {
status?: string;
userId?: string;
}
| undefined;
// Dev screens
CreateMock: undefined;

View File

@@ -5,6 +5,7 @@
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen';
import KYCVerifiedScreen from '@/screens/kyc/KYCVerifiedScreen';
import AccountVerifiedSuccessScreen from '@/screens/onboarding/AccountVerifiedSuccessScreen';
import DisclaimerScreen from '@/screens/onboarding/DisclaimerScreen';
import SaveRecoveryPhraseScreen from '@/screens/onboarding/SaveRecoveryPhraseScreen';
@@ -38,6 +39,13 @@ const onboardingScreens = {
animation: 'slide_from_bottom',
} as NativeStackNavigationOptions,
},
KYCVerified: {
screen: KYCVerifiedScreen,
options: {
headerShown: false,
animation: 'slide_from_bottom',
} as NativeStackNavigationOptions,
},
};
export default onboardingScreens;

View File

@@ -4,16 +4,89 @@
import type { PropsWithChildren } from 'react';
import React, { useEffect } from 'react';
import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import messaging from '@react-native-firebase/messaging';
import { NotificationEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { navigationRef } from '@/navigation';
import { trackEvent } from '@/services/analytics';
// Queue for pending navigation actions that need to wait for navigation to be ready
let pendingNavigation: FirebaseMessagingTypes.RemoteMessage | null = null;
/**
* Execute navigation for a notification
* @returns true if navigation was executed, false if it needs to be queued
*/
const executeNotificationNavigation = (
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
): boolean => {
if (!navigationRef.isReady()) {
return false;
}
const notificationType = remoteMessage.data?.type;
const status = remoteMessage.data?.status;
// Handle KYC result notifications
if (notificationType === 'kyc_result' && status === 'approved') {
navigationRef.navigate('KYCVerified', {
status: String(status),
userId: remoteMessage.data?.user_id
? String(remoteMessage.data.user_id)
: undefined,
});
return true;
}
// Add handling for other notification types here as needed
// For retry/rejected statuses, could navigate to appropriate screens in future
return true; // Navigation handled (or not applicable)
};
/**
* Handle navigation based on notification type and data
* Queues navigation if navigationRef is not ready yet
*/
const handleNotificationNavigation = (
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
) => {
const executed = executeNotificationNavigation(remoteMessage);
if (!executed) {
// Navigation not ready yet - queue for later
pendingNavigation = remoteMessage;
if (__DEV__) {
console.log(
'Navigation not ready, queuing notification navigation:',
remoteMessage.data?.type,
);
}
}
};
/**
* Process any pending navigation once navigation is ready
*/
const processPendingNavigation = () => {
if (pendingNavigation && navigationRef.isReady()) {
if (__DEV__) {
console.log(
'Processing pending notification navigation:',
pendingNavigation.data?.type,
);
}
executeNotificationNavigation(pendingNavigation);
pendingNavigation = null;
}
};
export const NotificationTrackingProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
useEffect(() => {
// Handle notification tap when app is in background
const unsubscribe = messaging().onNotificationOpenedApp(remoteMessage => {
trackEvent(NotificationEvents.BACKGROUND_NOTIFICATION_OPENED, {
messageId: remoteMessage.messageId,
@@ -22,8 +95,12 @@ export const NotificationTrackingProvider: React.FC<PropsWithChildren> = ({
// Track if user interacted with any actions
actionId: remoteMessage.data?.actionId,
});
// Handle navigation based on notification type
handleNotificationNavigation(remoteMessage);
});
// Handle notification tap when app is completely closed (cold start)
messaging()
.getInitialNotification()
.then(remoteMessage => {
@@ -35,11 +112,34 @@ export const NotificationTrackingProvider: React.FC<PropsWithChildren> = ({
// Track if user interacted with any actions
actionId: remoteMessage.data?.actionId,
});
// Handle navigation based on notification type
handleNotificationNavigation(remoteMessage);
}
});
return unsubscribe;
}, []);
// Monitor navigation readiness and process pending navigation
useEffect(() => {
// Check immediately if navigation is already ready
if (navigationRef.isReady()) {
processPendingNavigation();
return;
}
// Poll for navigation readiness if not ready yet
const checkInterval = setInterval(() => {
if (navigationRef.isReady()) {
processPendingNavigation();
clearInterval(checkInterval);
}
}, 100); // Check every 100ms
// Cleanup interval on unmount
return () => clearInterval(checkInterval);
}, []);
return <>{children}</>;
};

View File

@@ -380,7 +380,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
// Success case: navigate to KYC success screen
if (navigationRef.isReady()) {
navigationRef.navigate('KycSuccess');
navigationRef.navigate('KycSuccess', {
userId: accessToken.userId,
});
}
} catch (error) {
const safeInitError = sanitizeErrorMessage(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
// 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, useRef, useState } from 'react';
import { ScrollView, TouchableOpacity } from 'react-native';
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
import { Check, ChevronDown } from '@tamagui/lucide-icons';
import {
red500,
slate200,
slate500,
slate600,
slate800,
white,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
import type { InjectedErrorType } from '@/stores/errorInjectionStore';
import {
ERROR_GROUPS,
ERROR_LABELS,
useErrorInjectionStore,
} from '@/stores/errorInjectionStore';
import {
registerModalCallbacks,
unregisterModalCallbacks,
} from '@/utils/modalCallbackRegistry';
export const ErrorInjectionSelector = () => {
const injectedErrors = useErrorInjectionStore(state => state.injectedErrors);
const setInjectedErrors = useErrorInjectionStore(
state => state.setInjectedErrors,
);
const clearAllErrors = useErrorInjectionStore(state => state.clearAllErrors);
const [open, setOpen] = useState(false);
const callbackIdRef = useRef<number>();
const handleModalDismiss = useCallback(() => {
setOpen(false);
if (callbackIdRef.current !== undefined) {
unregisterModalCallbacks(callbackIdRef.current);
callbackIdRef.current = undefined;
}
}, []);
const openSheet = useCallback(() => {
setOpen(true);
const id = registerModalCallbacks({
onButtonPress: () => {},
onModalDismiss: handleModalDismiss,
});
callbackIdRef.current = id;
}, [handleModalDismiss]);
const closeSheet = useCallback(() => {
handleModalDismiss();
}, [handleModalDismiss]);
const handleSheetOpenChange = useCallback(
(isOpen: boolean) => {
if (!isOpen) {
handleModalDismiss();
} else {
setOpen(isOpen);
}
},
[handleModalDismiss],
);
// Single error selection - replace instead of toggle
const selectError = (errorType: InjectedErrorType) => {
// If clicking the same error, clear it; otherwise set the new one
if (injectedErrors.length === 1 && injectedErrors[0] === errorType) {
clearAllErrors();
} else {
setInjectedErrors([errorType]);
}
// Close the sheet after selection
closeSheet();
};
const currentError = injectedErrors.length > 0 ? injectedErrors[0] : null;
const currentErrorLabel = currentError ? ERROR_LABELS[currentError] : null;
return (
<YStack gap="$2">
<Button
style={{ backgroundColor: 'white' }}
borderColor={slate200}
borderRadius="$2"
height="$5"
padding={0}
onPress={openSheet}
>
<XStack
width="100%"
justifyContent="space-between"
paddingVertical="$3"
paddingLeft="$4"
paddingRight="$1.5"
>
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
{currentErrorLabel || 'Select onboarding error to test'}
</Text>
<ChevronDown color={slate500} strokeWidth={2.5} />
</XStack>
</Button>
{currentError && (
<Button
backgroundColor={red500}
borderRadius="$2"
height="$5"
onPress={clearAllErrors}
pressStyle={{
opacity: 0.8,
scale: 0.98,
}}
>
<Text color={white} fontSize="$5" fontFamily={dinot}>
Clear
</Text>
</Button>
)}
<Sheet
modal
open={open}
onOpenChange={handleSheetOpenChange}
snapPoints={[85]}
animation="medium"
dismissOnSnapToBottom
>
<Sheet.Overlay />
<Sheet.Frame
backgroundColor={white}
borderTopLeftRadius="$9"
borderTopRightRadius="$9"
>
<YStack padding="$4">
<XStack
alignItems="center"
justifyContent="space-between"
marginBottom="$4"
>
<Text fontSize="$8" fontFamily={dinot}>
Onboarding Error Testing
</Text>
<Button
onPress={closeSheet}
padding="$2"
backgroundColor="transparent"
>
<ChevronDown
color={slate500}
strokeWidth={2.5}
style={{ transform: [{ rotate: '180deg' }] }}
/>
</Button>
</XStack>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 100 }}
>
{Object.entries(ERROR_GROUPS).map(([groupName, errors]) => (
<YStack key={groupName} marginBottom="$4">
<Text
fontSize="$6"
fontFamily={dinot}
fontWeight="600"
color={slate800}
marginBottom="$2"
>
{groupName}
</Text>
{errors.map((errorType: InjectedErrorType) => (
<TouchableOpacity
key={errorType}
onPress={() => selectError(errorType)}
>
<XStack
paddingVertical="$3"
paddingHorizontal="$2"
borderBottomWidth={1}
borderBottomColor={slate200}
alignItems="center"
justifyContent="space-between"
>
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
{ERROR_LABELS[errorType]}
</Text>
{currentError === errorType && (
<Check color={slate600} size={20} />
)}
</XStack>
</TouchableOpacity>
))}
</YStack>
))}
</ScrollView>
</YStack>
</Sheet.Frame>
</Sheet>
</YStack>
);
};

View File

@@ -0,0 +1,175 @@
// 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, useRef, useState } from 'react';
import { ScrollView, TouchableOpacity } from 'react-native';
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
import { Check, ChevronDown } from '@tamagui/lucide-icons';
import {
slate200,
slate500,
slate600,
white,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
import {
registerModalCallbacks,
unregisterModalCallbacks,
} from '@/utils/modalCallbackRegistry';
interface LogLevelSelectorProps {
currentLevel: string;
onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void;
}
export const LogLevelSelector: React.FC<LogLevelSelectorProps> = ({
currentLevel,
onSelect,
}) => {
const [open, setOpen] = useState(false);
const callbackIdRef = useRef<number>();
const logLevels = ['debug', 'info', 'warn', 'error'] as const;
// Cleanup effect to unregister callbacks on unmount
useEffect(() => {
return () => {
if (callbackIdRef.current !== undefined) {
unregisterModalCallbacks(callbackIdRef.current);
callbackIdRef.current = undefined;
}
};
}, []);
const handleModalDismiss = useCallback(() => {
setOpen(false);
if (callbackIdRef.current !== undefined) {
unregisterModalCallbacks(callbackIdRef.current);
callbackIdRef.current = undefined;
}
}, []);
const openSheet = useCallback(() => {
setOpen(true);
const id = registerModalCallbacks({
onButtonPress: () => {},
onModalDismiss: handleModalDismiss,
});
callbackIdRef.current = id;
}, [handleModalDismiss]);
const closeSheet = useCallback(() => {
handleModalDismiss();
}, [handleModalDismiss]);
const handleSheetOpenChange = useCallback(
(isOpen: boolean) => {
if (!isOpen) {
handleModalDismiss();
} else {
setOpen(isOpen);
}
},
[handleModalDismiss],
);
const handleLevelSelect = useCallback(
(level: 'debug' | 'info' | 'warn' | 'error') => {
closeSheet();
onSelect(level);
},
[closeSheet, onSelect],
);
return (
<>
<Button
style={{ backgroundColor: 'white' }}
borderColor={slate200}
borderRadius="$2"
height="$5"
padding={0}
onPress={openSheet}
>
<XStack
width="100%"
justifyContent="space-between"
paddingVertical="$3"
paddingLeft="$4"
paddingRight="$1.5"
>
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
{currentLevel.toUpperCase()}
</Text>
<ChevronDown color={slate500} strokeWidth={2.5} />
</XStack>
</Button>
<Sheet
modal
open={open}
onOpenChange={handleSheetOpenChange}
snapPoints={[50]}
animation="medium"
dismissOnSnapToBottom
>
<Sheet.Overlay />
<Sheet.Frame
backgroundColor={white}
borderTopLeftRadius="$9"
borderTopRightRadius="$9"
>
<YStack padding="$4">
<XStack
alignItems="center"
justifyContent="space-between"
marginBottom="$4"
>
<Text fontSize="$8" fontFamily={dinot}>
Select log level
</Text>
<Button
onPress={closeSheet}
padding="$2"
backgroundColor="transparent"
>
<ChevronDown
color={slate500}
strokeWidth={2.5}
style={{ transform: [{ rotate: '180deg' }] }}
/>
</Button>
</XStack>
<ScrollView showsVerticalScrollIndicator={false}>
{logLevels.map(level => (
<TouchableOpacity
key={level}
onPress={() => handleLevelSelect(level)}
>
<XStack
paddingVertical="$3"
paddingHorizontal="$2"
borderBottomWidth={1}
borderBottomColor={slate200}
alignItems="center"
justifyContent="space-between"
>
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
{level.toUpperCase()}
</Text>
{currentLevel === level && (
<Check color={slate600} size={20} />
)}
</XStack>
</TouchableOpacity>
))}
</ScrollView>
</YStack>
</Sheet.Frame>
</Sheet>
</>
);
};

View File

@@ -0,0 +1,94 @@
// 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 type { PropsWithChildren } from 'react';
import React, { cloneElement, isValidElement } from 'react';
import { Text, XStack, YStack } from 'tamagui';
import {
slate100,
slate200,
slate400,
slate600,
slate800,
slate900,
white,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
interface ParameterSectionProps extends PropsWithChildren {
icon: React.ReactNode;
title: string;
description: string;
darkMode?: boolean;
}
export function ParameterSection({
icon,
title,
description,
darkMode,
children,
}: ParameterSectionProps) {
const renderIcon = () => {
const iconElement =
typeof icon === 'function'
? (icon as () => React.ReactNode)()
: isValidElement(icon)
? icon
: null;
return iconElement
? cloneElement(iconElement as React.ReactElement, {
width: '100%',
height: '100%',
})
: null;
};
return (
<YStack
width="100%"
backgroundColor={darkMode ? slate900 : slate100}
borderRadius="$4"
borderWidth={1}
borderColor={darkMode ? slate800 : slate200}
padding="$4"
flexDirection="column"
gap="$3"
>
<XStack
width="100%"
flexDirection="row"
justifyContent="flex-start"
gap="$4"
>
<YStack
backgroundColor="gray"
borderRadius={5}
width={46}
height={46}
justifyContent="center"
alignItems="center"
padding="$2"
>
{renderIcon()}
</YStack>
<YStack flexDirection="column" gap="$1">
<Text
fontSize="$5"
color={darkMode ? white : slate600}
fontFamily={dinot}
>
{title}
</Text>
<Text fontSize="$3" color={slate400} fontFamily={dinot}>
{description}
</Text>
</YStack>
</XStack>
{children}
</YStack>
);
}

View File

@@ -0,0 +1,122 @@
// 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, { useMemo, useState } from 'react';
import { ScrollView, TouchableOpacity } from 'react-native';
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import { ChevronDown } from '@tamagui/lucide-icons';
import {
slate200,
slate500,
slate600,
white,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
import { navigationScreens } from '@/navigation';
export const ScreenSelector = () => {
const navigation = useNavigation();
const [open, setOpen] = useState(false);
const screenList = useMemo(
() =>
(
Object.keys(navigationScreens) as (keyof typeof navigationScreens)[]
).sort(),
[],
);
return (
<>
<Button
style={{ backgroundColor: 'white' }}
borderColor={slate200}
borderRadius="$2"
height="$5"
padding={0}
onPress={() => setOpen(true)}
>
<XStack
width="100%"
justifyContent="space-between"
paddingVertical="$3"
paddingLeft="$4"
paddingRight="$1.5"
>
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
Select screen
</Text>
<ChevronDown color={slate500} strokeWidth={2.5} />
</XStack>
</Button>
<Sheet
modal
open={open}
onOpenChange={setOpen}
snapPoints={[85]}
animation="medium"
dismissOnSnapToBottom
>
<Sheet.Overlay />
<Sheet.Frame
backgroundColor={white}
borderTopLeftRadius="$9"
borderTopRightRadius="$9"
>
<YStack padding="$4">
<XStack
alignItems="center"
justifyContent="space-between"
marginBottom="$4"
>
<Text fontSize="$8" fontFamily={dinot}>
Select screen
</Text>
<Button
onPress={() => setOpen(false)}
padding="$2"
backgroundColor="transparent"
>
<ChevronDown
color={slate500}
strokeWidth={2.5}
style={{ transform: [{ rotate: '180deg' }] }}
/>
</Button>
</XStack>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 100 }}
>
{screenList.map(item => (
<TouchableOpacity
key={item}
onPress={() => {
setOpen(false);
navigation.navigate(item as never);
}}
>
<XStack
paddingVertical="$3"
paddingHorizontal="$2"
borderBottomWidth={1}
borderBottomColor={slate200}
>
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
{item}
</Text>
</XStack>
</TouchableOpacity>
))}
</ScrollView>
</YStack>
</Sheet.Frame>
</Sheet>
</>
);
};

View File

@@ -0,0 +1,58 @@
// 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 { Button, Text } from 'tamagui';
import {
slate200,
slate400,
slate600,
white,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
export interface TopicToggleButtonProps {
label: string;
isSubscribed: boolean;
onToggle: () => void;
}
export const TopicToggleButton: React.FC<TopicToggleButtonProps> = ({
label,
isSubscribed,
onToggle,
}) => {
return (
<Button
backgroundColor={isSubscribed ? '$green9' : slate200}
borderRadius="$2"
height="$5"
onPress={onToggle}
flexDirection="row"
justifyContent="space-between"
paddingHorizontal="$4"
pressStyle={{
opacity: 0.8,
scale: 0.98,
}}
>
<Text
color={isSubscribed ? white : slate600}
fontSize="$5"
fontFamily={dinot}
fontWeight="600"
>
{label}
</Text>
<Text
color={isSubscribed ? white : slate400}
fontSize="$3"
fontFamily={dinot}
>
{isSubscribed ? 'Enabled' : 'Disabled'}
</Text>
</Button>
);
};

View File

@@ -0,0 +1,10 @@
// 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.
export type { TopicToggleButtonProps } from '@/screens/dev/components/TopicToggleButton';
export { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector';
export { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector';
export { ParameterSection } from '@/screens/dev/components/ParameterSection';
export { ScreenSelector } from '@/screens/dev/components/ScreenSelector';
export { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';

View File

@@ -0,0 +1,197 @@
// 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 { Alert } from 'react-native';
import { unsafe_clearSecrets } from '@/providers/authProvider';
import { usePassport } from '@/providers/passportDataProvider';
import { usePointEventStore } from '@/stores/pointEventStore';
import { useSettingStore } from '@/stores/settingStore';
export const useDangerZoneActions = () => {
const { clearDocumentCatalogForMigrationTesting } = usePassport();
const clearPointEvents = usePointEventStore(state => state.clearEvents);
const { resetBackupForPoints } = useSettingStore();
const handleClearSecretsPress = () => {
Alert.alert(
'Delete Keychain Secrets',
"Are you sure you want to remove your keychain secrets?\n\nIf this secret is not backed up, your account will be lost and the ID documents attached to it won't be usable.",
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
try {
await unsafe_clearSecrets();
Alert.alert('Success', 'Keychain secrets cleared successfully.', [
{ text: 'OK' },
]);
} catch (error) {
console.error(
'Failed to clear keychain secrets:',
error instanceof Error ? error.message : String(error),
);
Alert.alert(
'Error',
'Failed to clear keychain secrets. Please try again.',
[{ text: 'OK' }],
);
}
},
},
],
);
};
const handleClearDocumentCatalogPress = () => {
Alert.alert(
'Clear Document Catalog',
'Are you sure you want to clear the document catalog?\n\nThis will remove all documents from the new storage system but preserve legacy storage for migration testing. You will need to restart the app to test migration.',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Clear',
style: 'destructive',
onPress: async () => {
try {
await clearDocumentCatalogForMigrationTesting();
Alert.alert(
'Success',
'Document catalog cleared successfully. Please restart the app to test migration.',
[{ text: 'OK' }],
);
} catch (error) {
console.error(
'Failed to clear document catalog:',
error instanceof Error ? error.message : String(error),
);
Alert.alert(
'Error',
'Failed to clear document catalog. Please try again.',
[{ text: 'OK' }],
);
}
},
},
],
);
};
const handleClearPointEventsPress = () => {
Alert.alert(
'Clear Point Events',
'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Clear',
style: 'destructive',
onPress: async () => {
try {
await clearPointEvents();
Alert.alert('Success', 'Point events cleared successfully.', [
{ text: 'OK' },
]);
} catch (error) {
console.error(
'Failed to clear point events:',
error instanceof Error ? error.message : String(error),
);
Alert.alert(
'Error',
'Failed to clear point events. Please try again.',
[{ text: 'OK' }],
);
}
},
},
],
);
};
const handleResetBackupStatePress = () => {
Alert.alert(
'Reset Backup State',
'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Reset',
style: 'destructive',
onPress: () => {
resetBackupForPoints();
Alert.alert('Success', 'Backup state reset successfully.', [
{ text: 'OK' },
]);
},
},
],
);
};
const handleClearBackupEventsPress = () => {
Alert.alert(
'Clear Backup Events',
'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Clear',
style: 'destructive',
onPress: async () => {
try {
const events = usePointEventStore.getState().events;
const backupEvents = events.filter(
event => event.type === 'backup',
);
await Promise.all(
backupEvents.map(event =>
usePointEventStore.getState().removeEvent(event.id),
),
);
Alert.alert('Success', 'Backup events cleared successfully.', [
{ text: 'OK' },
]);
} catch (error) {
console.error(
'Failed to clear backup events:',
error instanceof Error ? error.message : String(error),
);
Alert.alert(
'Error',
'Failed to clear backup events. Please try again.',
[{ text: 'OK' }],
);
}
},
},
],
);
};
return {
handleClearSecretsPress,
handleClearDocumentCatalogPress,
handleClearPointEventsPress,
handleResetBackupStatePress,
handleClearBackupEventsPress,
};
};

View File

@@ -0,0 +1,156 @@
// 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 { useCallback, useEffect, useState } from 'react';
import { Alert, AppState } from 'react-native';
import {
isNotificationSystemReady,
requestNotificationPermission,
subscribeToTopics,
unsubscribeFromTopics,
} from '@/services/notifications/notificationService';
import { useSettingStore } from '@/stores/settingStore';
export const useNotificationHandlers = () => {
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
const [hasNotificationPermission, setHasNotificationPermission] =
useState(false);
const checkPermissions = useCallback(async () => {
const readiness = await isNotificationSystemReady();
setHasNotificationPermission(readiness.ready);
}, []);
// Check notification permissions on mount and when app regains focus
useEffect(() => {
checkPermissions();
const subscription = AppState.addEventListener('change', nextAppState => {
if (nextAppState === 'active') {
checkPermissions();
}
});
return () => subscription.remove();
}, [checkPermissions]);
const handleTopicToggle = async (topics: string[], topicLabel: string) => {
// Check permissions first
if (!hasNotificationPermission) {
Alert.alert(
'Permissions Required',
'Push notifications are not enabled. Would you like to enable them?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Enable',
onPress: async () => {
try {
const granted = await requestNotificationPermission();
if (granted) {
// Update permission state
setHasNotificationPermission(true);
Alert.alert(
'Success',
'Permissions granted! You can now subscribe to topics.',
[{ text: 'OK' }],
);
} else {
Alert.alert(
'Failed',
'Could not enable notifications. Please enable them in your device Settings.',
[{ text: 'OK' }],
);
}
} catch (error) {
Alert.alert(
'Error',
error instanceof Error
? error.message
: 'Failed to request permissions',
[{ text: 'OK' }],
);
}
},
},
],
);
return;
}
const isCurrentlySubscribed = topics.every(topic =>
subscribedTopics.includes(topic),
);
if (isCurrentlySubscribed) {
// Show confirmation dialog for unsubscribe
Alert.alert(
'Disable Notifications',
`Are you sure you want to disable push notifications for ${topicLabel}?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Disable',
style: 'destructive',
onPress: async () => {
try {
const result = await unsubscribeFromTopics(topics);
if (result.successes.length > 0) {
Alert.alert(
'Success',
`Disabled notifications for ${topicLabel}`,
[{ text: 'OK' }],
);
} else {
Alert.alert(
'Error',
`Failed to disable: ${result.failures.map(f => f.error).join(', ')}`,
[{ text: 'OK' }],
);
}
} catch (error) {
Alert.alert(
'Error',
error instanceof Error
? error.message
: 'Failed to unsubscribe',
[{ text: 'OK' }],
);
}
},
},
],
);
} else {
// Subscribe without confirmation
try {
const result = await subscribeToTopics(topics);
if (result.successes.length > 0) {
Alert.alert('✅ Success', `Enabled notifications for ${topicLabel}`, [
{ text: 'OK' },
]);
} else {
Alert.alert(
'Error',
`Failed to enable: ${result.failures.map(f => f.error).join(', ')}`,
[{ text: 'OK' }],
);
}
} catch (error) {
Alert.alert(
'Error',
error instanceof Error ? error.message : 'Failed to subscribe',
[{ text: 'OK' }],
);
}
}
};
return {
hasNotificationPermission,
subscribedTopics,
handleTopicToggle,
};
};

View File

@@ -0,0 +1,90 @@
// 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 { Button, Text } from 'tamagui';
import {
red500,
slate500,
white,
yellow500,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
import WarningIcon from '@/assets/icons/warning.svg';
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
interface DangerZoneSectionProps {
onClearSecrets: () => void;
onClearDocumentCatalog: () => void;
onClearPointEvents: () => void;
onResetBackupState: () => void;
onClearBackupEvents: () => void;
}
export const DangerZoneSection: React.FC<DangerZoneSectionProps> = ({
onClearSecrets,
onClearDocumentCatalog,
onClearPointEvents,
onResetBackupState,
onClearBackupEvents,
}) => {
const dangerActions = [
{
label: 'Delete your private key',
onPress: onClearSecrets,
dangerTheme: true,
},
{
label: 'Clear document catalog',
onPress: onClearDocumentCatalog,
dangerTheme: true,
},
{
label: 'Clear point events',
onPress: onClearPointEvents,
dangerTheme: true,
},
{
label: 'Reset backup state',
onPress: onResetBackupState,
dangerTheme: true,
},
{
label: 'Clear backup events',
onPress: onClearBackupEvents,
dangerTheme: true,
},
];
return (
<ParameterSection
icon={<WarningIcon color={yellow500} />}
title="Danger Zone"
description="These actions are sensitive"
darkMode={true}
>
{dangerActions.map(({ label, onPress, dangerTheme }) => (
<Button
key={label}
style={{ backgroundColor: dangerTheme ? red500 : white }}
borderRadius="$2"
height="$5"
onPress={onPress}
flexDirection="row"
justifyContent="flex-start"
>
<Text
color={dangerTheme ? white : slate500}
fontSize="$5"
fontFamily={dinot}
>
{label}
</Text>
</Button>
))}
</ParameterSection>
);
};

View File

@@ -0,0 +1,108 @@
// 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 { Button, Text, XStack, YStack } from 'tamagui';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { ChevronRight } from '@tamagui/lucide-icons';
import { slate200, slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
import BugIcon from '@/assets/icons/bug_icon.svg';
import type { RootStackParamList } from '@/navigation';
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
import { ScreenSelector } from '@/screens/dev/components/ScreenSelector';
import { IS_DEV_MODE } from '@/utils/devUtils';
interface DebugShortcutsSectionProps {
navigation: NativeStackNavigationProp<RootStackParamList>;
}
export const DebugShortcutsSection: React.FC<DebugShortcutsSectionProps> = ({
navigation,
}) => {
return (
<ParameterSection
icon={<BugIcon />}
title="Debug Shortcuts"
description="Jump directly to any screen for testing"
>
<YStack gap="$2">
<Button
style={{ backgroundColor: 'white' }}
borderColor={slate200}
borderRadius="$2"
height="$5"
padding={0}
onPress={() => {
navigation.navigate('DevPrivateKey');
}}
>
<XStack
width="100%"
justifyContent="space-between"
paddingVertical="$3"
paddingLeft="$4"
paddingRight="$1.5"
>
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
View Private Key
</Text>
<ChevronRight color={slate500} strokeWidth={2.5} />
</XStack>
</Button>
<Button
style={{ backgroundColor: 'white' }}
borderColor={slate200}
borderRadius="$2"
height="$5"
padding={0}
onPress={() => {
navigation.navigate('SumsubTest');
}}
>
<XStack
width="100%"
justifyContent="space-between"
paddingVertical="$3"
paddingLeft="$4"
paddingRight="$1.5"
>
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
Sumsub Test Flow
</Text>
<ChevronRight color={slate500} strokeWidth={2.5} />
</XStack>
</Button>
{IS_DEV_MODE && (
<Button
style={{ backgroundColor: 'white' }}
borderColor={slate200}
borderRadius="$2"
height="$5"
padding={0}
onPress={() => {
navigation.navigate('Home', { testReferralFlow: true });
}}
>
<XStack
width="100%"
justifyContent="space-between"
paddingVertical="$3"
paddingLeft="$4"
paddingRight="$1.5"
>
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
Test Referral Flow
</Text>
<ChevronRight color={slate500} strokeWidth={2.5} />
</XStack>
</Button>
)}
<ScreenSelector />
</YStack>
</ParameterSection>
);
};

View File

@@ -0,0 +1,61 @@
// 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 { Alert, Platform } from 'react-native';
import BugIcon from '@/assets/icons/bug_icon.svg';
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';
interface DevTogglesSectionProps {
kycEnabled: boolean;
setKycEnabled: (enabled: boolean) => void;
useStrongBox: boolean;
setUseStrongBox: (useStrongBox: boolean) => void;
}
export const DevTogglesSection: React.FC<DevTogglesSectionProps> = ({
kycEnabled,
setKycEnabled,
useStrongBox,
setUseStrongBox,
}) => {
const handleToggleStrongBox = () => {
Alert.alert(
useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox',
useStrongBox
? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.'
: 'New keys will attempt to use StrongBox hardware backing for enhanced security.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: useStrongBox ? 'Disable' : 'Enable',
onPress: () => setUseStrongBox(!useStrongBox),
},
],
);
};
return (
<ParameterSection
icon={<BugIcon />}
title="Options"
description="Development and security options"
>
<TopicToggleButton
label="KYC Flow"
isSubscribed={kycEnabled}
onToggle={() => setKycEnabled(!kycEnabled)}
/>
{Platform.OS === 'android' && (
<TopicToggleButton
label="Use StrongBox"
isSubscribed={useStrongBox}
onToggle={handleToggleStrongBox}
/>
)}
</ParameterSection>
);
};

View File

@@ -0,0 +1,54 @@
// 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 { YStack } from 'tamagui';
import BugIcon from '@/assets/icons/bug_icon.svg';
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';
interface PushNotificationsSectionProps {
hasNotificationPermission: boolean;
subscribedTopics: string[];
onTopicToggle: (topics: string[], topicLabel: string) => void;
}
export const PushNotificationsSection: React.FC<
PushNotificationsSectionProps
> = ({ hasNotificationPermission, subscribedTopics, onTopicToggle }) => {
return (
<ParameterSection
icon={<BugIcon />}
title="Push Notifications"
description="Manage topic subscriptions"
>
<YStack gap="$2">
<TopicToggleButton
label="Starfall"
isSubscribed={
hasNotificationPermission && subscribedTopics.includes('nova')
}
onToggle={() => onTopicToggle(['nova'], 'Starfall')}
/>
<TopicToggleButton
label="General"
isSubscribed={
hasNotificationPermission && subscribedTopics.includes('general')
}
onToggle={() => onTopicToggle(['general'], 'General')}
/>
<TopicToggleButton
label="Both (Starfall + General)"
isSubscribed={
hasNotificationPermission &&
subscribedTopics.includes('nova') &&
subscribedTopics.includes('general')
}
onToggle={() => onTopicToggle(['nova', 'general'], 'both topics')}
/>
</YStack>
</ParameterSection>
);
};

View File

@@ -0,0 +1,8 @@
// 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.
export { DangerZoneSection } from '@/screens/dev/sections/DangerZoneSection';
export { DebugShortcutsSection } from '@/screens/dev/sections/DebugShortcutsSection';
export { DevTogglesSection } from '@/screens/dev/sections/DevTogglesSection';
export { PushNotificationsSection } from '@/screens/dev/sections/PushNotificationsSection';

View File

@@ -20,6 +20,7 @@ import useHapticNavigation from '@/hooks/useHapticNavigation';
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
import { flush as flushAnalytics } from '@/services/analytics';
import { useSettingStore } from '@/stores/settingStore';
const tips: TipProps[] = [
{
@@ -54,6 +55,7 @@ const DocumentCameraTroubleScreen: React.FC = () => {
const selfClient = useSelfClient();
const { useMRZStore } = selfClient;
const { countryCode } = useMRZStore();
const kycEnabled = useSettingStore(state => state.kycEnabled);
const { launchSumsubVerification, isLoading } = useSumsubLauncher({
countryCode,
errorSource: 'sumsub_initialization',
@@ -80,21 +82,25 @@ const DocumentCameraTroubleScreen: React.FC = () => {
page quickly and clearly!
</Caption>
<Caption
size="large"
style={{ color: slate500, marginTop: 12, marginBottom: 8 }}
>
Or try an alternative verification method:
</Caption>
{kycEnabled && (
<>
<Caption
size="large"
style={{ color: slate500, marginTop: 12, marginBottom: 8 }}
>
Or try an alternative verification method:
</Caption>
<SecondaryButton
onPress={launchSumsubVerification}
disabled={isLoading}
textColor={slate700}
style={{ marginBottom: 0 }}
>
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
</SecondaryButton>
<SecondaryButton
onPress={launchSumsubVerification}
disabled={isLoading}
textColor={slate700}
style={{ marginBottom: 0 }}
>
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
</SecondaryButton>
</>
)}
</YStack>
}
>

View File

@@ -19,6 +19,7 @@ import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
import { flushAllAnalytics } from '@/services/analytics';
import { openSupportForm, SUPPORT_FORM_BUTTON_TEXT } from '@/services/support';
import { useSettingStore } from '@/stores/settingStore';
const tips: TipProps[] = [
{
@@ -55,6 +56,7 @@ const DocumentNFCTroubleScreen: React.FC = () => {
const selfClient = useSelfClient();
const { useMRZStore } = selfClient;
const { countryCode } = useMRZStore();
const kycEnabled = useSettingStore(state => state.kycEnabled);
const { launchSumsubVerification, isLoading } = useSumsubLauncher({
countryCode,
errorSource: 'sumsub_initialization',
@@ -89,14 +91,16 @@ const DocumentNFCTroubleScreen: React.FC = () => {
{SUPPORT_FORM_BUTTON_TEXT}
</SecondaryButton>
<SecondaryButton
onPress={launchSumsubVerification}
disabled={isLoading}
textColor={slate700}
style={{ marginBottom: 0 }}
>
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
</SecondaryButton>
{kycEnabled && (
<SecondaryButton
onPress={launchSumsubVerification}
disabled={isLoading}
textColor={slate700}
style={{ marginBottom: 0 }}
>
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
</SecondaryButton>
)}
</YStack>
}
>

View File

@@ -13,6 +13,7 @@ import IDSelection from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-scree
import { DocumentFlowNavBar } from '@/components/navbar/DocumentFlowNavBar';
import type { RootStackParamList } from '@/navigation';
import { useSettingStore } from '@/stores/settingStore';
import { extraYPadding } from '@/utils/styleUtils';
type IDPickerScreenRouteProp = RouteProp<RootStackParamList, 'IDPicker'>;
@@ -21,6 +22,7 @@ const IDPickerScreen: React.FC = () => {
const route = useRoute<IDPickerScreenRouteProp>();
const { countryCode = '', documentTypes = [] } = route.params || {};
const bottom = useSafeAreaInsets().bottom;
const kycEnabled = useSettingStore(state => state.kycEnabled);
return (
<YStack
@@ -29,7 +31,11 @@ const IDPickerScreen: React.FC = () => {
paddingBottom={bottom + extraYPadding + 24}
>
<DocumentFlowNavBar title="GETTING STARTED" />
<IDSelection countryCode={countryCode} documentTypes={documentTypes} />
<IDSelection
countryCode={countryCode}
documentTypes={documentTypes}
showKyc={kycEnabled}
/>
</YStack>
);
};

View File

@@ -0,0 +1,83 @@
// 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 { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import {
AbstractButton,
Description,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { buttonTap } from '@/integrations/haptics';
import type { RootStackParamList } from '@/navigation';
const KYCVerifiedScreen: React.FC = () => {
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const insets = useSafeAreaInsets();
const handleGenerateProof = () => {
buttonTap();
navigation.navigate('ProvingScreenRouter');
};
return (
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
<View style={styles.spacer} />
<YStack
paddingHorizontal={24}
justifyContent="center"
alignItems="center"
gap={12}
marginBottom={64}
>
<Title style={styles.title}>Your ID has been verified</Title>
<Description style={styles.description}>
Next Self will generate a zk proof specifically for this device that
you can use to proof your identity.
</Description>
</YStack>
<YStack gap={12} paddingHorizontal={20} paddingBottom={24}>
<AbstractButton
bgColor={white}
color={black}
onPress={handleGenerateProof}
>
Generate proof
</AbstractButton>
</YStack>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: black,
},
spacer: {
flex: 1,
},
title: {
color: white,
textAlign: 'center',
fontSize: 28,
letterSpacing: 1,
},
description: {
color: white,
textAlign: 'center',
fontSize: 18,
lineHeight: 27,
},
});
export default KYCVerifiedScreen;

View File

@@ -2,37 +2,70 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import React, { useCallback } from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { YStack } from 'tamagui';
import { v5 as uuidv5 } from 'uuid';
import type { StaticScreenProps } from '@react-navigation/native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
import { DelayedLottieView, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
import {
AbstractButton,
Description,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { buttonTap } from '@/integrations/haptics';
import type { RootStackParamList } from '@/navigation';
import { requestNotificationPermission } from '@/services/notifications/notificationService';
import {
getFCMToken,
registerDeviceToken,
requestNotificationPermission,
SELF_UUID_NAMESPACE,
} from '@/services/notifications/notificationService';
import { useSettingStore } from '@/stores/settingStore';
const KycSuccessScreen: React.FC = () => {
type KycSuccessRouteParams = StaticScreenProps<
| {
userId?: string;
}
| undefined
>;
const KycSuccessScreen: React.FC<KycSuccessRouteParams> = ({
route: { params },
}) => {
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const userId = params?.userId;
const insets = useSafeAreaInsets();
const setFcmToken = useSettingStore(state => state.setFcmToken);
const selfClient = useSelfClient();
const { trackEvent } = selfClient;
const handleReceiveUpdates = async () => {
const handleReceiveUpdates = useCallback(async () => {
buttonTap();
await requestNotificationPermission();
if ((await requestNotificationPermission()) && userId) {
const token = await getFCMToken();
if (token) {
setFcmToken(token);
trackEvent(ProofEvents.FCM_TOKEN_STORED);
const sessionId = uuidv5(userId, SELF_UUID_NAMESPACE);
await registerDeviceToken(sessionId, token);
}
}
// Navigate to Home regardless of permission result
navigation.navigate('Home', {});
};
}, [navigation, setFcmToken, trackEvent, userId]);
const handleCheckLater = () => {
buttonTap();

View File

@@ -172,6 +172,20 @@ const ProveScreen: React.FC = () => {
if (selectedAppRef.current?.sessionId !== selectedApp.sessionId) {
hasInitializedScrollStateRef.current = false;
setHasScrolledToBottom(false);
// After state reset, check if content is short using current measurements.
// Use setTimeout(0) to ensure we read values AFTER React processes the reset,
// without adding measurements to dependencies (which causes race conditions).
setTimeout(() => {
const hasMeasurements =
scrollViewContentHeight > 0 && scrollViewHeight > 0;
const isShort = scrollViewContentHeight <= scrollViewHeight + 50;
if (hasMeasurements && isShort) {
setHasScrolledToBottom(true);
hasInitializedScrollStateRef.current = true;
}
}, 0);
}
setDefaultDocumentTypeIfNeeded();

View File

@@ -14,6 +14,8 @@ import {
} from '@/services/notifications/notificationService.shared';
import { useSettingStore } from '@/stores/settingStore';
export const SELF_UUID_NAMESPACE = '00000000-0000-8000-8000-531f00000000';
export async function getFCMToken(): Promise<string | null> {
try {
const token = await messaging().getToken();

View File

@@ -21,6 +21,7 @@ interface PersistedSettingsState {
homeScreenViewCount: number;
incrementHomeScreenViewCount: () => void;
isDevMode: boolean;
kycEnabled: boolean;
loggingSeverity: LoggingSeverity;
pointsAddress: string | null;
removeSubscribedTopic: (topic: string) => void;
@@ -32,6 +33,7 @@ interface PersistedSettingsState {
setFcmToken: (token: string | null) => void;
setHasViewedRecoveryPhrase: (viewed: boolean) => void;
setKeychainMigrationCompleted: () => void;
setKycEnabled: (enabled: boolean) => void;
setLoggingSeverity: (severity: LoggingSeverity) => void;
setPointsAddress: (address: string | null) => void;
setSkipDocumentSelector: (value: boolean) => void;
@@ -148,6 +150,10 @@ export const useSettingStore = create<SettingsState>()(
useStrongBox: false,
setUseStrongBox: (useStrongBox: boolean) => set({ useStrongBox }),
// KYC flow toggle (default: false, dev-only feature)
kycEnabled: false,
setKycEnabled: (enabled: boolean) => set({ kycEnabled: enabled }),
// Non-persisted state (will not be saved to storage)
hideNetworkModal: false,
setHideNetworkModal: (hideNetworkModal: boolean) => {

View File

@@ -85,6 +85,7 @@ describe('navigation', () => {
'Home',
'IDPicker',
'IdDetails',
'KYCVerified',
'KycSuccess',
'Loading',
'ManageDocuments',

View File

@@ -0,0 +1,511 @@
// 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 type { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import { render, waitFor } from '@testing-library/react-native';
import { navigationRef } from '@/navigation';
import { NotificationTrackingProvider } from '@/providers/notificationTrackingProvider';
import * as analytics from '@/services/analytics';
// Mock Firebase messaging
const mockOnNotificationOpenedApp = jest.fn();
const mockGetInitialNotification = jest.fn();
jest.mock('@react-native-firebase/messaging', () => {
return jest.fn(() => ({
onNotificationOpenedApp: mockOnNotificationOpenedApp,
getInitialNotification: mockGetInitialNotification,
}));
});
// Mock navigation
jest.mock('@/navigation', () => ({
navigationRef: {
isReady: jest.fn(),
navigate: jest.fn(),
},
}));
// Mock analytics
jest.mock('@/services/analytics', () => ({
trackEvent: jest.fn(),
}));
// Mock analytics constants
jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({
NotificationEvents: {
BACKGROUND_NOTIFICATION_OPENED: 'BACKGROUND_NOTIFICATION_OPENED',
COLD_START_NOTIFICATION_OPENED: 'COLD_START_NOTIFICATION_OPENED',
},
}));
const mockNavigationRef = navigationRef as jest.Mocked<typeof navigationRef>;
describe('NotificationTrackingProvider', () => {
const mockUserId = '19f21362-856a-4606-88e1-fa306036978f';
beforeEach(() => {
jest.clearAllMocks();
mockNavigationRef.isReady.mockReturnValue(true);
});
it('should render children without errors', () => {
mockGetInitialNotification.mockResolvedValue(null);
const { getByTestId } = render(
<NotificationTrackingProvider>
<mock-text testID="child">Test Child</mock-text>
</NotificationTrackingProvider>,
);
expect(getByTestId('child')).toHaveTextContent('Test Child');
});
describe('Background notification (onNotificationOpenedApp)', () => {
it('should navigate to KYCVerified when notification type is kyc_result and status is approved', async () => {
let notificationHandler:
| ((message: FirebaseMessagingTypes.RemoteMessage) => void)
| null = null;
mockOnNotificationOpenedApp.mockImplementation(handler => {
notificationHandler = handler;
return jest.fn(); // Return unsubscribe function
});
mockGetInitialNotification.mockResolvedValue(null);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
expect(mockOnNotificationOpenedApp).toHaveBeenCalled();
// Simulate notification tap
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'kyc_result',
status: 'approved',
user_id: mockUserId,
},
} as FirebaseMessagingTypes.RemoteMessage;
if (notificationHandler) {
notificationHandler(remoteMessage);
}
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalledWith(
'BACKGROUND_NOTIFICATION_OPENED',
{
messageId: 'test-message-id',
type: 'kyc_result',
actionId: undefined,
},
);
expect(mockNavigationRef.navigate).toHaveBeenCalledWith('KYCVerified', {
status: 'approved',
userId: mockUserId,
});
});
});
it('should not navigate when status is retry', async () => {
let notificationHandler:
| ((message: FirebaseMessagingTypes.RemoteMessage) => void)
| null = null;
mockOnNotificationOpenedApp.mockImplementation(handler => {
notificationHandler = handler;
return jest.fn();
});
mockGetInitialNotification.mockResolvedValue(null);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'kyc_result',
status: 'retry',
user_id: mockUserId,
},
} as FirebaseMessagingTypes.RemoteMessage;
if (notificationHandler) {
notificationHandler(remoteMessage);
}
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalled();
});
// Should not navigate for retry status
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
});
it('should not navigate when status is rejected', async () => {
let notificationHandler:
| ((message: FirebaseMessagingTypes.RemoteMessage) => void)
| null = null;
mockOnNotificationOpenedApp.mockImplementation(handler => {
notificationHandler = handler;
return jest.fn();
});
mockGetInitialNotification.mockResolvedValue(null);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'kyc_result',
status: 'rejected',
user_id: mockUserId,
},
} as FirebaseMessagingTypes.RemoteMessage;
if (notificationHandler) {
notificationHandler(remoteMessage);
}
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalled();
});
// Should not navigate for rejected status
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
});
it('should handle missing notification data gracefully', async () => {
let notificationHandler:
| ((message: FirebaseMessagingTypes.RemoteMessage) => void)
| null = null;
mockOnNotificationOpenedApp.mockImplementation(handler => {
notificationHandler = handler;
return jest.fn();
});
mockGetInitialNotification.mockResolvedValue(null);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
const remoteMessage = {
messageId: 'test-message-id',
data: undefined,
} as FirebaseMessagingTypes.RemoteMessage;
if (notificationHandler) {
notificationHandler(remoteMessage);
}
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalled();
});
// Should not navigate when data is missing
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
});
it('should not navigate when navigation is not ready', async () => {
mockNavigationRef.isReady.mockReturnValue(false);
let notificationHandler:
| ((message: FirebaseMessagingTypes.RemoteMessage) => void)
| null = null;
mockOnNotificationOpenedApp.mockImplementation(handler => {
notificationHandler = handler;
return jest.fn();
});
mockGetInitialNotification.mockResolvedValue(null);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'kyc_result',
status: 'approved',
user_id: mockUserId,
},
} as FirebaseMessagingTypes.RemoteMessage;
if (notificationHandler) {
notificationHandler(remoteMessage);
}
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalled();
});
// Should not navigate when navigationRef is not ready
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
});
});
describe('Cold start notification (getInitialNotification)', () => {
it('should navigate to KYCVerified when notification type is kyc_result and status is approved', async () => {
mockOnNotificationOpenedApp.mockReturnValue(jest.fn());
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'kyc_result',
status: 'approved',
user_id: mockUserId,
},
} as FirebaseMessagingTypes.RemoteMessage;
mockGetInitialNotification.mockResolvedValue(remoteMessage);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalledWith(
'COLD_START_NOTIFICATION_OPENED',
{
messageId: 'test-message-id',
type: 'kyc_result',
actionId: undefined,
},
);
expect(mockNavigationRef.navigate).toHaveBeenCalledWith('KYCVerified', {
status: 'approved',
userId: mockUserId,
});
});
});
it('should not navigate when getInitialNotification returns null', async () => {
mockOnNotificationOpenedApp.mockReturnValue(jest.fn());
mockGetInitialNotification.mockResolvedValue(null);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
await waitFor(() => {
expect(mockGetInitialNotification).toHaveBeenCalled();
});
// Should not track or navigate when there's no initial notification
expect(analytics.trackEvent).not.toHaveBeenCalledWith(
'COLD_START_NOTIFICATION_OPENED',
expect.anything(),
);
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
});
it('should not navigate when status is retry on cold start', async () => {
mockOnNotificationOpenedApp.mockReturnValue(jest.fn());
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'kyc_result',
status: 'retry',
user_id: mockUserId,
},
} as FirebaseMessagingTypes.RemoteMessage;
mockGetInitialNotification.mockResolvedValue(remoteMessage);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalledWith(
'COLD_START_NOTIFICATION_OPENED',
expect.anything(),
);
});
// Should not navigate for retry status
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
});
it('should queue navigation when navigationRef is not ready on cold start', async () => {
// Start with navigation not ready
mockNavigationRef.isReady.mockReturnValue(false);
mockOnNotificationOpenedApp.mockReturnValue(jest.fn());
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'kyc_result',
status: 'approved',
user_id: mockUserId,
},
} as FirebaseMessagingTypes.RemoteMessage;
mockGetInitialNotification.mockResolvedValue(remoteMessage);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
// Wait for initial notification to be processed
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalledWith(
'COLD_START_NOTIFICATION_OPENED',
expect.anything(),
);
});
// Navigation should not have been called yet
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
// Simulate navigation becoming ready
mockNavigationRef.isReady.mockReturnValue(true);
// Wait for the polling interval to detect navigation is ready
await waitFor(
() => {
expect(mockNavigationRef.navigate).toHaveBeenCalledWith(
'KYCVerified',
{
status: 'approved',
userId: mockUserId,
},
);
},
{ timeout: 2000 },
);
});
it('should process pending navigation immediately if navigation becomes ready', async () => {
// Start with navigation not ready
mockNavigationRef.isReady.mockReturnValue(false);
mockOnNotificationOpenedApp.mockReturnValue(jest.fn());
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'kyc_result',
status: 'approved',
user_id: mockUserId,
},
} as FirebaseMessagingTypes.RemoteMessage;
mockGetInitialNotification.mockResolvedValue(remoteMessage);
const { rerender } = render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
// Wait for initial notification to be processed
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalledWith(
'COLD_START_NOTIFICATION_OPENED',
expect.anything(),
);
});
// Navigation should not have been called yet
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
// Make navigation ready
mockNavigationRef.isReady.mockReturnValue(true);
// Trigger a re-render to simulate React's update cycle
rerender(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
// Navigation should be called after navigation becomes ready
await waitFor(
() => {
expect(mockNavigationRef.navigate).toHaveBeenCalledWith(
'KYCVerified',
{
status: 'approved',
userId: mockUserId,
},
);
},
{ timeout: 2000 },
);
});
it('should not queue navigation for non-KYC notifications when navigation is not ready', async () => {
mockNavigationRef.isReady.mockReturnValue(false);
mockOnNotificationOpenedApp.mockReturnValue(jest.fn());
const remoteMessage = {
messageId: 'test-message-id',
data: {
type: 'other_notification',
status: 'some_status',
},
} as FirebaseMessagingTypes.RemoteMessage;
mockGetInitialNotification.mockResolvedValue(remoteMessage);
render(
<NotificationTrackingProvider>
<mock-text testID="child">Test</mock-text>
</NotificationTrackingProvider>,
);
await waitFor(() => {
expect(analytics.trackEvent).toHaveBeenCalledWith(
'COLD_START_NOTIFICATION_OPENED',
expect.anything(),
);
});
// Make navigation ready
mockNavigationRef.isReady.mockReturnValue(true);
// Wait a bit to ensure no navigation happens
await new Promise(resolve => setTimeout(resolve, 300));
// Should not navigate for non-KYC notifications
expect(mockNavigationRef.navigate).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,156 @@
// 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 { useNavigation } from '@react-navigation/native';
import { fireEvent, render } from '@testing-library/react-native';
import * as haptics from '@/integrations/haptics';
import KYCVerifiedScreen from '@/screens/kyc/KYCVerifiedScreen';
// Note: While jest.setup.js provides comprehensive React Native mocking,
// react-test-renderer requires component-based mocks (functions) rather than
// string-based mocks for proper rendering. This minimal mock provides the
// specific components needed for this test without using requireActual to
// avoid memory issues (see .cursor/rules/test-memory-optimization.mdc).
jest.mock('react-native', () => ({
__esModule: true,
Platform: { OS: 'ios', select: jest.fn() },
StyleSheet: {
create: (styles: any) => styles,
flatten: (style: any) => style,
},
View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
}));
jest.mock('react-native-edge-to-edge', () => ({
SystemBars: () => null,
}));
jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: jest.fn(() => ({ top: 0, bottom: 0 })),
}));
jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));
// Mock Tamagui components
jest.mock('tamagui', () => ({
__esModule: true,
YStack: ({ children, ...props }: any) => <div {...props}>{children}</div>,
View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
}));
jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({
black: '#000000',
white: '#FFFFFF',
}));
jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({
AbstractButton: ({ children, onPress, testID, ...props }: any) => (
<button
onClick={onPress}
type="button"
data-testid={testID || 'generate-proof-button'}
{...props}
>
{children}
</button>
),
Title: ({ children, style, testID, ...props }: any) => (
<div data-testid={testID || 'title'} style={style} {...props}>
{children}
</div>
),
Description: ({ children, style, testID, ...props }: any) => (
<div data-testid={testID || 'description'} style={style} {...props}>
{children}
</div>
),
}));
jest.mock('@/integrations/haptics', () => ({
buttonTap: jest.fn(),
}));
jest.mock('@/config/sentry', () => ({
captureException: jest.fn(),
}));
const mockUseNavigation = useNavigation as jest.MockedFunction<
typeof useNavigation
>;
describe('KYCVerifiedScreen', () => {
const mockNavigate = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
mockUseNavigation.mockReturnValue({
navigate: mockNavigate,
} as any);
});
it('should render the screen without errors', () => {
const { root } = render(<KYCVerifiedScreen />);
expect(root).toBeTruthy();
});
it('should display the correct title', () => {
const { root } = render(<KYCVerifiedScreen />);
// Title is the first div child
const titleElement = root.findAll(
node =>
node.type === 'div' &&
node.props.children === 'Your ID has been verified',
)[0];
expect(titleElement).toBeTruthy();
});
it('should display the correct description text', () => {
const { root } = render(<KYCVerifiedScreen />);
// Description is a div with the description text
const descriptionElement = root.findAll(
node =>
node.type === 'div' &&
node.props.children ===
'Next Self will generate a zk proof specifically for this device that you can use to proof your identity.',
)[0];
expect(descriptionElement).toBeTruthy();
});
it('should have a "Generate proof" button that is visible', () => {
const { root } = render(<KYCVerifiedScreen />);
const buttons = root.findAllByType('button');
expect(buttons.length).toBeGreaterThan(0);
expect(buttons[0].props.children).toBe('Generate proof');
});
it('should trigger haptic feedback when "Generate proof" is pressed', () => {
const { root } = render(<KYCVerifiedScreen />);
const button = root.findAllByType('button')[0];
fireEvent.press(button);
expect(haptics.buttonTap).toHaveBeenCalledTimes(1);
});
it('should navigate to ProvingScreenRouter when "Generate proof" is pressed', () => {
const { root } = render(<KYCVerifiedScreen />);
const button = root.findAllByType('button')[0];
fireEvent.press(button);
expect(mockNavigate).toHaveBeenCalledWith('ProvingScreenRouter');
});
it('should have navigation available', () => {
render(<KYCVerifiedScreen />);
expect(mockUseNavigation).toHaveBeenCalled();
});
});

View File

@@ -3,8 +3,9 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import { v5 as uuidv5 } from 'uuid';
import { useNavigation } from '@react-navigation/native';
import { render } from '@testing-library/react-native';
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import ErrorBoundary from '@/components/ErrorBoundary';
import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen';
@@ -46,10 +47,6 @@ jest.mock('tamagui', () => ({
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
}));
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
DelayedLottieView: () => null,
}));
jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({
black: '#000000',
white: '#FFFFFF',
@@ -57,17 +54,17 @@ jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({
jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({
AbstractButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="abstract-button" type="button">
<button onClick={onPress} type="button">
{children}
</button>
),
PrimaryButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="primary-button" type="button">
<button onClick={onPress} type="button">
{children}
</button>
),
SecondaryButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="secondary-button" type="button">
<button onClick={onPress} type="button">
{children}
</button>
),
@@ -77,12 +74,23 @@ jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({
),
}));
jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({
ProofEvents: {
FCM_TOKEN_STORED: 'FCM_TOKEN_STORED',
},
}));
jest.mock('@selfxyz/mobile-sdk-alpha/animations/loading/misc.json', () => ({}));
jest.mock('@/integrations/haptics', () => ({
buttonTap: jest.fn(),
}));
jest.mock('@/services/notifications/notificationService', () => ({
...jest.requireActual('@/services/notifications/notificationService'),
requestNotificationPermission: jest.fn(),
getFCMToken: jest.fn(),
registerDeviceToken: jest.fn(),
}));
jest.mock('@/config/sentry', () => ({
@@ -94,12 +102,34 @@ jest.mock('@/services/analytics', () => ({
trackNfcEvent: jest.fn(),
}));
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
DelayedLottieView: () => null,
useSelfClient: jest.fn(),
}));
jest.mock('@/stores/settingStore', () => ({
useSettingStore: jest.fn(),
}));
const mockUseNavigation = useNavigation as jest.MockedFunction<
typeof useNavigation
>;
// Import mocked modules
const { useSelfClient } = jest.requireMock('@selfxyz/mobile-sdk-alpha');
const { useSettingStore } = jest.requireMock('@/stores/settingStore');
describe('KycSuccessScreen', () => {
const mockNavigate = jest.fn();
const mockTrackEvent = jest.fn();
const mockSetFcmToken = jest.fn();
const mockUserId = '19f21362-856a-4606-88e1-fa306036978f';
const mockFcmToken = 'mock-fcm-token';
const mockRoute = {
params: {
userId: mockUserId,
},
};
beforeEach(() => {
jest.clearAllMocks();
@@ -107,23 +137,165 @@ describe('KycSuccessScreen', () => {
mockUseNavigation.mockReturnValue({
navigate: mockNavigate,
} as any);
useSelfClient.mockReturnValue({
trackEvent: mockTrackEvent,
});
useSettingStore.mockReturnValue(mockSetFcmToken);
(
notificationService.requestNotificationPermission as jest.Mock
).mockResolvedValue(true);
(notificationService.getFCMToken as jest.Mock).mockResolvedValue(
mockFcmToken,
);
(notificationService.registerDeviceToken as jest.Mock).mockResolvedValue(
undefined,
);
});
it('should render the screen without errors', () => {
const { root } = render(<KycSuccessScreen />);
const { root } = render(<KycSuccessScreen route={mockRoute} />);
expect(root).toBeTruthy();
});
it('should have navigation available', () => {
render(<KycSuccessScreen />);
render(<KycSuccessScreen route={mockRoute} />);
expect(mockUseNavigation).toHaveBeenCalled();
});
it('should have notification service available', () => {
render(<KycSuccessScreen />);
render(<KycSuccessScreen route={mockRoute} />);
expect(notificationService.requestNotificationPermission).toBeDefined();
});
it('should fetch and register FCM token when "Receive live updates" is pressed', async () => {
const { root } = render(<KycSuccessScreen route={mockRoute} />);
const buttons = root.findAllByType('button');
const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates"
fireEvent.press(receiveUpdatesButton);
await waitFor(() => {
// Verify notification permission was requested
expect(
notificationService.requestNotificationPermission,
).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
// Verify FCM token was fetched
expect(notificationService.getFCMToken).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
// Verify FCM token was stored in settings store
expect(mockSetFcmToken).toHaveBeenCalledWith(mockFcmToken);
});
await waitFor(() => {
// Verify tracking event was sent
expect(mockTrackEvent).toHaveBeenCalledWith('FCM_TOKEN_STORED');
});
await waitFor(() => {
// Verify device token was registered with deterministic session ID
expect(notificationService.registerDeviceToken).toHaveBeenCalledWith(
uuidv5(mockUserId, notificationService.SELF_UUID_NAMESPACE),
mockFcmToken,
);
});
await waitFor(() => {
// Verify navigation to Home screen
expect(mockNavigate).toHaveBeenCalledWith('Home', {});
});
});
it('should navigate to Home without FCM token when permission is denied', async () => {
(
notificationService.requestNotificationPermission as jest.Mock
).mockResolvedValue(false);
const { root } = render(<KycSuccessScreen route={mockRoute} />);
const buttons = root.findAllByType('button');
const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates"
fireEvent.press(receiveUpdatesButton);
await waitFor(() => {
// Verify notification permission was requested
expect(
notificationService.requestNotificationPermission,
).toHaveBeenCalledTimes(1);
});
// Verify FCM token was NOT fetched
expect(notificationService.getFCMToken).not.toHaveBeenCalled();
// Verify FCM token was NOT stored
expect(mockSetFcmToken).not.toHaveBeenCalled();
// Verify device token was NOT registered
expect(notificationService.registerDeviceToken).not.toHaveBeenCalled();
await waitFor(() => {
// Verify navigation to Home screen still happens
expect(mockNavigate).toHaveBeenCalledWith('Home', {});
});
});
it('should navigate to Home when "I will check back later" is pressed', () => {
const { root } = render(<KycSuccessScreen route={mockRoute} />);
const buttons = root.findAllByType('button');
const checkLaterButton = buttons[1]; // Second button is "I will check back later"
fireEvent.press(checkLaterButton);
// Verify navigation to Home screen
expect(mockNavigate).toHaveBeenCalledWith('Home', {});
// Verify FCM-related functions were NOT called
expect(
notificationService.requestNotificationPermission,
).not.toHaveBeenCalled();
expect(notificationService.getFCMToken).not.toHaveBeenCalled();
expect(mockSetFcmToken).not.toHaveBeenCalled();
expect(notificationService.registerDeviceToken).not.toHaveBeenCalled();
});
it('should handle missing userId gracefully', async () => {
const routeWithoutUserId = {
params: {},
};
(
notificationService.requestNotificationPermission as jest.Mock
).mockResolvedValue(true);
const { root } = render(<KycSuccessScreen route={routeWithoutUserId} />);
const buttons = root.findAllByType('button');
const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates"
fireEvent.press(receiveUpdatesButton);
await waitFor(() => {
// Verify notification permission was requested
expect(
notificationService.requestNotificationPermission,
).toHaveBeenCalledTimes(1);
});
// Verify FCM token was NOT fetched (no userId)
expect(notificationService.getFCMToken).not.toHaveBeenCalled();
await waitFor(() => {
// Verify navigation to Home screen still happens
expect(mockNavigate).toHaveBeenCalledWith('Home', {});
});
});
it('renders fallback on render error', () => {
// Mock console.error to suppress error boundary error logs during test
const consoleErrorSpy = jest

View File

@@ -17,13 +17,13 @@ include "../utils/switcher.circom";
// Can check for 2 bigints equality if in is sub of each chunk of those numbers
template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) {
assert(CHUNK_NUMBER >= 2);
var EPSILON = 3;
assert(MAX_CHUNK_SIZE + EPSILON <= 253);
signal input in[CHUNK_NUMBER];
signal carry[CHUNK_NUMBER - 1];
component carryRangeChecks[CHUNK_NUMBER - 1];
for (var i = 0; i < CHUNK_NUMBER - 1; i++){
@@ -45,9 +45,9 @@ template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) {
// Works with overflowed signed chunks
// To handle megative values we use sign
// Sign is var and can be changed, but it should be a problem
// Sign change means that we can calculate for -in instead of in,
// Sign change means that we can calculate for -in instead of in,
// But if in % p == 0 means that -in % p == 0 too, so no exploit here
// Problem lies in other one:
// Problem lies in other one:
// k - is result of div func, and can be anything (var)
// we check k * p - in === 0
// k * p is result of big multiplication
@@ -71,9 +71,9 @@ template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) {
template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NUMBER, CHUNK_NUMBER_MODULUS){
signal input in[CHUNK_NUMBER];
signal input modulus[CHUNK_NUMBER_MODULUS];
var CHUNK_NUMBER_DIV = MAX_CHUNK_NUMBER - CHUNK_NUMBER_MODULUS + 1;
var reduced[200] = reduce_overflow_signed_dl(CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NUMBER, MAX_CHUNK_SIZE, in);
var div_result[2][200] = long_div_dl(CHUNK_SIZE, CHUNK_NUMBER_MODULUS, CHUNK_NUMBER_DIV - 1, reduced, modulus);
signal sign <-- reduced[199];
@@ -88,7 +88,7 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU
for (var i = 0; i < CHUNK_NUMBER_DIV; i++){
k[i] <-- div_result[0][i];
kRangeChecks[i] = Num2Bits(CHUNK_SIZE);
kRangeChecks[i].in <== k[i];
kRangeChecks[i].in <-- k[i];
}
component mult;
@@ -101,7 +101,7 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU
mult.in1 <== modulus;
mult.in2 <== k;
}
component swicher[CHUNK_NUMBER];
component isZero = BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, MAX_CHUNK_NUMBER);
@@ -116,5 +116,5 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU
for (var i = CHUNK_NUMBER; i < MAX_CHUNK_NUMBER; i++){
isZero.in[i] <== mult.out[i];
}
}
}

View File

@@ -156,7 +156,7 @@ for item in "${allowed_circuits[@]}"; do
continue
fi
while [[ ${#pids[@]} -ge 2 ]]; do
while [[ ${#pids[@]} -ge 1 ]]; do
new_pids=()
for pid in "${pids[@]}"; do
if kill -0 "$pid" 2>/dev/null; then

View File

@@ -92,7 +92,7 @@ interface IGCPJWTVerifier {
uint256[2] calldata pA,
uint256[2][2] calldata pB,
uint256[2] calldata pC,
uint256[19] calldata pubSignals
uint256[20] calldata pubSignals
) external view returns (bool);
}
@@ -450,7 +450,7 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
uint256[2] calldata pA,
uint256[2][2] calldata pB,
uint256[2] calldata pC,
uint256[19] calldata pubSignals
uint256[20] calldata pubSignals
) external onlyProxy onlyTEE {
// Check if the proof is valid
if (!IGCPJWTVerifier(_gcpJwtVerifier).verifyProof(pA, pB, pC, pubSignals)) revert INVALID_PROOF();
@@ -459,19 +459,19 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
if (pubSignals[0] != _gcpRootCAPubkeyHash) revert INVALID_ROOT_CA();
// Check if the TEE image hash is valid
bytes memory imageHash = GCPJWTHelper.unpackAndConvertImageHash(pubSignals[4], pubSignals[5], pubSignals[6]);
bytes memory imageHash = GCPJWTHelper.unpackAndConvertImageHash(pubSignals[5], pubSignals[6], pubSignals[7]);
if (!IPCR0Manager(_PCR0Manager).isPCR0Set(imageHash)) revert INVALID_IMAGE();
// Unpack the pubkey and register it
uint256 pubkeyCommitment = GCPJWTHelper.unpackAndDecodeHexPubkey(pubSignals[1], pubSignals[2], pubSignals[3]);
_isRegisteredPubkeyCommitment[pubkeyCommitment] = true;
uint256 currentYear = 2000 + pubSignals[7] * 10 + pubSignals[8];
uint256 currentMonth = pubSignals[9] * 10 + pubSignals[10];
uint256 currentDay = pubSignals[11] * 10 + pubSignals[12];
uint256 currentHour = pubSignals[13] * 10 + pubSignals[14];
uint256 currentMinute = pubSignals[15] * 10 + pubSignals[16];
uint256 currentSecond = pubSignals[17] * 10 + pubSignals[18];
uint256 currentYear = 2000 + pubSignals[8] * 10 + pubSignals[9];
uint256 currentMonth = pubSignals[10] * 10 + pubSignals[11];
uint256 currentDay = pubSignals[12] * 10 + pubSignals[13];
uint256 currentHour = pubSignals[14] * 10 + pubSignals[15];
uint256 currentMinute = pubSignals[16] * 10 + pubSignals[17];
uint256 currentSecond = pubSignals[18] * 10 + pubSignals[19];
uint256 currentTimestamp = Formatter.toTimeStampWithSeconds(
currentYear,
currentMonth,

View File

@@ -29,7 +29,7 @@ contract MockGCPJWTVerifier {
uint256[2] calldata pA,
uint256[2][2] calldata pB,
uint256[2] calldata pC,
uint256[19] calldata pubSignals
uint256[20] calldata pubSignals
) external view returns (bool) {
// Silence unused variable warnings
pA;

View File

@@ -0,0 +1,363 @@
// SPDX-License-Identifier: GPL-3.0
/*
Copyright 2021 0KIMS association.
This file is generated with [snarkJS](https://github.com/iden3/snarkjs).
snarkJS is a free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
snarkJS is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.
You should have received a copy of the GNU General Public License
along with snarkJS. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity >=0.7.0 <0.9.0;
contract Groth16Verifier {
// Scalar field size
uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
// Base field size
uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
// Verification Key data
uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042;
uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958;
uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132;
uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731;
uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679;
uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856;
uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634;
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
uint256 constant deltax1 = 1022502948747070596300631872305350196366208813582081229292413330002410493735;
uint256 constant deltax2 = 8307404806875602039009979465400882149520343934575147532878670270259674144681;
uint256 constant deltay1 = 8725996148009629609617423651062395041554350094385944632997372312828608644955;
uint256 constant deltay2 = 19505227144542990355285832777856832082655385455315296491381347497982380087331;
uint256 constant IC0x = 16649376790350306128495410672000438222835355361873864679185308928608342391377;
uint256 constant IC0y = 1365830659239397567654193478106544803466926587095831397836882385286292210457;
uint256 constant IC1x = 12768368041823022971486465099843313755353560181066686496309262693573983752166;
uint256 constant IC1y = 8959643464054312389755875312066576344157543684040889350558798123653714759323;
uint256 constant IC2x = 8026951355325092256379108005740615512895662065129471323964253392093201472413;
uint256 constant IC2y = 17729685419344675830181571225504519401370157618831493299320871505193568194542;
uint256 constant IC3x = 13865750614916211164740113517816425481179265306761612472818567385469595810190;
uint256 constant IC3y = 6210007189067774389269573600168370223250403017805496113623335642264819992738;
uint256 constant IC4x = 16855313964021865460083277912281502340407051430688518561820294646056966683723;
uint256 constant IC4y = 15265407205922489364865678414919162208795257265772110915033785419192236363960;
uint256 constant IC5x = 18598823774356508040525215881560556738983729535652356395586704599152692518280;
uint256 constant IC5y = 18145817576163407281749708126167770321482159783050035647989919114769433256079;
uint256 constant IC6x = 7929686493832109041041190086485345905029205802382475316611421597823511641043;
uint256 constant IC6y = 19169046602940406351907027759303697432610627026407453208752335429425017694574;
uint256 constant IC7x = 2605668546149689485076733864456601989800612639397730351435615085329568572059;
uint256 constant IC7y = 2242419572125099587271391127551951332349827207830958146376081280864531825864;
uint256 constant IC8x = 17230061988111645534990582267868011734783232047326494254312685097544413153459;
uint256 constant IC8y = 10806577457667861555253433417098515955632524053970338643826272138544403320442;
uint256 constant IC9x = 3751984630395628299497200107740113530312143585224331604497180428031979981854;
uint256 constant IC9y = 15676455188720477849218254715359881022685281346012746362600653176819367175994;
uint256 constant IC10x = 9038868170600703467507268624782850799834426621476374278712452873055805013104;
uint256 constant IC10y = 9698587198888135369066906249654396893723648003242241945599284193157738042248;
uint256 constant IC11x = 6050467884563375668249040797272149300003806466909114026944043296882360309360;
uint256 constant IC11y = 15900287959991498727296171595521639394049115178198151794906977584504380285297;
uint256 constant IC12x = 11084322708760789175416300406920316493444723572225966905156819463716045081320;
uint256 constant IC12y = 11218515196222567596688687943809578734267033209068034707100619316839921252394;
uint256 constant IC13x = 10645041863169277188776881369692412104739148582039109401067090622235062084156;
uint256 constant IC13y = 5266268354502390834581900591132009542571872858584466937449333517597831148030;
uint256 constant IC14x = 12641747272597271663246870871466152965248117816492334493753291231347523232168;
uint256 constant IC14y = 19526003775802419962730302158408658198175393685733749794278416969198861577034;
uint256 constant IC15x = 6139284918750361257008863566645097867991292622068199456332000872393801256773;
uint256 constant IC15y = 7099084867504428315337895159166860608559331005995192184490932820607010680845;
uint256 constant IC16x = 9370432203154443644773178040475615441452364961035990256255996609230750218064;
uint256 constant IC16y = 17951757691776403072537537626795200133221243393670030429694486485017127221358;
uint256 constant IC17x = 21581607541319264321515681298226106781535771321110191776762670932817827595844;
uint256 constant IC17y = 7631049069535860061742036261740730390300464507981117501570404056719958498930;
uint256 constant IC18x = 16588935529361800732448688229721305142336631834288163321894359880448688608191;
uint256 constant IC18y = 4976649298929967469596409013742801233623738930274577396507275281714439091100;
uint256 constant IC19x = 13336088316263130029440976636885322206279122461816212975585641922353453096719;
uint256 constant IC19y = 668527371723708514830022396101506352277923231593513590339198147917179128262;
uint256 constant IC20x = 7911418535344866382682474453536883970529338904273675929069409842800763592456;
uint256 constant IC20y = 6722145715621557485364045815849938484983110008747946723738151730812429418202;
uint256 constant IC21x = 610873214241184085635414594211441831430912772471117234461302269567691174096;
uint256 constant IC21y = 16969907768023728182903317862310886370963194429698287724301462949165910596854;
uint256 constant IC22x = 659738555556673077218073955988504765951032248025470001896149485964044510568;
uint256 constant IC22y = 2124464077179769137643014583429957482256390408775774347541901875987080182668;
uint256 constant IC23x = 11040330531093768074742977048495269267038172161278331102262692904222746927915;
uint256 constant IC23y = 20387648111599243028561521301140310714164415003338654058061856932087967245514;
uint256 constant IC24x = 6937058621269922207815167233155518898032328662416059831807664411944661190679;
uint256 constant IC24y = 3779340684837021741207549471402298796167963069596080462551336236827030143602;
uint256 constant IC25x = 20956067714892758188531163534075112952656779768842660715243328162174316184647;
uint256 constant IC25y = 9697689335367034906644638465039998846629732846527791686651080885302279721947;
uint256 constant IC26x = 10803066158517027587330447158982829324243112587050865062666733319696533170000;
uint256 constant IC26y = 16966880529095588436103115659246637747363575619917237189424029126730846465979;
uint256 constant IC27x = 12430600018955874842029331801839308658974272583893366935707885910189427842476;
uint256 constant IC27y = 14602780957678176966948503351865628319039612308733335242961008886115024541985;
uint256 constant IC28x = 10923748125791784887614451982072899321420747436037959145471646494829305705731;
uint256 constant IC28y = 6050274667868774010280923182747429242888928748472706014853484883020658961073;
uint256 constant IC29x = 1170885743391113947515531032472753161485583617637753865725092942330476093342;
uint256 constant IC29y = 19204742121781488340297839383055704899252648836617466985181418250802660585322;
// Memory data
uint16 constant pVk = 0;
uint16 constant pPairing = 128;
uint16 constant pLastMem = 896;
function verifyProof(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint[29] calldata _pubSignals
) public view returns (bool) {
assembly {
function checkField(v) {
if iszero(lt(v, r)) {
mstore(0, 0)
return(0, 0x20)
}
}
// G1 function to multiply a G1 value(x,y) to value in an address
function g1_mulAccC(pR, x, y, s) {
let success
let mIn := mload(0x40)
mstore(mIn, x)
mstore(add(mIn, 32), y)
mstore(add(mIn, 64), s)
success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
mstore(add(mIn, 64), mload(pR))
mstore(add(mIn, 96), mload(add(pR, 32)))
success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
}
function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk {
let _pPairing := add(pMem, pPairing)
let _pVk := add(pMem, pVk)
mstore(_pVk, IC0x)
mstore(add(_pVk, 32), IC0y)
// Compute the linear combination vk_x
g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0)))
g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32)))
g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64)))
g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96)))
g1_mulAccC(_pVk, IC5x, IC5y, calldataload(add(pubSignals, 128)))
g1_mulAccC(_pVk, IC6x, IC6y, calldataload(add(pubSignals, 160)))
g1_mulAccC(_pVk, IC7x, IC7y, calldataload(add(pubSignals, 192)))
g1_mulAccC(_pVk, IC8x, IC8y, calldataload(add(pubSignals, 224)))
g1_mulAccC(_pVk, IC9x, IC9y, calldataload(add(pubSignals, 256)))
g1_mulAccC(_pVk, IC10x, IC10y, calldataload(add(pubSignals, 288)))
g1_mulAccC(_pVk, IC11x, IC11y, calldataload(add(pubSignals, 320)))
g1_mulAccC(_pVk, IC12x, IC12y, calldataload(add(pubSignals, 352)))
g1_mulAccC(_pVk, IC13x, IC13y, calldataload(add(pubSignals, 384)))
g1_mulAccC(_pVk, IC14x, IC14y, calldataload(add(pubSignals, 416)))
g1_mulAccC(_pVk, IC15x, IC15y, calldataload(add(pubSignals, 448)))
g1_mulAccC(_pVk, IC16x, IC16y, calldataload(add(pubSignals, 480)))
g1_mulAccC(_pVk, IC17x, IC17y, calldataload(add(pubSignals, 512)))
g1_mulAccC(_pVk, IC18x, IC18y, calldataload(add(pubSignals, 544)))
g1_mulAccC(_pVk, IC19x, IC19y, calldataload(add(pubSignals, 576)))
g1_mulAccC(_pVk, IC20x, IC20y, calldataload(add(pubSignals, 608)))
g1_mulAccC(_pVk, IC21x, IC21y, calldataload(add(pubSignals, 640)))
g1_mulAccC(_pVk, IC22x, IC22y, calldataload(add(pubSignals, 672)))
g1_mulAccC(_pVk, IC23x, IC23y, calldataload(add(pubSignals, 704)))
g1_mulAccC(_pVk, IC24x, IC24y, calldataload(add(pubSignals, 736)))
g1_mulAccC(_pVk, IC25x, IC25y, calldataload(add(pubSignals, 768)))
g1_mulAccC(_pVk, IC26x, IC26y, calldataload(add(pubSignals, 800)))
g1_mulAccC(_pVk, IC27x, IC27y, calldataload(add(pubSignals, 832)))
g1_mulAccC(_pVk, IC28x, IC28y, calldataload(add(pubSignals, 864)))
g1_mulAccC(_pVk, IC29x, IC29y, calldataload(add(pubSignals, 896)))
// -A
mstore(_pPairing, calldataload(pA))
mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q))
// B
mstore(add(_pPairing, 64), calldataload(pB))
mstore(add(_pPairing, 96), calldataload(add(pB, 32)))
mstore(add(_pPairing, 128), calldataload(add(pB, 64)))
mstore(add(_pPairing, 160), calldataload(add(pB, 96)))
// alpha1
mstore(add(_pPairing, 192), alphax)
mstore(add(_pPairing, 224), alphay)
// beta2
mstore(add(_pPairing, 256), betax1)
mstore(add(_pPairing, 288), betax2)
mstore(add(_pPairing, 320), betay1)
mstore(add(_pPairing, 352), betay2)
// vk_x
mstore(add(_pPairing, 384), mload(add(pMem, pVk)))
mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32))))
// gamma2
mstore(add(_pPairing, 448), gammax1)
mstore(add(_pPairing, 480), gammax2)
mstore(add(_pPairing, 512), gammay1)
mstore(add(_pPairing, 544), gammay2)
// C
mstore(add(_pPairing, 576), calldataload(pC))
mstore(add(_pPairing, 608), calldataload(add(pC, 32)))
// delta2
mstore(add(_pPairing, 640), deltax1)
mstore(add(_pPairing, 672), deltax2)
mstore(add(_pPairing, 704), deltay1)
mstore(add(_pPairing, 736), deltay2)
let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20)
isOk := and(success, mload(_pPairing))
}
let pMem := mload(0x40)
mstore(0x40, add(pMem, pLastMem))
// Validate that all evaluations ∈ F
checkField(calldataload(add(_pubSignals, 0)))
checkField(calldataload(add(_pubSignals, 32)))
checkField(calldataload(add(_pubSignals, 64)))
checkField(calldataload(add(_pubSignals, 96)))
checkField(calldataload(add(_pubSignals, 128)))
checkField(calldataload(add(_pubSignals, 160)))
checkField(calldataload(add(_pubSignals, 192)))
checkField(calldataload(add(_pubSignals, 224)))
checkField(calldataload(add(_pubSignals, 256)))
checkField(calldataload(add(_pubSignals, 288)))
checkField(calldataload(add(_pubSignals, 320)))
checkField(calldataload(add(_pubSignals, 352)))
checkField(calldataload(add(_pubSignals, 384)))
checkField(calldataload(add(_pubSignals, 416)))
checkField(calldataload(add(_pubSignals, 448)))
checkField(calldataload(add(_pubSignals, 480)))
checkField(calldataload(add(_pubSignals, 512)))
checkField(calldataload(add(_pubSignals, 544)))
checkField(calldataload(add(_pubSignals, 576)))
checkField(calldataload(add(_pubSignals, 608)))
checkField(calldataload(add(_pubSignals, 640)))
checkField(calldataload(add(_pubSignals, 672)))
checkField(calldataload(add(_pubSignals, 704)))
checkField(calldataload(add(_pubSignals, 736)))
checkField(calldataload(add(_pubSignals, 768)))
checkField(calldataload(add(_pubSignals, 800)))
checkField(calldataload(add(_pubSignals, 832)))
checkField(calldataload(add(_pubSignals, 864)))
checkField(calldataload(add(_pubSignals, 896)))
// Validate all evaluations
let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem)
mstore(0, isValid)
return(0, 0x20)
}
}
}

View File

@@ -0,0 +1,300 @@
// SPDX-License-Identifier: GPL-3.0
/*
Copyright 2021 0KIMS association.
This file is generated with [snarkJS](https://github.com/iden3/snarkjs).
snarkJS is a free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
snarkJS is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.
You should have received a copy of the GNU General Public License
along with snarkJS. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity >=0.7.0 <0.9.0;
contract Verifier_gcp_jwt {
// Scalar field size
uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
// Base field size
uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
// Verification Key data
uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042;
uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958;
uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132;
uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731;
uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679;
uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856;
uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634;
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
uint256 constant deltax1 = 1804222383802986733937376810902861143401033555807870231731929239915419049861;
uint256 constant deltax2 = 15902885537441599351050098769394227668772388058868388096316964244217496511682;
uint256 constant deltay1 = 4195707504005103778106485021796359604414786496137920116128130440872062477216;
uint256 constant deltay2 = 20513207510859042996645896574478474889840017920990203652675014165180462273668;
uint256 constant IC0x = 6972951741762339913362267428319005943611938060812676091174501911982947323821;
uint256 constant IC0y = 4968121098705797351946375443564156998686441710551907423285338106315203657372;
uint256 constant IC1x = 3969479803545901558882616933276060341612655312403217371718193775571328202698;
uint256 constant IC1y = 10796516354190443333590906104065573186594421836191093099894208495600273943382;
uint256 constant IC2x = 5282886783908067346990928387588210996099802199800176473402519317523182497411;
uint256 constant IC2y = 13420701105707643769706876856296866111708803407614711871170325095961081369695;
uint256 constant IC3x = 14105950545034420261862110084277090993607573654064743638564927148396262651666;
uint256 constant IC3y = 13354956139782865997977495342720245140716772080136555810660173122394181127180;
uint256 constant IC4x = 17223368406124250621460330134418760536341963146179581332507963390797809647912;
uint256 constant IC4y = 19015620010364835231555497011683709184643217460850880718542989960325995808017;
uint256 constant IC5x = 11415362657438949221591074018468802007898322076011964898865456054649179831908;
uint256 constant IC5y = 17459573325598515038912928408360066384367356809087828399079121874232360528478;
uint256 constant IC6x = 15574545936483334745596750909280550198515448424427848182054643607937078179213;
uint256 constant IC6y = 13006549512473282147197122913454973085866920937923147249375738521329287066222;
uint256 constant IC7x = 14645989050046479540147134517500433000682841795623944679511623689017979403245;
uint256 constant IC7y = 16002146776744341769994596125501558460157837756621333957158039132600774201665;
uint256 constant IC8x = 17447612904927318100653430764709204605475101707883725218472729377143326600248;
uint256 constant IC8y = 16892886274335002504909275077153679691684214526248560805118560019125943648821;
uint256 constant IC9x = 17653661950237194880278154054792568909474176263902202958186273149474358670533;
uint256 constant IC9y = 11669219494719975955790450067861506164332870357879984076098486608481987018857;
uint256 constant IC10x = 13289207501149959620194929372715676920560830325500657282490914929267428690980;
uint256 constant IC10y = 12465657438099014694334055521610703216229866770917539818266695642349007426072;
uint256 constant IC11x = 18446654622136293276199162514838693836980616816456314636743905193625590745253;
uint256 constant IC11y = 12876916821064374752505779861869326377989533450827838519593872009453598320656;
uint256 constant IC12x = 11001381773587677694421240176598022327285567125732057704900785068521955604564;
uint256 constant IC12y = 15721905323957520285870204323317542530315127175554829712351392669354944626115;
uint256 constant IC13x = 19526090904722047042773905186611760547729403485756211734248157863388135796357;
uint256 constant IC13y = 6872421404352779784414693997079152972445035104903743503355279949152744176183;
uint256 constant IC14x = 15194138441068760983236111544251338084740306295420897247383092303969333517280;
uint256 constant IC14y = 17571382599242644993857901274570230804168370452582601899367177574780143361956;
uint256 constant IC15x = 584870595147362727880838486101127854955042037369856345600359023707849233383;
uint256 constant IC15y = 12343643073139461156226272211050331809098122200356986708169739203244290558425;
uint256 constant IC16x = 14164891277783985284859197223195840777194061449283527719178608169082529731883;
uint256 constant IC16y = 5769361895392815047832493230313789373949187154386769492255962435984388734;
uint256 constant IC17x = 5526583431755874525920531779957581117218605045719526246142282984128225259812;
uint256 constant IC17y = 15582261976988135470726322969910254124942972597198825965150134549937865280024;
uint256 constant IC18x = 11933687532433713666089789805193821666211611847890385200532102102696090562695;
uint256 constant IC18y = 13768581020150988368938923899239734752213497676691170616636813895788587803927;
uint256 constant IC19x = 21039243000302785560612608554208434709650210545299036143304628975668975303432;
uint256 constant IC19y = 3072044020424624557872621541718589400992098528118783904368755425332969903054;
uint256 constant IC20x = 13029408846315391045768292892963336300734709802776968717851605403617397448869;
uint256 constant IC20y = 21441391199269244274037661931659719640029973634066921385003370500690694569608;
// Memory data
uint16 constant pVk = 0;
uint16 constant pPairing = 128;
uint16 constant pLastMem = 896;
function verifyProof(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint[20] calldata _pubSignals
) public view returns (bool) {
assembly {
function checkField(v) {
if iszero(lt(v, r)) {
mstore(0, 0)
return(0, 0x20)
}
}
// G1 function to multiply a G1 value(x,y) to value in an address
function g1_mulAccC(pR, x, y, s) {
let success
let mIn := mload(0x40)
mstore(mIn, x)
mstore(add(mIn, 32), y)
mstore(add(mIn, 64), s)
success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
mstore(add(mIn, 64), mload(pR))
mstore(add(mIn, 96), mload(add(pR, 32)))
success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
}
function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk {
let _pPairing := add(pMem, pPairing)
let _pVk := add(pMem, pVk)
mstore(_pVk, IC0x)
mstore(add(_pVk, 32), IC0y)
// Compute the linear combination vk_x
g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0)))
g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32)))
g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64)))
g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96)))
g1_mulAccC(_pVk, IC5x, IC5y, calldataload(add(pubSignals, 128)))
g1_mulAccC(_pVk, IC6x, IC6y, calldataload(add(pubSignals, 160)))
g1_mulAccC(_pVk, IC7x, IC7y, calldataload(add(pubSignals, 192)))
g1_mulAccC(_pVk, IC8x, IC8y, calldataload(add(pubSignals, 224)))
g1_mulAccC(_pVk, IC9x, IC9y, calldataload(add(pubSignals, 256)))
g1_mulAccC(_pVk, IC10x, IC10y, calldataload(add(pubSignals, 288)))
g1_mulAccC(_pVk, IC11x, IC11y, calldataload(add(pubSignals, 320)))
g1_mulAccC(_pVk, IC12x, IC12y, calldataload(add(pubSignals, 352)))
g1_mulAccC(_pVk, IC13x, IC13y, calldataload(add(pubSignals, 384)))
g1_mulAccC(_pVk, IC14x, IC14y, calldataload(add(pubSignals, 416)))
g1_mulAccC(_pVk, IC15x, IC15y, calldataload(add(pubSignals, 448)))
g1_mulAccC(_pVk, IC16x, IC16y, calldataload(add(pubSignals, 480)))
g1_mulAccC(_pVk, IC17x, IC17y, calldataload(add(pubSignals, 512)))
g1_mulAccC(_pVk, IC18x, IC18y, calldataload(add(pubSignals, 544)))
g1_mulAccC(_pVk, IC19x, IC19y, calldataload(add(pubSignals, 576)))
g1_mulAccC(_pVk, IC20x, IC20y, calldataload(add(pubSignals, 608)))
// -A
mstore(_pPairing, calldataload(pA))
mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q))
// B
mstore(add(_pPairing, 64), calldataload(pB))
mstore(add(_pPairing, 96), calldataload(add(pB, 32)))
mstore(add(_pPairing, 128), calldataload(add(pB, 64)))
mstore(add(_pPairing, 160), calldataload(add(pB, 96)))
// alpha1
mstore(add(_pPairing, 192), alphax)
mstore(add(_pPairing, 224), alphay)
// beta2
mstore(add(_pPairing, 256), betax1)
mstore(add(_pPairing, 288), betax2)
mstore(add(_pPairing, 320), betay1)
mstore(add(_pPairing, 352), betay2)
// vk_x
mstore(add(_pPairing, 384), mload(add(pMem, pVk)))
mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32))))
// gamma2
mstore(add(_pPairing, 448), gammax1)
mstore(add(_pPairing, 480), gammax2)
mstore(add(_pPairing, 512), gammay1)
mstore(add(_pPairing, 544), gammay2)
// C
mstore(add(_pPairing, 576), calldataload(pC))
mstore(add(_pPairing, 608), calldataload(add(pC, 32)))
// delta2
mstore(add(_pPairing, 640), deltax1)
mstore(add(_pPairing, 672), deltax2)
mstore(add(_pPairing, 704), deltay1)
mstore(add(_pPairing, 736), deltay2)
let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20)
isOk := and(success, mload(_pPairing))
}
let pMem := mload(0x40)
mstore(0x40, add(pMem, pLastMem))
// Validate that all evaluations ∈ F
checkField(calldataload(add(_pubSignals, 0)))
checkField(calldataload(add(_pubSignals, 32)))
checkField(calldataload(add(_pubSignals, 64)))
checkField(calldataload(add(_pubSignals, 96)))
checkField(calldataload(add(_pubSignals, 128)))
checkField(calldataload(add(_pubSignals, 160)))
checkField(calldataload(add(_pubSignals, 192)))
checkField(calldataload(add(_pubSignals, 224)))
checkField(calldataload(add(_pubSignals, 256)))
checkField(calldataload(add(_pubSignals, 288)))
checkField(calldataload(add(_pubSignals, 320)))
checkField(calldataload(add(_pubSignals, 352)))
checkField(calldataload(add(_pubSignals, 384)))
checkField(calldataload(add(_pubSignals, 416)))
checkField(calldataload(add(_pubSignals, 448)))
checkField(calldataload(add(_pubSignals, 480)))
checkField(calldataload(add(_pubSignals, 512)))
checkField(calldataload(add(_pubSignals, 544)))
checkField(calldataload(add(_pubSignals, 576)))
checkField(calldataload(add(_pubSignals, 608)))
// Validate all evaluations
let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem)
mstore(0, isValid)
return(0, 0x20)
}
}
}

View File

@@ -0,0 +1,188 @@
// SPDX-License-Identifier: GPL-3.0
/*
Copyright 2021 0KIMS association.
This file is generated with [snarkJS](https://github.com/iden3/snarkjs).
snarkJS is a free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
snarkJS is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.
You should have received a copy of the GNU General Public License
along with snarkJS. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity >=0.7.0 <0.9.0;
contract Verifier_register_kyc {
// Scalar field size
uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
// Base field size
uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
// Verification Key data
uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042;
uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958;
uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132;
uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731;
uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679;
uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856;
uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634;
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
uint256 constant deltax1 = 5096083179356499711134631633887324869705417987781707067448982643113793288629;
uint256 constant deltax2 = 21697837263794337150638011065730493662458737594964062811076864693347158601584;
uint256 constant deltay1 = 10401404284625717188368140886450294801087446278285114268746933223843924747393;
uint256 constant deltay2 = 21623976071772575613470418289568781837131470676146510317928308200173145329920;
uint256 constant IC0x = 3168135977548073774669686196671110956985263260631963004209946350111009871783;
uint256 constant IC0y = 19251271161827058925074199219712324559154387560340229136388386911360884273664;
uint256 constant IC1x = 10113211405751296270501192543847397464767605934439509015058826831045146327835;
uint256 constant IC1y = 20906232714001423808044993672348326367907746369031125809295685889757083482955;
uint256 constant IC2x = 6698755477482983343149024614634334433817620579582112164753380215391423709716;
uint256 constant IC2y = 19611748192038263311129103965451949878445716642076010695268731681711285170849;
uint256 constant IC3x = 14337814476916517064830141950947112575746971807933737544800387322677759596630;
uint256 constant IC3y = 20134363192770038065525691357184427373049635942597185153353604022941231384818;
uint256 constant IC4x = 11598465374717791235735036209864180918816853983932860910077820062417244512066;
uint256 constant IC4y = 10915386471964999341016166937952548568058036159601535214565672698374193076432;
// Memory data
uint16 constant pVk = 0;
uint16 constant pPairing = 128;
uint16 constant pLastMem = 896;
function verifyProof(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint[4] calldata _pubSignals
) public view returns (bool) {
assembly {
function checkField(v) {
if iszero(lt(v, r)) {
mstore(0, 0)
return(0, 0x20)
}
}
// G1 function to multiply a G1 value(x,y) to value in an address
function g1_mulAccC(pR, x, y, s) {
let success
let mIn := mload(0x40)
mstore(mIn, x)
mstore(add(mIn, 32), y)
mstore(add(mIn, 64), s)
success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
mstore(add(mIn, 64), mload(pR))
mstore(add(mIn, 96), mload(add(pR, 32)))
success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
}
function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk {
let _pPairing := add(pMem, pPairing)
let _pVk := add(pMem, pVk)
mstore(_pVk, IC0x)
mstore(add(_pVk, 32), IC0y)
// Compute the linear combination vk_x
g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0)))
g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32)))
g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64)))
g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96)))
// -A
mstore(_pPairing, calldataload(pA))
mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q))
// B
mstore(add(_pPairing, 64), calldataload(pB))
mstore(add(_pPairing, 96), calldataload(add(pB, 32)))
mstore(add(_pPairing, 128), calldataload(add(pB, 64)))
mstore(add(_pPairing, 160), calldataload(add(pB, 96)))
// alpha1
mstore(add(_pPairing, 192), alphax)
mstore(add(_pPairing, 224), alphay)
// beta2
mstore(add(_pPairing, 256), betax1)
mstore(add(_pPairing, 288), betax2)
mstore(add(_pPairing, 320), betay1)
mstore(add(_pPairing, 352), betay2)
// vk_x
mstore(add(_pPairing, 384), mload(add(pMem, pVk)))
mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32))))
// gamma2
mstore(add(_pPairing, 448), gammax1)
mstore(add(_pPairing, 480), gammax2)
mstore(add(_pPairing, 512), gammay1)
mstore(add(_pPairing, 544), gammay2)
// C
mstore(add(_pPairing, 576), calldataload(pC))
mstore(add(_pPairing, 608), calldataload(add(pC, 32)))
// delta2
mstore(add(_pPairing, 640), deltax1)
mstore(add(_pPairing, 672), deltax2)
mstore(add(_pPairing, 704), deltay1)
mstore(add(_pPairing, 736), deltay2)
let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20)
isOk := and(success, mload(_pPairing))
}
let pMem := mload(0x40)
mstore(0x40, add(pMem, pLastMem))
// Validate that all evaluations ∈ F
checkField(calldataload(add(_pubSignals, 0)))
checkField(calldataload(add(_pubSignals, 32)))
checkField(calldataload(add(_pubSignals, 64)))
checkField(calldataload(add(_pubSignals, 96)))
// Validate all evaluations
let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem)
mstore(0, isValid)
return(0, 0x20)
}
}
}

View File

@@ -76,10 +76,10 @@ const config: HardhatUserConfig = {
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY as string,
// apiKey: {
// "celo-sepolia": process.env.ETHERSCAN_API_KEY as string,
// },
// apiKey: process.env.ETHERSCAN_API_KEY as string,
apiKey: {
"celo-sepolia": process.env.ETHERSCAN_API_KEY as string,
},
customChains: [
{
network: "celo",

View File

@@ -97,5 +97,12 @@
"DeployHubV2#IdentityVerificationHub": "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74",
"DeployNewHubAndUpgradee#IdentityVerificationHubV2": "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74",
"DeployNewHubAndUpgradee#CustomVerifier": "0x2711E535D68D8B8729a7d126fEb13aEc0fe29A27",
"DeployNewHubAndUpgradee#IdentityVerificationHubImplV2": "0x48985ec4f71cBC8f387c5C77143110018560c7eD"
"DeployNewHubAndUpgradee#IdentityVerificationHubImplV2": "0x48985ec4f71cBC8f387c5C77143110018560c7eD",
"DeployKycRegistryModule#PCR0Manager": "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2",
"DeployKycRegistryModule#PoseidonT3": "0x163983BAe19dE94A007C6C502b7389F6C359C818",
"DeployKycRegistryModule#Verifier_gcp_jwt": "0x13ee8CEa15a262D81a245b37889F7b4bEd015f4c",
"DeployKycRegistryModule#IdentityRegistryKycImplV1": "0x94f6DE38E10140B9E3963a770B5B769b38459a3B",
"DeployKycRegistryModule#IdentityRegistry": "0x90e907E4AaB6e9bcFB94997Af4A097e8CAadBdf3",
"UpdateAllRegistries#PCR0Manager": "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2",
"UpdateAllRegistries#a3": "0x90e907E4AaB6e9bcFB94997Af4A097e8CAadBdf3"
}

View File

@@ -8,13 +8,15 @@ const AttestationId = {
E_PASSPORT: "0x0000000000000000000000000000000000000000000000000000000000000001",
EU_ID_CARD: "0x0000000000000000000000000000000000000000000000000000000000000002",
AADHAAR: "0x0000000000000000000000000000000000000000000000000000000000000003",
KYC: "0x0000000000000000000000000000000000000000000000000000000000000004",
};
// Map registry deployment modules to their attestation IDs
const registryToAttestationId: Record<string, string> = {
// "DeployRegistryModule#IdentityRegistry": AttestationId.E_PASSPORT,
// "DeployIdCardRegistryModule#IdentityRegistry": AttestationId.EU_ID_CARD,
"DeployAadhaarRegistryModule#IdentityRegistry": AttestationId.AADHAAR,
// "DeployAadhaarRegistryModule#IdentityRegistry": AttestationId.AADHAAR,
"DeployKycRegistryModule#IdentityRegistry": AttestationId.KYC,
};
const ids = (() => {

View File

@@ -9,6 +9,7 @@ const AttestationId = {
E_PASSPORT: "0x0000000000000000000000000000000000000000000000000000000000000001",
EU_ID_CARD: "0x0000000000000000000000000000000000000000000000000000000000000002",
AADHAAR: "0x0000000000000000000000000000000000000000000000000000000000000003",
KYC: "0x0000000000000000000000000000000000000000000000000000000000000004",
};
// Circuit type mappings based on circuit names
@@ -21,6 +22,8 @@ const getCircuitType = (
return { attestationId: AttestationId.EU_ID_CARD, typeId, circuitType: "register" };
} else if (circuitName === "register_aadhaar") {
return { attestationId: AttestationId.AADHAAR, typeId, circuitType: "register" };
} else if (circuitName === "register_kyc") {
return { attestationId: AttestationId.KYC, typeId, circuitType: "register" };
} else {
return { attestationId: AttestationId.E_PASSPORT, typeId, circuitType: "register" };
}
@@ -33,6 +36,8 @@ const getCircuitType = (
return { attestationId: AttestationId.EU_ID_CARD, typeId: 0, circuitType: "vc_and_disclose" };
} else if (circuitName === "vc_and_disclose_aadhaar") {
return { attestationId: AttestationId.AADHAAR, typeId: 0, circuitType: "vc_and_disclose" };
} else if (circuitName === "vc_and_disclose_kyc") {
return { attestationId: AttestationId.KYC, typeId: 0, circuitType: "vc_and_disclose" };
} else {
return { attestationId: AttestationId.E_PASSPORT, typeId: 0, circuitType: "vc_and_disclose" };
}

View File

@@ -0,0 +1,52 @@
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
import { artifacts } from "hardhat";
import { ethers } from "ethers";
export default buildModule("DeployKycRegistryModule", (m) => {
// Deploy PoseidonT3
console.log("📚 Deploying PoseidonT3 library...");
const poseidonT3 = m.library("PoseidonT3");
console.log("🏗️ Deploying IdentityRegistryKycImplV1 implementation...");
// Deploy IdentityRegistryImplV1
const identityRegistryKycImpl = m.contract("IdentityRegistryKycImplV1", [], {
libraries: { PoseidonT3: poseidonT3 },
});
console.log("⚙️ Preparing registry initialization data...");
// Get the interface and encode the initialize function call
const registryInterface = getRegistryInitializeData();
const registryInitData = registryInterface.encodeFunctionData("initialize", [ethers.ZeroAddress, ethers.ZeroAddress]);
console.log(" Init data:", registryInitData);
console.log("🚀 Deploying IdentityRegistry proxy...");
// Deploy the proxy contract with the implementation address and initialization data
const registry = m.contract("IdentityRegistry", [identityRegistryKycImpl, registryInitData]);
const gcpKycVerifier = m.contract("Verifier_gcp_jwt", []);
const pcr0Manager = m.contract("PCR0Manager", []);
console.log("✅ Registry deployment module setup complete!");
console.log(" 📋 Summary:");
console.log(" - PoseidonT3: Library");
console.log(" - IdentityRegistryKycImplV1: Implementation contract");
console.log(" - IdentityRegistry: Proxy contract");
console.log(" - Verifier_gcp_jwt: GCP JWT verifier contract");
console.log(" - PCR0Manager: PCR0Manager contract");
return {
poseidonT3,
identityRegistryKycImpl,
registry,
gcpKycVerifier,
pcr0Manager,
};
});
function getRegistryInitializeData() {
const registryArtifact = artifacts.readArtifactSync("IdentityRegistryKycImplV1");
const registryInterface = new ethers.Interface(registryArtifact.abi);
return registryInterface;
}

View File

@@ -19,18 +19,29 @@ const registries = {
// hub: "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74",
// cscaRoot: "13859398115974385161464830211947258005860166431741677064758266112192747818198",
// },
"DeployAadhaarRegistryModule#IdentityRegistry": {
// "DeployAadhaarRegistryModule#IdentityRegistry": {
// shouldChange: true,
// nameAndDobOfac: "4183822562579010781434914867177251983368244626022840551534475857364967864437",
// nameAndYobOfac: "14316795765689804800341464910235935757494922653038299433675973925727164473934",
// hub: "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF",
// pubkeyCommitments: [
// "5648956411273136337349787488442520720416229937879112788241850936049694492145",
// "18304035373718681408213540837772113004961405604264885188535510276454415833542",
// "3099763118716361008062312602688327679110629275746483297740895929951765195538",
// "5960616419594750988984019912914733527854225713611991429799390436159340745422",
// "1312086597361744268424404341813751658452218312204370523713186983060138886330",
// ],
// },
"DeployKycRegistryModule#IdentityRegistry": {
shouldChange: true,
nameAndDobOfac: "4183822562579010781434914867177251983368244626022840551534475857364967864437",
nameAndYobOfac: "14316795765689804800341464910235935757494922653038299433675973925727164473934",
hub: "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF",
pubkeyCommitments: [
"5648956411273136337349787488442520720416229937879112788241850936049694492145",
"18304035373718681408213540837772113004961405604264885188535510276454415833542",
"3099763118716361008062312602688327679110629275746483297740895929951765195538",
"5960616419594750988984019912914733527854225713611991429799390436159340745422",
"1312086597361744268424404341813751658452218312204370523713186983060138886330",
],
hub: "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74",
nameAndDobOfac: "12056959379782485690824392224737824782985009863971097094085968061978428696483",
nameAndYobOfac: "14482015433179009576094845155298164108788397224633034095648782513909282765564",
onlyTEEAddress: "0xe6b2856a51a17bd4edeb88b3f74370d64475b0fc",
gcpJWTVerifier: "0x13ee8CEa15a262D81a245b37889F7b4bEd015f4c",
pcr0Manager: "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2",
imageDigest: "0x67368d91dc708dee7be8fd9d85eff1fce3181e6e5b9fdfa37fc2d99034ea88e6",
gcpRootCAPubkeyHash: "14165687497759817957828709957846495993787741657460065475757428560999622217191",
},
};
@@ -40,6 +51,7 @@ function getImplementationName(registryModule: string): string {
"DeployRegistryModule#IdentityRegistry": "IdentityRegistryImplV1",
"DeployIdCardRegistryModule#IdentityRegistry": "IdentityRegistryIdCardImplV1",
"DeployAadhaarRegistryModule#IdentityRegistry": "IdentityRegistryAadhaarImplV1",
"DeployKycRegistryModule#IdentityRegistry": "IdentityRegistryKycImplV1",
};
return implMap[registryModule] || "IdentityRegistryImplV1";
@@ -70,51 +82,69 @@ export function handleRegistryDeployment(
let currentOperation: any = registryContract;
if (registryData.shouldChange) {
// Update hub for all registries
if (registryData.hub) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updateHub", [registryData.hub], callOptions);
}
if (!registryData.shouldChange) {
return { registryContract, lastOperation: currentOperation };
}
if (registryData.cscaRoot) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updateCscaRoot", [registryData.cscaRoot], callOptions);
}
// Update hub for all registries
if (registryData.hub) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updateHub", [registryData.hub], callOptions);
}
if (registryData.passportNoOfac) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(
registryContract,
"updatePassportNoOfacRoot",
[registryData.passportNoOfac],
callOptions,
);
}
if (registryData.nameAndDobOfac) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(
registryContract,
"updateNameAndDobOfacRoot",
[registryData.nameAndDobOfac],
callOptions,
);
}
if (registryData.nameAndYobOfac) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(
registryContract,
"updateNameAndYobOfacRoot",
[registryData.nameAndYobOfac],
callOptions,
);
}
if (registryData.cscaRoot) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updateCscaRoot", [registryData.cscaRoot], callOptions);
}
if (registryData.pubkeyCommitments && registryData.pubkeyCommitments.length > 0) {
for (const pubkeyCommitment of registryData.pubkeyCommitments) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "registerUidaiPubkeyCommitment", [pubkeyCommitment], callOptions);
}
if (registryData.passportNoOfac) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updatePassportNoOfacRoot", [registryData.passportNoOfac], callOptions);
}
if (registryData.nameAndDobOfac) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updateNameAndDobOfacRoot", [registryData.nameAndDobOfac], callOptions);
}
if (registryData.nameAndYobOfac) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updateNameAndYobOfacRoot", [registryData.nameAndYobOfac], callOptions);
}
if (registryData.gcpRootCAPubkeyHash) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(
registryContract,
"updateGCPRootCAPubkeyHash",
[registryData.gcpRootCAPubkeyHash],
callOptions,
);
}
if (registryData.pubkeyCommitments && registryData.pubkeyCommitments.length > 0) {
for (const pubkeyCommitment of registryData.pubkeyCommitments) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "registerUidaiPubkeyCommitment", [pubkeyCommitment], callOptions);
}
}
if (registryData.onlyTEEAddress) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updateTEE", [registryData.onlyTEEAddress], callOptions);
}
if (registryData.gcpJWTVerifier) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updateGCPJWTVerifier", [registryData.gcpJWTVerifier], callOptions);
}
if (registryData.pcr0Manager) {
const callOptions = { after: [currentOperation], id: ids() };
currentOperation = m.call(registryContract, "updatePCR0Manager", [registryData.pcr0Manager], callOptions);
if (registryData.imageDigest) {
const callOptions = { after: [currentOperation], id: ids() };
const pcr0Manager = m.contractAt("PCR0Manager", registryData.pcr0Manager);
currentOperation = m.call(pcr0Manager, "addPCR0", [registryData.imageDigest], callOptions);
}
}

View File

@@ -55,6 +55,7 @@ export type CircuitName =
| "register_id_sha512_sha512_sha512_rsa_65537_4096"
| "register_id_sha512_sha512_sha512_rsapss_65537_64_2048"
| "register_aadhaar"
| "register_kyc"
| "register_sha1_sha1_sha1_rsa_64321_4096"
| "register_sha256_sha1_sha1_rsa_65537_4096"
| "register_sha256_sha256_sha256_rsapss_65537_32_4096"
@@ -86,7 +87,8 @@ export type CircuitName =
| "dsc_sha256_rsa_56611_4096"
| "vc_and_disclose"
| "vc_and_disclose_id"
| "vc_and_disclose_aadhaar";
| "vc_and_disclose_aadhaar"
| "vc_and_disclose_kyc";
// Record mapping circuit names to numbers
export const circuitIds: Record<CircuitName, [boolean, number]> = {
@@ -148,6 +150,7 @@ export const circuitIds: Record<CircuitName, [boolean, number]> = {
register_sha256_sha256_sha256_rsapss_65537_32_4096: [true, 55],
register_id_sha512_sha512_sha256_rsapss_65537_32_2048: [true, 56],
register_sha512_sha512_sha256_rsapss_65537_32_2048: [true, 57],
register_kyc: [true, 58],
dsc_sha1_ecdsa_brainpoolP256r1: [true, 0],
dsc_sha1_rsa_65537_4096: [true, 1],
@@ -177,6 +180,7 @@ export const circuitIds: Record<CircuitName, [boolean, number]> = {
vc_and_disclose: [true, 24],
vc_and_disclose_id: [true, 25],
vc_and_disclose_aadhaar: [true, 26],
vc_and_disclose_kyc: [true, 27],
};
export default buildModule("DeployAllVerifiers", (m) => {

View File

@@ -128,6 +128,7 @@ describe("KYC Registration test", function () {
p0,
p1,
p2,
0n,
testImageHash.p0,
testImageHash.p1,
testImageHash.p2,
@@ -242,6 +243,7 @@ describe("KYC Registration test", function () {
1n,
2n,
3n,
0n,
4n,
5n,
6n,
@@ -273,6 +275,7 @@ describe("KYC Registration test", function () {
1n,
2n,
3n,
0n,
4n,
5n,
6n,
@@ -322,6 +325,7 @@ describe("KYC Registration test", function () {
1n,
2n,
3n,
0n,
4n,
5n,
6n,
@@ -356,6 +360,7 @@ describe("KYC Registration test", function () {
p0,
p1,
p2,
0n,
177384435506496807268973340845468654286294928521500580044819492874465981028n,
175298970718174405520284770870231222447414486446296682893283627688949855078n,
13360n,
@@ -379,6 +384,7 @@ describe("KYC Registration test", function () {
p0,
p1,
p2,
0n,
177384435506496807268973340845468654286294928521500580044819492874465981028n,
175298970718174405520284770870231222447414486446296682893283627688949855078n,
13360n,
@@ -417,6 +423,7 @@ describe("KYC Registration test", function () {
1n,
2n,
3n,
0n,
4n,
5n,
6n,
@@ -434,6 +441,7 @@ describe("KYC Registration test", function () {
1n,
2n,
3n,
0n,
4n,
5n,
6n,
@@ -451,6 +459,7 @@ describe("KYC Registration test", function () {
1n,
2n,
3n,
0n,
4n,
5n,
6n,

View File

@@ -8,9 +8,5 @@
* Set to true when ready to launch the feature.
*/
export const FeatureFlags = {
/**
* Enable Sumsub/KYC "Other IDs" option in the ID selection screen.
* When false, the KYC button will be hidden from users.
*/
KYC_ENABLED: false,
// Add new flags here as needed
} as const;

View File

@@ -0,0 +1,256 @@
{
"ABW": ["p", "i"],
"AFG": ["p"],
"AGO": ["p", "i"],
"AIA": ["p", "i"],
"ALA": ["p", "i"],
"ALB": ["p", "i"],
"AND": ["p", "i"],
"ARE": ["p", "i"],
"ARG": ["p", "i"],
"ARM": ["p", "i"],
"ASM": ["p", "i"],
"ATA": ["p", "i"],
"ATF": ["p", "i"],
"ATG": ["p", "i"],
"AUS": ["p", "i"],
"AUT": ["p", "i"],
"AZE": ["p", "i"],
"BDI": ["p", "i"],
"BEL": ["p", "i"],
"BEN": ["p", "i"],
"BES": ["p", "i"],
"BFA": ["p", "i"],
"BGD": ["p", "i"],
"BGR": ["p", "i"],
"BHR": ["p", "i"],
"BHS": ["p", "i"],
"BIH": ["p", "i"],
"BLM": ["p", "i"],
"BLR": ["p", "i"],
"BLZ": ["p", "i"],
"BMU": ["p", "i"],
"BOL": ["p", "i"],
"BRA": ["p", "i"],
"BRB": ["p", "i"],
"BRN": ["p", "i"],
"BTN": ["p", "i"],
"BVT": ["p", "i"],
"BWA": ["p", "i"],
"CAF": ["p", "i"],
"CAN": ["p", "i"],
"CCK": ["p", "i"],
"CHE": ["p", "i"],
"CHL": ["p", "i"],
"CHN": ["p", "i"],
"CIV": ["p", "i"],
"CMR": ["p", "i"],
"COD": ["p", "i"],
"COG": ["p", "i"],
"COK": ["p", "i"],
"COL": ["p", "i"],
"COM": ["p", "i"],
"CPV": ["p", "i"],
"CRI": ["p", "i"],
"CUB": ["p", "i"],
"CUW": ["p", "i"],
"CXR": ["p", "i"],
"CYM": ["p", "i"],
"CYP": ["p", "i"],
"CZE": ["p", "i"],
"D<<": ["p", "i"],
"DJI": ["p", "i"],
"DMA": ["p", "i"],
"DNK": ["p", "i"],
"DOM": ["p", "i"],
"DZA": ["p", "i"],
"ECU": ["p", "i"],
"EGY": [],
"ERI": ["p", "i"],
"ESH": ["p", "i"],
"ESP": ["p", "i"],
"EST": ["p", "i"],
"ETH": ["p", "i"],
"EUE": ["p", "i"],
"FIN": ["p", "i"],
"FJI": ["p", "i"],
"FLK": ["p", "i"],
"FRA": ["p", "i"],
"FRO": ["p", "i"],
"FSM": ["p", "i"],
"GAB": ["p", "i"],
"GBR": ["p", "i"],
"GEO": ["p", "i"],
"GGY": ["p", "i"],
"GHA": ["p", "i"],
"GIB": ["p", "i"],
"GIN": ["p", "i"],
"GLP": ["p", "i"],
"GMB": ["p", "i"],
"GNB": ["p", "i"],
"GNQ": ["p", "i"],
"GRC": ["p", "i"],
"GRD": ["p", "i"],
"GRL": ["p", "i"],
"GTM": ["p", "i"],
"GUF": ["p", "i"],
"GUM": ["p", "i"],
"GUY": ["p", "i"],
"HKG": ["p", "i"],
"HMD": ["p", "i"],
"HND": ["p", "i"],
"HRV": ["p", "i"],
"HTI": ["p", "i"],
"HUN": ["p", "i"],
"IDN": ["p", "i"],
"IMN": ["p", "i"],
"IND": ["p", "a"],
"IOT": ["p", "i"],
"IRL": ["p", "i"],
"IRN": ["p", "i"],
"IRQ": ["p", "i"],
"ISL": ["p", "i"],
"ISR": ["p", "i"],
"ITA": ["p", "i"],
"JAM": ["p", "i"],
"JEY": ["p", "i"],
"JOR": ["p", "i"],
"JPN": ["p", "i"],
"KAZ": ["p", "i"],
"KEN": ["p", "i"],
"KGZ": ["p", "i"],
"KHM": ["p", "i"],
"KIR": ["p", "i"],
"KNA": ["p", "i"],
"KOR": ["p", "i"],
"KWT": ["p", "i"],
"LAO": ["p", "i"],
"LBN": ["p", "i"],
"LBR": ["p", "i"],
"LBY": ["p", "i"],
"LCA": ["p", "i"],
"LIE": ["p", "i"],
"LKA": ["p", "i"],
"LSO": ["p", "i"],
"LTU": ["p", "i"],
"LUX": ["p", "i"],
"LVA": ["p", "i"],
"MAC": ["p", "i"],
"MAF": ["p", "i"],
"MAR": ["p", "i"],
"MCO": ["p", "i"],
"MDA": ["p", "i"],
"MDG": ["p", "i"],
"MDV": ["p", "i"],
"MEX": ["p", "i"],
"MHL": ["p", "i"],
"MKD": ["p", "i"],
"MLI": ["p", "i"],
"MLT": ["p", "i"],
"MMR": ["p", "i"],
"MNE": ["p", "i"],
"MNG": ["p", "i"],
"MNP": ["p", "i"],
"MOZ": ["p", "i"],
"MRT": ["p", "i"],
"MSR": ["p", "i"],
"MTQ": ["p", "i"],
"MUS": ["p", "i"],
"MWI": ["p", "i"],
"MYS": ["p", "i"],
"MYT": ["p", "i"],
"NAM": ["p", "i"],
"NCL": ["p", "i"],
"NER": ["p", "i"],
"NFK": ["p", "i"],
"NGA": ["p", "i"],
"NIC": ["p", "i"],
"NIU": ["p", "i"],
"NLD": ["p", "i"],
"NOR": ["p", "i"],
"NPL": ["p", "i"],
"NRU": ["p", "i"],
"NZL": ["p", "i"],
"OMN": ["p", "i"],
"PAK": ["p", "i"],
"PAN": ["p", "i"],
"PCN": ["p", "i"],
"PER": ["p", "i"],
"PHL": ["p", "i"],
"PLW": ["p", "i"],
"PNG": ["p", "i"],
"POL": ["p", "i"],
"PRI": ["p", "i"],
"PRK": ["p", "i"],
"PRT": ["p", "i"],
"PRY": ["p", "i"],
"PSE": ["p", "i"],
"PYF": ["p", "i"],
"QAT": ["p", "i"],
"REU": ["p", "i"],
"ROU": ["p", "i"],
"RUS": ["p", "i"],
"RWA": ["p", "i"],
"SAU": ["p", "i"],
"SDN": ["p", "i"],
"SEN": ["p", "i"],
"SGP": ["p", "i"],
"SGS": ["p", "i"],
"SHN": ["p", "i"],
"SJM": ["p", "i"],
"SLB": ["p", "i"],
"SLE": ["p", "i"],
"SLV": ["p", "i"],
"SMR": ["p", "i"],
"SOM": ["p", "i"],
"SPM": ["p", "i"],
"SRB": ["p", "i"],
"SSD": ["p", "i"],
"STP": ["p", "i"],
"SUR": ["p", "i"],
"SVK": ["p", "i"],
"SVN": ["p", "i"],
"SWE": ["p", "i"],
"SWZ": ["p", "i"],
"SXM": ["p", "i"],
"SYC": ["p", "i"],
"SYR": ["p", "i"],
"TCA": ["p", "i"],
"TCD": ["p", "i"],
"TGO": ["p", "i"],
"THA": ["p", "i"],
"TJK": ["p", "i"],
"TKL": ["p", "i"],
"TKM": ["p", "i"],
"TLS": ["p", "i"],
"TON": ["p", "i"],
"TTO": ["p", "i"],
"TUN": ["p", "i"],
"TUR": ["p", "i"],
"TUV": ["p", "i"],
"TWN": ["p", "i"],
"TZA": ["p", "i"],
"UGA": ["p", "i"],
"UKR": ["p", "i"],
"UMI": ["p", "i"],
"UNO": ["p", "i"],
"URY": ["p", "i"],
"USA": ["p", "i"],
"UZB": ["p", "i"],
"VAT": ["p", "i"],
"VCT": ["p", "i"],
"VEN": ["p", "i"],
"VGB": ["p", "i"],
"VIR": ["p", "i"],
"VNM": ["p", "i"],
"VUT": ["p", "i"],
"WLF": ["p", "i"],
"WSM": ["p", "i"],
"XCE": ["p", "i"],
"XOM": ["p", "i"],
"XPO": ["p", "i"],
"YEM": ["p", "i"],
"ZAF": ["p", "i"],
"ZMB": ["p", "i"],
"ZWE": ["p", "i"]
}

View File

@@ -2,12 +2,14 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { useEffect, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { getCountry } from 'react-native-localize';
import { commonNames } from '@selfxyz/common';
import { alpha2ToAlpha3 } from '@selfxyz/common/constants/countries';
import countryDocumentTypesData from '../data/country-document-types.json';
export interface CountryData {
[countryCode: string]: string[];
}
@@ -29,38 +31,11 @@ function getUserCountryCode(): string | null {
}
return null;
}
export function useCountries() {
const [countryData, setCountryData] = useState<CountryData>({});
const [loading, setLoading] = useState(true);
const countryData = countryDocumentTypesData as CountryData;
const userCountryCode = useMemo(getUserCountryCode, []);
useEffect(() => {
const controller = new AbortController();
const fetchCountryData = async () => {
try {
const response = await fetch('https://api.staging.self.xyz/id-picker', {
signal: controller.signal,
});
const result = await response.json();
if (result.status === 'success') {
setCountryData(result.data);
// if (__DEV__) {
// console.log('Set country data:', result.data);
// }
} else {
console.error('API returned non-success status:', result.status);
}
} catch (error) {
console.error('Error fetching country data:', error);
} finally {
setLoading(false);
}
};
fetchCountryData();
return () => controller.abort();
}, []);
const countryList = useMemo(() => {
const allCountries = Object.keys(countryData).map(countryCode => ({
key: countryCode,
@@ -77,5 +52,5 @@ export function useCountries() {
const showSuggestion = userCountryCode && countryData[userCountryCode];
return { countryData, countryList, loading, userCountryCode, showSuggestion };
return { countryData, countryList, loading: false, userCountryCode, showSuggestion };
}

View File

@@ -11,7 +11,6 @@ import PassportCameraScanIcon from '../../../svgs/icons/passport_camera_scan.svg
import PlusIcon from '../../../svgs/icons/plus.svg';
import SelfLogo from '../../../svgs/logo.svg';
import { BodyText, RoundFlag, View, XStack, YStack } from '../../components';
import { FeatureFlags } from '../../config/features';
import { black, blue100, blue600, slate100, slate300, slate400, white } from '../../constants/colors';
import { advercase, dinot } from '../../constants/fonts';
import { useSelfClient } from '../../context';
@@ -129,10 +128,11 @@ const DocumentItem: React.FC<DocumentItemProps> = ({ docType, onPress }) => {
type IDSelectionScreenProps = {
countryCode: string;
documentTypes: string[];
showKyc?: boolean;
};
const IDSelectionScreen: React.FC<IDSelectionScreenProps> = props => {
const { countryCode = '', documentTypes = [] } = props;
const { countryCode = '', documentTypes = [], showKyc = false } = props;
const selfClient = useSelfClient();
const onSelectDocumentType = (docType: string) => {
@@ -173,7 +173,7 @@ const IDSelectionScreen: React.FC<IDSelectionScreenProps> = props => {
<DocumentItem key={docType} docType={docType} onPress={() => onSelectDocumentType(docType)} />
))}
<BodyText style={styles.footerText}>Be sure your document is ready to scan</BodyText>
{FeatureFlags.KYC_ENABLED && (
{showKyc && (
<View style={styles.kycContainer}>
<DocumentItem docType="kyc" onPress={() => onSelectDocumentType('kyc')} />
</View>

View File

@@ -211,6 +211,7 @@ export interface ProvingState {
sharedKey: Buffer | null;
wsConnection: WebSocket | null;
wsHandlers: WsHandlers | null;
wsReconnectAttempts: number;
socketConnection: Socket | null;
uuid: string | null;
userConfirmed: boolean;
@@ -251,6 +252,7 @@ export interface ProvingState {
_handleWsOpen: (selfClient: SelfClient) => void;
_handleWsError: (error: Event, selfClient: SelfClient) => void;
_handleWsClose: (event: CloseEvent, selfClient: SelfClient) => void;
_reconnectTeeWebSocket: (selfClient: SelfClient) => Promise<boolean>;
_handlePassportNotSupported: (selfClient: SelfClient) => void;
_handleAccountRecoveryChoice: (selfClient: SelfClient) => void;
@@ -498,6 +500,7 @@ export const useProvingStore = create<ProvingState>((set, get) => {
sharedKey: null,
wsConnection: null,
wsHandlers: null,
wsReconnectAttempts: 0,
socketConnection: null,
uuid: null,
userConfirmed: false,
@@ -823,6 +826,8 @@ export const useProvingStore = create<ProvingState>((set, get) => {
reason: event.reason,
});
const currentState = get().currentState;
// Handle unexpected close during active proving states
if (
currentState === 'init_tee_connexion' ||
currentState === 'proving' ||
@@ -836,11 +841,105 @@ export const useProvingStore = create<ProvingState>((set, get) => {
selfClient,
);
}
// In ready_to_prove state, attempt automatic reconnection to handle network interruptions.
// Users may lose connectivity briefly; reconnecting transparently improves UX.
if (currentState === 'ready_to_prove') {
const MAX_RECONNECT_ATTEMPTS = 3;
const attempts = get().wsReconnectAttempts;
if (attempts < MAX_RECONNECT_ATTEMPTS) {
selfClient.logProofEvent('info', 'TEE WebSocket reconnection attempt', context, {
attempt: attempts + 1,
max_attempts: MAX_RECONNECT_ATTEMPTS,
});
set({ wsConnection: null, wsReconnectAttempts: attempts + 1 });
const backoffMs = Math.min(1000 * Math.pow(2, attempts), 10000);
setTimeout(() => {
if (get().currentState === 'ready_to_prove') {
get()._reconnectTeeWebSocket(selfClient);
}
}, backoffMs);
return;
}
selfClient.logProofEvent('error', 'TEE WebSocket reconnection exhausted', context, {
failure: 'PROOF_FAILED_CONNECTION',
attempts: MAX_RECONNECT_ATTEMPTS,
});
get()._handleWebSocketMessage(
new MessageEvent('error', {
data: JSON.stringify({ error: 'WebSocket reconnection failed' }),
}),
selfClient,
);
}
if (get().wsConnection) {
set({ wsConnection: null });
}
},
/**
* Re-establishes the TEE WebSocket connection using stored circuit parameters.
* Called automatically when connection is lost in ready_to_prove state.
*/
_reconnectTeeWebSocket: async (selfClient: SelfClient): Promise<boolean> => {
const context = createProofContext(selfClient, '_reconnectTeeWebSocket');
const { passportData, circuitType } = get();
if (!passportData || !circuitType) {
selfClient.logProofEvent('error', 'Reconnect failed: missing prerequisites', context);
return false;
}
const typedCircuitType = circuitType as 'disclose' | 'register' | 'dsc';
const circuitName =
typedCircuitType === 'disclose'
? passportData.documentCategory === 'aadhaar'
? 'disclose_aadhaar'
: 'disclose'
: getCircuitNameFromPassportData(passportData, typedCircuitType as 'register' | 'dsc');
const wsRpcUrl = resolveWebSocketUrl(selfClient, typedCircuitType, passportData as PassportData, circuitName);
if (!wsRpcUrl) {
selfClient.logProofEvent('error', 'Reconnect failed: no WebSocket URL', context);
return false;
}
selfClient.logProofEvent('info', 'TEE WebSocket reconnection started', context);
return new Promise(resolve => {
const ws = new WebSocket(wsRpcUrl);
const RECONNECT_TIMEOUT_MS = 15000;
const wsHandlers: WsHandlers = {
message: (event: MessageEvent) => get()._handleWebSocketMessage(event, selfClient),
open: () => {
selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context);
set({ wsReconnectAttempts: 0 });
resolve(true);
},
error: (error: Event) => get()._handleWsError(error, selfClient),
close: (event: CloseEvent) => get()._handleWsClose(event, selfClient),
};
set({ wsConnection: ws, wsHandlers });
ws.addEventListener('message', wsHandlers.message);
ws.addEventListener('open', wsHandlers.open);
ws.addEventListener('error', wsHandlers.error);
ws.addEventListener('close', wsHandlers.close);
setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context);
resolve(false);
}
}, RECONNECT_TIMEOUT_MS);
});
},
init: async (
selfClient: SelfClient,
circuitType: 'dsc' | 'disclose' | 'register',
@@ -1293,7 +1392,7 @@ export const useProvingStore = create<ProvingState>((set, get) => {
close: (event: CloseEvent) => get()._handleWsClose(event, selfClient),
};
set({ wsConnection: ws, wsHandlers });
set({ wsConnection: ws, wsHandlers, wsReconnectAttempts: 0 });
ws.addEventListener('message', wsHandlers.message);
ws.addEventListener('open', wsHandlers.open);
@@ -1318,7 +1417,8 @@ export const useProvingStore = create<ProvingState>((set, get) => {
startProving: async (selfClient: SelfClient) => {
_checkActorInitialized(actor);
const startTime = Date.now();
const { wsConnection, sharedKey, passportData, secret, uuid } = get();
let { wsConnection } = get();
const { sharedKey, passportData, secret, uuid } = get();
const context = createProofContext(selfClient, 'startProving', {
sessionId: uuid || get().uuid || 'unknown-session',
});
@@ -1330,17 +1430,45 @@ export const useProvingStore = create<ProvingState>((set, get) => {
console.error('Cannot start proving: Not in ready_to_prove state.');
return;
}
if (!wsConnection || !sharedKey || !passportData || !secret || !uuid) {
// Check non-connection prerequisites first
if (!sharedKey || !passportData || !secret || !uuid) {
selfClient.logProofEvent('error', 'Missing proving prerequisites', context, {
failure: 'PROOF_FAILED_CONNECTION',
});
console.error('Cannot start proving: Missing wsConnection, sharedKey, passportData, secret, or uuid.');
console.error('Cannot start proving: Missing sharedKey, passportData, secret, or uuid.');
actor!.send({ type: 'PROVE_ERROR' });
return;
}
// Attempt reconnection if WebSocket is missing or not open
if (!wsConnection || wsConnection.readyState !== WebSocket.OPEN) {
selfClient.logProofEvent('warn', 'WebSocket not ready, attempting reconnection', context, {
wsConnectionExists: !!wsConnection,
readyState: wsConnection?.readyState,
});
const reconnected = await get()._reconnectTeeWebSocket(selfClient);
if (!reconnected) {
selfClient.logProofEvent('error', 'WebSocket reconnection failed', context, {
failure: 'PROOF_FAILED_CONNECTION',
});
actor!.send({ type: 'PROVE_ERROR' });
return;
}
// Get the new connection after reconnection
wsConnection = get().wsConnection;
if (!wsConnection || wsConnection.readyState !== WebSocket.OPEN) {
selfClient.logProofEvent('error', 'Reconnected WebSocket not ready', context, {
failure: 'PROOF_FAILED_CONNECTION',
});
actor!.send({ type: 'PROVE_ERROR' });
return;
}
}
try {
// Emit event for FCM token registration
selfClient.emit(SdkEvents.PROVING_BEGIN_GENERATION, {
uuid,
isMock: passportData?.mock ?? false,
@@ -1350,7 +1478,12 @@ export const useProvingStore = create<ProvingState>((set, get) => {
selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_STARTED);
selfClient.logProofEvent('info', 'Payload generation started', context);
const submitBody = await get()._generatePayload(selfClient);
wsConnection.send(JSON.stringify(submitBody));
const activeWsConnection = get().wsConnection;
if (!activeWsConnection) {
throw new Error('WebSocket connection lost during payload generation');
}
activeWsConnection.send(JSON.stringify(submitBody));
selfClient.logProofEvent('info', 'Payload sent over WebSocket', context);
selfClient.trackEvent(ProofEvents.PAYLOAD_SENT);
selfClient.trackEvent(ProofEvents.PROVING_PROCESS_STARTED);

View File

@@ -13,8 +13,6 @@ import { WS_DB_RELAYER } from '@selfxyz/common';
* Zustand state backing the in-app handoff between the SDK and the hosted Self
* application. The store tracks the active websocket session, latest
* {@link SelfApp} payload, and helper callbacks used by the proving machine.
* Consumers should treat the state as ephemeral and expect it to reset whenever
* the socket disconnects.
*/
export interface SelfAppState {
selfApp: SelfApp | null;
@@ -80,24 +78,17 @@ export const useSelfAppStore = create<SelfAppState>((set, get) => ({
socket.on('connect', () => {});
// Listen for the event only once per connection attempt
socket.once('self_app', (data: unknown) => {
try {
const appData: SelfApp = typeof data === 'string' ? JSON.parse(data) : (data as SelfApp);
// Basic validation
if (!appData || typeof appData !== 'object' || !appData.sessionId) {
console.error('[SelfAppStore] Invalid app data received:', appData);
// Optionally clear the app data or handle the error appropriately
console.error('[SelfAppStore] Invalid app data received');
set({ selfApp: null });
return;
}
if (appData.sessionId !== get().sessionId) {
console.warn(
`[SelfAppStore] Received SelfApp for session ${
appData.sessionId
}, but current session is ${get().sessionId}. Ignoring.`,
);
console.warn('[SelfAppStore] Session mismatch, ignoring payload');
return;
}
@@ -109,20 +100,22 @@ export const useSelfAppStore = create<SelfAppState>((set, get) => ({
});
socket.on('connect_error', error => {
console.error('[SelfAppStore] Mobile WS connection error:', error);
// Clean up on connection error
get().cleanSelfApp();
// Socket.io handles reconnection automatically with exponential backoff.
// State is preserved to allow seamless recovery when network returns.
console.error('[SelfAppStore] Connection error:', error.message);
});
socket.on('error', error => {
console.error('[SelfAppStore] Mobile WS error:', error);
// Consider if cleanup is needed here as well
console.error('[SelfAppStore] Socket error:', error);
});
socket.on('disconnect', (_reason: string) => {
// Prevent cleaning up if disconnect was initiated by cleanSelfApp
if (get().socket === socket) {
set({ socket: null, sessionId: null, selfApp: null });
socket.on('disconnect', (reason: string) => {
if (get().socket !== socket) return;
// Only clear state on intentional disconnects. For transient network issues
// (transport close, ping timeout), socket.io reconnects automatically.
if (reason === 'io server disconnect' || reason === 'io client disconnect') {
set({ socket: null, sessionId: null });
}
});
} catch (error) {

View File

@@ -0,0 +1,99 @@
// 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.
/**
* Integration test for country data synchronization.
*
* This test verifies that the bundled country-document-types.json matches
* the staging API response. It gracefully skips when network is unavailable
* to avoid CI flakiness from transient network issues.
*
* To run integration tests only: yarn test --grep="integration"
* To skip integration tests: yarn test --grep="^(?!.*integration)"
*/
import { describe, expect, it } from 'vitest';
import countryDocumentTypesData from '../../src/data/country-document-types.json';
/**
* Helper to check if an error is a network-related error that should cause
* the test to skip rather than fail.
*/
function isNetworkError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const networkErrorPatterns = [
'ENOTFOUND', // DNS resolution failed
'ECONNREFUSED', // Connection refused
'ECONNRESET', // Connection reset
'ETIMEDOUT', // Connection timed out
'EAI_AGAIN', // DNS temporary failure
'ENETUNREACH', // Network unreachable
'EHOSTUNREACH', // Host unreachable
'fetch failed', // Generic fetch failure
'network', // Generic network error
'AbortError', // Request aborted (timeout)
];
const errorMessage = error.message.toLowerCase();
const errorName = error.name;
return networkErrorPatterns.some(
pattern =>
errorMessage.includes(pattern.toLowerCase()) ||
errorName === pattern ||
('cause' in error &&
error.cause instanceof Error &&
error.cause.message.toLowerCase().includes(pattern.toLowerCase())),
);
}
describe('Country data synchronization [integration]', () => {
it('bundled data should match API response', async ({ skip }) => {
// Fetch current data from staging API with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
let response: Response;
try {
response = await fetch('https://api.staging.self.xyz/id-picker', {
signal: controller.signal,
});
} catch (error) {
// Network errors should skip the test, not fail it
if (isNetworkError(error)) {
skip();
return;
}
throw error;
} finally {
clearTimeout(timeoutId);
}
// Non-2xx responses that aren't network errors should also skip
// (e.g., 503 Service Unavailable, 502 Bad Gateway)
if (!response.ok) {
if (response.status >= 500) {
skip();
return;
}
// 4xx errors are likely real issues, so we let them fail
expect.fail(`API returned ${response.status}: ${response.statusText}`);
}
const result = await response.json();
expect(result.status).toBe('success');
const apiData = result.data;
const bundledData = countryDocumentTypesData;
// Compare the data structures
expect(bundledData).toEqual(apiData);
// If this test fails, it means the API has been updated with new countries
// or document types that aren't in the bundled data yet.
// To fix: Update src/data/country-document-types.json with the latest API data.
}, 10000); // 10s Vitest timeout
});

View File

@@ -1,7 +1,7 @@
diff --git a/node_modules/react-native-gesture-handler/android/build.gradle b/node_modules/react-native-gesture-handler/android/build.gradle
--- a/node_modules/react-native-gesture-handler/android/build.gradle
+++ b/node_modules/react-native-gesture-handler/android/build.gradle
@@ -229,9 +229,10 @@
@@ -178,9 +178,10 @@
}
def kotlin_version = safeExtGet('kotlinVersion', project.properties['RNGH_kotlinVersion'])
@@ -10,6 +10,6 @@ diff --git a/node_modules/react-native-gesture-handler/android/build.gradle b/no
dependencies {
- implementation 'com.facebook.react:react-native:+' // from node_modules
+ implementation reactNativeDependency
if (shouldUseCommonInterfaceFromReanimated()) {