From 63fd92da95ce674c6601f15bb2256bd4edca591a Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Thu, 5 Feb 2026 09:07:09 -0800 Subject: [PATCH] chore: clean up navigation index (#1703) * abstract nav types * fix points flow callbacks --- app/src/hooks/useEarnPointsFlow.ts | 17 +- app/src/navigation/index.tsx | 169 +-------------- app/src/navigation/types.ts | 204 +++++++++++++++++- app/src/screens/home/PointsInfoScreen.tsx | 8 +- app/tests/src/hooks/useEarnPointsFlow.test.ts | 37 +++- 5 files changed, 245 insertions(+), 190 deletions(-) diff --git a/app/src/hooks/useEarnPointsFlow.ts b/app/src/hooks/useEarnPointsFlow.ts index 53749da9a..c440edc2e 100644 --- a/app/src/hooks/useEarnPointsFlow.ts +++ b/app/src/hooks/useEarnPointsFlow.ts @@ -92,13 +92,22 @@ export const useEarnPointsFlow = ({ }, [hasReferrer, navigation, navigateToPointsProof]); const showPointsInfoScreen = useCallback(() => { - navigation.navigate('PointsInfo', { - showNextButton: true, - onNextButtonPress: () => { + const callbackId = registerModalCallbacks({ + onButtonPress: () => { showPointsDisclosureModal(); }, + onModalDismiss: () => { + if (hasReferrer) { + useUserStore.getState().clearDeepLinkReferrer(); + } + }, }); - }, [navigation, showPointsDisclosureModal]); + + navigation.navigate('PointsInfo', { + showNextButton: true, + callbackId, + }); + }, [hasReferrer, navigation, showPointsDisclosureModal]); const handleReferralFlow = useCallback(async () => { if (!referrer) { diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index 76fa3a846..e37fc45ea 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -13,7 +13,6 @@ import { import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import type { DocumentCategory } from '@selfxyz/common/utils/types'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { DefaultNavBar } from '@/components/navbar'; @@ -28,11 +27,9 @@ import homeScreens from '@/navigation/home'; import onboardingScreens from '@/navigation/onboarding'; import sharedScreens from '@/navigation/shared'; import starfallScreens from '@/navigation/starfall'; +import type { ExplicitRouteParams, OmittedRouteKeys } from '@/navigation/types'; import verificationScreens from '@/navigation/verification'; -import type { ModalNavigationParams } from '@/screens/app/ModalScreen'; -import type { WebViewScreenParams } from '@/screens/shared/WebViewScreen'; import { trackScreenView } from '@/services/analytics'; -import type { ProofHistory } from '@/stores/proofTypes'; export const navigationScreens = { ...appScreens, @@ -58,167 +55,13 @@ const AppNavigation = createNativeStackNavigator({ type BaseRootStackParamList = StaticParamList; -// Explicitly declare route params that are not inferred from initialParams +// Explicitly declare route params that are not inferred from initialParams. +// Route param types are defined in @/navigation/types for better organization. export type RootStackParamList = Omit< BaseRootStackParamList, - | 'AadhaarUpload' - | 'AadhaarUploadError' - | 'AadhaarUploadSuccess' - | 'AccountRecovery' - | 'AccountVerifiedSuccess' - | 'CloudBackupSettings' - | 'ComingSoon' - | 'ConfirmBelonging' - | 'CreateMock' - | 'Disclaimer' - | 'DocumentNFCScan' - | 'DocumentOnboarding' - | 'DocumentSelectorForProving' - | 'ProvingScreenRouter' - | 'Gratification' - | 'Home' - | 'IDPicker' - | 'IdDetails' - | 'KycSuccess' - | 'KYCVerified' - | 'RegistrationFallback' - | 'Loading' - | 'Modal' - | 'MockDataDeepLink' - | 'Points' - | 'PointsInfo' - | 'ProofHistoryDetail' - | 'Prove' - | 'SaveRecoveryPhrase' - | 'WebView' -> & { - // Shared screens - ComingSoon: { - countryCode?: string; - documentCategory?: string; - }; - WebView: WebViewScreenParams; - - // Document screens - IDPicker: { - countryCode: string; - documentTypes: string[]; - }; - ConfirmBelonging: - | { - documentCategory?: DocumentCategory; - signatureAlgorithm?: string; - curveOrExponent?: string; - } - | undefined; - DocumentNFCScan: - | { - passportNumber?: string; - dateOfBirth?: string; - dateOfExpiry?: string; - } - | undefined; - DocumentCameraTrouble: undefined; - DocumentOnboarding: undefined; - - // Aadhaar screens - AadhaarUpload: { - countryCode: string; - }; - AadhaarUploadSuccess: undefined; - AadhaarUploadError: { - errorType: string; - }; - - // Registration Fallback screens - RegistrationFallback: { - errorSource: - | 'mrz_scan_failed' - | 'nfc_scan_failed' - | 'sumsub_initialization' - | 'sumsub_verification'; - countryCode: string; - }; - - // Account/Recovery screens - AccountRecovery: - | { - nextScreen?: string; - } - | undefined; - SaveRecoveryPhrase: - | { - nextScreen?: string; - } - | undefined; - CloudBackupSettings: - | { - nextScreen?: 'SaveRecoveryPhrase'; - returnToScreen?: 'Points'; - } - | undefined; - ProofSettings: undefined; - AccountVerifiedSuccess: undefined; - - // Proof/Verification screens - ProofHistoryDetail: { - data: ProofHistory; - }; - Prove: - | { - scrollOffset?: number; - } - | undefined; - ProvingScreenRouter: undefined; - DocumentSelectorForProving: - | { - documentType?: string; - } - | undefined; - - // App screens - Loading: { - documentCategory?: DocumentCategory; - signatureAlgorithm?: string; - curveOrExponent?: string; - }; - Modal: ModalNavigationParams; - Gratification: { - points?: number; - }; - StarfallPushCode: undefined; - - // Home screens - Home: { - testReferralFlow?: boolean; - }; - Points: undefined; - PointsInfo: - | { - showNextButton?: boolean; - onNextButtonPress?: () => void; - } - | undefined; - IdDetails: undefined; - - // Onboarding screens - Disclaimer: undefined; - KycSuccess: - | { - userId?: string; - } - | undefined; - KYCVerified: - | { - status?: string; - userId?: string; - } - | undefined; - - // Dev screens - CreateMock: undefined; - MockDataDeepLink: undefined; -}; + OmittedRouteKeys +> & + ExplicitRouteParams; export type RootStackScreenProps = NativeStackScreenProps; diff --git a/app/src/navigation/types.ts b/app/src/navigation/types.ts index a31a56e98..c0e7de6dc 100644 --- a/app/src/navigation/types.ts +++ b/app/src/navigation/types.ts @@ -2,18 +2,204 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import type { DocumentCategory } from '@selfxyz/common/types'; +import type { DocumentCategory } from '@selfxyz/common/utils/types'; +import type { ModalNavigationParams } from '@/screens/app/ModalScreen'; +import type { WebViewScreenParams } from '@/screens/shared/WebViewScreen'; +import type { ProofHistory } from '@/stores/proofTypes'; + +// ============================================================================= +// Aadhaar Screens +// ============================================================================= + +export type AadhaarRoutesParamList = { + AadhaarUpload: { + countryCode: string; + }; + AadhaarUploadSuccess: undefined; + AadhaarUploadError: { + errorType: string; + }; +}; + +// ============================================================================= +// Account/Recovery Screens +// ============================================================================= + +export type AccountRoutesParamList = { + AccountRecovery: + | { + nextScreen?: string; + } + | undefined; + SaveRecoveryPhrase: + | { + nextScreen?: string; + } + | undefined; + CloudBackupSettings: + | { + nextScreen?: 'SaveRecoveryPhrase'; + returnToScreen?: 'Points'; + } + | undefined; + ProofSettings: undefined; + AccountVerifiedSuccess: undefined; +}; + +// ============================================================================= +// App Screens +// ============================================================================= + +export type AppRoutesParamList = { + Loading: { + documentCategory?: DocumentCategory; + signatureAlgorithm?: string; + curveOrExponent?: string; + }; + Modal: ModalNavigationParams; + Gratification: { + points?: number; + }; + StarfallPushCode: undefined; +}; + +// ============================================================================= +// Dev Screens +// ============================================================================= + +export type DevRoutesParamList = { + CreateMock: undefined; + MockDataDeepLink: undefined; +}; + +// ============================================================================= +// Document Screens +// ============================================================================= + +export type DocumentRoutesParamList = { + IDPicker: { + countryCode: string; + documentTypes: string[]; + }; + ConfirmBelonging: + | { + documentCategory?: DocumentCategory; + signatureAlgorithm?: string; + curveOrExponent?: string; + } + | undefined; + DocumentNFCScan: + | { + passportNumber?: string; + dateOfBirth?: string; + dateOfExpiry?: string; + } + | undefined; + DocumentCameraTrouble: undefined; + DocumentOnboarding: undefined; + IdDetails: undefined; +}; + +// ============================================================================= +// Combined Types +// ============================================================================= +/** + * All route param types that need to be explicitly defined (not inferred from initialParams). + * This is used to compose RootStackParamList in index.tsx. + */ +export type ExplicitRouteParams = AadhaarRoutesParamList & + AccountRoutesParamList & + AppRoutesParamList & + DevRoutesParamList & + DocumentRoutesParamList & + HomeRoutesParamList & + OnboardingRoutesParamList & + RegistrationRoutesParamList & + SharedRoutesParamList & + VerificationRoutesParamList; + +// ============================================================================= +// Home Screens +// ============================================================================= +export type HomeRoutesParamList = { + Home: { + testReferralFlow?: boolean; + }; + Points: undefined; + PointsInfo: + | { + showNextButton?: boolean; + callbackId?: number; + } + | undefined; +}; + +/** + * Keys that need to be omitted from BaseRootStackParamList before merging with ExplicitRouteParams. + * These are routes whose params are explicitly defined rather than inferred. + */ +export type OmittedRouteKeys = keyof ExplicitRouteParams; + +// ============================================================================= +// Onboarding Screens +// ============================================================================= +export type OnboardingRoutesParamList = { + Disclaimer: undefined; + KycSuccess: + | { + userId?: string; + } + | undefined; + KYCVerified: + | { + status?: string; + userId?: string; + } + | undefined; +}; + +// ============================================================================= +// Registration Fallback Screens +// ============================================================================= +export type RegistrationRoutesParamList = { + RegistrationFallback: { + errorSource: + | 'mrz_scan_failed' + | 'nfc_scan_failed' + | 'sumsub_initialization' + | 'sumsub_verification'; + countryCode: string; + }; +}; + +// ============================================================================= +// Shared Screens +// ============================================================================= export type SharedRoutesParamList = { ComingSoon: { countryCode?: string; - documentCategory?: DocumentCategory; - }; - WebView: { - url: string; - title?: string; - shareTitle?: string; - shareMessage?: string; - shareUrl?: string; + documentCategory?: string; }; + WebView: WebViewScreenParams; +}; + +// ============================================================================= +// Verification/Proof Screens +// ============================================================================= +export type VerificationRoutesParamList = { + ProofHistoryDetail: { + data: ProofHistory; + }; + Prove: + | { + scrollOffset?: number; + } + | undefined; + ProvingScreenRouter: undefined; + DocumentSelectorForProving: + | { + documentType?: string; + } + | undefined; }; diff --git a/app/src/screens/home/PointsInfoScreen.tsx b/app/src/screens/home/PointsInfoScreen.tsx index 9e08d2090..f5d913ac7 100644 --- a/app/src/screens/home/PointsInfoScreen.tsx +++ b/app/src/screens/home/PointsInfoScreen.tsx @@ -22,11 +22,12 @@ import CloudBackupIcon from '@/assets/icons/cloud_backup.svg'; import PushNotificationsIcon from '@/assets/icons/push_notifications.svg'; import StarIcon from '@/assets/icons/star.svg'; import Referral from '@/assets/images/referral.png'; +import { getModalCallbacks } from '@/utils/modalCallbackRegistry'; type PointsInfoScreenProps = StaticScreenProps< | { showNextButton?: boolean; - onNextButtonPress?: () => void; + callbackId?: number; } | undefined >; @@ -90,8 +91,9 @@ const EARN_POINTS_ITEMS = [ const PointsInfoScreen: React.FC = ({ route: { params }, }) => { - const { showNextButton, onNextButtonPress } = params || {}; + const { showNextButton, callbackId } = params || {}; const { left, right, bottom } = useSafeAreaInsets(); + const callbacks = callbackId ? getModalCallbacks(callbackId) : undefined; return ( @@ -138,7 +140,7 @@ const PointsInfoScreen: React.FC = ({ {showNextButton && ( - Next + Next )} diff --git a/app/tests/src/hooks/useEarnPointsFlow.test.ts b/app/tests/src/hooks/useEarnPointsFlow.test.ts index 5b003e3b7..5dd13e265 100644 --- a/app/tests/src/hooks/useEarnPointsFlow.test.ts +++ b/app/tests/src/hooks/useEarnPointsFlow.test.ts @@ -207,12 +207,15 @@ describe('useEarnPointsFlow', () => { expect(mockNavigate).toHaveBeenCalledWith('PointsInfo', { showNextButton: true, - onNextButtonPress: expect.any(Function), + callbackId: expect.any(Number), }); - // We pass onNextButtonPress() that displays the points disclosure modal + // We pass callbackId to retrieve and invoke the callback that displays the points disclosure modal + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + await act(async () => { - await mockNavigate.mock.calls[0][1].onNextButtonPress(); + await callbacks!.onButtonPress(); }); expect(mockNavigate).toHaveBeenCalledWith('Modal', { @@ -243,11 +246,14 @@ describe('useEarnPointsFlow', () => { expect(mockNavigate).toHaveBeenCalledWith('PointsInfo', { showNextButton: true, - onNextButtonPress: expect.any(Function), + callbackId: expect.any(Number), }); + const pointsInfoCallbackId = mockNavigate.mock.calls[0][1].callbackId; + const pointsInfoCallbacks = getModalCallbacks(pointsInfoCallbackId); + await act(async () => { - await mockNavigate.mock.calls[0][1].onNextButtonPress(); + await pointsInfoCallbacks!.onButtonPress(); }); const callbackId = mockNavigate.mock.calls[1][1].callbackId; @@ -290,11 +296,14 @@ describe('useEarnPointsFlow', () => { expect(mockNavigate).toHaveBeenCalledWith('PointsInfo', { showNextButton: true, - onNextButtonPress: expect.any(Function), + callbackId: expect.any(Number), }); + const pointsInfoCallbackId = mockNavigate.mock.calls[0][1].callbackId; + const pointsInfoCallbacks = getModalCallbacks(pointsInfoCallbackId); + await act(async () => { - await mockNavigate.mock.calls[0][1].onNextButtonPress(); + await pointsInfoCallbacks!.onButtonPress(); }); const callbackId = mockNavigate.mock.calls[1][1].callbackId; @@ -662,11 +671,14 @@ describe('useEarnPointsFlow', () => { expect(mockNavigate).toHaveBeenCalledWith('PointsInfo', { showNextButton: true, - onNextButtonPress: expect.any(Function), + callbackId: expect.any(Number), }); + const pointsInfoCallbackId = mockNavigate.mock.calls[0][1].callbackId; + const pointsInfoCallbacks = getModalCallbacks(pointsInfoCallbackId); + await act(async () => { - await mockNavigate.mock.calls[0][1].onNextButtonPress(); + await pointsInfoCallbacks!.onButtonPress(); }); // The function catches errors and returns false, so it should show points disclosure modal @@ -697,11 +709,14 @@ describe('useEarnPointsFlow', () => { expect(mockNavigate).toHaveBeenCalledWith('PointsInfo', { showNextButton: true, - onNextButtonPress: expect.any(Function), + callbackId: expect.any(Number), }); + const pointsInfoCallbackId = mockNavigate.mock.calls[0][1].callbackId; + const pointsInfoCallbacks = getModalCallbacks(pointsInfoCallbackId); + await act(async () => { - await mockNavigate.mock.calls[0][1].onNextButtonPress(); + await pointsInfoCallbacks!.onButtonPress(); }); const callbackId = mockNavigate.mock.calls[1][1].callbackId;