feat(kyc): register fcm token for sumsub verification (#1673)

* feat(kyc): register fcm token for sumsub verification

* fix tests

* remove unused import

* fix lint
This commit is contained in:
Leszek Stachowski
2026-01-30 18:35:32 +01:00
committed by GitHub
parent f11e860659
commit a6c84d80f7
6 changed files with 240 additions and 21 deletions

View File

@@ -3,8 +3,9 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import { v5 as uuidv5 } from 'uuid';
import { useNavigation } from '@react-navigation/native';
import { render } from '@testing-library/react-native';
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import ErrorBoundary from '@/components/ErrorBoundary';
import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen';
@@ -46,10 +47,6 @@ jest.mock('tamagui', () => ({
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',
@@ -57,17 +54,17 @@ jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({
jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({
AbstractButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="abstract-button" type="button">
<button onClick={onPress} type="button">
{children}
</button>
),
PrimaryButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="primary-button" type="button">
<button onClick={onPress} type="button">
{children}
</button>
),
SecondaryButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="secondary-button" type="button">
<button onClick={onPress} type="button">
{children}
</button>
),
@@ -77,12 +74,23 @@ jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({
),
}));
jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({
ProofEvents: {
FCM_TOKEN_STORED: 'FCM_TOKEN_STORED',
},
}));
jest.mock('@selfxyz/mobile-sdk-alpha/animations/loading/misc.json', () => ({}));
jest.mock('@/integrations/haptics', () => ({
buttonTap: jest.fn(),
}));
jest.mock('@/services/notifications/notificationService', () => ({
requestNotificationPermission: jest.fn(),
getFCMToken: jest.fn(),
registerDeviceToken: jest.fn(),
getSelfUuidNamespace: jest.fn(() => '1eebc0f5-eee9-45a4-9474-a0d103b9f20c'),
}));
jest.mock('@/config/sentry', () => ({
@@ -94,12 +102,36 @@ jest.mock('@/services/analytics', () => ({
trackNfcEvent: jest.fn(),
}));
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
DelayedLottieView: () => null,
useSelfClient: jest.fn(),
}));
jest.mock('@/stores/settingStore', () => ({
useSettingStore: jest.fn(),
}));
const mockUseNavigation = useNavigation as jest.MockedFunction<
typeof useNavigation
>;
// Import mocked modules
const { useSelfClient } = jest.requireMock('@selfxyz/mobile-sdk-alpha');
const { useSettingStore } = jest.requireMock('@/stores/settingStore');
const MOCK_SELF_UUID_NAMESPACE = '1eebc0f5-eee9-45a4-9474-a0d103b9f20c';
describe('KycSuccessScreen', () => {
const mockNavigate = jest.fn();
const mockTrackEvent = jest.fn();
const mockSetFcmToken = jest.fn();
const mockUserId = '19f21362-856a-4606-88e1-fa306036978f';
const mockFcmToken = 'mock-fcm-token';
const mockRoute = {
params: {
userId: mockUserId,
},
};
beforeEach(() => {
jest.clearAllMocks();
@@ -107,23 +139,165 @@ describe('KycSuccessScreen', () => {
mockUseNavigation.mockReturnValue({
navigate: mockNavigate,
} as any);
useSelfClient.mockReturnValue({
trackEvent: mockTrackEvent,
});
useSettingStore.mockReturnValue(mockSetFcmToken);
(
notificationService.requestNotificationPermission as jest.Mock
).mockResolvedValue(true);
(notificationService.getFCMToken as jest.Mock).mockResolvedValue(
mockFcmToken,
);
(notificationService.registerDeviceToken as jest.Mock).mockResolvedValue(
undefined,
);
});
it('should render the screen without errors', () => {
const { root } = render(<KycSuccessScreen />);
const { root } = render(<KycSuccessScreen route={mockRoute} />);
expect(root).toBeTruthy();
});
it('should have navigation available', () => {
render(<KycSuccessScreen />);
render(<KycSuccessScreen route={mockRoute} />);
expect(mockUseNavigation).toHaveBeenCalled();
});
it('should have notification service available', () => {
render(<KycSuccessScreen />);
render(<KycSuccessScreen route={mockRoute} />);
expect(notificationService.requestNotificationPermission).toBeDefined();
});
it('should fetch and register FCM token when "Receive live updates" is pressed', async () => {
const { root } = render(<KycSuccessScreen route={mockRoute} />);
const buttons = root.findAllByType('button');
const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates"
fireEvent.press(receiveUpdatesButton);
await waitFor(() => {
// Verify notification permission was requested
expect(
notificationService.requestNotificationPermission,
).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
// Verify FCM token was fetched
expect(notificationService.getFCMToken).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
// Verify FCM token was stored in settings store
expect(mockSetFcmToken).toHaveBeenCalledWith(mockFcmToken);
});
await waitFor(() => {
// Verify tracking event was sent
expect(mockTrackEvent).toHaveBeenCalledWith('FCM_TOKEN_STORED');
});
await waitFor(() => {
// Verify device token was registered with deterministic session ID
expect(notificationService.registerDeviceToken).toHaveBeenCalledWith(
uuidv5(mockUserId, MOCK_SELF_UUID_NAMESPACE),
mockFcmToken,
);
});
await waitFor(() => {
// Verify navigation to Home screen
expect(mockNavigate).toHaveBeenCalledWith('Home', {});
});
});
it('should navigate to Home without FCM token when permission is denied', async () => {
(
notificationService.requestNotificationPermission as jest.Mock
).mockResolvedValue(false);
const { root } = render(<KycSuccessScreen route={mockRoute} />);
const buttons = root.findAllByType('button');
const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates"
fireEvent.press(receiveUpdatesButton);
await waitFor(() => {
// Verify notification permission was requested
expect(
notificationService.requestNotificationPermission,
).toHaveBeenCalledTimes(1);
});
// Verify FCM token was NOT fetched
expect(notificationService.getFCMToken).not.toHaveBeenCalled();
// Verify FCM token was NOT stored
expect(mockSetFcmToken).not.toHaveBeenCalled();
// Verify device token was NOT registered
expect(notificationService.registerDeviceToken).not.toHaveBeenCalled();
await waitFor(() => {
// Verify navigation to Home screen still happens
expect(mockNavigate).toHaveBeenCalledWith('Home', {});
});
});
it('should navigate to Home when "I will check back later" is pressed', () => {
const { root } = render(<KycSuccessScreen route={mockRoute} />);
const buttons = root.findAllByType('button');
const checkLaterButton = buttons[1]; // Second button is "I will check back later"
fireEvent.press(checkLaterButton);
// Verify navigation to Home screen
expect(mockNavigate).toHaveBeenCalledWith('Home', {});
// Verify FCM-related functions were NOT called
expect(
notificationService.requestNotificationPermission,
).not.toHaveBeenCalled();
expect(notificationService.getFCMToken).not.toHaveBeenCalled();
expect(mockSetFcmToken).not.toHaveBeenCalled();
expect(notificationService.registerDeviceToken).not.toHaveBeenCalled();
});
it('should handle missing userId gracefully', async () => {
const routeWithoutUserId = {
params: {},
};
(
notificationService.requestNotificationPermission as jest.Mock
).mockResolvedValue(true);
const { root } = render(<KycSuccessScreen route={routeWithoutUserId} />);
const buttons = root.findAllByType('button');
const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates"
fireEvent.press(receiveUpdatesButton);
await waitFor(() => {
// Verify notification permission was requested
expect(
notificationService.requestNotificationPermission,
).toHaveBeenCalledTimes(1);
});
// Verify FCM token was NOT fetched (no userId)
expect(notificationService.getFCMToken).not.toHaveBeenCalled();
await waitFor(() => {
// Verify navigation to Home screen still happens
expect(mockNavigate).toHaveBeenCalledWith('Home', {});
});
});
it('renders fallback on render error', () => {
// Mock console.error to suppress error boundary error logs during test
const consoleErrorSpy = jest