From ec7ad1e66dae38850155ca15edbcd071084fd028 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Wed, 28 Jan 2026 21:27:22 -0800 Subject: [PATCH] SELF-1932: sumsub success screen (#1667) * fix typos * typo * match screen design. fix tests --- app/src/navigation/index.tsx | 1 + app/src/navigation/onboarding.ts | 8 + app/src/providers/selfClientProvider.tsx | 7 +- app/src/screens/kyc/KycSuccessScreen.tsx | 124 ++++++++++++++ app/tests/src/navigation.test.tsx | 1 + .../src/screens/kyc/KycSuccessScreen.test.tsx | 154 ++++++++++++++++++ 6 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 app/src/screens/kyc/KycSuccessScreen.tsx create mode 100644 app/tests/src/screens/kyc/KycSuccessScreen.test.tsx diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index 65d68e114..17f90209b 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -201,6 +201,7 @@ export type RootStackParamList = Omit< // Onboarding screens Disclaimer: undefined; + KycSuccess: undefined; // Dev screens CreateMock: undefined; diff --git a/app/src/navigation/onboarding.ts b/app/src/navigation/onboarding.ts index 4c034a545..72156f420 100644 --- a/app/src/navigation/onboarding.ts +++ b/app/src/navigation/onboarding.ts @@ -4,6 +4,7 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; +import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen'; import AccountVerifiedSuccessScreen from '@/screens/onboarding/AccountVerifiedSuccessScreen'; import DisclaimerScreen from '@/screens/onboarding/DisclaimerScreen'; import SaveRecoveryPhraseScreen from '@/screens/onboarding/SaveRecoveryPhraseScreen'; @@ -30,6 +31,13 @@ const onboardingScreens = { animation: 'slide_from_bottom', } as NativeStackNavigationOptions, }, + KycSuccess: { + screen: KycSuccessScreen, + options: { + headerShown: false, + animation: 'slide_from_bottom', + } as NativeStackNavigationOptions, + }, }; export default onboardingScreens; diff --git a/app/src/providers/selfClientProvider.tsx b/app/src/providers/selfClientProvider.tsx index 106133216..28f9abe5f 100644 --- a/app/src/providers/selfClientProvider.tsx +++ b/app/src/providers/selfClientProvider.tsx @@ -375,8 +375,13 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { countryCode, }); } + return; + } + + // Success case: navigate to KYC success screen + if (navigationRef.isReady()) { + navigationRef.navigate('KycSuccess'); } - // success case: provider handles its own success UI } catch (error) { const safeInitError = sanitizeErrorMessage( error instanceof Error ? error.message : String(error), diff --git a/app/src/screens/kyc/KycSuccessScreen.tsx b/app/src/screens/kyc/KycSuccessScreen.tsx new file mode 100644 index 000000000..22849692c --- /dev/null +++ b/app/src/screens/kyc/KycSuccessScreen.tsx @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha'; +import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json'; +import { + AbstractButton, + Description, + Title, +} from '@selfxyz/mobile-sdk-alpha/components'; +import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors'; + +import { buttonTap } from '@/integrations/haptics'; +import type { RootStackParamList } from '@/navigation'; +import { requestNotificationPermission } from '@/services/notifications/notificationService'; + +const KycSuccessScreen: React.FC = () => { + const navigation = + useNavigation>(); + const insets = useSafeAreaInsets(); + + const handleReceiveUpdates = async () => { + buttonTap(); + await requestNotificationPermission(); + // Navigate to Home regardless of permission result + navigation.navigate('Home', {}); + }; + + const handleCheckLater = () => { + buttonTap(); + navigation.navigate('Home', {}); + }; + + return ( + + + + + + + Your ID is being verified + + Turn on push notifications to receive an update on your + verification. It's also safe the close the app and come back later. + + + + + + Receive live updates + + + I will check back later + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: black, + }, + centerSection: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + animationContainer: { + width: 80, + height: 80, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 32, + }, + animation: { + width: 160, + height: 160, + }, + title: { + color: white, + textAlign: 'center', + fontSize: 28, + letterSpacing: 1, + }, + description: { + color: white, + textAlign: 'center', + fontSize: 18, + }, +}); + +export default KycSuccessScreen; diff --git a/app/tests/src/navigation.test.tsx b/app/tests/src/navigation.test.tsx index 4c9a7ef26..e32735297 100644 --- a/app/tests/src/navigation.test.tsx +++ b/app/tests/src/navigation.test.tsx @@ -85,6 +85,7 @@ describe('navigation', () => { 'Home', 'IDPicker', 'IdDetails', + 'KycSuccess', 'Loading', 'ManageDocuments', 'MockDataDeepLink', diff --git a/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx b/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx new file mode 100644 index 000000000..ea8cc6c72 --- /dev/null +++ b/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { render } from '@testing-library/react-native'; + +import ErrorBoundary from '@/components/ErrorBoundary'; +import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen'; +import * as notificationService from '@/services/notifications/notificationService'; + +// Note: While jest.setup.js provides comprehensive React Native mocking, +// react-test-renderer requires component-based mocks (functions) rather than +// string-based mocks for proper rendering. This minimal mock provides the +// specific components needed for this test without using requireActual to +// avoid memory issues (see .cursor/rules/test-memory-optimization.mdc). +jest.mock('react-native', () => ({ + __esModule: true, + Platform: { OS: 'ios', select: jest.fn() }, + StyleSheet: { + create: (styles: any) => styles, + flatten: (style: any) => style, + }, + View: ({ children, ...props }: any) =>
{children}
, + Text: ({ children, ...props }: any) => {children}, +})); + +jest.mock('react-native-edge-to-edge', () => ({ + SystemBars: () => null, +})); + +jest.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: jest.fn(() => ({ top: 0, bottom: 0 })), +})); + +jest.mock('@react-navigation/native', () => ({ + useNavigation: jest.fn(), +})); + +// Mock Tamagui components +jest.mock('tamagui', () => ({ + __esModule: true, + YStack: ({ children, ...props }: any) =>
{children}
, + View: ({ children, ...props }: any) =>
{children}
, + Text: ({ children, ...props }: any) => {children}, +})); + +jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ + DelayedLottieView: () => null, +})); + +jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({ + black: '#000000', + white: '#FFFFFF', +})); + +jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({ + AbstractButton: ({ children, onPress }: any) => ( + + ), + PrimaryButton: ({ children, onPress }: any) => ( + + ), + SecondaryButton: ({ children, onPress }: any) => ( + + ), + Title: ({ children }: any) =>
{children}
, + Description: ({ children }: any) => ( +
{children}
+ ), +})); + +jest.mock('@/integrations/haptics', () => ({ + buttonTap: jest.fn(), +})); + +jest.mock('@/services/notifications/notificationService', () => ({ + requestNotificationPermission: jest.fn(), +})); + +jest.mock('@/config/sentry', () => ({ + captureException: jest.fn(), +})); + +jest.mock('@/services/analytics', () => ({ + flushAllAnalytics: jest.fn(), + trackNfcEvent: jest.fn(), +})); + +const mockUseNavigation = useNavigation as jest.MockedFunction< + typeof useNavigation +>; + +describe('KycSuccessScreen', () => { + const mockNavigate = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseNavigation.mockReturnValue({ + navigate: mockNavigate, + } as any); + }); + + it('should render the screen without errors', () => { + const { root } = render(); + expect(root).toBeTruthy(); + }); + + it('should have navigation available', () => { + render(); + expect(mockUseNavigation).toHaveBeenCalled(); + }); + + it('should have notification service available', () => { + render(); + expect(notificationService.requestNotificationPermission).toBeDefined(); + }); + + it('renders fallback on render error', () => { + // Mock console.error to suppress error boundary error logs during test + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + // Create a component that throws an error during render + const ThrowError = () => { + throw new Error('Test render error'); + }; + + // Render the error-throwing component wrapped in ErrorBoundary + const { root } = render( + + + , + ); + + // Verify the error boundary fallback UI is displayed + // Use a more flexible matcher since the text is nested in mocked components + expect(root.findByType('span').props.children).toBe( + 'Something went wrong. Please restart the app.', + ); + + // Restore console.error + consoleErrorSpy.mockRestore(); + }); +});