SELF-1932: sumsub success screen (#1667)

* fix typos

* typo

* match screen design. fix tests
This commit is contained in:
Justin Hernandez
2026-01-28 21:27:22 -08:00
committed by GitHub
parent c7c9985d91
commit ec7ad1e66d
6 changed files with 294 additions and 1 deletions

View File

@@ -201,6 +201,7 @@ export type RootStackParamList = Omit<
// Onboarding screens
Disclaimer: undefined;
KycSuccess: undefined;
// Dev screens
CreateMock: undefined;

View File

@@ -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;

View File

@@ -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),

View File

@@ -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<NativeStackNavigationProp<RootStackParamList>>();
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 (
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
<View style={styles.centerSection}>
<View style={styles.animationContainer}>
<DelayedLottieView
autoPlay
loop={true}
source={loadingAnimation}
style={styles.animation}
cacheComposition={true}
renderMode="HARDWARE"
/>
</View>
<YStack
paddingHorizontal={24}
justifyContent="center"
alignItems="center"
gap={12}
>
<Title style={styles.title}>Your ID is being verified</Title>
<Description style={styles.description}>
Turn on push notifications to receive an update on your
verification. It's also safe the close the app and come back later.
</Description>
</YStack>
</View>
<YStack gap={12} paddingHorizontal={20} paddingBottom={24}>
<AbstractButton
bgColor={white}
color={black}
onPress={handleReceiveUpdates}
>
Receive live updates
</AbstractButton>
<AbstractButton
bgColor="transparent"
color={white}
borderColor="rgba(255, 255, 255, 0.3)"
borderWidth={1}
onPress={handleCheckLater}
>
I will check back later
</AbstractButton>
</YStack>
</View>
);
};
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;

View File

@@ -85,6 +85,7 @@ describe('navigation', () => {
'Home',
'IDPicker',
'IdDetails',
'KycSuccess',
'Loading',
'ManageDocuments',
'MockDataDeepLink',

View File

@@ -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) => <div {...props}>{children}</div>,
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
}));
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) => <div {...props}>{children}</div>,
View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
}));
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) => (
<button onClick={onPress} data-testid="abstract-button" type="button">
{children}
</button>
),
PrimaryButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="primary-button" type="button">
{children}
</button>
),
SecondaryButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="secondary-button" type="button">
{children}
</button>
),
Title: ({ children }: any) => <div data-testid="title">{children}</div>,
Description: ({ children }: any) => (
<div data-testid="description">{children}</div>
),
}));
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(<KycSuccessScreen />);
expect(root).toBeTruthy();
});
it('should have navigation available', () => {
render(<KycSuccessScreen />);
expect(mockUseNavigation).toHaveBeenCalled();
});
it('should have notification service available', () => {
render(<KycSuccessScreen />);
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(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>,
);
// 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();
});
});