diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index d7dfb4ed5..76fa3a846 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -80,6 +80,7 @@ export type RootStackParamList = Omit< | 'IDPicker' | 'IdDetails' | 'KycSuccess' + | 'KYCVerified' | 'RegistrationFallback' | 'Loading' | 'Modal' @@ -207,6 +208,12 @@ export type RootStackParamList = Omit< userId?: string; } | undefined; + KYCVerified: + | { + status?: string; + userId?: string; + } + | undefined; // Dev screens CreateMock: undefined; diff --git a/app/src/navigation/onboarding.ts b/app/src/navigation/onboarding.ts index 72156f420..bf5d4a769 100644 --- a/app/src/navigation/onboarding.ts +++ b/app/src/navigation/onboarding.ts @@ -5,6 +5,7 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen'; +import KYCVerifiedScreen from '@/screens/kyc/KYCVerifiedScreen'; import AccountVerifiedSuccessScreen from '@/screens/onboarding/AccountVerifiedSuccessScreen'; import DisclaimerScreen from '@/screens/onboarding/DisclaimerScreen'; import SaveRecoveryPhraseScreen from '@/screens/onboarding/SaveRecoveryPhraseScreen'; @@ -38,6 +39,13 @@ const onboardingScreens = { animation: 'slide_from_bottom', } as NativeStackNavigationOptions, }, + KYCVerified: { + screen: KYCVerifiedScreen, + options: { + headerShown: false, + animation: 'slide_from_bottom', + } as NativeStackNavigationOptions, + }, }; export default onboardingScreens; diff --git a/app/src/providers/notificationTrackingProvider.tsx b/app/src/providers/notificationTrackingProvider.tsx index a4346c8e1..5462f291c 100644 --- a/app/src/providers/notificationTrackingProvider.tsx +++ b/app/src/providers/notificationTrackingProvider.tsx @@ -4,16 +4,89 @@ import type { PropsWithChildren } from 'react'; import React, { useEffect } from 'react'; +import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import messaging from '@react-native-firebase/messaging'; import { NotificationEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { navigationRef } from '@/navigation'; import { trackEvent } from '@/services/analytics'; +// Queue for pending navigation actions that need to wait for navigation to be ready +let pendingNavigation: FirebaseMessagingTypes.RemoteMessage | null = null; + +/** + * Execute navigation for a notification + * @returns true if navigation was executed, false if it needs to be queued + */ +const executeNotificationNavigation = ( + remoteMessage: FirebaseMessagingTypes.RemoteMessage, +): boolean => { + if (!navigationRef.isReady()) { + return false; + } + + const notificationType = remoteMessage.data?.type; + const status = remoteMessage.data?.status; + + // Handle KYC result notifications + if (notificationType === 'kyc_result' && status === 'approved') { + navigationRef.navigate('KYCVerified', { + status: String(status), + userId: remoteMessage.data?.user_id + ? String(remoteMessage.data.user_id) + : undefined, + }); + return true; + } + // Add handling for other notification types here as needed + // For retry/rejected statuses, could navigate to appropriate screens in future + + return true; // Navigation handled (or not applicable) +}; + +/** + * Handle navigation based on notification type and data + * Queues navigation if navigationRef is not ready yet + */ +const handleNotificationNavigation = ( + remoteMessage: FirebaseMessagingTypes.RemoteMessage, +) => { + const executed = executeNotificationNavigation(remoteMessage); + + if (!executed) { + // Navigation not ready yet - queue for later + pendingNavigation = remoteMessage; + if (__DEV__) { + console.log( + 'Navigation not ready, queuing notification navigation:', + remoteMessage.data?.type, + ); + } + } +}; + +/** + * Process any pending navigation once navigation is ready + */ +const processPendingNavigation = () => { + if (pendingNavigation && navigationRef.isReady()) { + if (__DEV__) { + console.log( + 'Processing pending notification navigation:', + pendingNavigation.data?.type, + ); + } + executeNotificationNavigation(pendingNavigation); + pendingNavigation = null; + } +}; + export const NotificationTrackingProvider: React.FC = ({ children, }) => { useEffect(() => { + // Handle notification tap when app is in background const unsubscribe = messaging().onNotificationOpenedApp(remoteMessage => { trackEvent(NotificationEvents.BACKGROUND_NOTIFICATION_OPENED, { messageId: remoteMessage.messageId, @@ -22,8 +95,12 @@ export const NotificationTrackingProvider: React.FC = ({ // Track if user interacted with any actions actionId: remoteMessage.data?.actionId, }); + + // Handle navigation based on notification type + handleNotificationNavigation(remoteMessage); }); + // Handle notification tap when app is completely closed (cold start) messaging() .getInitialNotification() .then(remoteMessage => { @@ -35,11 +112,34 @@ export const NotificationTrackingProvider: React.FC = ({ // Track if user interacted with any actions actionId: remoteMessage.data?.actionId, }); + + // Handle navigation based on notification type + handleNotificationNavigation(remoteMessage); } }); return unsubscribe; }, []); + // Monitor navigation readiness and process pending navigation + useEffect(() => { + // Check immediately if navigation is already ready + if (navigationRef.isReady()) { + processPendingNavigation(); + return; + } + + // Poll for navigation readiness if not ready yet + const checkInterval = setInterval(() => { + if (navigationRef.isReady()) { + processPendingNavigation(); + clearInterval(checkInterval); + } + }, 100); // Check every 100ms + + // Cleanup interval on unmount + return () => clearInterval(checkInterval); + }, []); + return <>{children}; }; diff --git a/app/src/screens/kyc/KYCVerifiedScreen.tsx b/app/src/screens/kyc/KYCVerifiedScreen.tsx new file mode 100644 index 000000000..f8d71fbde --- /dev/null +++ b/app/src/screens/kyc/KYCVerifiedScreen.tsx @@ -0,0 +1,83 @@ +// 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 { + 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'; + +const KYCVerifiedScreen: React.FC = () => { + const navigation = + useNavigation>(); + const insets = useSafeAreaInsets(); + + const handleGenerateProof = () => { + buttonTap(); + navigation.navigate('ProvingScreenRouter'); + }; + + return ( + + + + Your ID has been verified + + Next Self will generate a zk proof specifically for this device that + you can use to proof your identity. + + + + + Generate proof + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: black, + }, + spacer: { + flex: 1, + }, + title: { + color: white, + textAlign: 'center', + fontSize: 28, + letterSpacing: 1, + }, + description: { + color: white, + textAlign: 'center', + fontSize: 18, + lineHeight: 27, + }, +}); + +export default KYCVerifiedScreen; diff --git a/app/tests/src/navigation.test.tsx b/app/tests/src/navigation.test.tsx index e32735297..a46550ba5 100644 --- a/app/tests/src/navigation.test.tsx +++ b/app/tests/src/navigation.test.tsx @@ -85,6 +85,7 @@ describe('navigation', () => { 'Home', 'IDPicker', 'IdDetails', + 'KYCVerified', 'KycSuccess', 'Loading', 'ManageDocuments', diff --git a/app/tests/src/providers/notificationTrackingProvider.test.tsx b/app/tests/src/providers/notificationTrackingProvider.test.tsx new file mode 100644 index 000000000..84f1e43b9 --- /dev/null +++ b/app/tests/src/providers/notificationTrackingProvider.test.tsx @@ -0,0 +1,511 @@ +// 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 type { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; +import { render, waitFor } from '@testing-library/react-native'; + +import { navigationRef } from '@/navigation'; +import { NotificationTrackingProvider } from '@/providers/notificationTrackingProvider'; +import * as analytics from '@/services/analytics'; + +// Mock Firebase messaging +const mockOnNotificationOpenedApp = jest.fn(); +const mockGetInitialNotification = jest.fn(); + +jest.mock('@react-native-firebase/messaging', () => { + return jest.fn(() => ({ + onNotificationOpenedApp: mockOnNotificationOpenedApp, + getInitialNotification: mockGetInitialNotification, + })); +}); + +// Mock navigation +jest.mock('@/navigation', () => ({ + navigationRef: { + isReady: jest.fn(), + navigate: jest.fn(), + }, +})); + +// Mock analytics +jest.mock('@/services/analytics', () => ({ + trackEvent: jest.fn(), +})); + +// Mock analytics constants +jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({ + NotificationEvents: { + BACKGROUND_NOTIFICATION_OPENED: 'BACKGROUND_NOTIFICATION_OPENED', + COLD_START_NOTIFICATION_OPENED: 'COLD_START_NOTIFICATION_OPENED', + }, +})); + +const mockNavigationRef = navigationRef as jest.Mocked; + +describe('NotificationTrackingProvider', () => { + const mockUserId = '19f21362-856a-4606-88e1-fa306036978f'; + + beforeEach(() => { + jest.clearAllMocks(); + mockNavigationRef.isReady.mockReturnValue(true); + }); + + it('should render children without errors', () => { + mockGetInitialNotification.mockResolvedValue(null); + + const { getByTestId } = render( + + Test Child + , + ); + + expect(getByTestId('child')).toHaveTextContent('Test Child'); + }); + + describe('Background notification (onNotificationOpenedApp)', () => { + it('should navigate to KYCVerified when notification type is kyc_result and status is approved', async () => { + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); // Return unsubscribe function + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + expect(mockOnNotificationOpenedApp).toHaveBeenCalled(); + + // Simulate notification tap + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'BACKGROUND_NOTIFICATION_OPENED', + { + messageId: 'test-message-id', + type: 'kyc_result', + actionId: undefined, + }, + ); + + expect(mockNavigationRef.navigate).toHaveBeenCalledWith('KYCVerified', { + status: 'approved', + userId: mockUserId, + }); + }); + }); + + it('should not navigate when status is retry', async () => { + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'retry', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalled(); + }); + + // Should not navigate for retry status + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should not navigate when status is rejected', async () => { + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'rejected', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalled(); + }); + + // Should not navigate for rejected status + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should handle missing notification data gracefully', async () => { + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + const remoteMessage = { + messageId: 'test-message-id', + data: undefined, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalled(); + }); + + // Should not navigate when data is missing + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should not navigate when navigation is not ready', async () => { + mockNavigationRef.isReady.mockReturnValue(false); + + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalled(); + }); + + // Should not navigate when navigationRef is not ready + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('Cold start notification (getInitialNotification)', () => { + it('should navigate to KYCVerified when notification type is kyc_result and status is approved', async () => { + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + render( + + Test + , + ); + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + { + messageId: 'test-message-id', + type: 'kyc_result', + actionId: undefined, + }, + ); + + expect(mockNavigationRef.navigate).toHaveBeenCalledWith('KYCVerified', { + status: 'approved', + userId: mockUserId, + }); + }); + }); + + it('should not navigate when getInitialNotification returns null', async () => { + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + await waitFor(() => { + expect(mockGetInitialNotification).toHaveBeenCalled(); + }); + + // Should not track or navigate when there's no initial notification + expect(analytics.trackEvent).not.toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should not navigate when status is retry on cold start', async () => { + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'retry', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + render( + + Test + , + ); + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + }); + + // Should not navigate for retry status + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should queue navigation when navigationRef is not ready on cold start', async () => { + // Start with navigation not ready + mockNavigationRef.isReady.mockReturnValue(false); + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + render( + + Test + , + ); + + // Wait for initial notification to be processed + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + }); + + // Navigation should not have been called yet + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + + // Simulate navigation becoming ready + mockNavigationRef.isReady.mockReturnValue(true); + + // Wait for the polling interval to detect navigation is ready + await waitFor( + () => { + expect(mockNavigationRef.navigate).toHaveBeenCalledWith( + 'KYCVerified', + { + status: 'approved', + userId: mockUserId, + }, + ); + }, + { timeout: 2000 }, + ); + }); + + it('should process pending navigation immediately if navigation becomes ready', async () => { + // Start with navigation not ready + mockNavigationRef.isReady.mockReturnValue(false); + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + const { rerender } = render( + + Test + , + ); + + // Wait for initial notification to be processed + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + }); + + // Navigation should not have been called yet + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + + // Make navigation ready + mockNavigationRef.isReady.mockReturnValue(true); + + // Trigger a re-render to simulate React's update cycle + rerender( + + Test + , + ); + + // Navigation should be called after navigation becomes ready + await waitFor( + () => { + expect(mockNavigationRef.navigate).toHaveBeenCalledWith( + 'KYCVerified', + { + status: 'approved', + userId: mockUserId, + }, + ); + }, + { timeout: 2000 }, + ); + }); + + it('should not queue navigation for non-KYC notifications when navigation is not ready', async () => { + mockNavigationRef.isReady.mockReturnValue(false); + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'other_notification', + status: 'some_status', + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + render( + + Test + , + ); + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + }); + + // Make navigation ready + mockNavigationRef.isReady.mockReturnValue(true); + + // Wait a bit to ensure no navigation happens + await new Promise(resolve => setTimeout(resolve, 300)); + + // Should not navigate for non-KYC notifications + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx b/app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx new file mode 100644 index 000000000..e28b85888 --- /dev/null +++ b/app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx @@ -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 React from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { fireEvent, render } from '@testing-library/react-native'; + +import * as haptics from '@/integrations/haptics'; +import KYCVerifiedScreen from '@/screens/kyc/KYCVerifiedScreen'; + +// 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/constants/colors', () => ({ + black: '#000000', + white: '#FFFFFF', +})); + +jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({ + AbstractButton: ({ children, onPress, testID, ...props }: any) => ( + + ), + Title: ({ children, style, testID, ...props }: any) => ( +
+ {children} +
+ ), + Description: ({ children, style, testID, ...props }: any) => ( +
+ {children} +
+ ), +})); + +jest.mock('@/integrations/haptics', () => ({ + buttonTap: jest.fn(), +})); + +jest.mock('@/config/sentry', () => ({ + captureException: jest.fn(), +})); + +const mockUseNavigation = useNavigation as jest.MockedFunction< + typeof useNavigation +>; + +describe('KYCVerifiedScreen', () => { + 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 display the correct title', () => { + const { root } = render(); + // Title is the first div child + const titleElement = root.findAll( + node => + node.type === 'div' && + node.props.children === 'Your ID has been verified', + )[0]; + expect(titleElement).toBeTruthy(); + }); + + it('should display the correct description text', () => { + const { root } = render(); + // Description is a div with the description text + const descriptionElement = root.findAll( + node => + node.type === 'div' && + node.props.children === + 'Next Self will generate a zk proof specifically for this device that you can use to proof your identity.', + )[0]; + expect(descriptionElement).toBeTruthy(); + }); + + it('should have a "Generate proof" button that is visible', () => { + const { root } = render(); + const buttons = root.findAllByType('button'); + expect(buttons.length).toBeGreaterThan(0); + expect(buttons[0].props.children).toBe('Generate proof'); + }); + + it('should trigger haptic feedback when "Generate proof" is pressed', () => { + const { root } = render(); + const button = root.findAllByType('button')[0]; + + fireEvent.press(button); + + expect(haptics.buttonTap).toHaveBeenCalledTimes(1); + }); + + it('should navigate to ProvingScreenRouter when "Generate proof" is pressed', () => { + const { root } = render(); + const button = root.findAllByType('button')[0]; + + fireEvent.press(button); + + expect(mockNavigate).toHaveBeenCalledWith('ProvingScreenRouter'); + }); + + it('should have navigation available', () => { + render(); + expect(mockUseNavigation).toHaveBeenCalled(); + }); +});