chore: tighten types across mobile surface areas (#1209)

* chore(app): tighten types across app workspace

* fixes

* save wip

* save wip linting

* fix any types

* fix types

* fix: import forwardRef directly from react

- Changed from React.forwardRef to forwardRef import
- Resolves linting warning about named exports

* save typing and linting updates

* cr feedback. final pass

* more cr feedback

* pipeline fixes
This commit is contained in:
Justin Hernandez
2025-10-09 09:53:29 -07:00
committed by GitHub
parent 04562d185f
commit 8255a9ac56
92 changed files with 803 additions and 395 deletions

View File

@@ -11,20 +11,20 @@ on:
workflow_dispatch:
inputs:
circuit-type:
description: 'Circuits to build (comma-separated: register, register_id, register_aadhaar, disclose, dsc). Leave empty to build all.'
description: "Circuits to build (comma-separated: register, register_id, register_aadhaar, disclose, dsc). Leave empty to build all."
required: false
type: string
default: ''
default: ""
circuit-name:
description: 'Circuit names to build (comma-separated: register_sha256_sha224_sha224_ecdsa_secp224r1, dsc_sha256_rsa_65537_4096). Cannot be used with circuit-type.'
description: "Circuit names to build (comma-separated: register_sha256_sha224_sha224_ecdsa_secp224r1, dsc_sha256_rsa_65537_4096). Cannot be used with circuit-type."
required: false
type: string
default: ''
default: ""
run-id:
description: 'Run ID to download artifacts .'
description: "Run ID to download artifacts ."
required: false
type: string
default: ''
default: ""
concurrency:
group: circuits-build-${{ github.workflow }}-${{ github.ref }}

View File

@@ -173,6 +173,16 @@ module.exports = {
'@typescript-eslint/ban-ts-comment': 'off',
'no-empty': 'off',
// TypeScript Import Rules
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: false,
},
],
// Override rules conflicting with TypeScript union formatting
'@typescript-eslint/indent': 'off',

View File

@@ -170,8 +170,9 @@
"@testing-library/react-native": "^13.3.3",
"@tsconfig/react-native": "^3.0.6",
"@types/add": "^2",
"@types/bn.js": "^5.2.0",
"@types/dompurify": "^3.2.0",
"@types/elliptic": "^6",
"@types/elliptic": "^6.4.18",
"@types/jest": "^29.5.14",
"@types/node": "^22.18.3",
"@types/node-forge": "^1.3.14",

View File

