mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
Merge pull request #1676 from selfxyz/release/staging-2026-01-30
Release to Staging - 2026-01-30
This commit is contained in:
63
.github/workflows/circuits-build.yml
vendored
63
.github/workflows/circuits-build.yml
vendored
@@ -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:
|
||||
|
||||
31
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
31
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
@@ -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..."
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}</>;
|
||||
};
|
||||
|
||||
@@ -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
208
app/src/screens/dev/components/ErrorInjectionSelector.tsx
Normal file
208
app/src/screens/dev/components/ErrorInjectionSelector.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
175
app/src/screens/dev/components/LogLevelSelector.tsx
Normal file
175
app/src/screens/dev/components/LogLevelSelector.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
94
app/src/screens/dev/components/ParameterSection.tsx
Normal file
94
app/src/screens/dev/components/ParameterSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
122
app/src/screens/dev/components/ScreenSelector.tsx
Normal file
122
app/src/screens/dev/components/ScreenSelector.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
58
app/src/screens/dev/components/TopicToggleButton.tsx
Normal file
58
app/src/screens/dev/components/TopicToggleButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
10
app/src/screens/dev/components/index.ts
Normal file
10
app/src/screens/dev/components/index.ts
Normal 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';
|
||||
197
app/src/screens/dev/hooks/useDangerZoneActions.ts
Normal file
197
app/src/screens/dev/hooks/useDangerZoneActions.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
156
app/src/screens/dev/hooks/useNotificationHandlers.ts
Normal file
156
app/src/screens/dev/hooks/useNotificationHandlers.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
90
app/src/screens/dev/sections/DangerZoneSection.tsx
Normal file
90
app/src/screens/dev/sections/DangerZoneSection.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
108
app/src/screens/dev/sections/DebugShortcutsSection.tsx
Normal file
108
app/src/screens/dev/sections/DebugShortcutsSection.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
61
app/src/screens/dev/sections/DevTogglesSection.tsx
Normal file
61
app/src/screens/dev/sections/DevTogglesSection.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
54
app/src/screens/dev/sections/PushNotificationsSection.tsx
Normal file
54
app/src/screens/dev/sections/PushNotificationsSection.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
8
app/src/screens/dev/sections/index.ts
Normal file
8
app/src/screens/dev/sections/index.ts
Normal 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';
|
||||
@@ -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>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
83
app/src/screens/kyc/KYCVerifiedScreen.tsx
Normal file
83
app/src/screens/kyc/KYCVerifiedScreen.tsx
Normal 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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -85,6 +85,7 @@ describe('navigation', () => {
|
||||
'Home',
|
||||
'IDPicker',
|
||||
'IdDetails',
|
||||
'KYCVerified',
|
||||
'KycSuccess',
|
||||
'Loading',
|
||||
'ManageDocuments',
|
||||
|
||||
511
app/tests/src/providers/notificationTrackingProvider.test.tsx
Normal file
511
app/tests/src/providers/notificationTrackingProvider.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
156
app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx
Normal file
156
app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
300
contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol
Normal file
300
contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 = (() => {
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
|
||||
52
contracts/ignition/modules/registry/deployKycRegistry.ts
Normal file
52
contracts/ignition/modules/registry/deployKycRegistry.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
256
packages/mobile-sdk-alpha/src/data/country-document-types.json
Normal file
256
packages/mobile-sdk-alpha/src/data/country-document-types.json
Normal 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"]
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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()) {
|
||||
Reference in New Issue
Block a user