diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx index 22f80fff9..a790935c3 100644 --- a/app/src/screens/dev/DevSettingsScreen.tsx +++ b/app/src/screens/dev/DevSettingsScreen.tsx @@ -2,796 +2,53 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import type { PropsWithChildren } from 'react'; -import React, { - cloneElement, - isValidElement, - useEffect, - useMemo, - useState, -} from 'react'; -import type { StyleProp, TextStyle, ViewStyle } from 'react-native'; -import { Alert, Platform, ScrollView, TouchableOpacity } from 'react-native'; -import { Button, Sheet, Text, XStack, YStack } from 'tamagui'; +import React from 'react'; +import { ScrollView } from 'react-native'; +import { YStack } from 'tamagui'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { Check, ChevronDown, ChevronRight } from '@tamagui/lucide-icons'; -import { - red500, - slate100, - slate200, - slate400, - slate500, - slate600, - slate800, - slate900, - white, - yellow500, -} from '@selfxyz/mobile-sdk-alpha/constants/colors'; -import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks'; import BugIcon from '@/assets/icons/bug_icon.svg'; -import WarningIcon from '@/assets/icons/warning.svg'; import type { RootStackParamList } from '@/navigation'; -import { navigationScreens } from '@/navigation'; -import { unsafe_clearSecrets } from '@/providers/authProvider'; -import { usePassport } from '@/providers/passportDataProvider'; +import { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector'; +import { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; +import { useDangerZoneActions } from '@/screens/dev/hooks/useDangerZoneActions'; +import { useNotificationHandlers } from '@/screens/dev/hooks/useNotificationHandlers'; import { - isNotificationSystemReady, - requestNotificationPermission, - subscribeToTopics, - unsubscribeFromTopics, -} from '@/services/notifications/notificationService'; -import type { InjectedErrorType } from '@/stores/errorInjectionStore'; -import { - ERROR_GROUPS, - ERROR_LABELS, - useErrorInjectionStore, -} from '@/stores/errorInjectionStore'; -import { usePointEventStore } from '@/stores/pointEventStore'; + DangerZoneSection, + DebugShortcutsSection, + DevTogglesSection, + PushNotificationsSection, +} from '@/screens/dev/sections'; import { useSettingStore } from '@/stores/settingStore'; import { IS_DEV_MODE } from '@/utils/devUtils'; -interface TopicToggleButtonProps { - label: string; - isSubscribed: boolean; - onToggle: () => void; -} - -const TopicToggleButton: React.FC = ({ - label, - isSubscribed, - onToggle, -}) => { - return ( - - ); -}; - -interface DevSettingsScreenProps extends PropsWithChildren { - color?: string; - width?: number; - justifyContent?: - | 'center' - | 'unset' - | 'flex-start' - | 'flex-end' - | 'space-between' - | 'space-around' - | 'space-evenly'; - userSelect?: 'all' | 'text' | 'none' | 'contain'; - textAlign?: 'center' | 'left' | 'right'; - style?: StyleProp; -} - -function ParameterSection({ - icon, - title, - description, - darkMode, - children, -}: { - icon: React.ReactNode; - title: string; - description: string; - darkMode?: boolean; - children: React.ReactNode; -}) { - const renderIcon = () => { - const iconElement = - typeof icon === 'function' - ? (icon as () => React.ReactNode)() - : isValidElement(icon) - ? icon - : null; - - return iconElement - ? cloneElement(iconElement as React.ReactElement, { - width: '100%', - height: '100%', - }) - : null; - }; - - return ( - - - - {renderIcon()} - - - - {title} - - - {description} - - - - {children} - - ); -} - -const ScreenSelector = ({}) => { - const navigation = useNavigation(); - const [open, setOpen] = useState(false); - - const screenList = useMemo( - () => - ( - Object.keys(navigationScreens) as (keyof typeof navigationScreens)[] - ).sort(), - [], - ); - - return ( - <> - - - - - - - - - Select screen - - - - - {screenList.map(item => ( - { - setOpen(false); - navigation.navigate(item as never); - }} - > - - - {item} - - - - ))} - - - - - - ); -}; - -const LogLevelSelector = ({ - currentLevel, - onSelect, -}: { - currentLevel: string; - onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void; -}) => { - const [open, setOpen] = useState(false); - - const logLevels = ['debug', 'info', 'warn', 'error'] as const; - - return ( - <> - - - - - - - - - Select log level - - - - - {logLevels.map(level => ( - { - setOpen(false); - onSelect(level); - }} - > - - - {level.toUpperCase()} - - {currentLevel === level && ( - - )} - - - ))} - - - - - - ); -}; - -const ErrorInjectionSelector = () => { - const injectedErrors = useErrorInjectionStore(state => state.injectedErrors); - const setInjectedErrors = useErrorInjectionStore( - state => state.setInjectedErrors, - ); - const clearAllErrors = useErrorInjectionStore(state => state.clearAllErrors); - const [open, setOpen] = useState(false); - - // Single error selection - replace instead of toggle - const selectError = (errorType: InjectedErrorType) => { - // If clicking the same error, clear it; otherwise set the new one - if (injectedErrors.length === 1 && injectedErrors[0] === errorType) { - clearAllErrors(); - } else { - setInjectedErrors([errorType]); - } - // Close the sheet after selection - setOpen(false); - }; - - const currentError = injectedErrors.length > 0 ? injectedErrors[0] : null; - const currentErrorLabel = currentError ? ERROR_LABELS[currentError] : null; - - return ( - - - - {currentError && ( - - )} - - - - - - - - Onboarding Error Testing - - - - - {Object.entries(ERROR_GROUPS).map(([groupName, errors]) => ( - - - {groupName} - - {errors.map((errorType: InjectedErrorType) => ( - selectError(errorType)} - > - - - {ERROR_LABELS[errorType]} - - {currentError === errorType && ( - - )} - - - ))} - - ))} - - - - - - ); -}; - -const DevSettingsScreen: React.FC = ({}) => { - const { clearDocumentCatalogForMigrationTesting } = usePassport(); - const clearPointEvents = usePointEventStore(state => state.clearEvents); - const { resetBackupForPoints } = useSettingStore(); +const DevSettingsScreen: React.FC = () => { const navigation = useNavigation() as NativeStackScreenProps['navigation']; - const subscribedTopics = useSettingStore(state => state.subscribedTopics); + const paddingBottom = useSafeBottomPadding(20); + + // Settings store const loggingSeverity = useSettingStore(state => state.loggingSeverity); const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity); const useStrongBox = useSettingStore(state => state.useStrongBox); const setUseStrongBox = useSettingStore(state => state.setUseStrongBox); - const [hasNotificationPermission, setHasNotificationPermission] = - useState(false); - const paddingBottom = useSafeBottomPadding(20); + const kycEnabled = useSettingStore(state => state.kycEnabled); + const setKycEnabled = useSettingStore(state => state.setKycEnabled); - // Check notification permissions on mount - useEffect(() => { - const checkPermissions = async () => { - const readiness = await isNotificationSystemReady(); - setHasNotificationPermission(readiness.ready); - }; - checkPermissions(); - }, []); - - const handleTopicToggle = async (topics: string[], topicLabel: string) => { - // Check permissions first - if (!hasNotificationPermission) { - Alert.alert( - 'Permissions Required', - 'Push notifications are not enabled. Would you like to enable them?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Enable', - onPress: async () => { - try { - const granted = await requestNotificationPermission(); - if (granted) { - // Update permission state - setHasNotificationPermission(true); - Alert.alert( - 'Success', - 'Permissions granted! You can now subscribe to topics.', - [{ text: 'OK' }], - ); - } else { - Alert.alert( - 'Failed', - 'Could not enable notifications. Please enable them in your device Settings.', - [{ text: 'OK' }], - ); - } - } catch (error) { - Alert.alert( - 'Error', - error instanceof Error - ? error.message - : 'Failed to request permissions', - [{ text: 'OK' }], - ); - } - }, - }, - ], - ); - return; - } - - const isCurrentlySubscribed = topics.every(topic => - subscribedTopics.includes(topic), - ); - - if (isCurrentlySubscribed) { - // Show confirmation dialog for unsubscribe - Alert.alert( - 'Disable Notifications', - `Are you sure you want to disable push notifications for ${topicLabel}?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Disable', - style: 'destructive', - onPress: async () => { - try { - const result = await unsubscribeFromTopics(topics); - if (result.successes.length > 0) { - Alert.alert( - 'Success', - `Disabled notifications for ${topicLabel}`, - [{ text: 'OK' }], - ); - } else { - Alert.alert( - 'Error', - `Failed to disable: ${result.failures.map(f => f.error).join(', ')}`, - [{ text: 'OK' }], - ); - } - } catch (error) { - Alert.alert( - 'Error', - error instanceof Error - ? error.message - : 'Failed to unsubscribe', - [{ text: 'OK' }], - ); - } - }, - }, - ], - ); - } else { - // Subscribe without confirmation - try { - const result = await subscribeToTopics(topics); - if (result.successes.length > 0) { - Alert.alert('✅ Success', `Enabled notifications for ${topicLabel}`, [ - { text: 'OK' }, - ]); - } else { - Alert.alert( - 'Error', - `Failed to enable: ${result.failures.map(f => f.error).join(', ')}`, - [{ text: 'OK' }], - ); - } - } catch (error) { - Alert.alert( - 'Error', - error instanceof Error ? error.message : 'Failed to subscribe', - [{ text: 'OK' }], - ); - } - } - }; - - const handleClearSecretsPress = () => { - Alert.alert( - 'Delete Keychain Secrets', - "Are you sure you want to remove your keychain secrets?\n\nIf this secret is not backed up, your account will be lost and the ID documents attached to it won't be usable.", - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - await unsafe_clearSecrets(); - }, - }, - ], - ); - }; - - const handleClearDocumentCatalogPress = () => { - Alert.alert( - 'Clear Document Catalog', - 'Are you sure you want to clear the document catalog?\n\nThis will remove all documents from the new storage system but preserve legacy storage for migration testing. You will need to restart the app to test migration.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - await clearDocumentCatalogForMigrationTesting(); - }, - }, - ], - ); - }; - - const handleClearPointEventsPress = () => { - Alert.alert( - 'Clear Point Events', - 'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - await clearPointEvents(); - Alert.alert('Success', 'Point events cleared successfully.', [ - { text: 'OK' }, - ]); - }, - }, - ], - ); - }; - - const handleResetBackupStatePress = () => { - Alert.alert( - 'Reset Backup State', - 'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Reset', - style: 'destructive', - onPress: () => { - resetBackupForPoints(); - Alert.alert('Success', 'Backup state reset successfully.', [ - { text: 'OK' }, - ]); - }, - }, - ], - ); - }; - - const handleClearBackupEventsPress = () => { - Alert.alert( - 'Clear Backup Events', - 'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - const events = usePointEventStore.getState().events; - const backupEvents = events.filter( - event => event.type === 'backup', - ); - for (const event of backupEvents) { - await usePointEventStore.getState().removeEvent(event.id); - } - Alert.alert('Success', 'Backup events cleared successfully.', [ - { text: 'OK' }, - ]); - }, - }, - ], - ); - }; + // Custom hooks + const { hasNotificationPermission, subscribedTopics, handleTopicToggle } = + useNotificationHandlers(); + const { + handleClearSecretsPress, + handleClearDocumentCatalogPress, + handleClearPointEventsPress, + handleResetBackupStatePress, + handleClearBackupEventsPress, + } = useDangerZoneActions(); return ( @@ -804,121 +61,22 @@ const DevSettingsScreen: React.FC = ({}) => { paddingTop="$4" paddingBottom={paddingBottom} > - } - title="Debug Shortcuts" - description="Jump directly to any screen for testing" - > - - - - {IS_DEV_MODE && ( - - )} - - - + - } - title="Push Notifications" - description="Manage topic subscriptions" - > - - handleTopicToggle(['nova'], 'Starfall')} - /> - handleTopicToggle(['general'], 'General')} - /> - - handleTopicToggle(['nova', 'general'], 'both topics') - } - /> - - + {IS_DEV_MODE && ( + + )} + + } @@ -941,86 +99,13 @@ const DevSettingsScreen: React.FC = ({}) => { )} - {Platform.OS === 'android' && ( - } - title="Android Keystore" - description="Configure keystore security options" - > - { - Alert.alert( - useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox', - useStrongBox - ? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.' - : 'New keys will attempt to use StrongBox hardware backing for enhanced security.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: useStrongBox ? 'Disable' : 'Enable', - onPress: () => setUseStrongBox(!useStrongBox), - }, - ], - ); - }} - /> - - )} - - } - title="Danger Zone" - description="These actions are sensitive" - darkMode={true} - > - {[ - { - label: 'Delete your private key', - onPress: handleClearSecretsPress, - dangerTheme: true, - }, - { - label: 'Clear document catalog', - onPress: handleClearDocumentCatalogPress, - dangerTheme: true, - }, - { - label: 'Clear point events', - onPress: handleClearPointEventsPress, - dangerTheme: true, - }, - { - label: 'Reset backup state', - onPress: handleResetBackupStatePress, - dangerTheme: true, - }, - { - label: 'Clear backup events', - onPress: handleClearBackupEventsPress, - dangerTheme: true, - }, - ].map(({ label, onPress, dangerTheme }) => ( - - ))} - + ); diff --git a/app/src/screens/dev/components/ErrorInjectionSelector.tsx b/app/src/screens/dev/components/ErrorInjectionSelector.tsx new file mode 100644 index 000000000..7feb243f0 --- /dev/null +++ b/app/src/screens/dev/components/ErrorInjectionSelector.tsx @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useCallback, useRef, useState } from 'react'; +import { ScrollView, TouchableOpacity } from 'react-native'; +import { Button, Sheet, Text, XStack, YStack } from 'tamagui'; +import { Check, ChevronDown } from '@tamagui/lucide-icons'; + +import { + red500, + slate200, + slate500, + slate600, + slate800, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import type { InjectedErrorType } from '@/stores/errorInjectionStore'; +import { + ERROR_GROUPS, + ERROR_LABELS, + useErrorInjectionStore, +} from '@/stores/errorInjectionStore'; +import { + registerModalCallbacks, + unregisterModalCallbacks, +} from '@/utils/modalCallbackRegistry'; + +export const ErrorInjectionSelector = () => { + const injectedErrors = useErrorInjectionStore(state => state.injectedErrors); + const setInjectedErrors = useErrorInjectionStore( + state => state.setInjectedErrors, + ); + const clearAllErrors = useErrorInjectionStore(state => state.clearAllErrors); + const [open, setOpen] = useState(false); + const callbackIdRef = useRef(); + + const handleModalDismiss = useCallback(() => { + setOpen(false); + if (callbackIdRef.current !== undefined) { + unregisterModalCallbacks(callbackIdRef.current); + callbackIdRef.current = undefined; + } + }, []); + + const openSheet = useCallback(() => { + setOpen(true); + const id = registerModalCallbacks({ + onButtonPress: () => {}, + onModalDismiss: handleModalDismiss, + }); + callbackIdRef.current = id; + }, [handleModalDismiss]); + + const closeSheet = useCallback(() => { + handleModalDismiss(); + }, [handleModalDismiss]); + + const handleSheetOpenChange = useCallback( + (isOpen: boolean) => { + if (!isOpen) { + handleModalDismiss(); + } else { + setOpen(isOpen); + } + }, + [handleModalDismiss], + ); + + // Single error selection - replace instead of toggle + const selectError = (errorType: InjectedErrorType) => { + // If clicking the same error, clear it; otherwise set the new one + if (injectedErrors.length === 1 && injectedErrors[0] === errorType) { + clearAllErrors(); + } else { + setInjectedErrors([errorType]); + } + // Close the sheet after selection + closeSheet(); + }; + + const currentError = injectedErrors.length > 0 ? injectedErrors[0] : null; + const currentErrorLabel = currentError ? ERROR_LABELS[currentError] : null; + + return ( + + + + {currentError && ( + + )} + + + + + + + + Onboarding Error Testing + + + + + {Object.entries(ERROR_GROUPS).map(([groupName, errors]) => ( + + + {groupName} + + {errors.map((errorType: InjectedErrorType) => ( + selectError(errorType)} + > + + + {ERROR_LABELS[errorType]} + + {currentError === errorType && ( + + )} + + + ))} + + ))} + + + + + + ); +}; diff --git a/app/src/screens/dev/components/LogLevelSelector.tsx b/app/src/screens/dev/components/LogLevelSelector.tsx new file mode 100644 index 000000000..d919669dd --- /dev/null +++ b/app/src/screens/dev/components/LogLevelSelector.tsx @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { ScrollView, TouchableOpacity } from 'react-native'; +import { Button, Sheet, Text, XStack, YStack } from 'tamagui'; +import { Check, ChevronDown } from '@tamagui/lucide-icons'; + +import { + slate200, + slate500, + slate600, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import { + registerModalCallbacks, + unregisterModalCallbacks, +} from '@/utils/modalCallbackRegistry'; + +interface LogLevelSelectorProps { + currentLevel: string; + onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void; +} + +export const LogLevelSelector: React.FC = ({ + currentLevel, + onSelect, +}) => { + const [open, setOpen] = useState(false); + const callbackIdRef = useRef(); + + const logLevels = ['debug', 'info', 'warn', 'error'] as const; + + // Cleanup effect to unregister callbacks on unmount + useEffect(() => { + return () => { + if (callbackIdRef.current !== undefined) { + unregisterModalCallbacks(callbackIdRef.current); + callbackIdRef.current = undefined; + } + }; + }, []); + + const handleModalDismiss = useCallback(() => { + setOpen(false); + if (callbackIdRef.current !== undefined) { + unregisterModalCallbacks(callbackIdRef.current); + callbackIdRef.current = undefined; + } + }, []); + + const openSheet = useCallback(() => { + setOpen(true); + const id = registerModalCallbacks({ + onButtonPress: () => {}, + onModalDismiss: handleModalDismiss, + }); + callbackIdRef.current = id; + }, [handleModalDismiss]); + + const closeSheet = useCallback(() => { + handleModalDismiss(); + }, [handleModalDismiss]); + + const handleSheetOpenChange = useCallback( + (isOpen: boolean) => { + if (!isOpen) { + handleModalDismiss(); + } else { + setOpen(isOpen); + } + }, + [handleModalDismiss], + ); + + const handleLevelSelect = useCallback( + (level: 'debug' | 'info' | 'warn' | 'error') => { + closeSheet(); + onSelect(level); + }, + [closeSheet, onSelect], + ); + + return ( + <> + + + + + + + + + Select log level + + + + + {logLevels.map(level => ( + handleLevelSelect(level)} + > + + + {level.toUpperCase()} + + {currentLevel === level && ( + + )} + + + ))} + + + + + + ); +}; diff --git a/app/src/screens/dev/components/ParameterSection.tsx b/app/src/screens/dev/components/ParameterSection.tsx new file mode 100644 index 000000000..f99dc6c25 --- /dev/null +++ b/app/src/screens/dev/components/ParameterSection.tsx @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type { PropsWithChildren } from 'react'; +import React, { cloneElement, isValidElement } from 'react'; +import { Text, XStack, YStack } from 'tamagui'; + +import { + slate100, + slate200, + slate400, + slate600, + slate800, + slate900, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +interface ParameterSectionProps extends PropsWithChildren { + icon: React.ReactNode; + title: string; + description: string; + darkMode?: boolean; +} + +export function ParameterSection({ + icon, + title, + description, + darkMode, + children, +}: ParameterSectionProps) { + const renderIcon = () => { + const iconElement = + typeof icon === 'function' + ? (icon as () => React.ReactNode)() + : isValidElement(icon) + ? icon + : null; + + return iconElement + ? cloneElement(iconElement as React.ReactElement, { + width: '100%', + height: '100%', + }) + : null; + }; + + return ( + + + + {renderIcon()} + + + + {title} + + + {description} + + + + {children} + + ); +} diff --git a/app/src/screens/dev/components/ScreenSelector.tsx b/app/src/screens/dev/components/ScreenSelector.tsx new file mode 100644 index 000000000..0e3084684 --- /dev/null +++ b/app/src/screens/dev/components/ScreenSelector.tsx @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useMemo, useState } from 'react'; +import { ScrollView, TouchableOpacity } from 'react-native'; +import { Button, Sheet, Text, XStack, YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; +import { ChevronDown } from '@tamagui/lucide-icons'; + +import { + slate200, + slate500, + slate600, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import { navigationScreens } from '@/navigation'; + +export const ScreenSelector = () => { + const navigation = useNavigation(); + const [open, setOpen] = useState(false); + + const screenList = useMemo( + () => + ( + Object.keys(navigationScreens) as (keyof typeof navigationScreens)[] + ).sort(), + [], + ); + + return ( + <> + + + + + + + + + Select screen + + + + + {screenList.map(item => ( + { + setOpen(false); + navigation.navigate(item as never); + }} + > + + + {item} + + + + ))} + + + + + + ); +}; diff --git a/app/src/screens/dev/components/TopicToggleButton.tsx b/app/src/screens/dev/components/TopicToggleButton.tsx new file mode 100644 index 000000000..5aaa89503 --- /dev/null +++ b/app/src/screens/dev/components/TopicToggleButton.tsx @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Button, Text } from 'tamagui'; + +import { + slate200, + slate400, + slate600, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +export interface TopicToggleButtonProps { + label: string; + isSubscribed: boolean; + onToggle: () => void; +} + +export const TopicToggleButton: React.FC = ({ + label, + isSubscribed, + onToggle, +}) => { + return ( + + ); +}; diff --git a/app/src/screens/dev/components/index.ts b/app/src/screens/dev/components/index.ts new file mode 100644 index 000000000..4193b34e0 --- /dev/null +++ b/app/src/screens/dev/components/index.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +export type { TopicToggleButtonProps } from '@/screens/dev/components/TopicToggleButton'; +export { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector'; +export { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector'; +export { ParameterSection } from '@/screens/dev/components/ParameterSection'; +export { ScreenSelector } from '@/screens/dev/components/ScreenSelector'; +export { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton'; diff --git a/app/src/screens/dev/hooks/useDangerZoneActions.ts b/app/src/screens/dev/hooks/useDangerZoneActions.ts new file mode 100644 index 000000000..9b90b8860 --- /dev/null +++ b/app/src/screens/dev/hooks/useDangerZoneActions.ts @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { Alert } from 'react-native'; + +import { unsafe_clearSecrets } from '@/providers/authProvider'; +import { usePassport } from '@/providers/passportDataProvider'; +import { usePointEventStore } from '@/stores/pointEventStore'; +import { useSettingStore } from '@/stores/settingStore'; + +export const useDangerZoneActions = () => { + const { clearDocumentCatalogForMigrationTesting } = usePassport(); + const clearPointEvents = usePointEventStore(state => state.clearEvents); + const { resetBackupForPoints } = useSettingStore(); + + const handleClearSecretsPress = () => { + Alert.alert( + 'Delete Keychain Secrets', + "Are you sure you want to remove your keychain secrets?\n\nIf this secret is not backed up, your account will be lost and the ID documents attached to it won't be usable.", + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + try { + await unsafe_clearSecrets(); + Alert.alert('Success', 'Keychain secrets cleared successfully.', [ + { text: 'OK' }, + ]); + } catch (error) { + console.error( + 'Failed to clear keychain secrets:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Failed to clear keychain secrets. Please try again.', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + }; + + const handleClearDocumentCatalogPress = () => { + Alert.alert( + 'Clear Document Catalog', + 'Are you sure you want to clear the document catalog?\n\nThis will remove all documents from the new storage system but preserve legacy storage for migration testing. You will need to restart the app to test migration.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Clear', + style: 'destructive', + onPress: async () => { + try { + await clearDocumentCatalogForMigrationTesting(); + Alert.alert( + 'Success', + 'Document catalog cleared successfully. Please restart the app to test migration.', + [{ text: 'OK' }], + ); + } catch (error) { + console.error( + 'Failed to clear document catalog:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Failed to clear document catalog. Please try again.', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + }; + + const handleClearPointEventsPress = () => { + Alert.alert( + 'Clear Point Events', + 'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Clear', + style: 'destructive', + onPress: async () => { + try { + await clearPointEvents(); + Alert.alert('Success', 'Point events cleared successfully.', [ + { text: 'OK' }, + ]); + } catch (error) { + console.error( + 'Failed to clear point events:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Failed to clear point events. Please try again.', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + }; + + const handleResetBackupStatePress = () => { + Alert.alert( + 'Reset Backup State', + 'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Reset', + style: 'destructive', + onPress: () => { + resetBackupForPoints(); + Alert.alert('Success', 'Backup state reset successfully.', [ + { text: 'OK' }, + ]); + }, + }, + ], + ); + }; + + const handleClearBackupEventsPress = () => { + Alert.alert( + 'Clear Backup Events', + 'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Clear', + style: 'destructive', + onPress: async () => { + try { + const events = usePointEventStore.getState().events; + const backupEvents = events.filter( + event => event.type === 'backup', + ); + await Promise.all( + backupEvents.map(event => + usePointEventStore.getState().removeEvent(event.id), + ), + ); + Alert.alert('Success', 'Backup events cleared successfully.', [ + { text: 'OK' }, + ]); + } catch (error) { + console.error( + 'Failed to clear backup events:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Failed to clear backup events. Please try again.', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + }; + + return { + handleClearSecretsPress, + handleClearDocumentCatalogPress, + handleClearPointEventsPress, + handleResetBackupStatePress, + handleClearBackupEventsPress, + }; +}; diff --git a/app/src/screens/dev/hooks/useNotificationHandlers.ts b/app/src/screens/dev/hooks/useNotificationHandlers.ts new file mode 100644 index 000000000..de23c95cd --- /dev/null +++ b/app/src/screens/dev/hooks/useNotificationHandlers.ts @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback, useEffect, useState } from 'react'; +import { Alert, AppState } from 'react-native'; + +import { + isNotificationSystemReady, + requestNotificationPermission, + subscribeToTopics, + unsubscribeFromTopics, +} from '@/services/notifications/notificationService'; +import { useSettingStore } from '@/stores/settingStore'; + +export const useNotificationHandlers = () => { + const subscribedTopics = useSettingStore(state => state.subscribedTopics); + const [hasNotificationPermission, setHasNotificationPermission] = + useState(false); + + const checkPermissions = useCallback(async () => { + const readiness = await isNotificationSystemReady(); + setHasNotificationPermission(readiness.ready); + }, []); + + // Check notification permissions on mount and when app regains focus + useEffect(() => { + checkPermissions(); + + const subscription = AppState.addEventListener('change', nextAppState => { + if (nextAppState === 'active') { + checkPermissions(); + } + }); + + return () => subscription.remove(); + }, [checkPermissions]); + + const handleTopicToggle = async (topics: string[], topicLabel: string) => { + // Check permissions first + if (!hasNotificationPermission) { + Alert.alert( + 'Permissions Required', + 'Push notifications are not enabled. Would you like to enable them?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Enable', + onPress: async () => { + try { + const granted = await requestNotificationPermission(); + if (granted) { + // Update permission state + setHasNotificationPermission(true); + Alert.alert( + 'Success', + 'Permissions granted! You can now subscribe to topics.', + [{ text: 'OK' }], + ); + } else { + Alert.alert( + 'Failed', + 'Could not enable notifications. Please enable them in your device Settings.', + [{ text: 'OK' }], + ); + } + } catch (error) { + Alert.alert( + 'Error', + error instanceof Error + ? error.message + : 'Failed to request permissions', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + return; + } + + const isCurrentlySubscribed = topics.every(topic => + subscribedTopics.includes(topic), + ); + + if (isCurrentlySubscribed) { + // Show confirmation dialog for unsubscribe + Alert.alert( + 'Disable Notifications', + `Are you sure you want to disable push notifications for ${topicLabel}?`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Disable', + style: 'destructive', + onPress: async () => { + try { + const result = await unsubscribeFromTopics(topics); + if (result.successes.length > 0) { + Alert.alert( + 'Success', + `Disabled notifications for ${topicLabel}`, + [{ text: 'OK' }], + ); + } else { + Alert.alert( + 'Error', + `Failed to disable: ${result.failures.map(f => f.error).join(', ')}`, + [{ text: 'OK' }], + ); + } + } catch (error) { + Alert.alert( + 'Error', + error instanceof Error + ? error.message + : 'Failed to unsubscribe', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + } else { + // Subscribe without confirmation + try { + const result = await subscribeToTopics(topics); + if (result.successes.length > 0) { + Alert.alert('✅ Success', `Enabled notifications for ${topicLabel}`, [ + { text: 'OK' }, + ]); + } else { + Alert.alert( + 'Error', + `Failed to enable: ${result.failures.map(f => f.error).join(', ')}`, + [{ text: 'OK' }], + ); + } + } catch (error) { + Alert.alert( + 'Error', + error instanceof Error ? error.message : 'Failed to subscribe', + [{ text: 'OK' }], + ); + } + } + }; + + return { + hasNotificationPermission, + subscribedTopics, + handleTopicToggle, + }; +}; diff --git a/app/src/screens/dev/sections/DangerZoneSection.tsx b/app/src/screens/dev/sections/DangerZoneSection.tsx new file mode 100644 index 000000000..10f6a8565 --- /dev/null +++ b/app/src/screens/dev/sections/DangerZoneSection.tsx @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Button, Text } from 'tamagui'; + +import { + red500, + slate500, + white, + yellow500, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import WarningIcon from '@/assets/icons/warning.svg'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; + +interface DangerZoneSectionProps { + onClearSecrets: () => void; + onClearDocumentCatalog: () => void; + onClearPointEvents: () => void; + onResetBackupState: () => void; + onClearBackupEvents: () => void; +} + +export const DangerZoneSection: React.FC = ({ + onClearSecrets, + onClearDocumentCatalog, + onClearPointEvents, + onResetBackupState, + onClearBackupEvents, +}) => { + const dangerActions = [ + { + label: 'Delete your private key', + onPress: onClearSecrets, + dangerTheme: true, + }, + { + label: 'Clear document catalog', + onPress: onClearDocumentCatalog, + dangerTheme: true, + }, + { + label: 'Clear point events', + onPress: onClearPointEvents, + dangerTheme: true, + }, + { + label: 'Reset backup state', + onPress: onResetBackupState, + dangerTheme: true, + }, + { + label: 'Clear backup events', + onPress: onClearBackupEvents, + dangerTheme: true, + }, + ]; + + return ( + } + title="Danger Zone" + description="These actions are sensitive" + darkMode={true} + > + {dangerActions.map(({ label, onPress, dangerTheme }) => ( + + ))} + + ); +}; diff --git a/app/src/screens/dev/sections/DebugShortcutsSection.tsx b/app/src/screens/dev/sections/DebugShortcutsSection.tsx new file mode 100644 index 000000000..d44903ad6 --- /dev/null +++ b/app/src/screens/dev/sections/DebugShortcutsSection.tsx @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Button, Text, XStack, YStack } from 'tamagui'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { ChevronRight } from '@tamagui/lucide-icons'; + +import { slate200, slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import BugIcon from '@/assets/icons/bug_icon.svg'; +import type { RootStackParamList } from '@/navigation'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; +import { ScreenSelector } from '@/screens/dev/components/ScreenSelector'; +import { IS_DEV_MODE } from '@/utils/devUtils'; + +interface DebugShortcutsSectionProps { + navigation: NativeStackNavigationProp; +} + +export const DebugShortcutsSection: React.FC = ({ + navigation, +}) => { + return ( + } + title="Debug Shortcuts" + description="Jump directly to any screen for testing" + > + + + + {IS_DEV_MODE && ( + + )} + + + + ); +}; diff --git a/app/src/screens/dev/sections/DevTogglesSection.tsx b/app/src/screens/dev/sections/DevTogglesSection.tsx new file mode 100644 index 000000000..0c2972b6c --- /dev/null +++ b/app/src/screens/dev/sections/DevTogglesSection.tsx @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Alert, Platform } from 'react-native'; + +import BugIcon from '@/assets/icons/bug_icon.svg'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; +import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton'; + +interface DevTogglesSectionProps { + kycEnabled: boolean; + setKycEnabled: (enabled: boolean) => void; + useStrongBox: boolean; + setUseStrongBox: (useStrongBox: boolean) => void; +} + +export const DevTogglesSection: React.FC = ({ + kycEnabled, + setKycEnabled, + useStrongBox, + setUseStrongBox, +}) => { + const handleToggleStrongBox = () => { + Alert.alert( + useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox', + useStrongBox + ? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.' + : 'New keys will attempt to use StrongBox hardware backing for enhanced security.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: useStrongBox ? 'Disable' : 'Enable', + onPress: () => setUseStrongBox(!useStrongBox), + }, + ], + ); + }; + + return ( + } + title="Options" + description="Development and security options" + > + setKycEnabled(!kycEnabled)} + /> + {Platform.OS === 'android' && ( + + )} + + ); +}; diff --git a/app/src/screens/dev/sections/PushNotificationsSection.tsx b/app/src/screens/dev/sections/PushNotificationsSection.tsx new file mode 100644 index 000000000..98771d826 --- /dev/null +++ b/app/src/screens/dev/sections/PushNotificationsSection.tsx @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { YStack } from 'tamagui'; + +import BugIcon from '@/assets/icons/bug_icon.svg'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; +import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton'; + +interface PushNotificationsSectionProps { + hasNotificationPermission: boolean; + subscribedTopics: string[]; + onTopicToggle: (topics: string[], topicLabel: string) => void; +} + +export const PushNotificationsSection: React.FC< + PushNotificationsSectionProps +> = ({ hasNotificationPermission, subscribedTopics, onTopicToggle }) => { + return ( + } + title="Push Notifications" + description="Manage topic subscriptions" + > + + onTopicToggle(['nova'], 'Starfall')} + /> + onTopicToggle(['general'], 'General')} + /> + onTopicToggle(['nova', 'general'], 'both topics')} + /> + + + ); +}; diff --git a/app/src/screens/dev/sections/index.ts b/app/src/screens/dev/sections/index.ts new file mode 100644 index 000000000..2981b8244 --- /dev/null +++ b/app/src/screens/dev/sections/index.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +export { DangerZoneSection } from '@/screens/dev/sections/DangerZoneSection'; +export { DebugShortcutsSection } from '@/screens/dev/sections/DebugShortcutsSection'; +export { DevTogglesSection } from '@/screens/dev/sections/DevTogglesSection'; +export { PushNotificationsSection } from '@/screens/dev/sections/PushNotificationsSection'; diff --git a/app/src/screens/documents/scanning/DocumentCameraTroubleScreen.tsx b/app/src/screens/documents/scanning/DocumentCameraTroubleScreen.tsx index c99d5e242..684d7b8f5 100644 --- a/app/src/screens/documents/scanning/DocumentCameraTroubleScreen.tsx +++ b/app/src/screens/documents/scanning/DocumentCameraTroubleScreen.tsx @@ -20,6 +20,7 @@ import useHapticNavigation from '@/hooks/useHapticNavigation'; import { useSumsubLauncher } from '@/hooks/useSumsubLauncher'; import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout'; import { flush as flushAnalytics } from '@/services/analytics'; +import { useSettingStore } from '@/stores/settingStore'; const tips: TipProps[] = [ { @@ -54,6 +55,7 @@ const DocumentCameraTroubleScreen: React.FC = () => { const selfClient = useSelfClient(); const { useMRZStore } = selfClient; const { countryCode } = useMRZStore(); + const kycEnabled = useSettingStore(state => state.kycEnabled); const { launchSumsubVerification, isLoading } = useSumsubLauncher({ countryCode, errorSource: 'sumsub_initialization', @@ -80,21 +82,25 @@ const DocumentCameraTroubleScreen: React.FC = () => { page quickly and clearly! - - Or try an alternative verification method: - + {kycEnabled && ( + <> + + Or try an alternative verification method: + - - {isLoading ? 'Loading...' : 'Try Alternative Verification'} - + + {isLoading ? 'Loading...' : 'Try Alternative Verification'} + + + )} } > diff --git a/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx b/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx index 0af2728d0..a75e96c36 100644 --- a/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx +++ b/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx @@ -19,6 +19,7 @@ import { useSumsubLauncher } from '@/hooks/useSumsubLauncher'; import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout'; import { flushAllAnalytics } from '@/services/analytics'; import { openSupportForm, SUPPORT_FORM_BUTTON_TEXT } from '@/services/support'; +import { useSettingStore } from '@/stores/settingStore'; const tips: TipProps[] = [ { @@ -55,6 +56,7 @@ const DocumentNFCTroubleScreen: React.FC = () => { const selfClient = useSelfClient(); const { useMRZStore } = selfClient; const { countryCode } = useMRZStore(); + const kycEnabled = useSettingStore(state => state.kycEnabled); const { launchSumsubVerification, isLoading } = useSumsubLauncher({ countryCode, errorSource: 'sumsub_initialization', @@ -89,14 +91,16 @@ const DocumentNFCTroubleScreen: React.FC = () => { {SUPPORT_FORM_BUTTON_TEXT} - - {isLoading ? 'Loading...' : 'Try Alternative Verification'} - + {kycEnabled && ( + + {isLoading ? 'Loading...' : 'Try Alternative Verification'} + + )} } > diff --git a/app/src/screens/documents/selection/IDPickerScreen.tsx b/app/src/screens/documents/selection/IDPickerScreen.tsx index 3d8415110..f3164f34d 100644 --- a/app/src/screens/documents/selection/IDPickerScreen.tsx +++ b/app/src/screens/documents/selection/IDPickerScreen.tsx @@ -13,6 +13,7 @@ import IDSelection from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-scree import { DocumentFlowNavBar } from '@/components/navbar/DocumentFlowNavBar'; import type { RootStackParamList } from '@/navigation'; +import { useSettingStore } from '@/stores/settingStore'; import { extraYPadding } from '@/utils/styleUtils'; type IDPickerScreenRouteProp = RouteProp; @@ -21,6 +22,7 @@ const IDPickerScreen: React.FC = () => { const route = useRoute(); const { countryCode = '', documentTypes = [] } = route.params || {}; const bottom = useSafeAreaInsets().bottom; + const kycEnabled = useSettingStore(state => state.kycEnabled); return ( { paddingBottom={bottom + extraYPadding + 24} > - + ); }; diff --git a/app/src/stores/settingStore.ts b/app/src/stores/settingStore.ts index eee144d61..abecfc444 100644 --- a/app/src/stores/settingStore.ts +++ b/app/src/stores/settingStore.ts @@ -21,6 +21,7 @@ interface PersistedSettingsState { homeScreenViewCount: number; incrementHomeScreenViewCount: () => void; isDevMode: boolean; + kycEnabled: boolean; loggingSeverity: LoggingSeverity; pointsAddress: string | null; removeSubscribedTopic: (topic: string) => void; @@ -32,6 +33,7 @@ interface PersistedSettingsState { setFcmToken: (token: string | null) => void; setHasViewedRecoveryPhrase: (viewed: boolean) => void; setKeychainMigrationCompleted: () => void; + setKycEnabled: (enabled: boolean) => void; setLoggingSeverity: (severity: LoggingSeverity) => void; setPointsAddress: (address: string | null) => void; setSkipDocumentSelector: (value: boolean) => void; @@ -148,6 +150,10 @@ export const useSettingStore = create()( useStrongBox: false, setUseStrongBox: (useStrongBox: boolean) => set({ useStrongBox }), + // KYC flow toggle (default: false, dev-only feature) + kycEnabled: false, + setKycEnabled: (enabled: boolean) => set({ kycEnabled: enabled }), + // Non-persisted state (will not be saved to storage) hideNetworkModal: false, setHideNetworkModal: (hideNetworkModal: boolean) => { diff --git a/packages/mobile-sdk-alpha/src/config/features.ts b/packages/mobile-sdk-alpha/src/config/features.ts index 488e4a5b4..596851505 100644 --- a/packages/mobile-sdk-alpha/src/config/features.ts +++ b/packages/mobile-sdk-alpha/src/config/features.ts @@ -8,9 +8,5 @@ * Set to true when ready to launch the feature. */ export const FeatureFlags = { - /** - * Enable Sumsub/KYC "Other IDs" option in the ID selection screen. - * When false, the KYC button will be hidden from users. - */ - KYC_ENABLED: false, + // Add new flags here as needed } as const; diff --git a/packages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsx b/packages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsx index 3f3bfd973..4803fff46 100644 --- a/packages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsx +++ b/packages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsx @@ -11,7 +11,6 @@ import PassportCameraScanIcon from '../../../svgs/icons/passport_camera_scan.svg import PlusIcon from '../../../svgs/icons/plus.svg'; import SelfLogo from '../../../svgs/logo.svg'; import { BodyText, RoundFlag, View, XStack, YStack } from '../../components'; -import { FeatureFlags } from '../../config/features'; import { black, blue100, blue600, slate100, slate300, slate400, white } from '../../constants/colors'; import { advercase, dinot } from '../../constants/fonts'; import { useSelfClient } from '../../context'; @@ -129,10 +128,11 @@ const DocumentItem: React.FC = ({ docType, onPress }) => { type IDSelectionScreenProps = { countryCode: string; documentTypes: string[]; + showKyc?: boolean; }; const IDSelectionScreen: React.FC = props => { - const { countryCode = '', documentTypes = [] } = props; + const { countryCode = '', documentTypes = [], showKyc = false } = props; const selfClient = useSelfClient(); const onSelectDocumentType = (docType: string) => { @@ -173,7 +173,7 @@ const IDSelectionScreen: React.FC = props => { onSelectDocumentType(docType)} /> ))} Be sure your document is ready to scan - {FeatureFlags.KYC_ENABLED && ( + {showKyc && ( onSelectDocumentType('kyc')} />