@@ -20,70 +20,124 @@ function transformProjectToAliasImports(project, appRootPath) {
// Handle import declarations
for (const declaration of sourceFile.getImportDeclarations()) {
const spec = declaration.getModuleSpecifierValue();
// Skip existing alias imports
if (spec.startsWith('@/') || spec.startsWith('@tests/')) {
continue;
}
// Handle relative imports
if (!spec.startsWith('./') && !spec.startsWith('../')) continue;
const abs = path.resolve(dir, spec);
let baseDir = null;
let baseAlias = null;
// Determine containment safely using path.relative to avoid startsWith false positives
const relFromSrc = path.relative(srcDir, abs);
if (!relFromSrc.startsWith('..') && !path.isAbsolute(relFromSrc)) {
baseDir = srcDir;
baseAlias = '@';
} else {
const relFromTests = path.relative(testsDir, abs);
if (!relFromTests.startsWith('..') && !path.isAbsolute(relFromTests)) {
baseDir = testsDir;
baseAlias = '@tests';
} else {
try {
// Skip if no module specifier or not a string literal
const moduleSpecifier = declaration.getModuleSpecifier();
if (
!moduleSpecifier ||
moduleSpecifier.getKind() !== SyntaxKind.StringLiteral
) {
continue;
}
}
const newSpec = determineAliasStrategy(dir, abs, baseDir, baseAlias);
declaration.setModuleSpecifier(newSpec);
const spec = declaration.getModuleSpecifierValue();
// Skip existing alias imports
if (spec.startsWith('@/') || spec.startsWith('@tests/')) {
continue;
}
// Handle relative imports
if (!spec.startsWith('./') && !spec.startsWith('../')) continue;
const abs = path.resolve(dir, spec);
let baseDir = null;
let baseAlias = null;
// Determine containment safely using path.relative to avoid startsWith false positives
const relFromSrc = path.relative(srcDir, abs);
if (!relFromSrc.startsWith('..') && !path.isAbsolute(relFromSrc)) {
baseDir = srcDir;
baseAlias = '@';
} else {
const relFromTests = path.relative(testsDir, abs);
if (
!relFromTests.startsWith('..') &&
!path.isAbsolute(relFromTests)
) {
baseDir = testsDir;
baseAlias = '@tests';
} else {
continue;
}
}
const newSpec = determineAliasStrategy(dir, abs, baseDir, baseAlias);
declaration.setModuleSpecifier(newSpec);
} catch (error) {
// Skip declarations that can't be processed (e.g., type-only imports with issues)
const msg = error instanceof Error ? error.message : String(error);
console.warn(
`Skipping import declaration in ${sourceFile.getFilePath()}: ${msg}`,
);
try {
console.debug('Import declaration text:', declaration.getText());
} catch {}
if (error && typeof error === 'object' && 'stack' in error) {
console.debug('Error stack:', error.stack);
}
continue;
}
}
// Handle export declarations like: export * from '../x' or export {A} from '../x'
for (const declaration of sourceFile.getExportDeclarations()) {
const spec = declaration.getModuleSpecifierValue();
if (!spec) continue;
// Skip existing alias exports
if (spec.startsWith('@/') || spec.startsWith('@tests/')) {
continue;
}
// Handle relative exports
if (!spec.startsWith('./') && !spec.startsWith('../')) continue;
const abs = path.resolve(dir, spec);
let baseDir = null;
let baseAlias = null;
const relFromSrc = path.relative(srcDir, abs);
if (!relFromSrc.startsWith('..') && !path.isAbsolute(relFromSrc)) {
baseDir = srcDir;
baseAlias = '@';
} else {
const relFromTests = path.relative(testsDir, abs);
if (!relFromTests.startsWith('..') && !path.isAbsolute(relFromTests)) {
baseDir = testsDir;
baseAlias = '@tests';
} else {
try {
// Skip if no module specifier or not a string literal
const moduleSpecifier = declaration.getModuleSpecifier();
if (
!moduleSpecifier ||
moduleSpecifier.getKind() !== SyntaxKind.StringLiteral
) {
continue;
}
}
const newSpec = determineAliasStrategy(dir, abs, baseDir, baseAlias);
declaration.setModuleSpecifier(newSpec);
const spec = declaration.getModuleSpecifierValue();
if (!spec) continue;
// Skip existing alias exports
if (spec.startsWith('@/') || spec.startsWith('@tests/')) {
continue;
}
// Handle relative exports
if (!spec.startsWith('./') && !spec.startsWith('../')) continue;
const abs = path.resolve(dir, spec);
let baseDir = null;
let baseAlias = null;
const relFromSrc = path.relative(srcDir, abs);
if (!relFromSrc.startsWith('..') && !path.isAbsolute(relFromSrc)) {
baseDir = srcDir;
baseAlias = '@';
} else {
const relFromTests = path.relative(testsDir, abs);
if (
!relFromTests.startsWith('..') &&
!path.isAbsolute(relFromTests)
) {
baseDir = testsDir;
baseAlias = '@tests';
} else {
continue;
}
}
const newSpec = determineAliasStrategy(dir, abs, baseDir, baseAlias);
declaration.setModuleSpecifier(newSpec);
} catch (error) {
// Skip declarations that can't be processed
const msg = error instanceof Error ? error.message : String(error);
console.warn(
`Skipping export declaration in ${sourceFile.getFilePath()}: ${msg}`,
);
try {
console.debug('Export declaration text:', declaration.getText());
} catch {}
if (error && typeof error === 'object' && 'stack' in error) {
console.debug('Error stack:', error.stack);
}
continue;
}
}
// Handle require() calls

View File

@@ -4,7 +4,7 @@
import type { LottieViewProps } from 'lottie-react-native';
import LottieView from 'lottie-react-native';
import React, { useEffect, useRef } from 'react';
import React, { forwardRef, useEffect, useRef } from 'react';
/**
* Wrapper around LottieView that fixes iOS native module initialization timing.
@@ -20,7 +20,7 @@ import React, { useEffect, useRef } from 'react';
* @example
* <DelayedLottieView autoPlay loop source={animation} style={styles.animation} />
*/
export const DelayedLottieView = React.forwardRef<LottieView, LottieViewProps>(
export const DelayedLottieView = forwardRef<LottieView, LottieViewProps>(
(props, forwardedRef) => {
const internalRef = useRef<LottieView>(null);
const ref = (forwardedRef as React.RefObject<LottieView>) || internalRef;

View File

@@ -3,7 +3,11 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import type { GestureResponderEvent, ViewStyle } from 'react-native';
import type {
GestureResponderEvent,
LayoutChangeEvent,
ViewStyle,
} from 'react-native';
import { Platform, StyleSheet } from 'react-native';
import type { ViewProps } from 'tamagui';
import { Button, Text } from 'tamagui';
@@ -16,6 +20,7 @@ export interface ButtonProps extends ViewProps {
children: React.ReactNode;
animatedComponent?: React.ReactNode;
trackEvent?: string;
onLayout?: (event: LayoutChangeEvent) => void;
}
interface AbstractButtonProps extends ButtonProps {

View File

@@ -93,7 +93,6 @@ export function HeldPrimaryButton({
{...props}
onPressIn={onPressIn}
onPressOut={onPressOut}
// @ts-expect-error actually it is there
onLayout={getButtonSize}
animatedComponent={renderAnimatedComponent()}
>

View File

@@ -83,7 +83,6 @@ export function HeldPrimaryButton({
{...props}
onPressIn={onPressIn}
onPressOut={onPressOut}
// @ts-expect-error actually it is there
onLayout={getButtonSize}
animatedComponent={renderAnimatedComponent()}
>

View File

@@ -2,17 +2,12 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { forwardRef } from 'react';
import type { StyleProp, ViewStyle } from 'react-native';
import type { ComponentProps } from 'react';
import React from 'react';
import { SvgXml as RNSvgXml } from 'react-native-svg';
type Props = {
xml: string;
width?: number;
height?: number;
style?: StyleProp<ViewStyle>;
};
type Props = ComponentProps<typeof RNSvgXml>;
export const SvgXml = forwardRef<any, Props>((p, _ref) => <RNSvgXml {...p} />);
export const SvgXml: React.FC<Props> = props => <RNSvgXml {...props} />;
SvgXml.displayName = 'SvgXml';
export default SvgXml;

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import DOMPurify from 'dompurify';
import createDOMPurify from 'dompurify';
import {
createElement,
type CSSProperties,
@@ -20,7 +20,7 @@ type Props = {
export const SvgXml = forwardRef<HTMLDivElement, Props>(
({ xml, width, height, style, ...props }, ref) => {
// Initialize DOMPurify for web browser environment
const purify = DOMPurify(window);
const purify = createDOMPurify(window);
const safe = purify.sanitize(xml, {
USE_PROFILES: { svg: true, svgFilters: true },
});

View File

@@ -2,7 +2,8 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { type FC } from 'react';
import type { FC } from 'react';
import React from 'react';
import { Dimensions } from 'react-native';
import { Separator, Text, XStack, YStack } from 'tamagui';

View File

@@ -6,7 +6,8 @@ import React, { useCallback } from 'react';
import type { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native';
import { PixelRatio, Platform, requireNativeComponent } from 'react-native';
import { type SelfClient, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import type { SelfClient } from '@selfxyz/mobile-sdk-alpha';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { RCTFragment } from '@/components/native/RCTFragment';
@@ -69,9 +70,13 @@ export const PassportCamera: React.FC<PassportCameraProps> = ({
if (!isMounted) {
return;
}
/* eslint-disable @typescript-eslint/no-unused-vars */
const { error, errorMessage, stackTrace } = event.nativeEvent;
const {
error: nativeError,
errorMessage,
stackTrace,
} = event.nativeEvent;
const e = new Error(errorMessage);
e.name = nativeError;
e.stack = stackTrace;
onPassportRead(e);
},

View File

@@ -4,7 +4,8 @@
import React, { useCallback, useEffect } from 'react';
import { type SelfClient, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import type { SelfClient } from '@selfxyz/mobile-sdk-alpha';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
// TODO: Web find a lightweight ocr or mrz scanner.

View File

@@ -53,9 +53,13 @@ export const QRCodeScannerView: React.FC<QRCodeScannerViewProps> = ({
if (!isMounted) {
return;
}
/* eslint-disable @typescript-eslint/no-unused-vars */
const { error, errorMessage, stackTrace } = event.nativeEvent;
const {
error: nativeError,
errorMessage,
stackTrace,
} = event.nativeEvent;
const e = new Error(errorMessage);
e.name = nativeError;
e.stack = stackTrace;
onQRData(e);
},

View File

@@ -6,14 +6,17 @@ import { useEffect, useState } from 'react';
import { Linking } from 'react-native';
import { checkVersion } from 'react-native-check-version';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import type { RootStackParamList } from '@/navigation';
import { registerModalCallbacks } from '@/utils/modalCallbackRegistry';
export const useAppUpdates = (): [boolean, () => void, boolean] => {
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const [newVersionUrl, setNewVersionUrl] = useState<string | null>(null);
const [isModalDismissed, setIsModalDismissed] = useState(false);
const selfClient = useSelfClient();

View File

@@ -4,14 +4,17 @@
import { useState } from 'react';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import type { RootStackParamList } from '@/navigation';
import { registerModalCallbacks } from '@/utils/modalCallbackRegistry';
export const useAppUpdates = (): [boolean, () => void, boolean] => {
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const [isModalDismissed, setIsModalDismissed] = useState(false);
const { trackEvent } = useSelfClient();
const showAppUpdateModal = () => {

View File

@@ -4,7 +4,9 @@
import { useCallback, useRef, useState } from 'react';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RootStackParamList } from '@/navigation';
import type { ModalParams } from '@/screens/app/ModalScreen';
import {
getModalCallbacks,
@@ -14,7 +16,8 @@ import {
export const useModal = (params: ModalParams) => {
const [visible, setVisible] = useState(false);
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const callbackIdRef = useRef<number>();
const showModal = useCallback(() => {

View File

@@ -2,7 +2,8 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { createElement, forwardRef } from 'react';
import type React from 'react';
import { createElement, forwardRef } from 'react';
type BlurViewProps = React.HTMLAttributes<HTMLDivElement> & {
blurType?: string;

View File

@@ -2,7 +2,8 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { createElement, forwardRef } from 'react';
import type React from 'react';
import { createElement, forwardRef } from 'react';
export const Circle = forwardRef<
SVGCircleElement,

View File

@@ -11,6 +11,7 @@ import type { DocumentCategory } from '@selfxyz/common/utils/types';
import DeferredLinkingInfoScreen from '@/screens/app/DeferredLinkingInfoScreen';
import LaunchScreen from '@/screens/app/LaunchScreen';
import LoadingScreen from '@/screens/app/LoadingScreen';
import type { ModalNavigationParams } from '@/screens/app/ModalScreen';
import ModalScreen from '@/screens/app/ModalScreen';
import SplashScreen from '@/screens/app/SplashScreen';
@@ -40,6 +41,7 @@ const appScreens = {
animation: 'fade',
contentStyle: { backgroundColor: 'transparent' },
} as NativeStackNavigationOptions,
params: {} as ModalNavigationParams,
},
DeferredLinkingInfo: {
screen: DeferredLinkingInfoScreen,

View File

@@ -10,6 +10,7 @@ import {
createNavigationContainerRef,
createStaticNavigation,
} from '@react-navigation/native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
@@ -47,13 +48,39 @@ const AppNavigation = createNativeStackNavigator({
screens: navigationScreens,
});
export type RootStackParamList = StaticParamList<typeof AppNavigation>;
type BaseRootStackParamList = StaticParamList<typeof AppNavigation>;
// Explicitly declare route params that are not inferred from initialParams
export type RootStackParamList = Omit<
BaseRootStackParamList,
'ComingSoon' | 'IDPicker' | 'AadhaarUpload' | 'AadhaarUploadError'
> & {
ComingSoon: {
countryCode?: string;
documentCategory?: string;
};
IDPicker: {
countryCode: string;
documentTypes: string[];
};
AadhaarUpload: {
countryCode: string;
};
AadhaarUploadError: {
errorType: string;
};
};
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
// Create a ref that we can use to access the navigation state
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
declare global {
namespace ReactNavigation {
// Allow React Navigation helpers to infer route params from our stack
// Use interface merging to avoid duplicate identifier errors
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface RootParamList extends RootStackParamList {}
}

View File

@@ -12,17 +12,18 @@ import React, {
useState,
} from 'react';
import ReactNativeBiometrics from 'react-native-biometrics';
import Keychain, { GetOptions, SetOptions } from 'react-native-keychain';
import type { GetOptions, SetOptions } from 'react-native-keychain';
import Keychain from 'react-native-keychain';
import { AuthEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { useSettingStore } from '@/stores/settingStore';
import type { Mnemonic } from '@/types/mnemonic';
import analytics from '@/utils/analytics';
import type { GetSecureOptions } from '@/utils/keychainSecurity';
import {
createKeychainOptions,
detectSecurityCapabilities,
GetSecureOptions,
} from '@/utils/keychainSecurity';
const { trackEvent } = analytics();

View File

@@ -2,20 +2,24 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { type PropsWithChildren, useMemo } from 'react';
import type { PropsWithChildren } from 'react';
import { useMemo } from 'react';
import { Platform } from 'react-native';
import {
import type {
Adapters,
TrackEventParams,
WsConn,
} from '@selfxyz/mobile-sdk-alpha';
import {
createListenersMap,
reactNativeScannerAdapter,
SdkEvents,
SelfClientProvider as SDKSelfClientProvider,
type TrackEventParams,
webNFCScannerShim,
type WsConn,
} from '@selfxyz/mobile-sdk-alpha';
import type { RootStackParamList } from '@/navigation';
import { navigationRef } from '@/navigation';
import { unsafe_getPrivateKey } from '@/providers/authProvider';
import { selfClientDocumentsAdapter } from '@/providers/passportDataProvider';
@@ -24,7 +28,6 @@ import { useSettingStore } from '@/stores/settingStore';
import analytics from '@/utils/analytics';
type GlobalCrypto = { crypto?: { subtle?: Crypto['subtle'] } };
/**
* Provides a configured Self SDK client instance to all descendants.
*
@@ -33,6 +36,25 @@ type GlobalCrypto = { crypto?: { subtle?: Crypto['subtle'] } };
* - `fetch`/`WebSocket` for network communication
* - Web Crypto hashing with a stub signer
*/
function navigateIfReady<RouteName extends keyof RootStackParamList>(
route: RouteName,
...args: undefined extends RootStackParamList[RouteName]
? [params?: RootStackParamList[RouteName]]
: [params: RootStackParamList[RouteName]]
): void {
if (navigationRef.isReady()) {
const params = args[0];
if (params !== undefined) {
(navigationRef.navigate as (r: RouteName, p: typeof params) => void)(
route,
params,
);
} else {
navigationRef.navigate(route as never);
}
}
}
export const SelfClientProvider = ({ children }: PropsWithChildren) => {
const config = useMemo(() => ({}), []);
const adapters: Adapters = useMemo(
@@ -134,13 +156,17 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
addListener(
SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED,
({ countryCode, documentCategory }) => {
if (navigationRef.isReady()) {
navigationRef.navigate('ComingSoon', {
countryCode,
documentCategory,
} as any);
}
({
countryCode,
documentCategory,
}: {
countryCode: string | null;
documentCategory: string | null;
}) => {
navigateIfReady('ComingSoon', {
countryCode: countryCode ?? undefined,
documentCategory: documentCategory ?? undefined,
});
},
);
@@ -207,17 +233,19 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
}
});
addListener(SdkEvents.PROVING_AADHAAR_UPLOAD_FAILURE, ({ errorType }) => {
if (navigationRef.isReady()) {
// @ts-expect-error
navigationRef.navigate('AadhaarUploadError', { errorType });
}
navigateIfReady('AadhaarUploadError', { errorType });
});
addListener(
SdkEvents.DOCUMENT_COUNTRY_SELECTED,
({ countryCode, documentTypes }) => {
({
countryCode,
documentTypes,
}: {
countryCode: string;
documentTypes: string[];
}) => {
if (navigationRef.isReady()) {
// @ts-expect-error
navigationRef.navigate('IDPicker', { countryCode, documentTypes });
}
},
@@ -234,10 +262,14 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
navigationRef.navigate('DocumentOnboarding');
break;
case 'a':
navigationRef.navigate('AadhaarUpload', { countryCode } as never);
if (countryCode) {
navigationRef.navigate('AadhaarUpload', { countryCode });
}
break;
default:
navigationRef.navigate('ComingSoon', { countryCode } as never);
if (countryCode) {
navigationRef.navigate('ComingSoon', { countryCode });
}
break;
}
}

View File

@@ -5,6 +5,7 @@
import React, { useCallback, useState } from 'react';
import { Separator, View, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { isUserRegisteredWithAlternativeCSCA } from '@selfxyz/common/utils/passports/validate';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
@@ -19,6 +20,7 @@ import useHapticNavigation from '@/hooks/useHapticNavigation';
import Keyboard from '@/images/icons/keyboard.svg';
import RestoreAccountSvg from '@/images/icons/restore_account.svg';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { useAuth } from '@/providers/authProvider';
import {
loadPassportDataAndSecret,
@@ -37,7 +39,8 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } =
useSettingStore();
const { download } = useBackupMnemonic();
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const onRestoreFromCloudNext = useHapticNavigation('AccountVerifiedSuccess');
const onEnterRecoveryPress = useHapticNavigation('RecoverWithPhrase');

View File

@@ -8,6 +8,7 @@ import { Keyboard, StyleSheet } from 'react-native';
import { Text, TextArea, View, XStack, YStack } from 'tamagui';
import Clipboard from '@react-native-clipboard/clipboard';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { isUserRegisteredWithAlternativeCSCA } from '@selfxyz/common/utils/passports/validate';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
@@ -16,6 +17,7 @@ import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { SecondaryButton } from '@/components/buttons/SecondaryButton';
import Description from '@/components/typography/Description';
import Paste from '@/images/icons/paste.svg';
import type { RootStackParamList } from '@/navigation';
import { useAuth } from '@/providers/authProvider';
import {
loadPassportDataAndSecret,
@@ -31,7 +33,8 @@ import {
} from '@/utils/colors';
const RecoverWithPhraseScreen: React.FC = () => {
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const selfClient = useSelfClient();
const { useProtocolStore } = selfClient;
const { restoreAccountFromMnemonic } = useAuth();

View File

@@ -6,6 +6,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import { YStack } from 'tamagui';
import type { StaticScreenProps } from '@react-navigation/native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
@@ -181,7 +182,8 @@ function BottomButton({
nextScreen?: NextScreen;
}) {
const { trackEvent } = useSelfClient();
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const goBack = () => {
confirmTap();

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import LottieView from 'lottie-react-native';
import type LottieView from 'lottie-react-native';
import { useCallback, useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import type { StaticScreenProps } from '@react-navigation/native';
@@ -10,7 +10,7 @@ import { useFocusEffect, useIsFocused } from '@react-navigation/native';
import type { DocumentCategory } from '@selfxyz/common/utils/types';
import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { ProvingStateType } from '@selfxyz/mobile-sdk-alpha/browser';
import type { ProvingStateType } from '@selfxyz/mobile-sdk-alpha/browser';
import failAnimation from '@/assets/animations/loading/fail.json';
import proveLoadingAnimation from '@/assets/animations/loading/prove.json';
@@ -98,7 +98,7 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ route }) => {
} else {
await init(selfClient, 'dsc', true);
}
} catch (_error) {
} catch {
console.error('Error loading selected document:');
await init(selfClient, 'dsc', true);
} finally {

View File

@@ -19,6 +19,7 @@ import {
YStack,
} from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { ChevronDown, Minus, Plus, X } from '@tamagui/lucide-icons';
import { countryCodes } from '@selfxyz/common/constants';
@@ -37,6 +38,7 @@ import { useMockDataForm } from '@/hooks/useMockDataForm';
import SelfDevCard from '@/images/card-dev.svg';
import IdIcon from '@/images/icons/id_icon.svg';
import NoteIcon from '@/images/icons/note.svg';
import type { RootStackParamList } from '@/navigation';
import { storePassportData } from '@/providers/passportDataProvider';
import {
black,
@@ -159,7 +161,8 @@ const FormSection: React.FC<FormSectionProps> = ({
const CreateMockScreen: React.FC = () => {
const { trackEvent } = useSelfClient();
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const {
age,
setAge,

View File

@@ -8,6 +8,7 @@ import { ActivityIndicator, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ScrollView, Text, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { countryCodes } from '@selfxyz/common/constants';
import { getCountryISO2 } from '@selfxyz/common/constants/countries';
@@ -20,13 +21,15 @@ import ButtonsContainer from '@/components/ButtonsContainer';
import { BodyText } from '@/components/typography/BodyText';
import Description from '@/components/typography/Description';
import { Title } from '@/components/typography/Title';
import type { RootStackParamList } from '@/navigation';
import { storePassportData } from '@/providers/passportDataProvider';
import useUserStore from '@/stores/userStore';
import { black, borderColor, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
const CreateMockScreenDeepLink: React.FC = () => {
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const [selectedCountry, setSelectedCountry] = useState('USA');

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Button,
Input,
@@ -44,6 +44,7 @@ const DevFeatureFlagsScreen: React.FC = () => {
const [debounceTimers, setDebounceTimers] = useState<
Record<string, NodeJS.Timeout>
>({});
const debounceTimersRef = useRef<Record<string, NodeJS.Timeout>>({});
const loadFeatureFlags = useCallback(async () => {
try {
@@ -186,17 +187,19 @@ const DevFeatureFlagsScreen: React.FC = () => {
loadFeatureFlags();
}, [loadFeatureFlags]);
useEffect(() => {
debounceTimersRef.current = debounceTimers;
}, [debounceTimers]);
// Cleanup debounce timers on unmount
useEffect(() => {
return () => {
Object.values(debounceTimers).forEach(timer => {
Object.values(debounceTimersRef.current).forEach(timer => {
if (timer) {
clearTimeout(timer);
}
});
};
// only clean up on unmount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasLocalOverrides = featureFlags.some(

View File

@@ -2,12 +2,12 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import LottieView from 'lottie-react-native';
import type LottieView from 'lottie-react-native';
import React, { useEffect, useMemo, useState } from 'react';
import { Adapt, Button, Select, Sheet, Text, XStack, YStack } from 'tamagui';
import { Check, ChevronDown } from '@tamagui/lucide-icons';
import {
import type {
provingMachineCircuitType,
ProvingStateType,
} from '@selfxyz/mobile-sdk-alpha';
@@ -58,20 +58,22 @@ const DevLoadingScreen: React.FC = () => {
const [canCloseApp, setCanCloseApp] = useState(false);
const [shouldLoopAnimation, setShouldLoopAnimation] = useState(true);
const terminalStates: ProvingStateType[] = [
'completed',
'error',
'failure',
'passport_not_supported',
'account_recovery_choice',
'passport_data_not_found',
];
const terminalStates = useMemo<ProvingStateType[]>(
() => [
'completed',
'error',
'failure',
'passport_not_supported',
'account_recovery_choice',
'passport_data_not_found',
],
[],
);
const safeToCloseStates: ProvingStateType[] = [
'proving',
'post_proving',
'completed',
];
const safeToCloseStates = useMemo<ProvingStateType[]>(
() => ['proving', 'post_proving', 'completed'],
[],
);
useEffect(() => {
const { actionText, actionSubText, estimatedTime, statusBarProgress } =

View File

@@ -3,6 +3,7 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useEffect, useState } from 'react';
import type { ImageSourcePropType } from 'react-native';
import { Linking } from 'react-native';
import { Image, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
@@ -32,6 +33,7 @@ const AadhaarUploadScreen: React.FC = () => {
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { trackEvent } = useSelfClient();
const [isProcessing, setIsProcessing] = useState(false);
const aadhaarImageSource: ImageSourcePropType = AadhaarImage;
const { showModal: showPermissionModal } = useModal({
titleText: 'Photo Library Access Required',
@@ -117,16 +119,16 @@ const AadhaarUploadScreen: React.FC = () => {
errorMessage.includes('Failed to process') ||
errorMessage.includes('Invalid')
) {
(navigation.navigate as any)('AadhaarUploadError', {
errorType: 'general' as const,
});
navigation.navigate('AadhaarUploadError', {
errorType: 'general',
} as never);
return;
}
// Handle any other errors by showing error screen
(navigation.navigate as any)('AadhaarUploadError', {
errorType: 'general' as const,
});
navigation.navigate('AadhaarUploadError', {
errorType: 'general',
} as never);
} finally {
setIsProcessing(false);
}
@@ -152,7 +154,7 @@ const AadhaarUploadScreen: React.FC = () => {
paddingVertical={20}
>
<Image
source={AadhaarImage as any}
source={aadhaarImageSource}
width="100%"
height="100%"
objectFit="contain"

View File

@@ -5,6 +5,7 @@
import React from 'react';
import { YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
@@ -13,12 +14,14 @@ import { PrimaryButton } from '@/components/buttons/PrimaryButton';
import { BodyText } from '@/components/typography/BodyText';
import BlueCheckIcon from '@/images/blue_check.svg';
import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context';
import type { RootStackParamList } from '@/navigation';
import { black, slate100, slate200, slate500, white } from '@/utils/colors';
import { extraYPadding } from '@/utils/constants';
const AadhaarUploadedSuccessScreen: React.FC = () => {
const { bottom } = useSafeAreaInsets();
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { trackEvent } = useSelfClient();
return (

View File

@@ -8,7 +8,7 @@ import { Button, Text, XStack, YStack, ZStack } from 'tamagui';
import { BlurView } from '@react-native-community/blur';
import { useNavigation } from '@react-navigation/native';
import { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types';
import type { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types';
import IdCardLayout from '@/components/homeScreen/idCard';
import { usePassport } from '@/providers/passportDataProvider';

View File

@@ -6,6 +6,7 @@ import React, { useState } from 'react';
import { Platform, ScrollView } from 'react-native';
import { Input, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
@@ -16,6 +17,7 @@ import { BodyText } from '@/components/typography/BodyText';
import Description from '@/components/typography/Description';
import { Title } from '@/components/typography/Title';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { white } from '@/utils/colors';
type NFCParams = {
@@ -90,7 +92,8 @@ const NFC_METHODS = [
];
const DocumentNFCMethodSelectionScreen: React.FC = () => {
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const [selectedMethod, setSelectedMethod] = useState('standard');
const [canValue, setCanValue] = useState('');
const [error, setError] = useState('');
@@ -130,6 +133,7 @@ const DocumentNFCMethodSelectionScreen: React.FC = () => {
if (selectedMethod === 'can') {
params.canNumber = canValue;
}
// Type assertion needed because static navigation doesn't infer optional params
navigation.navigate('DocumentNFCScan', params as never);
};

View File

@@ -28,6 +28,7 @@ import {
useNavigation,
useRoute,
} from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { CircleHelp } from '@tamagui/lucide-icons';
import type { PassportData } from '@selfxyz/common/types';
@@ -48,6 +49,7 @@ import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import NFC_IMAGE from '@/images/nfc.png';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { useFeedback } from '@/providers/feedbackProvider';
import { storePassportData } from '@/providers/passportDataProvider';
import { logNFCEvent } from '@/Sentry';
@@ -92,7 +94,8 @@ const DocumentNFCScanScreen: React.FC = () => {
const selfClient = useSelfClient();
const { trackEvent, useMRZStore } = selfClient;
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const route = useRoute<DocumentNFCScanRoute>();
const { showModal } = useFeedback();
useFeedbackAutoHide();
@@ -460,12 +463,6 @@ const DocumentNFCScanScreen: React.FC = () => {
}
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _cancelScanIfRunning = useCallback(async () => {
// // TODO: cancel if scanning
// setIsNfcSheetOpen(false);
}, []);
useFocusEffect(
useCallback(() => {
logNFCEvent('info', 'screen_focus', { ...baseContext, stage: 'focus' });

View File

@@ -79,7 +79,7 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
};
}
setDocumentMetadata(metadata);
} catch (_error) {
} catch {
// setting defaults on error
setDocumentMetadata({
documentCategory: 'passport',

View File

@@ -11,14 +11,17 @@ import {
useNavigation,
usePreventRemove,
} from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types';
import { DocumentMetadata, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import type { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types';
import type { DocumentMetadata } from '@selfxyz/mobile-sdk-alpha';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { DocumentEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import IdCardLayout from '@/components/homeScreen/idCard';
import { useAppUpdates } from '@/hooks/useAppUpdates';
import useConnectionModal from '@/hooks/useConnectionModal';
import type { RootStackParamList } from '@/navigation';
import { usePassport } from '@/providers/passportDataProvider';
import useUserStore from '@/stores/userStore';
import { slate50 } from '@/utils/colors';
@@ -27,7 +30,8 @@ import { extraYPadding } from '@/utils/constants';
const HomeScreen: React.FC = () => {
const selfClient = useSelfClient();
useConnectionModal();
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { setIdDetailsDocumentId } = useUserStore();
const { getAllDocuments, loadDocumentCatalog } = usePassport();
const [isNewVersionAvailable, showAppUpdateModal, isModalDismissed] =

View File

@@ -11,9 +11,11 @@ import {
} from 'react-native';
import { Card, Image, Text, View, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { CheckSquare2, Wallet, XCircle } from '@tamagui/lucide-icons';
import { BodyText } from '@/components/typography/BodyText';
import type { RootStackParamList } from '@/navigation';
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
import type { ProofHistory } from '@/stores/proofTypes';
import { ProofStatus } from '@/stores/proofTypes';
@@ -62,7 +64,8 @@ export const ProofHistoryList: React.FC<ProofHistoryListProps> = ({
hasMore,
} = useProofHistoryStore();
const [refreshing, setRefreshing] = useState(false);
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
useEffect(() => {
initDatabase();

View File

@@ -12,9 +12,11 @@ import {
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Card, Image, Text, View, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { CheckSquare2, Wallet, XCircle } from '@tamagui/lucide-icons';
import { BodyText } from '@/components/typography/BodyText';
import type { RootStackParamList } from '@/navigation';
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
import type { ProofHistory } from '@/stores/proofTypes';
import { ProofStatus } from '@/stores/proofTypes';
@@ -57,7 +59,8 @@ const ProofHistoryScreen: React.FC = () => {
hasMore,
} = useProofHistoryStore();
const [refreshing, setRefreshing] = useState(false);
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { bottom } = useSafeAreaInsets();
useEffect(() => {

View File

@@ -5,6 +5,7 @@
import React from 'react';
import { YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
@@ -14,12 +15,14 @@ import { DelayedLottieView } from '@/components/DelayedLottieView';
import Description from '@/components/typography/Description';
import { Title } from '@/components/typography/Title';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { styles } from '@/screens/verification/ProofRequestStatusScreen';
import { black, white } from '@/utils/colors';
import { buttonTap } from '@/utils/haptic';
const AccountVerifiedSuccessScreen: React.FC = ({}) => {
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
return (
<ExpandableBottomLayout.Layout backgroundColor={white}>

View File

@@ -6,6 +6,7 @@ import React, { useEffect } from 'react';
import { StyleSheet } from 'react-native';
import { YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
@@ -15,12 +16,14 @@ import { DelayedLottieView } from '@/components/DelayedLottieView';
import Caution from '@/components/typography/Caution';
import { SubHeader } from '@/components/typography/SubHeader';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { useSettingStore } from '@/stores/settingStore';
import { black, white } from '@/utils/colors';
import { confirmTap, notificationWarning } from '@/utils/haptic';
const DisclaimerScreen: React.FC = () => {
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { dismissPrivacyNote } = useSettingStore();
useEffect(() => {

View File

@@ -2,7 +2,8 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import LottieView, { type LottieViewProps } from 'lottie-react-native';
import type { LottieViewProps } from 'lottie-react-native';
import LottieView from 'lottie-react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Linking, StyleSheet, View } from 'react-native';
import { SystemBars } from 'react-native-edge-to-edge';

View File

@@ -18,6 +18,7 @@ import type {
import { ScrollView, StyleSheet, TouchableOpacity } from 'react-native';
import { Image, Text, View, XStack, YStack } from 'tamagui';
import { useIsFocused, useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Eye, EyeOff } from '@tamagui/lucide-icons';
import type { SelfAppDisclosureConfig } from '@selfxyz/common/utils/appType';
@@ -31,6 +32,7 @@ import Disclosures from '@/components/Disclosures';
import { BodyText } from '@/components/typography/BodyText';
import { Caption } from '@/components/typography/Caption';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import {
setDefaultDocumentTypeIfNeeded,
usePassport,
@@ -44,7 +46,8 @@ import { buttonTap } from '@/utils/haptic';
const ProveScreen: React.FC = () => {
const selfClient = useSelfClient();
const { trackEvent } = selfClient;
const { navigate } = useNavigation();
const { navigate } =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const isFocused = useIsFocused();
const { useProvingStore, useSelfAppStore } = selfClient;
const selectedApp = useSelfAppStore(state => state.selfApp);

View File

@@ -11,6 +11,7 @@ import {
useIsFocused,
useNavigation,
} from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
@@ -25,6 +26,7 @@ import useConnectionModal from '@/hooks/useConnectionModal';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import QRScan from '@/images/icons/qr_code.svg';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { black, slate800, white } from '@/utils/colors';
import { parseAndValidateUrlParams } from '@/utils/deeplinks';
@@ -32,7 +34,8 @@ const QRCodeViewFinderScreen: React.FC = () => {
const selfClient = useSelfClient();
const { trackEvent } = selfClient;
const { visible: connectionModalVisible } = useConnectionModal();
const navigation = useNavigation();
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const isFocused = useIsFocused();
const [doneScanningQR, setDoneScanningQR] = useState(false);
const navigateToProve = useHapticNavigation('Prove');

View File

@@ -90,9 +90,9 @@ export const useSettingStore = create<SettingsState>()(
storage: createJSONStorage(() => AsyncStorage),
onRehydrateStorage: () => undefined,
partialize: state => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hideNetworkModal, setHideNetworkModal, ...persistedState } =
state;
const persistedState = { ...state };
delete (persistedState as Partial<SettingsState>).hideNetworkModal;
delete (persistedState as Partial<SettingsState>).setHideNetworkModal;
return persistedState;
},
},

View File

@@ -1,8 +0,0 @@
// 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.
declare module 'elliptic' {
const elliptic: any;
export = elliptic;
}

View File

@@ -2,12 +2,13 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { AppState, type AppStateStatus } from 'react-native';
import type { AppStateStatus } from 'react-native';
import { AppState } from 'react-native';
import { ENABLE_DEBUG_LOGS, MIXPANEL_NFC_PROJECT_TOKEN } from '@env';
import NetInfo from '@react-native-community/netinfo';
import type { JsonMap, JsonValue } from '@segment/analytics-react-native';
import { TrackEventParams } from '@selfxyz/mobile-sdk-alpha';
import type { TrackEventParams } from '@selfxyz/mobile-sdk-alpha';
import { createSegmentClient } from '@/Segment';
import { PassportReader } from '@/utils/passportReader';
@@ -186,25 +187,37 @@ const flushMixpanelEvents = async () => {
if (__DEV__) console.log('[Mixpanel] flush skipped - NFC scanning active');
return;
}
// Ensure we don't drop events if the native reader isn't available
if (!PassportReader?.trackEvent) {
if (__DEV__)
console.warn('[Mixpanel] flush skipped - NFC module unavailable');
return;
}
try {
if (__DEV__) console.log('[Mixpanel] flush');
// Send any queued events before flushing
while (eventQueue.length > 0) {
const evt = eventQueue.shift()!;
if (PassportReader.trackEvent) {
try {
await Promise.resolve(
PassportReader.trackEvent(evt.name, evt.properties),
);
} catch (trackErr) {
// Put the event back and abort; we'll retry on the next flush
eventQueue.unshift(evt);
throw trackErr;
}
}
if (PassportReader.flush) await Promise.resolve(PassportReader.flush());
if (PassportReader.flush) {
await Promise.resolve(PassportReader.flush());
}
// Only reset event count after successful send/flush
eventCount = 0;
} catch (err) {
if (__DEV__) console.warn('Mixpanel flush failed', err);
// re-queue on failure
if (typeof err !== 'undefined') {
// no-op, events are already queued if failure happened before flush
}
// Events have been re-queued on failure, so they're not lost
}
};
@@ -277,7 +290,7 @@ export const trackNfcEvent = async (
}
try {
if (PassportReader.trackEvent) {
if (PassportReader && PassportReader.trackEvent) {
await Promise.resolve(PassportReader.trackEvent(name, properties));
}
eventCount++;

View File

@@ -7,7 +7,7 @@ import { Linking, Platform } from 'react-native';
import { countries } from '@selfxyz/common/constants/countries';
import type { IdDocInput } from '@selfxyz/common/utils';
import { SelfClient } from '@selfxyz/mobile-sdk-alpha';
import type { SelfClient } from '@selfxyz/mobile-sdk-alpha';
import { navigationRef } from '@/navigation';
import useUserStore from '@/stores/userStore';

View File

@@ -2,13 +2,14 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import Keychain, {
type ACCESS_CONTROL,
type ACCESSIBLE,
import type {
ACCESS_CONTROL,
ACCESSIBLE,
GetOptions,
type SECURITY_LEVEL,
SECURITY_LEVEL,
SetOptions,
} from 'react-native-keychain';
import Keychain from 'react-native-keychain';
/**
* Security configuration for keychain operations
@@ -45,7 +46,7 @@ export async function checkBiometricsAvailable(): Promise<boolean> {
const rnBiometrics = new ReactNativeBiometrics();
const { available } = await rnBiometrics.isSensorAvailable();
return available;
} catch (_error) {
} catch {
console.log('Biometrics not available');
return false;
}
@@ -64,7 +65,7 @@ export async function checkPasscodeAvailable(): Promise<boolean> {
// Clean up test entry
await Keychain.resetGenericPassword({ service: testService });
return true;
} catch (_error) {
} catch {
console.log('Device passcode not available');
return false;
}
@@ -187,7 +188,7 @@ export async function getMaxSecurityLevel(): Promise<SECURITY_LEVEL> {
// Try to get the device's security level
const securityLevel = await Keychain.getSecurityLevel();
return securityLevel || Keychain.SECURITY_LEVEL.ANY;
} catch (_error) {
} catch {
console.log('Could not determine security level, defaulting to ANY');
return Keychain.SECURITY_LEVEL.ANY;
}

View File

@@ -17,13 +17,18 @@ type Locale = {
export function getLocales(): Locale[] {
return navigator.languages.map(lang => {
const locale = new Intl.Locale(lang);
type LocaleWithTextInfo = Intl.Locale & {
textInfo?: {
direction?: string;
};
};
const locale = new Intl.Locale(lang) as LocaleWithTextInfo;
return {
languageCode: locale.language,
countryCode: locale.region ?? '',
scriptCode: locale.script,
languageTag: lang,
// @ts-expect-error this not in type but appears to be in browsers
isRTL: locale.textInfo?.direction === 'rtl',
};
});

View File

@@ -2,31 +2,24 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { configLoggerType } from 'react-native-logs';
import { logger } from 'react-native-logs';
import {
type configLoggerType,
type defLvlType,
logger,
type transportFunctionType,
} from 'react-native-logs';
import { interceptConsole } from '@/utils/logger/consoleInterceptor';
import { lokiTransport } from '@/utils/logger/lokiTransport';
import { setupNativeLoggerBridge } from '@/utils/logger/nativeLoggerBridge';
export {
AppLogger,
AuthLogger,
BackupLogger,
DocumentLogger,
Logger,
MockDataLogger,
NfcLogger,
NotificationLogger,
PassportLogger,
ProofLogger,
SettingsLogger,
};
const defaultConfig: configLoggerType<any, any> = {
const defaultConfig: configLoggerType<
transportFunctionType<object> | transportFunctionType<object>[],
defLvlType
> = {
enabled: __DEV__ ? false : true,
severity: __DEV__ ? 'debug' : 'warn', //TODO configure this using remote-config
transport: [lokiTransport],
transport: [lokiTransport as unknown as transportFunctionType<object>],
transportOptions: {
colors: {
info: 'blueBright',
@@ -42,6 +35,9 @@ const defaultConfig: configLoggerType<any, any> = {
const Logger = logger.createLogger(defaultConfig);
type RootLogger = typeof Logger;
type LoggerExtension = ReturnType<RootLogger['extend']>;
// loggers based on src/consts/analytics.ts
const AppLogger = Logger.extend('APP');
const NotificationLogger = Logger.extend('NOTIFICATION');
@@ -60,7 +56,7 @@ const NfcLogger = Logger.extend('NFC');
interceptConsole(AppLogger);
// Define log levels
export const logLevels = {
const logLevels = {
debug: 0,
info: 1,
warn: 2,
@@ -70,3 +66,20 @@ export const logLevels = {
// Initialize native logger bridge after all loggers are defined
// This avoids module cycle by injecting dependencies instead of importing them
setupNativeLoggerBridge({ AppLogger, NfcLogger, Logger });
export type { LoggerExtension, RootLogger };
export {
AppLogger,
AuthLogger,
BackupLogger,
DocumentLogger,
Logger,
MockDataLogger,
NfcLogger,
NotificationLogger,
PassportLogger,
ProofLogger,
SettingsLogger,
logLevels,
};

View File

@@ -11,28 +11,35 @@ const originalConsole = {
debug: console.debug,
};
const interceptConsole = (appLogger: any) => {
console.log = (...args: any[]) => {
type LoggerMethods = {
debug: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
};
const interceptConsole = (appLogger: LoggerMethods) => {
console.log = (...args: unknown[]) => {
appLogger.info(...args);
originalConsole.log(...args);
};
console.info = (...args: any[]) => {
console.info = (...args: unknown[]) => {
appLogger.info(...args);
originalConsole.info(...args);
};
console.warn = (...args: any[]) => {
console.warn = (...args: unknown[]) => {
appLogger.warn(...args);
originalConsole.warn(...args);
};
console.error = (...args: any[]) => {
console.error = (...args: unknown[]) => {
appLogger.error(...args);
originalConsole.error(...args);
};
console.debug = (...args: any[]) => {
console.debug = (...args: unknown[]) => {
appLogger.debug(...args);
originalConsole.debug(...args);
};

View File

@@ -135,7 +135,7 @@ registerDocumentChangeCallback((isMock: boolean) => {
isCurrentPassportMockFlag = isMock;
});
export const cleanupLokiTransport = () => {
const cleanupLokiTransport = () => {
try {
appStateSubscription.remove?.();
} catch {}
@@ -143,7 +143,7 @@ export const cleanupLokiTransport = () => {
};
// Export flush function for manual flushing if needed
export const flushLokiTransport = () => {
const flushLokiTransport = () => {
if (batch.length > 0) {
sendBatch([...batch], 'default');
batch = [];
@@ -165,8 +165,10 @@ const appStateSubscription = AppState.addEventListener(
handleAppStateChange,
);
type LokiTransportOptions = Record<string, never>;
// Create react-native-logs transport function
export const lokiTransport: transportFunctionType<any> = props => {
const lokiTransport: transportFunctionType<LokiTransportOptions> = props => {
const { msg, rawMsg, level, extension } = props;
if (isCurrentPassportMockFlag) {
@@ -189,7 +191,12 @@ export const lokiTransport: transportFunctionType<any> = props => {
}
// Create the log object
const logObject: any = {
const logObject: {
level: string;
message: string;
timestamp: string;
data?: unknown;
} = {
level: level.text,
message: actualMessage,
timestamp,
@@ -209,3 +216,10 @@ export const lokiTransport: transportFunctionType<any> = props => {
addToBatch(entry, namespace);
};
export {
type LokiTransportOptions,
cleanupLokiTransport,
flushLokiTransport,
lokiTransport,
};

View File

@@ -4,6 +4,8 @@
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import type { LoggerExtension, RootLogger } from '@/utils/logger';
// Remove direct imports to avoid module cycle
// Dependencies will be injected via setupNativeLoggerBridge
@@ -11,21 +13,21 @@ interface NativeLogEvent {
level: 'debug' | 'info' | 'warn' | 'error';
category: string;
message: string;
data?: any;
data?: unknown;
}
let eventEmitter: NativeEventEmitter | null = null;
let isInitialized = false;
let injectedLoggers: {
AppLogger: any;
NfcLogger: any;
Logger: any;
AppLogger: LoggerExtension;
NfcLogger: LoggerExtension;
Logger: RootLogger;
} | null = null;
const setupNativeLoggerBridge = (loggers: {
AppLogger: any;
NfcLogger: any;
Logger: any;
AppLogger: LoggerExtension;
NfcLogger: LoggerExtension;
Logger: RootLogger;
}) => {
if (isInitialized) return;
@@ -71,7 +73,7 @@ const handleNativeLogEvent = (event: NativeLogEvent) => {
const { level, category, message, data } = event;
// Route to appropriate logger based on category
let logger: any;
let logger: LoggerExtension;
switch (category.toLowerCase()) {
case 'nfc':
logger = injectedLoggers.NfcLogger;

View File

@@ -10,25 +10,11 @@ import type { NFCScanContext } from '@selfxyz/mobile-sdk-alpha';
import { logNFCEvent } from '@/Sentry';
import {
PassportReader,
type AndroidScanResponse,
reset,
scan as scanDocument,
} from '@/utils/passportReader';
interface AndroidScanResponse {
mrz: string;
eContent: string;
encryptedDigest: string;
_photo: string;
_digestAlgorithm: string;
_signerInfoDigestAlgorithm: string;
_digestEncryptionAlgorithm: string;
_LDSVersion: string;
_unicodeVersion: string;
encapContent: string;
documentSigningCertificate: string;
dataGroupHashes: string;
}
import { PassportReader } from '@/utils/passportReader';
interface Inputs {
passportNumber: string;
@@ -44,6 +30,11 @@ interface Inputs {
userId?: string;
}
interface DataGroupHash {
sodHash?: string;
[key: string]: unknown;
}
export const parseScanResponse = (response: unknown) => {
return Platform.OS === 'android'
? handleResponseAndroid(response as AndroidScanResponse)
@@ -108,7 +99,39 @@ const scanIOS = async (
inputs: Inputs,
context: Omit<NFCScanContext, 'stage'>,
) => {
if (!PassportReader?.scanPassport) {
// Prefer direct native scanPassport when available (tests stub this directly)
const iosReader = PassportReader as unknown as {
scanPassport?: (
passportNumber: string,
dateOfBirth: string,
dateOfExpiry: string,
canNumber: string,
useCan: boolean,
skipPACE: boolean,
skipCA: boolean,
extendedMode: boolean,
usePacePolling: boolean,
sessionId: string,
) => Promise<unknown>;
} | null;
if (iosReader?.scanPassport) {
return await iosReader.scanPassport(
inputs.passportNumber,
inputs.dateOfBirth,
inputs.dateOfExpiry,
inputs.canNumber ?? '',
inputs.useCan ?? false,
inputs.skipPACE ?? false,
inputs.skipCA ?? false,
inputs.extendedMode ?? false,
inputs.usePacePolling ?? false,
inputs.sessionId,
);
}
// Fallback to normalized cross-platform scan
if (!scanDocument) {
console.warn(
'iOS passport scanner is not available - native module failed to load',
);
@@ -123,34 +146,56 @@ const scanIOS = async (
);
}
return await Promise.resolve(
PassportReader.scanPassport(
inputs.passportNumber,
inputs.dateOfBirth,
inputs.dateOfExpiry,
inputs.canNumber ?? '',
inputs.useCan ?? false,
inputs.skipPACE ?? false,
inputs.skipCA ?? false,
inputs.extendedMode ?? false,
inputs.usePacePolling ?? false,
inputs.sessionId,
),
);
return await scanDocument({
documentNumber: inputs.passportNumber,
dateOfBirth: inputs.dateOfBirth,
dateOfExpiry: inputs.dateOfExpiry,
canNumber: inputs.canNumber ?? '',
useCan: inputs.useCan ?? false,
skipPACE: inputs.skipPACE ?? false,
skipCA: inputs.skipCA ?? false,
extendedMode: inputs.extendedMode ?? false,
usePacePolling: inputs.usePacePolling ?? false,
sessionId: inputs.sessionId,
});
};
const handleResponseIOS = (response: unknown) => {
const parsed = JSON.parse(String(response));
const dgHashesObj = JSON.parse(parsed?.dataGroupHashes);
const dg1HashString = dgHashesObj?.DG1?.sodHash;
const dg1Hash = Array.from(Buffer.from(dg1HashString, 'hex'));
const dg2HashString = dgHashesObj?.DG2?.sodHash;
const dg2Hash = Array.from(Buffer.from(dg2HashString, 'hex'));
// Support string or object response
const parsed: Record<string, unknown> =
typeof response === 'string'
? (JSON.parse(response) as Record<string, unknown>)
: ((response as Record<string, unknown>) ?? {});
const eContentBase64 = parsed?.eContentBase64;
const signedAttributes = parsed?.signedAttributes;
const mrz = parsed?.passportMRZ;
const signatureBase64 = parsed?.signatureBase64;
const dgHashesObj = JSON.parse(
String(parsed?.dataGroupHashes ?? '{}'),
) as Record<string, DataGroupHash>;
const dg1HashString = dgHashesObj?.DG1?.sodHash as string | undefined;
const dg2HashString = dgHashesObj?.DG2?.sodHash as string | undefined;
const mrz = parsed?.passportMRZ as string | undefined;
if (!mrz || typeof mrz !== 'string') {
throw new Error('Invalid iOS NFC response: missing passportMRZ');
}
const isHex = (s: string) => /^[0-9a-fA-F]*$/.test(s);
if (dg1HashString !== undefined && !isHex(dg1HashString)) {
throw new Error('Invalid DG1 sodHash hex string');
}
if (dg2HashString !== undefined && !isHex(String(dg2HashString))) {
throw new Error('Invalid DG2 sodHash hex string');
}
const dg1Hash = dg1HashString
? Array.from(Buffer.from(dg1HashString, 'hex'))
: [];
const dg2Hash = dg2HashString
? Array.from(Buffer.from(String(dg2HashString), 'hex'))
: [];
const eContentBase64 = parsed?.eContentBase64 as string | undefined;
const signedAttributes = parsed?.signedAttributes as string | undefined;
const signatureBase64 = parsed?.signatureBase64 as string | undefined;
// const _dataGroupsPresent = parsed?.dataGroupsPresent;
// const _placeOfBirth = parsed?.placeOfBirth;
// const _activeAuthenticationPassed = parsed?.activeAuthenticationPassed;
@@ -160,25 +205,32 @@ const handleResponseIOS = (response: unknown) => {
// const passportPhoto = parsed?.passportPhoto;
// const _encapsulatedContentDigestAlgorithm =
// parsed?.encapsulatedContentDigestAlgorithm;
const documentSigningCertificate = parsed?.documentSigningCertificate;
const pem = JSON.parse(documentSigningCertificate).PEM.replace(/\n/g, '');
const eContentArray = Array.from(Buffer.from(signedAttributes, 'base64'));
const documentSigningCertificate = parsed?.documentSigningCertificate as
| string
| undefined;
const pem = JSON.parse(String(documentSigningCertificate)).PEM.replace(
/\n/g,
'',
);
const eContentArray = Array.from(
Buffer.from(String(signedAttributes ?? ''), 'base64'),
);
const signedEContentArray = eContentArray.map(byte =>
byte > 127 ? byte - 256 : byte,
);
const concatenatedDataHashesArray = Array.from(
Buffer.from(eContentBase64, 'base64'),
Buffer.from(String(eContentBase64 ?? ''), 'base64'),
);
const concatenatedDataHashesArraySigned = concatenatedDataHashesArray.map(
byte => (byte > 127 ? byte - 256 : byte),
);
const encryptedDigestArray = Array.from(
Buffer.from(signatureBase64, 'base64'),
Buffer.from(String(signatureBase64 ?? ''), 'base64'),
).map(byte => (byte > 127 ? byte - 256 : byte));
const document_type = mrz.length === 88 ? 'passport' : 'id_card';
const document_type = String(mrz).length === 88 ? 'passport' : 'id_card';
return {
mrz,
@@ -222,7 +274,7 @@ const handleResponseAndroid = (response: AndroidScanResponse): PassportData => {
'-----END CERTIFICATE-----';
const dgPresents = Object.keys(dgHashesObj)
.map(key => parseInt(key)) // eslint-disable-line radix
.map(key => parseInt(key, 10))
.filter(num => !isNaN(num))
.sort((a, b) => a - b);

View File

@@ -15,7 +15,7 @@ export interface RemoteMessage {
title?: string;
body?: string;
};
[key: string]: any;
[key: string]: unknown;
}
export const API_URL = 'https://notification.self.xyz';

View File

@@ -3,12 +3,10 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { PermissionsAndroid, Platform } from 'react-native';
import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import messaging from '@react-native-firebase/messaging';
import type {
DeviceTokenRegistration,
RemoteMessage,
} from '@/utils/notifications/notificationService.shared';
import type { DeviceTokenRegistration } from '@/utils/notifications/notificationService.shared';
import {
API_URL,
API_URL_STAGING,
@@ -121,13 +119,13 @@ export async function requestNotificationPermission(): Promise<boolean> {
export function setupNotifications(): () => void {
messaging().setBackgroundMessageHandler(
async (remoteMessage: RemoteMessage) => {
async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
log('Message handled in the background!', remoteMessage);
},
);
const unsubscribeForeground = messaging().onMessage(
async (remoteMessage: RemoteMessage) => {
async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
log('Foreground message received:', remoteMessage);
},
);

View File

@@ -15,78 +15,151 @@ type ScanOptions = {
extendedMode?: boolean;
usePacePolling?: boolean;
sessionId?: string;
quality?: number;
};
export interface AndroidScanResponse {
mrz: string;
eContent: string;
encryptedDigest: string;
photo: {
base64: string;
};
digestAlgorithm: string;
signerInfoDigestAlgorithm: string;
digestEncryptionAlgorithm: string;
LDSVersion: string;
unicodeVersion: string;
encapContent: string;
documentSigningCertificate: string;
dataGroupHashes: string;
}
type AndroidPassportReaderModule = {
configure?: (token: string, enableDebug?: boolean) => void;
trackEvent?: (name: string, properties?: Record<string, unknown>) => void;
flush?: () => void | Promise<void>;
reset?: () => void;
scan?: (options: ScanOptions) => Promise<AndroidScanResponse>;
};
type IOSPassportReaderModule = {
configure?: (token: string, enableDebug?: boolean) => void;
trackEvent?: (name: string, properties?: Record<string, unknown>) => void;
flush?: () => void | Promise<void>;
scanPassport?: (
passportNumber: string,
dateOfBirth: string,
dateOfExpiry: string,
canNumber: string,
useCan: boolean,
skipPACE: boolean,
skipCA: boolean,
extendedMode: boolean,
usePacePolling: boolean,
sessionId: string,
) => Promise<string | Record<string, unknown>>;
};
type PassportReaderModule =
| AndroidPassportReaderModule
| IOSPassportReaderModule;
// Platform-specific PassportReader implementation
let PassportReader: any;
let reset: any;
let scan: ((options: ScanOptions) => Promise<any>) | null;
let PassportReader: PassportReaderModule | null = null;
let scan: ((options: ScanOptions) => Promise<unknown>) | null = null;
let resetImpl: (() => void) | undefined;
if (Platform.OS === 'android') {
// Android uses the react-native-passport-reader package
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const AndroidPassportReader = require('react-native-passport-reader');
const AndroidPassportReader = NativeModules.RNPassportReader as
| AndroidPassportReaderModule
| undefined;
if (AndroidPassportReader) {
PassportReader = AndroidPassportReader;
reset = AndroidPassportReader.reset;
scan = AndroidPassportReader.scan;
} catch (error) {
console.warn('Failed to load Android PassportReader:', error);
PassportReader = null;
reset = null;
scan = null;
}
} else if (Platform.OS === 'ios') {
// iOS uses the native PassportReader module directly
PassportReader = NativeModules.PassportReader || null;
// iOS doesn't have reset function
reset = null;
// iOS uses scanPassport method with different signature
scan = PassportReader?.scanPassport
? async (options: ScanOptions) => {
resetImpl = () => AndroidPassportReader.reset?.();
if (AndroidPassportReader.scan) {
const androidScan = AndroidPassportReader.scan.bind(
AndroidPassportReader,
);
scan = async options => {
const {
documentNumber,
dateOfBirth,
dateOfExpiry,
canNumber = '',
useCan = false,
skipPACE = false,
skipCA = false,
extendedMode = false,
usePacePolling = true,
sessionId = '',
quality = 1,
} = options;
const result = await PassportReader.scanPassport(
return androidScan({
documentNumber,
dateOfBirth,
dateOfExpiry,
canNumber,
useCan,
skipPACE,
skipCA,
extendedMode,
usePacePolling,
sessionId,
);
// iOS native returns a JSON string; normalize to object.
try {
return typeof result === 'string' ? JSON.parse(result) : result;
} catch {
return result;
}
quality,
});
};
}
} else {
console.warn('Failed to load Android PassportReader: module not found');
}
} else if (Platform.OS === 'ios') {
// iOS uses the native PassportReader module directly
const IOSPassportReader = NativeModules.PassportReader as
| IOSPassportReaderModule
| undefined;
PassportReader = IOSPassportReader ?? null;
// iOS uses scanPassport method with different signature
if (IOSPassportReader?.scanPassport) {
const scanPassport = IOSPassportReader.scanPassport.bind(IOSPassportReader);
scan = async options => {
const {
documentNumber,
dateOfBirth,
dateOfExpiry,
canNumber = '',
useCan = false,
skipPACE = false,
skipCA = false,
extendedMode = false,
usePacePolling = true,
sessionId = '',
} = options;
const result = await scanPassport(
documentNumber,
dateOfBirth,
dateOfExpiry,
canNumber,
useCan,
skipPACE,
skipCA,
extendedMode,
usePacePolling,
sessionId,
);
// iOS native returns a JSON string; normalize to object.
try {
return typeof result === 'string' ? JSON.parse(result) : result;
} catch {
return result;
}
: null;
};
}
} else {
// Unsupported platform
console.warn('PassportReader: Unsupported platform');
PassportReader = null;
reset = null;
scan = null;
}
const reset = () => {
resetImpl?.();
};
export type { ScanOptions };
export { PassportReader, reset, scan };
export default PassportReader;

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import {
import type {
provingMachineCircuitType,
ProvingStateType,
} from '@selfxyz/mobile-sdk-alpha';

View File

@@ -46,7 +46,7 @@ export async function checkAndUpdateRegistrationStates(
const logValidationError = (
error: string,
data?: PassportData,
additionalContext?: Record<string, any>,
additionalContext?: Record<string, unknown>,
) => {
anyFailureReported = true;
trackEvent(DocumentEvents.VALIDATE_DOCUMENT_FAILED, {
@@ -176,25 +176,33 @@ export async function checkAndUpdateRegistrationStates(
// UNUSED?
interface MigratedPassportData extends Omit<PassportData, 'documentType'> {
documentType?: string;
}
type MigratedPassportData = Omit<PassportData, 'documentCategory' | 'mock'> & {
documentCategory?: PassportData['documentCategory'];
mock?: PassportData['mock'];
};
export function migratePassportData(passportData: PassportData): PassportData {
const migratedData: MigratedPassportData = { ...passportData };
if (!('documentCategory' in migratedData) || !('mock' in migratedData)) {
const documentType = (migratedData as any).documentType;
if (documentType) {
(migratedData as any).mock = documentType.startsWith('mock');
(migratedData as any).documentCategory = documentType.includes('passport')
? 'passport'
: 'id_card';
} else {
(migratedData as any).documentType = 'passport';
(migratedData as any).documentCategory = 'passport';
(migratedData as any).mock = false;
}
// console.log('Migrated passport data:', migratedData);
}
return migratedData as PassportData;
const existingDocumentType = migratedData.documentType;
const inferredMock =
migratedData.mock ?? existingDocumentType?.startsWith('mock');
const inferredCategory =
migratedData.documentCategory ??
(existingDocumentType?.includes('passport') ? 'passport' : 'id_card');
const normalizedDocumentType = existingDocumentType ?? 'passport';
const normalizedMock = inferredMock ?? false;
const normalizedCategory =
inferredCategory ??
(normalizedDocumentType.includes('passport') ? 'passport' : 'id_card');
const normalizedData: PassportData = {
...migratedData,
documentType: normalizedDocumentType,
mock: normalizedMock,
documentCategory: normalizedCategory,
};
return normalizedData;
}

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { ReactNode } from 'react';
import type { ReactNode } from 'react';
import { checkVersion } from 'react-native-check-version';
import { useNavigation } from '@react-navigation/native';
import { act, renderHook, waitFor } from '@testing-library/react-native';

View File

@@ -4,7 +4,7 @@
import { Linking } from 'react-native';
import { SelfClient } from '@selfxyz/mobile-sdk-alpha';
import type { SelfClient } from '@selfxyz/mobile-sdk-alpha';
jest.mock('@/navigation', () => ({
navigationRef: {

View File

@@ -16,9 +16,11 @@
"../packages/mobile-sdk-alpha/dist"
],
"@selfxyz/mobile-sdk-alpha/onboarding/*": [
"../packages/mobile-sdk-alpha/src/flows/onboarding/*",
"../packages/mobile-sdk-alpha/dist/esm/flows/onboarding/*"
],
"@selfxyz/mobile-sdk-alpha/disclosing/*": [
"../packages/mobile-sdk-alpha/src/flows/disclosing/*",
"../packages/mobile-sdk-alpha/dist/esm/flows/disclosing/*"
],
"@/*": ["./src/*"]

View File

@@ -70,6 +70,14 @@ module.exports = {
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-empty-object-type': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
// TypeScript Import Rules
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: false,
},
],
// Add prettier rule to show prettier errors as ESLint errors
'prettier/prettier': ['warn', {}, { usePrettierrc: true }],
// Disable prop-types for TypeScript files since we use TypeScript types

View File

@@ -6,12 +6,13 @@ import { defaultConfig } from './config/defaults';
import { mergeConfig } from './config/merge';
import { notImplemented } from './errors';
import { extractMRZInfo as parseMRZInfo } from './processing/mrz';
import { ProofContext } from './proving/internal/logging';
import type { ProofContext } from './proving/internal/logging';
import { useProvingStore } from './proving/provingMachine';
import { useMRZStore } from './stores/mrzStore';
import { useProtocolStore } from './stores/protocolStore';
import { useSelfAppStore } from './stores/selfAppStore';
import { SDKEvent, SDKEventMap, SdkEvents } from './types/events';
import type { SDKEvent, SDKEventMap } from './types/events';
import { SdkEvents } from './types/events';
import type {
Adapters,
Config,
@@ -21,9 +22,9 @@ import type {
NFCScanOpts,
NFCScanResult,
SelfClient,
TrackEventParams,
Unsubscribe,
} from './types/public';
import { TrackEventParams } from './types/public';
/**
* Optional adapter implementations used when a consumer does not provide their
* own. These defaults are intentionally minimal no-ops suitable for tests and

View File

@@ -7,7 +7,7 @@ import type { DimensionValue, NativeSyntheticEvent, ViewProps, ViewStyle } from
import { NativeModules, PixelRatio, Platform, requireNativeComponent, StyleSheet, View } from 'react-native';
import { extractMRZInfo, formatDateToYYMMDD } from '../mrz';
import { MRZInfo } from '../types/public';
import type { MRZInfo } from '../types/public';
import { RCTFragment } from './RCTFragment';
interface SelfMRZScannerViewProps extends ViewProps {

View File

@@ -9,7 +9,7 @@ import { View } from 'tamagui';
import { getSKIPEM, initPassportDataParsing } from '@selfxyz/common';
import { useSelfClient } from '../../context';
import { NFCScanResult } from '../../types/public';
import type { NFCScanResult } from '../../types/public';
import type { ScreenProps } from '../../types/ui';
export const NFCScannerScreen = ({ onSuccess, onFailure }: ScreenProps) => {

View File

@@ -2,10 +2,11 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { createContext, type PropsWithChildren, useContext, useMemo } from 'react';
import type { PropsWithChildren } from 'react';
import { createContext, useContext, useMemo } from 'react';
import { createSelfClient } from './client';
import { SdkEvents } from './types/events';
import type { SdkEvents } from './types/events';
import type { Adapters, Config, SelfClient } from './types/public';
/**

View File

@@ -19,7 +19,7 @@ import {
} from '@selfxyz/common';
import { extractNameFromMRZ } from '../processing/mrz';
import { SelfClient } from '../types/public';
import type { SelfClient } from '../types/public';
export async function clearPassportData(selfClient: SelfClient) {
const catalog = await selfClient.loadDocumentCatalog();

View File

@@ -2,16 +2,18 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { RefObject, useCallback } from 'react';
import type { RefObject } from 'react';
import { useCallback } from 'react';
import { Platform } from 'react-native';
import { PassportEvents } from '../../constants/analytics';
import { useSelfClient } from '../../context';
import { checkScannedInfo, formatDateToYYMMDD } from '../../processing/mrz';
import { SdkEvents } from '../../types/events';
import { MRZInfo } from '../../types/public';
import type { MRZInfo } from '../../types/public';
export { MRZScannerView, MRZScannerViewProps } from '../../components/MRZScannerView';
export type { MRZScannerViewProps } from '../../components/MRZScannerView';
export { MRZScannerView } from '../../components/MRZScannerView';
export function mrzReadInstructions() {
return 'Lay your document flat and position the machine readable text in the viewfinder';

View File

@@ -4,7 +4,7 @@
import { create } from 'zustand';
import { MRZInfo } from '../types/public';
import type { MRZInfo } from '../types/public';
type MRZNeededForNFC = Pick<MRZInfo, 'documentNumber' | 'dateOfBirth' | 'dateOfExpiry'>;

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { DocumentCategory } from '@selfxyz/common';
import type { DocumentCategory } from '@selfxyz/common';
import type { NFCScanContext, ProofContext } from '../proving/internal/logging';
import type { LogLevel, Progress } from './public';

View File

@@ -2,16 +2,16 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { create } from 'zustand';
import type { create } from 'zustand';
import type { DocumentCatalog, IDDocument, PassportData } from '@selfxyz/common';
import type { ProofContext } from '../proving/internal/logging';
import { ProvingState } from '../proving/provingMachine';
import { MRZState } from '../stores/mrzStore';
import { ProtocolState } from '../stores/protocolStore';
import { SelfAppState } from '../stores/selfAppStore';
import { SDKEvent, SDKEventMap } from './events';
import type { ProvingState } from '../proving/provingMachine';
import type { MRZState } from '../stores/mrzStore';
import type { ProtocolState } from '../stores/protocolStore';
import type { SelfAppState } from '../stores/selfAppStore';
import type { SDKEvent, SDKEventMap } from './events';
export type { PassportValidationCallbacks } from '../validation/document';
export type { DocumentCatalog, IDDocument, PassportData };

View File

@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
import type { CryptoAdapter, DocumentsAdapter, NetworkAdapter, NFCScannerAdapter } from '../src';
import { createListenersMap, createSelfClient, SdkEvents } from '../src/index';
import { AuthAdapter } from '../src/types/public';
import type { AuthAdapter } from '../src/types/public';
describe('createSelfClient', () => {
// Test eager validation during client creation

View File

@@ -7,7 +7,8 @@ import { describe, expect, it } from 'vitest';
import type { DocumentCatalog } from '@selfxyz/common/types';
import type { PassportData } from '@selfxyz/common/types/passport';
import { createSelfClient, defaultConfig, DocumentsAdapter, loadSelectedDocument, SelfClient } from '../../src';
import type { DocumentsAdapter, SelfClient } from '../../src';
import { createSelfClient, defaultConfig, loadSelectedDocument } from '../../src';
const createMockSelfClientWithDocumentsAdapter = (documentsAdapter: DocumentsAdapter): SelfClient => {
return createSelfClient({

View File

@@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PassportEvents } from '../../../src/constants/analytics';
import { useReadMRZ } from '../../../src/flows/onboarding/read-mrz';
import { SdkEvents } from '../../../src/types/events';
import { MRZInfo } from '../../../src/types/public';
import type { MRZInfo } from '../../../src/types/public';
import { renderHook } from '@testing-library/react';

View File

@@ -9,7 +9,8 @@
import { describe, expect, it } from 'vitest';
import { handleStatusCode, parseStatusMessage, type StatusMessage } from '../../../src/proving/internal/statusHandlers';
import type { StatusMessage } from '../../../src/proving/internal/statusHandlers';
import { handleStatusCode, parseStatusMessage } from '../../../src/proving/internal/statusHandlers';
describe('parseStatusMessage', () => {
it('parses valid JSON string', () => {

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { SelfClient } from '../../src';
import type { SelfClient } from '../../src';
import { useProvingStore } from '../../src/proving/provingMachine';
import { useProtocolStore } from '../../src/stores/protocolStore';
import { useSelfAppStore } from '../../src/stores/selfAppStore';

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { SelfClient } from '../../src';
import type { SelfClient } from '../../src';
import { ProofEvents } from '../../src/constants/analytics';
import * as documentUtils from '../../src/documents/utils';
import { useProvingStore } from '../../src/proving/provingMachine';

View File

@@ -7,7 +7,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import type { PassportData } from '@selfxyz/common/types';
import { SdkEvents, SelfClient } from '../../src';
import type { SelfClient } from '../../src';
import { SdkEvents } from '../../src';
import * as documentsUtils from '../../src/documents/utils';
import { useProvingStore } from '../../src/proving/provingMachine';

View File

@@ -34,5 +34,13 @@ module.exports = {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/no-require-imports': 'off',
'prettier/prettier': ['warn', {}, { usePrettierrc: true }],
// TypeScript Import Rules
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: false,
},
],
},
};

View File

@@ -8,7 +8,8 @@ import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/com
import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import HomeScreen from './src/screens/HomeScreen';
import { screenMap, type ScreenContext, type ScreenRoute } from './src/screens';
import type { ScreenContext, ScreenRoute } from './src/screens';
import { screenMap } from './src/screens';
import SelfClientProvider from './src/providers/SelfClientProvider';
type SelectedDocumentState = {

View File

@@ -5,7 +5,8 @@
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { buildValidationRows, humanizeDocumentType, type NormalizedMRZResult } from '../utils/camera';
import type { NormalizedMRZResult } from '../utils/camera';
import { buildValidationRows, humanizeDocumentType } from '../utils/camera';
interface Props {
result: NormalizedMRZResult;

View File

@@ -3,7 +3,8 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import { ScrollView, ScrollViewProps, StyleSheet } from 'react-native';
import type { ScrollViewProps } from 'react-native';
import { ScrollView, StyleSheet } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
type Props = ScrollViewProps & {

View File

@@ -3,7 +3,8 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import { StyleSheet, View, type ViewStyle } from 'react-native';
import type { ViewStyle } from 'react-native';
import { StyleSheet, View } from 'react-native';
import SafeAreaScrollView from './SafeAreaScrollView';
import StandardHeader from './StandardHeader';

View File

@@ -8,7 +8,8 @@ import { AccessibilityInfo, PermissionsAndroid, Platform } from 'react-native';
import type { MRZInfo } from '@selfxyz/mobile-sdk-alpha';
import { useReadMRZ } from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
import { normalizeMRZPayload, type NormalizedMRZResult } from '../utils/camera';
import type { NormalizedMRZResult } from '../utils/camera';
import { normalizeMRZPayload } from '../utils/camera';
type PermissionState = 'loading' | 'granted' | 'denied';
type ScanState = 'idle' | 'scanning' | 'success' | 'error';

View File

@@ -8,7 +8,8 @@ import { StyleSheet, Text, View } from 'react-native';
import Logo from '../assets/images/logo.svg';
import SafeAreaScrollView from '../components/SafeAreaScrollView';
import MenuButton from '../components/MenuButton';
import { orderedSectionEntries, type ScreenContext } from './index';
import type { ScreenContext } from './index';
import { orderedSectionEntries } from './index';
type Props = {
screenContext: ScreenContext;

View File

@@ -2,7 +2,8 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { type MRZInfo, type MRZValidation, extractMRZInfo } from '@selfxyz/mobile-sdk-alpha';
import type { MRZInfo, MRZValidation } from '@selfxyz/mobile-sdk-alpha';
import { extractMRZInfo } from '@selfxyz/mobile-sdk-alpha';
export type MRZPayload = MRZInfo | (MRZInfo & { rawMRZ?: string; raw?: string; mrzString?: string }) | string;

View File

@@ -4,7 +4,8 @@ import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import HomeScreen from '../../src/screens/HomeScreen';
import { orderedSectionEntries, type ScreenContext } from '../../src/screens';
import type { ScreenContext } from '../../src/screens';
import { orderedSectionEntries } from '../../src/screens';
describe('HomeScreen', () => {
const createContext = (): ScreenContext => ({

View File

@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import { orderedSectionEntries, screenDescriptors, screenMap, type ScreenContext } from '../../src/screens';
import type { ScreenContext } from '../../src/screens';
import { orderedSectionEntries, screenDescriptors, screenMap } from '../../src/screens';
describe('screen descriptor index', () => {
const createContext = (): ScreenContext => ({

View File

@@ -7346,8 +7346,9 @@ __metadata:
"@testing-library/react-native": "npm:^13.3.3"
"@tsconfig/react-native": "npm:^3.0.6"
"@types/add": "npm:^2"
"@types/bn.js": "npm:^5.2.0"
"@types/dompurify": "npm:^3.2.0"
"@types/elliptic": "npm:^6"
"@types/elliptic": "npm:^6.4.18"
"@types/jest": "npm:^29.5.14"
"@types/node": "npm:^22.18.3"
"@types/node-forge": "npm:^1.3.14"
@@ -11506,7 +11507,7 @@ __metadata:
languageName: node
linkType: hard
"@types/bn.js@npm:*, @types/bn.js@npm:^5.1.0":
"@types/bn.js@npm:*, @types/bn.js@npm:^5.1.0, @types/bn.js@npm:^5.2.0":
version: 5.2.0
resolution: "@types/bn.js@npm:5.2.0"
dependencies:
@@ -11629,7 +11630,7 @@ __metadata:
languageName: node
linkType: hard
"@types/elliptic@npm:^6":
"@types/elliptic@npm:^6.4.18":
version: 6.4.18
resolution: "@types/elliptic@npm:6.4.18"
dependencies: