mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -05:00
Move Country and ID Picker and more (#1286)
* update fonts * move haptics * fix sorting isues * dynamic import of peer dependency seems wise * moves Country Picker Screen * remove unused and incorrect countryname from doc pick event. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * svg works Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2,199 +2,17 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { memo, useCallback } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import { commonNames } from '@selfxyz/common/constants/countries';
|
||||
import {
|
||||
SdkEvents,
|
||||
useCountries,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
BodyText,
|
||||
RoundFlag,
|
||||
XStack,
|
||||
YStack,
|
||||
} from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { YStack } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
|
||||
|
||||
import { DocumentFlowNavBar } from '@/components/NavBar/DocumentFlowNavBar';
|
||||
import { black, slate100, slate500 } from '@/utils/colors';
|
||||
import { advercase, dinot } from '@/utils/fonts';
|
||||
import { buttonTap } from '@/utils/haptic';
|
||||
|
||||
interface CountryListItem {
|
||||
key: string;
|
||||
countryCode: string;
|
||||
}
|
||||
|
||||
const ITEM_HEIGHT = 65;
|
||||
const FLAG_SIZE = 32;
|
||||
|
||||
const CountryItem = memo<{
|
||||
countryCode: string;
|
||||
onSelect: (code: string) => void;
|
||||
}>(({ countryCode, onSelect }) => {
|
||||
const countryName = commonNames[countryCode as keyof typeof commonNames];
|
||||
|
||||
if (!countryName) return null;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => onSelect(countryCode)}
|
||||
style={{
|
||||
paddingVertical: 13,
|
||||
}}
|
||||
>
|
||||
<XStack alignItems="center" gap={16}>
|
||||
<RoundFlag countryCode={countryCode} size={FLAG_SIZE} />
|
||||
<BodyText style={{ fontSize: 16, color: black, flex: 1 }}>
|
||||
{countryName}
|
||||
</BodyText>
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
CountryItem.displayName = 'CountryItem';
|
||||
|
||||
const CountryPickerScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
|
||||
const { countryData, countryList, loading, userCountryCode, showSuggestion } =
|
||||
useCountries();
|
||||
|
||||
const onPressCountry = useCallback(
|
||||
(countryCode: string) => {
|
||||
buttonTap();
|
||||
if (__DEV__) {
|
||||
console.log('Selected country code:', countryCode);
|
||||
console.log('Current countryData:', countryData);
|
||||
console.log('Available country codes:', Object.keys(countryData));
|
||||
}
|
||||
const documentTypes = countryData[countryCode];
|
||||
if (__DEV__) {
|
||||
console.log('documentTypes for', countryCode, ':', documentTypes);
|
||||
}
|
||||
|
||||
if (documentTypes && documentTypes.length > 0) {
|
||||
const countryName =
|
||||
commonNames[countryCode as keyof typeof commonNames] || countryCode;
|
||||
|
||||
// Emit the country selection event
|
||||
selfClient.emit(SdkEvents.DOCUMENT_COUNTRY_SELECTED, {
|
||||
countryCode: countryCode,
|
||||
countryName: countryName,
|
||||
documentTypes: documentTypes,
|
||||
});
|
||||
} else {
|
||||
selfClient.emit(SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED, {
|
||||
countryCode: countryCode,
|
||||
documentCategory: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
[countryData, selfClient],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: CountryListItem }) => (
|
||||
<CountryItem countryCode={item.countryCode} onSelect={onPressCountry} />
|
||||
),
|
||||
[onPressCountry],
|
||||
);
|
||||
|
||||
const keyExtractor = useCallback(
|
||||
(item: CountryListItem) => item.countryCode,
|
||||
[],
|
||||
);
|
||||
|
||||
const renderLoadingState = () => (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="small" />
|
||||
</View>
|
||||
);
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
(_data: ArrayLike<CountryListItem> | null | undefined, index: number) => ({
|
||||
length: ITEM_HEIGHT,
|
||||
offset: ITEM_HEIGHT * index,
|
||||
index,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
import { slate100 } from '@/utils/colors';
|
||||
|
||||
export default function CountryPickerScreen() {
|
||||
return (
|
||||
<YStack flex={1} backgroundColor={slate100}>
|
||||
<DocumentFlowNavBar title="GETTING STARTED" />
|
||||
<YStack flex={1} paddingTop="$4" paddingHorizontal="$4">
|
||||
<YStack marginTop="$4" marginBottom="$6">
|
||||
<BodyText style={{ fontSize: 29, fontFamily: advercase }}>
|
||||
Select the country that issued your ID
|
||||
</BodyText>
|
||||
<BodyText style={{ fontSize: 16, color: slate500, marginTop: 20 }}>
|
||||
Self has support for over 300 ID types. You can select the type of
|
||||
ID in the next step
|
||||
</BodyText>
|
||||
</YStack>
|
||||
{loading ? (
|
||||
renderLoadingState()
|
||||
) : (
|
||||
<YStack flex={1}>
|
||||
{showSuggestion && (
|
||||
<YStack marginBottom="$2">
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: black,
|
||||
fontFamily: dinot,
|
||||
letterSpacing: 0.8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
SUGGESTION
|
||||
</BodyText>
|
||||
<CountryItem
|
||||
countryCode={
|
||||
userCountryCode as string /*safe due to showSuggestion*/
|
||||
}
|
||||
onSelect={onPressCountry}
|
||||
/>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: black,
|
||||
fontFamily: dinot,
|
||||
letterSpacing: 0.8,
|
||||
marginTop: 20,
|
||||
}}
|
||||
>
|
||||
SELECT AN ISSUING COUNTRY
|
||||
</BodyText>
|
||||
</YStack>
|
||||
)}
|
||||
<FlatList
|
||||
data={countryList}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
showsVerticalScrollIndicator={false}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
windowSize={10}
|
||||
initialNumToRender={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
getItemLayout={getItemLayout}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
</YStack>
|
||||
<SDKCountryPickerScreen />
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CountryPickerScreen;
|
||||
}
|
||||
|
||||
@@ -7,98 +7,20 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
import { useRoute } from '@react-navigation/native';
|
||||
|
||||
import { SdkEvents, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
BodyText,
|
||||
RoundFlag,
|
||||
View,
|
||||
XStack,
|
||||
YStack,
|
||||
} from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import AadhaarLogo from '@selfxyz/mobile-sdk-alpha/svgs/icons/aadhaar.svg';
|
||||
import EPassportLogoRounded from '@selfxyz/mobile-sdk-alpha/svgs/icons/epassport_rounded.svg';
|
||||
import PlusIcon from '@selfxyz/mobile-sdk-alpha/svgs/icons/plus.svg';
|
||||
import SelfLogo from '@selfxyz/mobile-sdk-alpha/svgs/logo.svg';
|
||||
import { YStack } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import IDSelection from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-screen';
|
||||
|
||||
import { DocumentFlowNavBar } from '@/components/NavBar/DocumentFlowNavBar';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { black, slate100, slate300, slate400, white } from '@/utils/colors';
|
||||
import { slate100 } from '@/utils/colors';
|
||||
import { extraYPadding } from '@/utils/constants';
|
||||
import { advercase, dinot } from '@/utils/fonts';
|
||||
import { buttonTap } from '@/utils/haptic';
|
||||
|
||||
type IDPickerScreenRouteProp = RouteProp<RootStackParamList, 'IDPicker'>;
|
||||
|
||||
const getDocumentName = (docType: string): string => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return 'Passport';
|
||||
case 'i':
|
||||
return 'ID card';
|
||||
case 'a':
|
||||
return 'Aadhaar';
|
||||
default:
|
||||
return 'Unknown Document';
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentNameForEvent = (docType: string): string => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return 'passport';
|
||||
case 'i':
|
||||
return 'id_card';
|
||||
case 'a':
|
||||
return 'aadhaar';
|
||||
default:
|
||||
return 'unknown_document';
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentDescription = (docType: string): string => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return 'Verified Biometric Passport';
|
||||
case 'i':
|
||||
return 'Verified Biometric ID card';
|
||||
case 'a':
|
||||
return 'Verified mAadhaar QR code';
|
||||
default:
|
||||
return 'Unknown Document';
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentLogo = (docType: string): React.ReactNode => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return <EPassportLogoRounded />;
|
||||
case 'i':
|
||||
return <EPassportLogoRounded />;
|
||||
case 'a':
|
||||
return <AadhaarLogo />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const IDPickerScreen: React.FC = () => {
|
||||
const route = useRoute<IDPickerScreenRouteProp>();
|
||||
const { countryCode = '', documentTypes = [] } = route.params || {};
|
||||
const bottom = useSafeAreaInsets().bottom;
|
||||
const selfClient = useSelfClient();
|
||||
|
||||
const onSelectDocumentType = (docType: string) => {
|
||||
buttonTap();
|
||||
|
||||
const countryName = getDocumentName(docType);
|
||||
|
||||
selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, {
|
||||
documentType: docType,
|
||||
documentName: getDocumentNameForEvent(docType),
|
||||
countryCode: countryCode,
|
||||
countryName: countryName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<YStack
|
||||
@@ -107,94 +29,7 @@ const IDPickerScreen: React.FC = () => {
|
||||
paddingBottom={bottom + extraYPadding + 24}
|
||||
>
|
||||
<DocumentFlowNavBar title="GETTING STARTED" />
|
||||
<YStack
|
||||
flex={1}
|
||||
paddingTop="$4"
|
||||
paddingHorizontal="$4"
|
||||
justifyContent="center"
|
||||
>
|
||||
<YStack marginTop="$4" marginBottom="$6">
|
||||
<XStack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
borderRadius={'$2'}
|
||||
gap={'$2.5'}
|
||||
>
|
||||
<View width={48} height={48}>
|
||||
<RoundFlag countryCode={countryCode} size={48} />
|
||||
</View>
|
||||
<PlusIcon width={18} height={18} color={slate400} />
|
||||
<YStack
|
||||
backgroundColor={black}
|
||||
borderRadius={'$2'}
|
||||
height={48}
|
||||
width={48}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<SelfLogo width={24} height={24} />
|
||||
</YStack>
|
||||
</XStack>
|
||||
<BodyText
|
||||
style={{
|
||||
marginTop: 48,
|
||||
fontSize: 29,
|
||||
fontFamily: advercase,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Select an ID type
|
||||
</BodyText>
|
||||
</YStack>
|
||||
<YStack gap="$3">
|
||||
{documentTypes.map((docType: string) => (
|
||||
<XStack
|
||||
key={docType}
|
||||
backgroundColor={white}
|
||||
borderWidth={1}
|
||||
borderColor={slate300}
|
||||
elevation={4}
|
||||
borderRadius={'$5'}
|
||||
padding={'$3'}
|
||||
pressStyle={{
|
||||
transform: [{ scale: 0.97 }],
|
||||
backgroundColor: slate100,
|
||||
}}
|
||||
onPress={() => onSelectDocumentType(docType)}
|
||||
>
|
||||
<XStack alignItems="center" gap={'$3'} flex={1}>
|
||||
{getDocumentLogo(docType)}
|
||||
<YStack gap={'$1'}>
|
||||
<BodyText
|
||||
style={{ fontSize: 24, fontFamily: dinot, color: black }}
|
||||
>
|
||||
{getDocumentName(docType)}
|
||||
</BodyText>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontFamily: dinot,
|
||||
color: slate400,
|
||||
}}
|
||||
>
|
||||
{getDocumentDescription(docType)}
|
||||
</BodyText>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
))}
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 18,
|
||||
fontFamily: dinot,
|
||||
color: slate400,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Be sure your document is ready to scan
|
||||
</BodyText>
|
||||
</YStack>
|
||||
</YStack>
|
||||
<IDSelection countryCode={countryCode} documentTypes={documentTypes} />
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,9 +2,4 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export const advercase = 'Advercase-Regular';
|
||||
export const dinot = 'DINOT-Medium';
|
||||
export const plexMono =
|
||||
Platform.OS === 'ios' ? 'IBM Plex Mono' : 'IBMPlexMono-Regular';
|
||||
export { advercase, dinot, plexMono } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
@@ -2,140 +2,21 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Platform, Vibration } from 'react-native';
|
||||
|
||||
import { triggerFeedback } from '@/utils/haptic/trigger';
|
||||
|
||||
// Keep track of the loading screen interval
|
||||
let loadingScreenInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// Define the base functions first
|
||||
export const impactLight = () => triggerFeedback('impactLight');
|
||||
export const impactMedium = () => triggerFeedback('impactMedium');
|
||||
export const selectionChange = () => triggerFeedback('selection');
|
||||
|
||||
// Then define the aliases
|
||||
export const buttonTap = impactLight;
|
||||
export const cancelTap = selectionChange;
|
||||
export const confirmTap = impactMedium;
|
||||
|
||||
// consistent light feedback at a steady interval
|
||||
export const feedbackProgress = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
// Pattern: [delay, duration, delay, duration, ...]
|
||||
// Three light impacts at 750ms intervals
|
||||
triggerFeedback('custom', {
|
||||
pattern: [
|
||||
0,
|
||||
50, // First light impact
|
||||
750,
|
||||
50, // Second light impact
|
||||
750,
|
||||
50, // Third light impact
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Match the timing of the light impacts in the Android pattern
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 750); // First light impact
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 1500); // Second light impact (750ms after first)
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 2250); // Third light impact (750ms after second)
|
||||
};
|
||||
|
||||
// light -> medium -> heavy intensity in sequence
|
||||
export const feedbackSuccess = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
// Pattern: [delay, duration, delay, duration, ...]
|
||||
// Increasing intensity sequence: light -> medium -> heavy
|
||||
triggerFeedback('custom', {
|
||||
pattern: [
|
||||
500,
|
||||
50, // Initial delay, then light impact
|
||||
200,
|
||||
100, // Medium impact
|
||||
150,
|
||||
150, // Heavy impact
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactMedium');
|
||||
}, 750);
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactHeavy');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// heavy -> medium -> light intensity in sequence
|
||||
export const feedbackUnsuccessful = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
// Pattern: [delay, duration, delay, duration, ...]
|
||||
// Decreasing intensity sequence: heavy -> medium -> light
|
||||
triggerFeedback('custom', {
|
||||
pattern: [
|
||||
500,
|
||||
150, // Initial delay, then heavy impact
|
||||
100,
|
||||
100, // Medium impact
|
||||
150,
|
||||
50, // Light impact
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactHeavy');
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactMedium');
|
||||
}, 750);
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Haptic actions
|
||||
*/
|
||||
|
||||
// Custom feedback events
|
||||
export const loadingScreenProgress = (shouldVibrate: boolean = true) => {
|
||||
// Clear any existing interval
|
||||
if (loadingScreenInterval) {
|
||||
clearInterval(loadingScreenInterval);
|
||||
loadingScreenInterval = null;
|
||||
}
|
||||
|
||||
// If we shouldn't vibrate, just stop here
|
||||
if (!shouldVibrate) {
|
||||
Vibration.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
triggerFeedback('impactHeavy');
|
||||
|
||||
loadingScreenInterval = setInterval(() => {
|
||||
triggerFeedback('impactHeavy');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
export const notificationError = () => triggerFeedback('notificationError');
|
||||
|
||||
export const notificationSuccess = () => triggerFeedback('notificationSuccess');
|
||||
|
||||
export const notificationWarning = () => triggerFeedback('notificationWarning');
|
||||
|
||||
export { triggerFeedback } from '@/utils/haptic/trigger';
|
||||
// Re-export all haptic functionality from the mobile SDK
|
||||
export {
|
||||
buttonTap,
|
||||
cancelTap,
|
||||
confirmTap,
|
||||
feedbackProgress,
|
||||
feedbackSuccess,
|
||||
feedbackUnsuccessful,
|
||||
impactLight,
|
||||
impactMedium,
|
||||
loadingScreenProgress,
|
||||
notificationError,
|
||||
notificationSuccess,
|
||||
notificationWarning,
|
||||
selectionChange,
|
||||
triggerFeedback,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
export type { HapticOptions, HapticType } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
@@ -2,25 +2,6 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export type HapticOptions = {
|
||||
enableVibrateFallback?: boolean;
|
||||
ignoreAndroidSystemSettings?: boolean;
|
||||
pattern?: number[];
|
||||
increaseIosIntensity?: boolean;
|
||||
};
|
||||
|
||||
export type HapticType =
|
||||
| 'selection'
|
||||
| 'impactLight'
|
||||
| 'impactMedium'
|
||||
| 'impactHeavy'
|
||||
| 'notificationSuccess'
|
||||
| 'notificationWarning'
|
||||
| 'notificationError';
|
||||
|
||||
export const defaultOptions: HapticOptions = {
|
||||
enableVibrateFallback: true,
|
||||
ignoreAndroidSystemSettings: false,
|
||||
pattern: [50, 100, 50],
|
||||
increaseIosIntensity: true,
|
||||
};
|
||||
// Re-export types and defaults from the mobile SDK
|
||||
export type { HapticOptions, HapticType } from '@selfxyz/mobile-sdk-alpha';
|
||||
export { defaultOptions } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
@@ -2,39 +2,5 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Platform, Vibration } from 'react-native';
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||
|
||||
import type { HapticOptions, HapticType } from '@/utils/haptic/shared';
|
||||
import { defaultOptions } from '@/utils/haptic/shared';
|
||||
/**
|
||||
* Triggers haptic feedback or vibration based on platform.
|
||||
* @param type - The haptic feedback type.
|
||||
* @param options - Custom options (optional).
|
||||
*/
|
||||
export const triggerFeedback = (
|
||||
type: HapticType | 'custom',
|
||||
options: HapticOptions = {},
|
||||
) => {
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
if (Platform.OS === 'ios' && type !== 'custom') {
|
||||
if (mergedOptions.increaseIosIntensity) {
|
||||
if (type === 'impactLight') {
|
||||
type = 'impactMedium';
|
||||
} else if (type === 'impactMedium') {
|
||||
type = 'impactHeavy';
|
||||
}
|
||||
}
|
||||
|
||||
ReactNativeHapticFeedback.trigger(type, {
|
||||
enableVibrateFallback: mergedOptions.enableVibrateFallback,
|
||||
ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings,
|
||||
});
|
||||
} else {
|
||||
if (mergedOptions.pattern) {
|
||||
Vibration.vibrate(mergedOptions.pattern, false);
|
||||
} else {
|
||||
Vibration.vibrate(100);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Re-export triggerFeedback from the mobile SDK
|
||||
export { triggerFeedback } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
@@ -2,29 +2,5 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { HapticOptions, HapticType } from '@/utils/haptic/shared';
|
||||
import { defaultOptions } from '@/utils/haptic/shared';
|
||||
|
||||
/**
|
||||
* Triggers haptic feedback or vibration based on platform.
|
||||
* @param type - The haptic feedback type. (only here for compatibility, not used in web)
|
||||
* @param options - Custom options (optional).
|
||||
*/
|
||||
export const triggerFeedback = (
|
||||
_type: HapticType | 'custom',
|
||||
options: HapticOptions = {},
|
||||
) => {
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
// Check if Vibration API is available
|
||||
if (!navigator.vibrate) {
|
||||
console.warn('Vibration API not supported in this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
if (mergedOptions.pattern) {
|
||||
navigator.vibrate(mergedOptions.pattern);
|
||||
} else {
|
||||
navigator.vibrate(100);
|
||||
}
|
||||
};
|
||||
// Re-export triggerFeedback from the mobile SDK
|
||||
export { triggerFeedback } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
@@ -108,6 +108,13 @@ module.exports = {
|
||||
'sort-exports/sort-exports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Disable export sorting for files with dependency issues
|
||||
files: ['src/haptic/index.ts'],
|
||||
rules: {
|
||||
'sort-exports/sort-exports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Allow require imports only in the NFC decoder shim that conditionally imports node:util
|
||||
files: ['src/processing/nfc.ts'],
|
||||
|
||||
@@ -209,8 +209,8 @@ selfClient.on(SdkEvents.DOCUMENT_COUNTRY_SELECTED, payload => {
|
||||
|
||||
```ts
|
||||
selfClient.on(SdkEvents.DOCUMENT_TYPE_SELECTED, payload => {
|
||||
// payload: { documentType: string, documentName: string, countryCode: string, countryName: string }
|
||||
console.log(`Document selected: ${payload.documentName} from ${payload.countryName}`);
|
||||
// payload: { documentType: string, documentName: string, countryCode: string }
|
||||
console.log(`Document selected: ${payload.documentName} from ${payload.countryCode}`);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -70,14 +70,14 @@
|
||||
"require": "./dist/cjs/hooks/index.cjs"
|
||||
},
|
||||
"./svgs/*.svg": {
|
||||
"react-native": "./svgs/*.svg",
|
||||
"import": "./svgs/*.svg",
|
||||
"require": "./svgs/*.svg"
|
||||
"react-native": "./dist/svgs/*.svg",
|
||||
"import": "./dist/svgs/*.svg",
|
||||
"require": "./dist/svgs/*.svg"
|
||||
},
|
||||
"./svgs/icons/*.svg": {
|
||||
"react-native": "./svgs/icons/*.svg",
|
||||
"import": "./svgs/icons/*.svg",
|
||||
"require": "./svgs/icons/*.svg"
|
||||
"react-native": "./dist/svgs/icons/*.svg",
|
||||
"import": "./dist/svgs/icons/*.svg",
|
||||
"require": "./dist/svgs/icons/*.svg"
|
||||
}
|
||||
},
|
||||
"main": "./dist/cjs/index.cjs",
|
||||
@@ -141,6 +141,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-localize": "^3.5.2",
|
||||
"react-native-web": "^0.21.1",
|
||||
"tsup": "^8.0.1",
|
||||
@@ -150,6 +151,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-haptic-feedback": "*",
|
||||
"react-native-localize": "*",
|
||||
"react-native-svg": "*"
|
||||
},
|
||||
|
||||
36
packages/mobile-sdk-alpha/scripts/copy-assets.mjs
Normal file
36
packages/mobile-sdk-alpha/scripts/copy-assets.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
// 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 { cpSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
function copyAssets() {
|
||||
const sourceDir = join(rootDir, 'svgs');
|
||||
const targetSvgDir = join(rootDir, 'dist/svgs');
|
||||
|
||||
if (!existsSync(sourceDir)) {
|
||||
console.log('No svgs directory found, skipping asset copy');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create target directory if it doesn't exist
|
||||
mkdirSync(targetSvgDir, { recursive: true });
|
||||
|
||||
// Copy SVGs to single shared location in dist
|
||||
try {
|
||||
cpSync(sourceDir, targetSvgDir, { recursive: true });
|
||||
console.log('✅ SVG assets copied to dist/svgs');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to copy SVG assets:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
copyAssets();
|
||||
@@ -27,18 +27,39 @@ export type {
|
||||
} from './types/public';
|
||||
|
||||
export type { DG1, DG2, ParsedNFCResponse } from './nfc';
|
||||
export type { HapticOptions, HapticType } from './haptic/shared';
|
||||
export type { MRZScanOptions } from './mrz';
|
||||
export type { PassportValidationCallbacks } from './validation/document';
|
||||
|
||||
export type { SDKEvent, SDKEventMap } from './types/events';
|
||||
|
||||
export type { SdkErrorCategory } from './errors';
|
||||
export { type ProvingStateType } from './proving/provingMachine';
|
||||
|
||||
export { type ProvingStateType } from './proving/provingMachine';
|
||||
export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors';
|
||||
|
||||
export { SdkEvents } from './types/events';
|
||||
|
||||
export { SelfClientContext, SelfClientProvider, useSelfClient } from './context';
|
||||
|
||||
export { advercase, dinot, plexMono } from './constants/fonts';
|
||||
|
||||
export {
|
||||
buttonTap,
|
||||
cancelTap,
|
||||
confirmTap,
|
||||
feedbackProgress,
|
||||
feedbackSuccess,
|
||||
feedbackUnsuccessful,
|
||||
impactLight,
|
||||
impactMedium,
|
||||
loadingScreenProgress,
|
||||
notificationError,
|
||||
notificationSuccess,
|
||||
notificationWarning,
|
||||
selectionChange,
|
||||
triggerFeedback,
|
||||
} from './haptic';
|
||||
|
||||
export {
|
||||
clearPassportData,
|
||||
getAllDocuments,
|
||||
@@ -56,6 +77,7 @@ export { defaultConfig } from './config/defaults';
|
||||
export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD } from './mrz';
|
||||
|
||||
export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator';
|
||||
|
||||
// Core functions
|
||||
export { isPassportDataValid } from './validation/document';
|
||||
|
||||
@@ -64,6 +86,5 @@ export { mergeConfig } from './config/merge';
|
||||
export { parseNFCResponse, scanNFC } from './nfc';
|
||||
|
||||
export { reactNativeScannerAdapter } from './adapters/react-native/nfc-scanner';
|
||||
|
||||
export { useCountries } from './documents/useCountries';
|
||||
export { webNFCScannerShim } from './adapters/web/shims';
|
||||
|
||||
@@ -6,8 +6,8 @@ import type React from 'react';
|
||||
import type { GestureResponderEvent, LayoutChangeEvent, PressableProps, ViewStyle } from 'react-native';
|
||||
import { Platform, Pressable, StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { dinot } from '../../constants/fonts';
|
||||
import { useSelfClient } from '../../context';
|
||||
import { dinot } from '../../utils/fonts';
|
||||
import { pressedStyle } from './pressedStyle';
|
||||
|
||||
export interface ButtonProps extends PressableProps {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { TextProps } from 'react-native';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { slate400 } from '../../constants/colors';
|
||||
import { dinot } from '../../utils/fonts';
|
||||
import { dinot } from '../../constants/fonts';
|
||||
|
||||
type AdditionalProps = TextProps;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { TextProps } from 'react-native';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { slate500 } from '../../constants/colors';
|
||||
import { dinot } from '../../utils/fonts';
|
||||
import { dinot } from '../../constants/fonts';
|
||||
|
||||
export const BodyText: React.FC<TextProps> = ({ style, ...props }) => (
|
||||
<Text style={[{ fontFamily: dinot, color: slate500 }, style]} {...props} />
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { TextProps } from 'react-native';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { slate700 } from '../../constants/colors';
|
||||
import { dinot } from '../../utils/fonts';
|
||||
import { dinot } from '../../constants/fonts';
|
||||
|
||||
type CautionProps = TextProps;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { TextProps } from 'react-native';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { slate500 } from '../../constants/colors';
|
||||
import { dinot } from '../../utils/fonts';
|
||||
import { dinot } from '../../constants/fonts';
|
||||
|
||||
type DescriptionProps = TextProps & {
|
||||
color?: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import type React from 'react';
|
||||
import type { TextProps } from 'react-native';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { dinot } from '../../utils/fonts';
|
||||
import { dinot } from '../../constants/fonts';
|
||||
|
||||
export const DescriptionTitle: React.FC<TextProps> = ({ style, ...props }) => (
|
||||
<Text
|
||||
|
||||
@@ -6,7 +6,7 @@ import type React from 'react';
|
||||
import type { TextProps } from 'react-native';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { dinot } from '../../utils/fonts';
|
||||
import { dinot } from '../../constants/fonts';
|
||||
|
||||
export const SubHeader: React.FC<TextProps> = ({ style, ...props }) => (
|
||||
<Text
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { StyleProp, TextProps, TextStyle } from 'react-native';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { black } from '../../constants/colors';
|
||||
import { advercase } from '../../utils/fonts';
|
||||
import { advercase } from '../../constants/fonts';
|
||||
|
||||
type TitleProps = TextProps & {
|
||||
size?: 'large';
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// 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 { memo, useCallback } from 'react';
|
||||
import { ActivityIndicator, FlatList, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
import { commonNames } from '@selfxyz/common/constants/countries';
|
||||
|
||||
import { BodyText, RoundFlag, XStack, YStack } from '../../components';
|
||||
import { black, slate100, slate500 } from '../../constants/colors';
|
||||
import { advercase, dinot } from '../../constants/fonts';
|
||||
import { useSelfClient } from '../../context';
|
||||
import { useCountries } from '../../documents/useCountries';
|
||||
import { buttonTap } from '../../haptic';
|
||||
import { SdkEvents } from '../../types/events';
|
||||
|
||||
interface CountryListItem {
|
||||
key: string;
|
||||
countryCode: string;
|
||||
}
|
||||
|
||||
const ITEM_HEIGHT = 65;
|
||||
const FLAG_SIZE = 32;
|
||||
|
||||
const CountryItem = memo<{
|
||||
countryCode: string;
|
||||
onSelect: (code: string) => void;
|
||||
}>(({ countryCode, onSelect }) => {
|
||||
const countryName = commonNames[countryCode as keyof typeof commonNames];
|
||||
|
||||
if (!countryName) return null;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => onSelect(countryCode)}
|
||||
style={{
|
||||
paddingVertical: 13,
|
||||
}}
|
||||
>
|
||||
<XStack alignItems="center" gap={16}>
|
||||
<RoundFlag countryCode={countryCode} size={FLAG_SIZE} />
|
||||
<BodyText style={{ fontSize: 16, color: black, flex: 1 }}>{countryName}</BodyText>
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
CountryItem.displayName = 'CountryItem';
|
||||
|
||||
const Loading = () => (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="small" />
|
||||
</View>
|
||||
);
|
||||
Loading.displayName = 'Loading';
|
||||
|
||||
const CountryPickerScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
|
||||
const { countryData, countryList, loading, userCountryCode, showSuggestion } = useCountries();
|
||||
|
||||
const onPressCountry = useCallback(
|
||||
(countryCode: string) => {
|
||||
buttonTap();
|
||||
if (__DEV__) {
|
||||
console.log('Selected country code:', countryCode);
|
||||
console.log('Current countryData:', countryData);
|
||||
console.log('Available country codes:', Object.keys(countryData));
|
||||
}
|
||||
const documentTypes = countryData[countryCode];
|
||||
if (__DEV__) {
|
||||
console.log('documentTypes for', countryCode, ':', documentTypes);
|
||||
}
|
||||
|
||||
if (documentTypes && documentTypes.length > 0) {
|
||||
const countryName = commonNames[countryCode as keyof typeof commonNames] || countryCode;
|
||||
|
||||
// Emit the country selection event
|
||||
selfClient.emit(SdkEvents.DOCUMENT_COUNTRY_SELECTED, {
|
||||
countryCode: countryCode,
|
||||
countryName: countryName,
|
||||
documentTypes: documentTypes,
|
||||
});
|
||||
} else {
|
||||
selfClient.emit(SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED, {
|
||||
countryCode: countryCode,
|
||||
documentCategory: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
[countryData, selfClient],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: CountryListItem }) => <CountryItem countryCode={item.countryCode} onSelect={onPressCountry} />,
|
||||
[onPressCountry],
|
||||
);
|
||||
|
||||
const keyExtractor = useCallback((item: CountryListItem) => item.countryCode, []);
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
(_data: ArrayLike<CountryListItem> | null | undefined, index: number) => ({
|
||||
length: ITEM_HEIGHT,
|
||||
offset: ITEM_HEIGHT * index,
|
||||
index,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<YStack flex={1} paddingTop="$4" paddingHorizontal="$4" backgroundColor={slate100}>
|
||||
<YStack marginTop="$4" marginBottom="$6">
|
||||
<BodyText style={{ fontSize: 29, fontFamily: advercase }}>Select the country that issued your ID</BodyText>
|
||||
<BodyText style={{ fontSize: 16, color: slate500, marginTop: 20 }}>
|
||||
Self has support for over 300 ID types. You can select the type of ID in the next step
|
||||
</BodyText>
|
||||
</YStack>
|
||||
{loading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<YStack flex={1}>
|
||||
{showSuggestion && (
|
||||
<YStack marginBottom="$2">
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: black,
|
||||
fontFamily: dinot,
|
||||
letterSpacing: 0.8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
SUGGESTION
|
||||
</BodyText>
|
||||
<CountryItem
|
||||
countryCode={userCountryCode as string /*safe due to showSuggestion*/}
|
||||
onSelect={onPressCountry}
|
||||
/>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: black,
|
||||
fontFamily: dinot,
|
||||
letterSpacing: 0.8,
|
||||
marginTop: 20,
|
||||
}}
|
||||
>
|
||||
SELECT AN ISSUING COUNTRY
|
||||
</BodyText>
|
||||
</YStack>
|
||||
)}
|
||||
<FlatList
|
||||
data={countryList}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
showsVerticalScrollIndicator={false}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
windowSize={10}
|
||||
initialNumToRender={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
getItemLayout={getItemLayout}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
CountryPickerScreen.displayName = 'CountryPickerScreen';
|
||||
|
||||
export default CountryPickerScreen;
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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 React from 'react';
|
||||
|
||||
import AadhaarLogo from '../../../svgs/icons/aadhaar.svg';
|
||||
import EPassportLogoRounded from '../../../svgs/icons/epassport_rounded.svg';
|
||||
import PlusIcon from '../../../svgs/icons/plus.svg';
|
||||
import SelfLogo from '../../../svgs/logo.svg';
|
||||
import { BodyText, RoundFlag, View, XStack, YStack } from '../../components';
|
||||
import { black, slate100, slate300, slate400, white } from '../../constants/colors';
|
||||
import { advercase, dinot } from '../../constants/fonts';
|
||||
import { useSelfClient } from '../../context';
|
||||
import { buttonTap } from '../../haptic';
|
||||
import { SdkEvents } from '../../types/events';
|
||||
|
||||
const getDocumentName = (docType: string): string => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return 'Passport';
|
||||
case 'i':
|
||||
return 'ID card';
|
||||
case 'a':
|
||||
return 'Aadhaar';
|
||||
default:
|
||||
return 'Unknown Document';
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentNameForEvent = (docType: string): string => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return 'passport';
|
||||
case 'i':
|
||||
return 'id_card';
|
||||
case 'a':
|
||||
return 'aadhaar';
|
||||
default:
|
||||
return 'unknown_document';
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentDescription = (docType: string): string => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return 'Verified Biometric Passport';
|
||||
case 'i':
|
||||
return 'Verified Biometric ID card';
|
||||
case 'a':
|
||||
return 'Verified mAadhaar QR code';
|
||||
default:
|
||||
return 'Unknown Document';
|
||||
}
|
||||
};
|
||||
|
||||
const getDocumentLogo = (docType: string): React.ReactNode => {
|
||||
switch (docType) {
|
||||
case 'p':
|
||||
return <EPassportLogoRounded />;
|
||||
case 'i':
|
||||
return <EPassportLogoRounded />;
|
||||
case 'a':
|
||||
return <AadhaarLogo />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
type IDSelectionScreenProps = {
|
||||
countryCode: string;
|
||||
documentTypes: string[];
|
||||
};
|
||||
|
||||
const IDSelectionScreen: React.FC<IDSelectionScreenProps> = props => {
|
||||
const { countryCode = '', documentTypes = [] } = props;
|
||||
const selfClient = useSelfClient();
|
||||
|
||||
const onSelectDocumentType = (docType: string) => {
|
||||
buttonTap();
|
||||
|
||||
selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, {
|
||||
documentType: docType,
|
||||
documentName: getDocumentNameForEvent(docType),
|
||||
countryCode: countryCode,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<YStack flex={1} paddingTop="$4" paddingHorizontal="$4" justifyContent="center">
|
||||
<YStack marginTop="$4" marginBottom="$6">
|
||||
<XStack justifyContent="center" alignItems="center" borderRadius={'$2'} gap={'$2.5'}>
|
||||
<View width={48} height={48}>
|
||||
<RoundFlag countryCode={countryCode} size={48} />
|
||||
</View>
|
||||
<PlusIcon width={18} height={18} color={slate400} />
|
||||
<YStack
|
||||
backgroundColor={black}
|
||||
borderRadius={'$2'}
|
||||
height={48}
|
||||
width={48}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<SelfLogo width={24} height={24} />
|
||||
</YStack>
|
||||
</XStack>
|
||||
<BodyText
|
||||
style={{
|
||||
marginTop: 48,
|
||||
fontSize: 29,
|
||||
fontFamily: advercase,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Select an ID type
|
||||
</BodyText>
|
||||
</YStack>
|
||||
<YStack gap="$3">
|
||||
{documentTypes.map((docType: string) => (
|
||||
<XStack
|
||||
key={docType}
|
||||
backgroundColor={white}
|
||||
borderWidth={1}
|
||||
borderColor={slate300}
|
||||
elevation={4}
|
||||
borderRadius={'$5'}
|
||||
padding={'$3'}
|
||||
pressStyle={{
|
||||
transform: [{ scale: 0.97 }],
|
||||
backgroundColor: slate100,
|
||||
}}
|
||||
onPress={() => onSelectDocumentType(docType)}
|
||||
>
|
||||
<XStack alignItems="center" gap={'$3'} flex={1}>
|
||||
{getDocumentLogo(docType)}
|
||||
<YStack gap={'$1'}>
|
||||
<BodyText style={{ fontSize: 24, fontFamily: dinot, color: black }}>
|
||||
{getDocumentName(docType)}
|
||||
</BodyText>
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontFamily: dinot,
|
||||
color: slate400,
|
||||
}}
|
||||
>
|
||||
{getDocumentDescription(docType)}
|
||||
</BodyText>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
))}
|
||||
<BodyText
|
||||
style={{
|
||||
fontSize: 18,
|
||||
fontFamily: dinot,
|
||||
color: slate400,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Be sure your document is ready to scan
|
||||
</BodyText>
|
||||
</YStack>
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default IDSelectionScreen;
|
||||
144
packages/mobile-sdk-alpha/src/haptic/index.ts
Normal file
144
packages/mobile-sdk-alpha/src/haptic/index.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
// 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 { Platform, Vibration } from 'react-native';
|
||||
|
||||
import { triggerFeedback } from './trigger';
|
||||
|
||||
// Keep track of the loading screen interval
|
||||
let loadingScreenInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// consistent light feedback at a steady interval
|
||||
export const feedbackProgress = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
// Pattern: [delay, duration, delay, duration, ...]
|
||||
// Three light impacts at 750ms intervals
|
||||
triggerFeedback('custom', {
|
||||
pattern: [
|
||||
0,
|
||||
50, // First light impact
|
||||
750,
|
||||
50, // Second light impact
|
||||
750,
|
||||
50, // Third light impact
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Match the timing of the light impacts in the Android pattern
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 750); // First light impact
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 1500); // Second light impact (750ms after first)
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 2250); // Third light impact (750ms after second)
|
||||
};
|
||||
|
||||
// light -> medium -> heavy intensity in sequence
|
||||
export const feedbackSuccess = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
// Pattern: [delay, duration, delay, duration, ...]
|
||||
// Increasing intensity sequence: light -> medium -> heavy
|
||||
triggerFeedback('custom', {
|
||||
pattern: [
|
||||
500,
|
||||
50, // Initial delay, then light impact
|
||||
200,
|
||||
100, // Medium impact
|
||||
150,
|
||||
150, // Heavy impact
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactMedium');
|
||||
}, 750);
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactHeavy');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// heavy -> medium -> light intensity in sequence
|
||||
export const feedbackUnsuccessful = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
// Pattern: [delay, duration, delay, duration, ...]
|
||||
// Decreasing intensity sequence: heavy -> medium -> light
|
||||
triggerFeedback('custom', {
|
||||
pattern: [
|
||||
500,
|
||||
150, // Initial delay, then heavy impact
|
||||
100,
|
||||
100, // Medium impact
|
||||
150,
|
||||
50, // Light impact
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactHeavy');
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactMedium');
|
||||
}, 750);
|
||||
setTimeout(() => {
|
||||
triggerFeedback('impactLight');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// Define the base functions first
|
||||
export const impactLight = () => triggerFeedback('impactLight');
|
||||
|
||||
export const impactMedium = () => triggerFeedback('impactMedium');
|
||||
|
||||
export const selectionChange = () => triggerFeedback('selection');
|
||||
|
||||
// Then define the aliases
|
||||
export const buttonTap = impactLight;
|
||||
|
||||
export const cancelTap = selectionChange;
|
||||
|
||||
export const confirmTap = impactMedium;
|
||||
|
||||
/**
|
||||
* Haptic actions
|
||||
*/
|
||||
// Custom feedback events
|
||||
export const loadingScreenProgress = (shouldVibrate: boolean = true) => {
|
||||
// Clear any existing interval
|
||||
if (loadingScreenInterval) {
|
||||
clearInterval(loadingScreenInterval);
|
||||
loadingScreenInterval = null;
|
||||
}
|
||||
|
||||
// If we shouldn't vibrate, just stop here
|
||||
if (!shouldVibrate) {
|
||||
Vibration.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
triggerFeedback('impactHeavy');
|
||||
|
||||
loadingScreenInterval = setInterval(() => {
|
||||
triggerFeedback('impactHeavy');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
export const notificationError = () => triggerFeedback('notificationError');
|
||||
|
||||
export const notificationSuccess = () => triggerFeedback('notificationSuccess');
|
||||
|
||||
export const notificationWarning = () => triggerFeedback('notificationWarning');
|
||||
|
||||
export { triggerFeedback } from './trigger';
|
||||
26
packages/mobile-sdk-alpha/src/haptic/shared.ts
Normal file
26
packages/mobile-sdk-alpha/src/haptic/shared.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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 HapticOptions = {
|
||||
enableVibrateFallback?: boolean;
|
||||
ignoreAndroidSystemSettings?: boolean;
|
||||
pattern?: number[];
|
||||
increaseIosIntensity?: boolean;
|
||||
};
|
||||
|
||||
export type HapticType =
|
||||
| 'selection'
|
||||
| 'impactLight'
|
||||
| 'impactMedium'
|
||||
| 'impactHeavy'
|
||||
| 'notificationSuccess'
|
||||
| 'notificationWarning'
|
||||
| 'notificationError';
|
||||
|
||||
export const defaultOptions: HapticOptions = {
|
||||
enableVibrateFallback: true,
|
||||
ignoreAndroidSystemSettings: false,
|
||||
pattern: [50, 100, 50],
|
||||
increaseIosIntensity: true,
|
||||
};
|
||||
52
packages/mobile-sdk-alpha/src/haptic/trigger.ts
Normal file
52
packages/mobile-sdk-alpha/src/haptic/trigger.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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 { Platform, Vibration } from 'react-native';
|
||||
|
||||
import type { HapticOptions, HapticType } from './shared';
|
||||
import { defaultOptions } from './shared';
|
||||
/**
|
||||
* Triggers haptic feedback or vibration based on platform.
|
||||
* @param type - The haptic feedback type.
|
||||
* @param options - Custom options (optional).
|
||||
*/
|
||||
export const triggerFeedback = (type: HapticType | 'custom', options: HapticOptions = {}) => {
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
if (Platform.OS === 'ios' && type !== 'custom') {
|
||||
if (mergedOptions.increaseIosIntensity) {
|
||||
if (type === 'impactLight') {
|
||||
type = 'impactMedium';
|
||||
} else if (type === 'impactMedium') {
|
||||
type = 'impactHeavy';
|
||||
}
|
||||
}
|
||||
// Use dynamic import to avoid loading the module on Android or if its not installed
|
||||
(async () => {
|
||||
try {
|
||||
const trigger = await import('react-native-haptic-feedback').then(mod => mod.trigger);
|
||||
trigger(type, {
|
||||
enableVibrateFallback: mergedOptions.enableVibrateFallback,
|
||||
ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings,
|
||||
});
|
||||
} catch {
|
||||
standardVibration(mergedOptions);
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
standardVibration(mergedOptions);
|
||||
}
|
||||
};
|
||||
function standardVibration(mergedOptions: {
|
||||
enableVibrateFallback?: boolean;
|
||||
ignoreAndroidSystemSettings?: boolean;
|
||||
pattern?: number[];
|
||||
increaseIosIntensity?: boolean;
|
||||
}) {
|
||||
if (mergedOptions.pattern) {
|
||||
Vibration.vibrate(mergedOptions.pattern, false);
|
||||
} else {
|
||||
Vibration.vibrate(100);
|
||||
}
|
||||
}
|
||||
27
packages/mobile-sdk-alpha/src/haptic/trigger.web.ts
Normal file
27
packages/mobile-sdk-alpha/src/haptic/trigger.web.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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 { HapticOptions, HapticType } from './shared';
|
||||
import { defaultOptions } from './shared';
|
||||
|
||||
/**
|
||||
* Triggers haptic feedback or vibration based on platform.
|
||||
* @param type - The haptic feedback type. (only here for compatibility, not used in web)
|
||||
* @param options - Custom options (optional).
|
||||
*/
|
||||
export const triggerFeedback = (_type: HapticType | 'custom', options: HapticOptions = {}) => {
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
// Check if Vibration API is available
|
||||
if (!navigator.vibrate) {
|
||||
console.warn('Vibration API not supported in this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
if (mergedOptions.pattern) {
|
||||
navigator.vibrate(mergedOptions.pattern);
|
||||
} else {
|
||||
navigator.vibrate(100);
|
||||
}
|
||||
};
|
||||
@@ -36,6 +36,8 @@ export type { DG1, DG2, ParsedNFCResponse } from './nfc';
|
||||
|
||||
export type { DocumentData, DocumentMetadata, PassportCameraProps, ScreenProps } from './types/ui';
|
||||
|
||||
export type { HapticOptions, HapticType } from './haptic/shared';
|
||||
|
||||
export type { MRZScanOptions } from './mrz';
|
||||
|
||||
// QR module
|
||||
@@ -45,7 +47,6 @@ export type { SDKEvent, SDKEventMap } from './types/events';
|
||||
|
||||
// Error handling
|
||||
export type { SdkErrorCategory } from './errors';
|
||||
|
||||
// Screen Components (React Native-based)
|
||||
export type { provingMachineCircuitType } from './proving/provingMachine';
|
||||
export {
|
||||
@@ -59,17 +60,39 @@ export {
|
||||
sdkError,
|
||||
} from './errors';
|
||||
export { NFCScannerScreen } from './components/screens/NFCScannerScreen';
|
||||
export { PassportCameraScreen } from './components/screens/PassportCameraScreen';
|
||||
|
||||
// Context and Client
|
||||
export { type ProvingStateType } from './proving/provingMachine';
|
||||
export { PassportCameraScreen } from './components/screens/PassportCameraScreen';
|
||||
|
||||
export { QRCodeScreen } from './components/screens/QRCodeScreen';
|
||||
export { type ProvingStateType } from './proving/provingMachine';
|
||||
// Components
|
||||
export { SdkEvents } from './types/events';
|
||||
export { QRCodeScreen } from './components/screens/QRCodeScreen';
|
||||
// Documents utils
|
||||
export { SdkEvents } from './types/events';
|
||||
|
||||
export { SelfClientContext, SelfClientProvider, useSelfClient } from './context';
|
||||
|
||||
// Haptic feedback utilities
|
||||
export { advercase, dinot, plexMono } from './constants/fonts';
|
||||
|
||||
export {
|
||||
buttonTap,
|
||||
cancelTap,
|
||||
confirmTap,
|
||||
feedbackProgress,
|
||||
feedbackSuccess,
|
||||
feedbackUnsuccessful,
|
||||
impactLight,
|
||||
impactMedium,
|
||||
loadingScreenProgress,
|
||||
notificationError,
|
||||
notificationSuccess,
|
||||
notificationWarning,
|
||||
selectionChange,
|
||||
triggerFeedback,
|
||||
} from './haptic';
|
||||
|
||||
/** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */
|
||||
export {
|
||||
clearPassportData,
|
||||
getAllDocuments,
|
||||
@@ -79,20 +102,21 @@ export {
|
||||
reStorePassportDataWithRightCSCA,
|
||||
} from './documents/utils';
|
||||
|
||||
/** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */
|
||||
export { createListenersMap, createSelfClient } from './client';
|
||||
|
||||
// Document utils
|
||||
export { defaultConfig } from './config/defaults';
|
||||
|
||||
// Document utils
|
||||
export { defaultOptions } from './haptic/shared';
|
||||
|
||||
export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD } from './mrz';
|
||||
|
||||
// Core functions
|
||||
export { extractNameFromDocument } from './documents/utils';
|
||||
|
||||
// Core functions
|
||||
// Document validation
|
||||
export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator';
|
||||
|
||||
// Document validation
|
||||
export { isPassportDataValid } from './validation/document';
|
||||
|
||||
export { mergeConfig } from './config/merge';
|
||||
@@ -100,7 +124,5 @@ export { mergeConfig } from './config/merge';
|
||||
export { parseNFCResponse, scanNFC } from './nfc';
|
||||
|
||||
export { reactNativeScannerAdapter } from './adapters/react-native/nfc-scanner';
|
||||
|
||||
export { useCountries } from './documents/useCountries';
|
||||
|
||||
export { webNFCScannerShim } from './adapters/web/shims';
|
||||
|
||||
@@ -160,7 +160,6 @@ export interface SDKEventMap {
|
||||
documentType: string;
|
||||
documentName: string;
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
};
|
||||
[SdkEvents.PROVING_BEGIN_GENERATION]: {
|
||||
uuid: string;
|
||||
|
||||
11
packages/mobile-sdk-alpha/src/types/svg.d.ts
vendored
Normal file
11
packages/mobile-sdk-alpha/src/types/svg.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// 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 '*.svg' {
|
||||
import type React from 'react';
|
||||
import type { SvgProps } from 'react-native-svg';
|
||||
|
||||
const content: React.FC<SvgProps>;
|
||||
export default content;
|
||||
}
|
||||
@@ -23,8 +23,8 @@ function findFlowFiles(dir: string, basePath = ''): Record<string, string> {
|
||||
|
||||
if (item.isDirectory()) {
|
||||
Object.assign(entries, findFlowFiles(itemPath, relativePath));
|
||||
} else if (item.isFile() && item.name.endsWith('.ts')) {
|
||||
const key = path.join('flows', relativePath).replace(/\.ts$/, '');
|
||||
} else if (item.isFile() && (item.name.endsWith('.ts') || item.name.endsWith('.tsx'))) {
|
||||
const key = path.join('flows', relativePath).replace(/\.tsx?$/, '');
|
||||
entries[key] = path.join('src', 'flows', relativePath);
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,8 @@ export default defineConfig([
|
||||
'react-native-keychain',
|
||||
'react-native-sqlite-storage',
|
||||
// State management (xstate included in bundle)
|
||||
// SVG files should be handled by React Native's SVG transformer
|
||||
/\.svg$/,
|
||||
],
|
||||
esbuildOptions(options) {
|
||||
options.supported = {
|
||||
@@ -98,6 +100,7 @@ export default defineConfig([
|
||||
splitting: true,
|
||||
clean: false,
|
||||
outDir: 'dist/cjs',
|
||||
onSuccess: 'node ./scripts/copy-assets.mjs',
|
||||
tsconfig: './tsconfig.cjs.json',
|
||||
target: 'es2020',
|
||||
external: [
|
||||
@@ -116,6 +119,8 @@ export default defineConfig([
|
||||
'react-native-keychain',
|
||||
'react-native-sqlite-storage',
|
||||
// State management (xstate included in bundle)
|
||||
// SVG files should be handled by React Native's SVG transformer
|
||||
/\.svg$/,
|
||||
],
|
||||
outExtension: ({ format }) => ({ js: format === 'cjs' ? '.cjs' : '.js' }),
|
||||
esbuildOptions(options) {
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"react-native": "0.76.9",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-keychain": "^10.0.0",
|
||||
"react-native-localize": "^3.5.4",
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-vector-icons": "^10.3.0",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type TrackEventParams,
|
||||
type WsConn,
|
||||
webNFCScannerShim,
|
||||
SdkEvents,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import { persistentDocumentsAdapter } from '../utils/documentStore';
|
||||
@@ -153,7 +154,11 @@ export function SelfClientProvider({ children }: PropsWithChildren) {
|
||||
);
|
||||
|
||||
const listeners = useMemo(() => {
|
||||
const { map } = createListenersMap();
|
||||
const { map, addListener } = createListenersMap();
|
||||
|
||||
addListener(SdkEvents.DOCUMENT_COUNTRY_SELECTED, event => {
|
||||
console.info('go to id picker', event);
|
||||
});
|
||||
return map;
|
||||
}, []);
|
||||
|
||||
|
||||
11
packages/mobile-sdk-demo/src/screens/CountrySelection.tsx
Normal file
11
packages/mobile-sdk-demo/src/screens/CountrySelection.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
|
||||
|
||||
import ScreenLayout from '../components/ScreenLayout';
|
||||
|
||||
export default function CountrySelection({ onBack }: { onBack: () => void }) {
|
||||
return (
|
||||
<ScreenLayout title="GETTING STARTED" onBack={onBack}>
|
||||
<SDKCountryPickerScreen />
|
||||
</ScreenLayout>
|
||||
);
|
||||
}
|
||||
11
packages/mobile-sdk-demo/src/screens/IDSelection.tsx
Normal file
11
packages/mobile-sdk-demo/src/screens/IDSelection.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import IDSelectionScreen from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-screen';
|
||||
|
||||
import ScreenLayout from '../components/ScreenLayout';
|
||||
|
||||
export default function IDSelection({ onBack }: { onBack: () => void }) {
|
||||
return (
|
||||
<ScreenLayout title="GETTING STARTED" onBack={onBack}>
|
||||
<IDSelectionScreen countryCode="USA" documentTypes={['p']} />
|
||||
</ScreenLayout>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,15 @@ import type { ComponentType } from 'react';
|
||||
|
||||
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
|
||||
|
||||
export type ScreenId = 'generate' | 'register' | 'prove' | 'camera' | 'nfc' | 'documents';
|
||||
export type ScreenId =
|
||||
| 'generate'
|
||||
| 'register'
|
||||
| 'prove'
|
||||
| 'camera'
|
||||
| 'nfc'
|
||||
| 'documents'
|
||||
| 'country-selection'
|
||||
| 'id-selection';
|
||||
|
||||
export type ScreenContext = {
|
||||
navigate: (next: ScreenRoute) => void;
|
||||
@@ -89,6 +97,30 @@ export const screenDescriptors: ScreenDescriptor[] = [
|
||||
catalog: documentCatalog,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'country-selection',
|
||||
title: 'Country Selection',
|
||||
subtitle: 'Select the country that issued your ID',
|
||||
sectionTitle: '📋 Selection',
|
||||
status: 'working',
|
||||
load: () => require('./CountrySelection').default,
|
||||
getProps: ({ navigate, documentCatalog }) => ({
|
||||
onBack: () => navigate('home'),
|
||||
catalog: documentCatalog,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'id-selection',
|
||||
title: 'ID Selection',
|
||||
subtitle: 'Choose the type of ID you want to verify',
|
||||
sectionTitle: '📋 Selection',
|
||||
status: 'working',
|
||||
load: () => require('./IDSelection').default,
|
||||
getProps: ({ navigate, documentCatalog }) => ({
|
||||
onBack: () => navigate('home'),
|
||||
catalog: documentCatalog,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export const screenMap = screenDescriptors.reduce<Record<ScreenId, ScreenDescriptor>>(
|
||||
|
||||
20
yarn.lock
20
yarn.lock
@@ -7462,6 +7462,7 @@ __metadata:
|
||||
react: "npm:^18.3.1"
|
||||
react-dom: "npm:^18.3.1"
|
||||
react-native: "npm:0.76.9"
|
||||
react-native-haptic-feedback: "npm:^2.3.3"
|
||||
react-native-localize: "npm:^3.5.2"
|
||||
react-native-svg-circle-country-flags: "npm:^0.2.2"
|
||||
react-native-web: "npm:^0.21.1"
|
||||
@@ -7475,6 +7476,7 @@ __metadata:
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
react-native: 0.76.9
|
||||
react-native-haptic-feedback: "*"
|
||||
react-native-localize: "*"
|
||||
react-native-svg: "*"
|
||||
languageName: unknown
|
||||
@@ -23823,6 +23825,7 @@ __metadata:
|
||||
react-native: "npm:0.76.9"
|
||||
react-native-get-random-values: "npm:^1.11.0"
|
||||
react-native-keychain: "npm:^10.0.0"
|
||||
react-native-localize: "npm:^3.5.4"
|
||||
react-native-safe-area-context: "npm:^5.6.1"
|
||||
react-native-svg: "npm:15.12.1"
|
||||
react-native-svg-transformer: "npm:^1.5.1"
|
||||
@@ -26378,6 +26381,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-localize@npm:^3.5.4":
|
||||
version: 3.5.4
|
||||
resolution: "react-native-localize@npm:3.5.4"
|
||||
peerDependencies:
|
||||
"@expo/config-plugins": ^9.0.0 || ^10.0.0
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
react-native-macos: "*"
|
||||
peerDependenciesMeta:
|
||||
"@expo/config-plugins":
|
||||
optional: true
|
||||
react-native-macos:
|
||||
optional: true
|
||||
checksum: 10c0/b1366c2cce740163f5337e842b537a7e5132e0914d9023f1cf07861ea141b0a6d4e7b360b6bb6d8af16860ef56f1d9ca7a59508dcc4619f42991123298207f1b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-logs@npm:^5.3.0":
|
||||
version: 5.5.0
|
||||
resolution: "react-native-logs@npm:5.5.0"
|
||||
|
||||
Reference in New Issue
Block a user