mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
SELF-1951: prep for sumsub release (#1680)
* enable sumsub in mobile sdk * refactor dev settings screen * combine sections * agent feedback * gate kyc button on troubel screens * inline simple sections
This commit is contained in:
File diff suppressed because it is too large
Load Diff
208
app/src/screens/dev/components/ErrorInjectionSelector.tsx
Normal file
208
app/src/screens/dev/components/ErrorInjectionSelector.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { Check, ChevronDown } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
red500,
|
||||
slate200,
|
||||
slate500,
|
||||
slate600,
|
||||
slate800,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import type { InjectedErrorType } from '@/stores/errorInjectionStore';
|
||||
import {
|
||||
ERROR_GROUPS,
|
||||
ERROR_LABELS,
|
||||
useErrorInjectionStore,
|
||||
} from '@/stores/errorInjectionStore';
|
||||
import {
|
||||
registerModalCallbacks,
|
||||
unregisterModalCallbacks,
|
||||
} from '@/utils/modalCallbackRegistry';
|
||||
|
||||
export const ErrorInjectionSelector = () => {
|
||||
const injectedErrors = useErrorInjectionStore(state => state.injectedErrors);
|
||||
const setInjectedErrors = useErrorInjectionStore(
|
||||
state => state.setInjectedErrors,
|
||||
);
|
||||
const clearAllErrors = useErrorInjectionStore(state => state.clearAllErrors);
|
||||
const [open, setOpen] = useState(false);
|
||||
const callbackIdRef = useRef<number>();
|
||||
|
||||
const handleModalDismiss = useCallback(() => {
|
||||
setOpen(false);
|
||||
if (callbackIdRef.current !== undefined) {
|
||||
unregisterModalCallbacks(callbackIdRef.current);
|
||||
callbackIdRef.current = undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openSheet = useCallback(() => {
|
||||
setOpen(true);
|
||||
const id = registerModalCallbacks({
|
||||
onButtonPress: () => {},
|
||||
onModalDismiss: handleModalDismiss,
|
||||
});
|
||||
callbackIdRef.current = id;
|
||||
}, [handleModalDismiss]);
|
||||
|
||||
const closeSheet = useCallback(() => {
|
||||
handleModalDismiss();
|
||||
}, [handleModalDismiss]);
|
||||
|
||||
const handleSheetOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
handleModalDismiss();
|
||||
} else {
|
||||
setOpen(isOpen);
|
||||
}
|
||||
},
|
||||
[handleModalDismiss],
|
||||
);
|
||||
|
||||
// Single error selection - replace instead of toggle
|
||||
const selectError = (errorType: InjectedErrorType) => {
|
||||
// If clicking the same error, clear it; otherwise set the new one
|
||||
if (injectedErrors.length === 1 && injectedErrors[0] === errorType) {
|
||||
clearAllErrors();
|
||||
} else {
|
||||
setInjectedErrors([errorType]);
|
||||
}
|
||||
// Close the sheet after selection
|
||||
closeSheet();
|
||||
};
|
||||
|
||||
const currentError = injectedErrors.length > 0 ? injectedErrors[0] : null;
|
||||
const currentErrorLabel = currentError ? ERROR_LABELS[currentError] : null;
|
||||
|
||||
return (
|
||||
<YStack gap="$2">
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={openSheet}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
{currentErrorLabel || 'Select onboarding error to test'}
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
{currentError && (
|
||||
<Button
|
||||
backgroundColor={red500}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={clearAllErrors}
|
||||
pressStyle={{
|
||||
opacity: 0.8,
|
||||
scale: 0.98,
|
||||
}}
|
||||
>
|
||||
<Text color={white} fontSize="$5" fontFamily={dinot}>
|
||||
Clear
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={handleSheetOpenChange}
|
||||
snapPoints={[85]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Onboarding Error Testing
|
||||
</Text>
|
||||
<Button
|
||||
onPress={closeSheet}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
>
|
||||
{Object.entries(ERROR_GROUPS).map(([groupName, errors]) => (
|
||||
<YStack key={groupName} marginBottom="$4">
|
||||
<Text
|
||||
fontSize="$6"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
color={slate800}
|
||||
marginBottom="$2"
|
||||
>
|
||||
{groupName}
|
||||
</Text>
|
||||
{errors.map((errorType: InjectedErrorType) => (
|
||||
<TouchableOpacity
|
||||
key={errorType}
|
||||
onPress={() => selectError(errorType)}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{ERROR_LABELS[errorType]}
|
||||
</Text>
|
||||
{currentError === errorType && (
|
||||
<Check color={slate600} size={20} />
|
||||
)}
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</YStack>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
175
app/src/screens/dev/components/LogLevelSelector.tsx
Normal file
175
app/src/screens/dev/components/LogLevelSelector.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { Check, ChevronDown } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
slate200,
|
||||
slate500,
|
||||
slate600,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import {
|
||||
registerModalCallbacks,
|
||||
unregisterModalCallbacks,
|
||||
} from '@/utils/modalCallbackRegistry';
|
||||
|
||||
interface LogLevelSelectorProps {
|
||||
currentLevel: string;
|
||||
onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void;
|
||||
}
|
||||
|
||||
export const LogLevelSelector: React.FC<LogLevelSelectorProps> = ({
|
||||
currentLevel,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const callbackIdRef = useRef<number>();
|
||||
|
||||
const logLevels = ['debug', 'info', 'warn', 'error'] as const;
|
||||
|
||||
// Cleanup effect to unregister callbacks on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (callbackIdRef.current !== undefined) {
|
||||
unregisterModalCallbacks(callbackIdRef.current);
|
||||
callbackIdRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleModalDismiss = useCallback(() => {
|
||||
setOpen(false);
|
||||
if (callbackIdRef.current !== undefined) {
|
||||
unregisterModalCallbacks(callbackIdRef.current);
|
||||
callbackIdRef.current = undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openSheet = useCallback(() => {
|
||||
setOpen(true);
|
||||
const id = registerModalCallbacks({
|
||||
onButtonPress: () => {},
|
||||
onModalDismiss: handleModalDismiss,
|
||||
});
|
||||
callbackIdRef.current = id;
|
||||
}, [handleModalDismiss]);
|
||||
|
||||
const closeSheet = useCallback(() => {
|
||||
handleModalDismiss();
|
||||
}, [handleModalDismiss]);
|
||||
|
||||
const handleSheetOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
handleModalDismiss();
|
||||
} else {
|
||||
setOpen(isOpen);
|
||||
}
|
||||
},
|
||||
[handleModalDismiss],
|
||||
);
|
||||
|
||||
const handleLevelSelect = useCallback(
|
||||
(level: 'debug' | 'info' | 'warn' | 'error') => {
|
||||
closeSheet();
|
||||
onSelect(level);
|
||||
},
|
||||
[closeSheet, onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={openSheet}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
{currentLevel.toUpperCase()}
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={handleSheetOpenChange}
|
||||
snapPoints={[50]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Select log level
|
||||
</Text>
|
||||
<Button
|
||||
onPress={closeSheet}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
{logLevels.map(level => (
|
||||
<TouchableOpacity
|
||||
key={level}
|
||||
onPress={() => handleLevelSelect(level)}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{level.toUpperCase()}
|
||||
</Text>
|
||||
{currentLevel === level && (
|
||||
<Check color={slate600} size={20} />
|
||||
)}
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
94
app/src/screens/dev/components/ParameterSection.tsx
Normal file
94
app/src/screens/dev/components/ParameterSection.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { cloneElement, isValidElement } from 'react';
|
||||
import { Text, XStack, YStack } from 'tamagui';
|
||||
|
||||
import {
|
||||
slate100,
|
||||
slate200,
|
||||
slate400,
|
||||
slate600,
|
||||
slate800,
|
||||
slate900,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
interface ParameterSectionProps extends PropsWithChildren {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
darkMode?: boolean;
|
||||
}
|
||||
|
||||
export function ParameterSection({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
darkMode,
|
||||
children,
|
||||
}: ParameterSectionProps) {
|
||||
const renderIcon = () => {
|
||||
const iconElement =
|
||||
typeof icon === 'function'
|
||||
? (icon as () => React.ReactNode)()
|
||||
: isValidElement(icon)
|
||||
? icon
|
||||
: null;
|
||||
|
||||
return iconElement
|
||||
? cloneElement(iconElement as React.ReactElement, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})
|
||||
: null;
|
||||
};
|
||||
|
||||
return (
|
||||
<YStack
|
||||
width="100%"
|
||||
backgroundColor={darkMode ? slate900 : slate100}
|
||||
borderRadius="$4"
|
||||
borderWidth={1}
|
||||
borderColor={darkMode ? slate800 : slate200}
|
||||
padding="$4"
|
||||
flexDirection="column"
|
||||
gap="$3"
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
flexDirection="row"
|
||||
justifyContent="flex-start"
|
||||
gap="$4"
|
||||
>
|
||||
<YStack
|
||||
backgroundColor="gray"
|
||||
borderRadius={5}
|
||||
width={46}
|
||||
height={46}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
padding="$2"
|
||||
>
|
||||
{renderIcon()}
|
||||
</YStack>
|
||||
<YStack flexDirection="column" gap="$1">
|
||||
<Text
|
||||
fontSize="$5"
|
||||
color={darkMode ? white : slate600}
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text fontSize="$3" color={slate400} fontFamily={dinot}>
|
||||
{description}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
{children}
|
||||
</YStack>
|
||||
);
|
||||
}
|
||||
122
app/src/screens/dev/components/ScreenSelector.tsx
Normal file
122
app/src/screens/dev/components/ScreenSelector.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { ChevronDown } from '@tamagui/lucide-icons';
|
||||
|
||||
import {
|
||||
slate200,
|
||||
slate500,
|
||||
slate600,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import { navigationScreens } from '@/navigation';
|
||||
|
||||
export const ScreenSelector = () => {
|
||||
const navigation = useNavigation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const screenList = useMemo(
|
||||
() =>
|
||||
(
|
||||
Object.keys(navigationScreens) as (keyof typeof navigationScreens)[]
|
||||
).sort(),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => setOpen(true)}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Select screen
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
snapPoints={[85]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Select screen
|
||||
</Text>
|
||||
<Button
|
||||
onPress={() => setOpen(false)}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 100 }}
|
||||
>
|
||||
{screenList.map(item => (
|
||||
<TouchableOpacity
|
||||
key={item}
|
||||
onPress={() => {
|
||||
setOpen(false);
|
||||
navigation.navigate(item as never);
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{item}
|
||||
</Text>
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
58
app/src/screens/dev/components/TopicToggleButton.tsx
Normal file
58
app/src/screens/dev/components/TopicToggleButton.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Text } from 'tamagui';
|
||||
|
||||
import {
|
||||
slate200,
|
||||
slate400,
|
||||
slate600,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
export interface TopicToggleButtonProps {
|
||||
label: string;
|
||||
isSubscribed: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export const TopicToggleButton: React.FC<TopicToggleButtonProps> = ({
|
||||
label,
|
||||
isSubscribed,
|
||||
onToggle,
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
backgroundColor={isSubscribed ? '$green9' : slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={onToggle}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
paddingHorizontal="$4"
|
||||
pressStyle={{
|
||||
opacity: 0.8,
|
||||
scale: 0.98,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={isSubscribed ? white : slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
color={isSubscribed ? white : slate400}
|
||||
fontSize="$3"
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{isSubscribed ? 'Enabled' : 'Disabled'}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
10
app/src/screens/dev/components/index.ts
Normal file
10
app/src/screens/dev/components/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export type { TopicToggleButtonProps } from '@/screens/dev/components/TopicToggleButton';
|
||||
export { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector';
|
||||
export { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector';
|
||||
export { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
export { ScreenSelector } from '@/screens/dev/components/ScreenSelector';
|
||||
export { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';
|
||||
197
app/src/screens/dev/hooks/useDangerZoneActions.ts
Normal file
197
app/src/screens/dev/hooks/useDangerZoneActions.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
import { unsafe_clearSecrets } from '@/providers/authProvider';
|
||||
import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { usePointEventStore } from '@/stores/pointEventStore';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
export const useDangerZoneActions = () => {
|
||||
const { clearDocumentCatalogForMigrationTesting } = usePassport();
|
||||
const clearPointEvents = usePointEventStore(state => state.clearEvents);
|
||||
const { resetBackupForPoints } = useSettingStore();
|
||||
|
||||
const handleClearSecretsPress = () => {
|
||||
Alert.alert(
|
||||
'Delete Keychain Secrets',
|
||||
"Are you sure you want to remove your keychain secrets?\n\nIf this secret is not backed up, your account will be lost and the ID documents attached to it won't be usable.",
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await unsafe_clearSecrets();
|
||||
Alert.alert('Success', 'Keychain secrets cleared successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to clear keychain secrets:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to clear keychain secrets. Please try again.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearDocumentCatalogPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Document Catalog',
|
||||
'Are you sure you want to clear the document catalog?\n\nThis will remove all documents from the new storage system but preserve legacy storage for migration testing. You will need to restart the app to test migration.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await clearDocumentCatalogForMigrationTesting();
|
||||
Alert.alert(
|
||||
'Success',
|
||||
'Document catalog cleared successfully. Please restart the app to test migration.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to clear document catalog:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to clear document catalog. Please try again.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearPointEventsPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Point Events',
|
||||
'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await clearPointEvents();
|
||||
Alert.alert('Success', 'Point events cleared successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to clear point events:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to clear point events. Please try again.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleResetBackupStatePress = () => {
|
||||
Alert.alert(
|
||||
'Reset Backup State',
|
||||
'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Reset',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
resetBackupForPoints();
|
||||
Alert.alert('Success', 'Backup state reset successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearBackupEventsPress = () => {
|
||||
Alert.alert(
|
||||
'Clear Backup Events',
|
||||
'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const events = usePointEventStore.getState().events;
|
||||
const backupEvents = events.filter(
|
||||
event => event.type === 'backup',
|
||||
);
|
||||
await Promise.all(
|
||||
backupEvents.map(event =>
|
||||
usePointEventStore.getState().removeEvent(event.id),
|
||||
),
|
||||
);
|
||||
Alert.alert('Success', 'Backup events cleared successfully.', [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to clear backup events:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to clear backup events. Please try again.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
handleClearSecretsPress,
|
||||
handleClearDocumentCatalogPress,
|
||||
handleClearPointEventsPress,
|
||||
handleResetBackupStatePress,
|
||||
handleClearBackupEventsPress,
|
||||
};
|
||||
};
|
||||
156
app/src/screens/dev/hooks/useNotificationHandlers.ts
Normal file
156
app/src/screens/dev/hooks/useNotificationHandlers.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Alert, AppState } from 'react-native';
|
||||
|
||||
import {
|
||||
isNotificationSystemReady,
|
||||
requestNotificationPermission,
|
||||
subscribeToTopics,
|
||||
unsubscribeFromTopics,
|
||||
} from '@/services/notifications/notificationService';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
export const useNotificationHandlers = () => {
|
||||
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
|
||||
const [hasNotificationPermission, setHasNotificationPermission] =
|
||||
useState(false);
|
||||
|
||||
const checkPermissions = useCallback(async () => {
|
||||
const readiness = await isNotificationSystemReady();
|
||||
setHasNotificationPermission(readiness.ready);
|
||||
}, []);
|
||||
|
||||
// Check notification permissions on mount and when app regains focus
|
||||
useEffect(() => {
|
||||
checkPermissions();
|
||||
|
||||
const subscription = AppState.addEventListener('change', nextAppState => {
|
||||
if (nextAppState === 'active') {
|
||||
checkPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [checkPermissions]);
|
||||
|
||||
const handleTopicToggle = async (topics: string[], topicLabel: string) => {
|
||||
// Check permissions first
|
||||
if (!hasNotificationPermission) {
|
||||
Alert.alert(
|
||||
'Permissions Required',
|
||||
'Push notifications are not enabled. Would you like to enable them?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Enable',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const granted = await requestNotificationPermission();
|
||||
if (granted) {
|
||||
// Update permission state
|
||||
setHasNotificationPermission(true);
|
||||
Alert.alert(
|
||||
'Success',
|
||||
'Permissions granted! You can now subscribe to topics.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Failed',
|
||||
'Could not enable notifications. Please enable them in your device Settings.',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to request permissions',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentlySubscribed = topics.every(topic =>
|
||||
subscribedTopics.includes(topic),
|
||||
);
|
||||
|
||||
if (isCurrentlySubscribed) {
|
||||
// Show confirmation dialog for unsubscribe
|
||||
Alert.alert(
|
||||
'Disable Notifications',
|
||||
`Are you sure you want to disable push notifications for ${topicLabel}?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Disable',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const result = await unsubscribeFromTopics(topics);
|
||||
if (result.successes.length > 0) {
|
||||
Alert.alert(
|
||||
'Success',
|
||||
`Disabled notifications for ${topicLabel}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
`Failed to disable: ${result.failures.map(f => f.error).join(', ')}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to unsubscribe',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// Subscribe without confirmation
|
||||
try {
|
||||
const result = await subscribeToTopics(topics);
|
||||
if (result.successes.length > 0) {
|
||||
Alert.alert('✅ Success', `Enabled notifications for ${topicLabel}`, [
|
||||
{ text: 'OK' },
|
||||
]);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
`Failed to enable: ${result.failures.map(f => f.error).join(', ')}`,
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error instanceof Error ? error.message : 'Failed to subscribe',
|
||||
[{ text: 'OK' }],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
hasNotificationPermission,
|
||||
subscribedTopics,
|
||||
handleTopicToggle,
|
||||
};
|
||||
};
|
||||
90
app/src/screens/dev/sections/DangerZoneSection.tsx
Normal file
90
app/src/screens/dev/sections/DangerZoneSection.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Text } from 'tamagui';
|
||||
|
||||
import {
|
||||
red500,
|
||||
slate500,
|
||||
white,
|
||||
yellow500,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import WarningIcon from '@/assets/icons/warning.svg';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
|
||||
interface DangerZoneSectionProps {
|
||||
onClearSecrets: () => void;
|
||||
onClearDocumentCatalog: () => void;
|
||||
onClearPointEvents: () => void;
|
||||
onResetBackupState: () => void;
|
||||
onClearBackupEvents: () => void;
|
||||
}
|
||||
|
||||
export const DangerZoneSection: React.FC<DangerZoneSectionProps> = ({
|
||||
onClearSecrets,
|
||||
onClearDocumentCatalog,
|
||||
onClearPointEvents,
|
||||
onResetBackupState,
|
||||
onClearBackupEvents,
|
||||
}) => {
|
||||
const dangerActions = [
|
||||
{
|
||||
label: 'Delete your private key',
|
||||
onPress: onClearSecrets,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear document catalog',
|
||||
onPress: onClearDocumentCatalog,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear point events',
|
||||
onPress: onClearPointEvents,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Reset backup state',
|
||||
onPress: onResetBackupState,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Clear backup events',
|
||||
onPress: onClearBackupEvents,
|
||||
dangerTheme: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ParameterSection
|
||||
icon={<WarningIcon color={yellow500} />}
|
||||
title="Danger Zone"
|
||||
description="These actions are sensitive"
|
||||
darkMode={true}
|
||||
>
|
||||
{dangerActions.map(({ label, onPress, dangerTheme }) => (
|
||||
<Button
|
||||
key={label}
|
||||
style={{ backgroundColor: dangerTheme ? red500 : white }}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={onPress}
|
||||
flexDirection="row"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
<Text
|
||||
color={dangerTheme ? white : slate500}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
</ParameterSection>
|
||||
);
|
||||
};
|
||||
108
app/src/screens/dev/sections/DebugShortcutsSection.tsx
Normal file
108
app/src/screens/dev/sections/DebugShortcutsSection.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Text, XStack, YStack } from 'tamagui';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { ChevronRight } from '@tamagui/lucide-icons';
|
||||
|
||||
import { slate200, slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import BugIcon from '@/assets/icons/bug_icon.svg';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
import { ScreenSelector } from '@/screens/dev/components/ScreenSelector';
|
||||
import { IS_DEV_MODE } from '@/utils/devUtils';
|
||||
|
||||
interface DebugShortcutsSectionProps {
|
||||
navigation: NativeStackNavigationProp<RootStackParamList>;
|
||||
}
|
||||
|
||||
export const DebugShortcutsSection: React.FC<DebugShortcutsSectionProps> = ({
|
||||
navigation,
|
||||
}) => {
|
||||
return (
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Debug Shortcuts"
|
||||
description="Jump directly to any screen for testing"
|
||||
>
|
||||
<YStack gap="$2">
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('DevPrivateKey');
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
View Private Key
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('SumsubTest');
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Sumsub Test Flow
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
{IS_DEV_MODE && (
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('Home', { testReferralFlow: true });
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
Test Referral Flow
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
)}
|
||||
<ScreenSelector />
|
||||
</YStack>
|
||||
</ParameterSection>
|
||||
);
|
||||
};
|
||||
61
app/src/screens/dev/sections/DevTogglesSection.tsx
Normal file
61
app/src/screens/dev/sections/DevTogglesSection.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Alert, Platform } from 'react-native';
|
||||
|
||||
import BugIcon from '@/assets/icons/bug_icon.svg';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';
|
||||
|
||||
interface DevTogglesSectionProps {
|
||||
kycEnabled: boolean;
|
||||
setKycEnabled: (enabled: boolean) => void;
|
||||
useStrongBox: boolean;
|
||||
setUseStrongBox: (useStrongBox: boolean) => void;
|
||||
}
|
||||
|
||||
export const DevTogglesSection: React.FC<DevTogglesSectionProps> = ({
|
||||
kycEnabled,
|
||||
setKycEnabled,
|
||||
useStrongBox,
|
||||
setUseStrongBox,
|
||||
}) => {
|
||||
const handleToggleStrongBox = () => {
|
||||
Alert.alert(
|
||||
useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox',
|
||||
useStrongBox
|
||||
? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.'
|
||||
: 'New keys will attempt to use StrongBox hardware backing for enhanced security.',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: useStrongBox ? 'Disable' : 'Enable',
|
||||
onPress: () => setUseStrongBox(!useStrongBox),
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Options"
|
||||
description="Development and security options"
|
||||
>
|
||||
<TopicToggleButton
|
||||
label="KYC Flow"
|
||||
isSubscribed={kycEnabled}
|
||||
onToggle={() => setKycEnabled(!kycEnabled)}
|
||||
/>
|
||||
{Platform.OS === 'android' && (
|
||||
<TopicToggleButton
|
||||
label="Use StrongBox"
|
||||
isSubscribed={useStrongBox}
|
||||
onToggle={handleToggleStrongBox}
|
||||
/>
|
||||
)}
|
||||
</ParameterSection>
|
||||
);
|
||||
};
|
||||
54
app/src/screens/dev/sections/PushNotificationsSection.tsx
Normal file
54
app/src/screens/dev/sections/PushNotificationsSection.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { YStack } from 'tamagui';
|
||||
|
||||
import BugIcon from '@/assets/icons/bug_icon.svg';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton';
|
||||
|
||||
interface PushNotificationsSectionProps {
|
||||
hasNotificationPermission: boolean;
|
||||
subscribedTopics: string[];
|
||||
onTopicToggle: (topics: string[], topicLabel: string) => void;
|
||||
}
|
||||
|
||||
export const PushNotificationsSection: React.FC<
|
||||
PushNotificationsSectionProps
|
||||
> = ({ hasNotificationPermission, subscribedTopics, onTopicToggle }) => {
|
||||
return (
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Push Notifications"
|
||||
description="Manage topic subscriptions"
|
||||
>
|
||||
<YStack gap="$2">
|
||||
<TopicToggleButton
|
||||
label="Starfall"
|
||||
isSubscribed={
|
||||
hasNotificationPermission && subscribedTopics.includes('nova')
|
||||
}
|
||||
onToggle={() => onTopicToggle(['nova'], 'Starfall')}
|
||||
/>
|
||||
<TopicToggleButton
|
||||
label="General"
|
||||
isSubscribed={
|
||||
hasNotificationPermission && subscribedTopics.includes('general')
|
||||
}
|
||||
onToggle={() => onTopicToggle(['general'], 'General')}
|
||||
/>
|
||||
<TopicToggleButton
|
||||
label="Both (Starfall + General)"
|
||||
isSubscribed={
|
||||
hasNotificationPermission &&
|
||||
subscribedTopics.includes('nova') &&
|
||||
subscribedTopics.includes('general')
|
||||
}
|
||||
onToggle={() => onTopicToggle(['nova', 'general'], 'both topics')}
|
||||
/>
|
||||
</YStack>
|
||||
</ParameterSection>
|
||||
);
|
||||
};
|
||||
8
app/src/screens/dev/sections/index.ts
Normal file
8
app/src/screens/dev/sections/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export { DangerZoneSection } from '@/screens/dev/sections/DangerZoneSection';
|
||||
export { DebugShortcutsSection } from '@/screens/dev/sections/DebugShortcutsSection';
|
||||
export { DevTogglesSection } from '@/screens/dev/sections/DevTogglesSection';
|
||||
export { PushNotificationsSection } from '@/screens/dev/sections/PushNotificationsSection';
|
||||
@@ -20,6 +20,7 @@ import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
|
||||
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
|
||||
import { flush as flushAnalytics } from '@/services/analytics';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
const tips: TipProps[] = [
|
||||
{
|
||||
@@ -54,6 +55,7 @@ const DocumentCameraTroubleScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
const { useMRZStore } = selfClient;
|
||||
const { countryCode } = useMRZStore();
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
const { launchSumsubVerification, isLoading } = useSumsubLauncher({
|
||||
countryCode,
|
||||
errorSource: 'sumsub_initialization',
|
||||
@@ -80,21 +82,25 @@ const DocumentCameraTroubleScreen: React.FC = () => {
|
||||
page quickly and clearly!
|
||||
</Caption>
|
||||
|
||||
<Caption
|
||||
size="large"
|
||||
style={{ color: slate500, marginTop: 12, marginBottom: 8 }}
|
||||
>
|
||||
Or try an alternative verification method:
|
||||
</Caption>
|
||||
{kycEnabled && (
|
||||
<>
|
||||
<Caption
|
||||
size="large"
|
||||
style={{ color: slate500, marginTop: 12, marginBottom: 8 }}
|
||||
>
|
||||
Or try an alternative verification method:
|
||||
</Caption>
|
||||
|
||||
<SecondaryButton
|
||||
onPress={launchSumsubVerification}
|
||||
disabled={isLoading}
|
||||
textColor={slate700}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
onPress={launchSumsubVerification}
|
||||
disabled={isLoading}
|
||||
textColor={slate700}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
|
||||
</SecondaryButton>
|
||||
</>
|
||||
)}
|
||||
</YStack>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
|
||||
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
|
||||
import { flushAllAnalytics } from '@/services/analytics';
|
||||
import { openSupportForm, SUPPORT_FORM_BUTTON_TEXT } from '@/services/support';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
const tips: TipProps[] = [
|
||||
{
|
||||
@@ -55,6 +56,7 @@ const DocumentNFCTroubleScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
const { useMRZStore } = selfClient;
|
||||
const { countryCode } = useMRZStore();
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
const { launchSumsubVerification, isLoading } = useSumsubLauncher({
|
||||
countryCode,
|
||||
errorSource: 'sumsub_initialization',
|
||||
@@ -89,14 +91,16 @@ const DocumentNFCTroubleScreen: React.FC = () => {
|
||||
{SUPPORT_FORM_BUTTON_TEXT}
|
||||
</SecondaryButton>
|
||||
|
||||
<SecondaryButton
|
||||
onPress={launchSumsubVerification}
|
||||
disabled={isLoading}
|
||||
textColor={slate700}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
|
||||
</SecondaryButton>
|
||||
{kycEnabled && (
|
||||
<SecondaryButton
|
||||
onPress={launchSumsubVerification}
|
||||
disabled={isLoading}
|
||||
textColor={slate700}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
|
||||
</SecondaryButton>
|
||||
)}
|
||||
</YStack>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import IDSelection from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-scree
|
||||
|
||||
import { DocumentFlowNavBar } from '@/components/navbar/DocumentFlowNavBar';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
type IDPickerScreenRouteProp = RouteProp<RootStackParamList, 'IDPicker'>;
|
||||
@@ -21,6 +22,7 @@ const IDPickerScreen: React.FC = () => {
|
||||
const route = useRoute<IDPickerScreenRouteProp>();
|
||||
const { countryCode = '', documentTypes = [] } = route.params || {};
|
||||
const bottom = useSafeAreaInsets().bottom;
|
||||
const kycEnabled = useSettingStore(state => state.kycEnabled);
|
||||
|
||||
return (
|
||||
<YStack
|
||||
@@ -29,7 +31,11 @@ const IDPickerScreen: React.FC = () => {
|
||||
paddingBottom={bottom + extraYPadding + 24}
|
||||
>
|
||||
<DocumentFlowNavBar title="GETTING STARTED" />
|
||||
<IDSelection countryCode={countryCode} documentTypes={documentTypes} />
|
||||
<IDSelection
|
||||
countryCode={countryCode}
|
||||
documentTypes={documentTypes}
|
||||
showKyc={kycEnabled}
|
||||
/>
|
||||
</YStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ interface PersistedSettingsState {
|
||||
homeScreenViewCount: number;
|
||||
incrementHomeScreenViewCount: () => void;
|
||||
isDevMode: boolean;
|
||||
kycEnabled: boolean;
|
||||
loggingSeverity: LoggingSeverity;
|
||||
pointsAddress: string | null;
|
||||
removeSubscribedTopic: (topic: string) => void;
|
||||
@@ -32,6 +33,7 @@ interface PersistedSettingsState {
|
||||
setFcmToken: (token: string | null) => void;
|
||||
setHasViewedRecoveryPhrase: (viewed: boolean) => void;
|
||||
setKeychainMigrationCompleted: () => void;
|
||||
setKycEnabled: (enabled: boolean) => void;
|
||||
setLoggingSeverity: (severity: LoggingSeverity) => void;
|
||||
setPointsAddress: (address: string | null) => void;
|
||||
setSkipDocumentSelector: (value: boolean) => void;
|
||||
@@ -148,6 +150,10 @@ export const useSettingStore = create<SettingsState>()(
|
||||
useStrongBox: false,
|
||||
setUseStrongBox: (useStrongBox: boolean) => set({ useStrongBox }),
|
||||
|
||||
// KYC flow toggle (default: false, dev-only feature)
|
||||
kycEnabled: false,
|
||||
setKycEnabled: (enabled: boolean) => set({ kycEnabled: enabled }),
|
||||
|
||||
// Non-persisted state (will not be saved to storage)
|
||||
hideNetworkModal: false,
|
||||
setHideNetworkModal: (hideNetworkModal: boolean) => {
|
||||
|
||||
Reference in New Issue
Block a user