chore: clean up navigation index (#1703)

* abstract nav types

* fix points flow callbacks
This commit is contained in:
Justin Hernandez
2026-02-05 09:07:09 -08:00
committed by GitHub
parent 2ccc6600cb
commit 63fd92da95
5 changed files with 245 additions and 190 deletions

View File

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

View File

@@ -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<typeof AppNavigation>;
// 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<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;

View File

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

View File

@@ -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<PointsInfoScreenProps> = ({
route: { params },
}) => {
const { showNextButton, onNextButtonPress } = params || {};
const { showNextButton, callbackId } = params || {};
const { left, right, bottom } = useSafeAreaInsets();
const callbacks = callbackId ? getModalCallbacks(callbackId) : undefined;
return (
<YStack flex={1} gap={40} paddingBottom={bottom} backgroundColor={white}>
@@ -138,7 +140,7 @@ const PointsInfoScreen: React.FC<PointsInfoScreenProps> = ({
</ScrollView>
{showNextButton && (
<View paddingTop={20} paddingLeft={20 + left} paddingRight={20 + right}>
<PrimaryButton onPress={onNextButtonPress}>Next</PrimaryButton>
<PrimaryButton onPress={callbacks?.onButtonPress}>Next</PrimaryButton>
</View>
)}
</YStack>

View File

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