Skip to main content
DOSS uses React Navigation with the native-stack driver for all screen transitions. The navigator tree has three levels: a root stack, conditional auth stacks, and a bottom tab bar.
Root (NavigationContainer)
└── Stack.Navigator
    ├── SPLASH
    ├── ONBOARDING
    └── MAIN_STACK
        └── Stack.Navigator
            ├── AUTHED_STACK   (when loggedIn === true)
            │   └── Stack.Navigator
            │       ├── TAB
            │       │   └── BottomTab.Navigator
            │       │       ├── HOME
            │       │       ├── TRANSACTION_HISTORY
            │       │       ├── PROFILE
            │       │       └── MERCHANT
            │       ├── SCAN
            │       ├── PAY
            │       ├── CONFIRM_PURCHASE
            │       ├── PAYMENT_SUCCESSFUL
            │       ├── SCANNER_PROFILE
            │       ├── MERCHANT
            │       ├── IDENTITY_VERIFICATION
            │       ├── DOSS_NOTIFICATIONS
            │       ├── ALERT_NOTIFICATIONS
            │       ├── PREFERENCES
            │       ├── DOCUMENTS
            │       ├── CUSTOMER_SUPPORT
            │       ├── GROUPS
            │       ├── ENTER_GROUP_PIN
            │       ├── RESET_PIN_INPUT
            │       ├── RESET_CONFIRM_PIN
            │       ├── RESET_PIN_SUCCESS
            │       ├── UPDATE_NAME
            │       ├── UPDATE_EMAIL
            │       ├── UPDATE_NUMBER
            │       ├── PAYMENT_REQUESTED
            │       ├── PAY_ENTER_PIN
            │       ├── REQ_PAYMENT_SUCCESS
            │       └── REQ_PAYMENT_FAIL
            └── UNAUTHED_STACK (when loggedIn === false)
                └── Stack.Navigator  (initialRoute: AUTH_INTRO)
                    ├── AUTH_INTRO
                    ├── LOGIN
                    ├── ENTER_PIN
                    ├── EMAIL_INPUT
                    ├── EMAIL_VERIFY
                    ├── EMAIL_VERIFY_SUCCESS
                    ├── NUMBER_INPUT
                    ├── NUMBER_VERIFY
                    ├── NUMBER_VERIFY_SUCCESS
                    ├── EMAIL_NUMBER_VERIFY_SUCCESS
                    ├── CREATE_PIN_INPUT
                    ├── CONFIRM_PIN
                    ├── CREATE_PIN_SUCCESS
                    └── SIGN_UP_SUCCESS

Root navigator

src/navigation/Root/Root.jsx is the application entry point for navigation. On mount it reads the token key from AsyncStorage to decide the initial auth state, then renders the appropriate stack.
const Root = () => {
  const [initialLoading, setInitalLoading] = useState(false);
  const {setLoggedIn, loggedIn, setUser} = useAuthStore();

  // Fetch user profile once a token is confirmed
  const {data, isPending} = useGetMethod({
    endpoint: endpoints.profile,
    key: endpoints.profile,
    config: { enabled: !!loggedIn },
  });

  useEffect(() => {
    setInitalLoading(true);
    const init = async () => {
      try {
        const token = await storage.getData('token');
        setLoggedIn(!!token);
        setInitalLoading(false);
      } catch (e) {
        setInitalLoading(false);
        setLoggedIn(false);
      }
    };
    init();
  }, [setLoggedIn]);

  useEffect(() => {
    if (data) setUser(data);
  }, [data, setUser]);

  if (initialLoading) return <Loader />;

  return (
    <NavigationContainer
      ref={setNavigationRef}
      fallback={<ActivityIndicator animating />}
      linking={linking}>
      <Stack.Navigator screenOptions={screenOptions}>
        <Stack.Screen name={SPLASH} component={Splash} />
        <Stack.Screen name={ONBOARDING} component={Onboarding} />
        <Stack.Screen name={MAIN_STACK} component={MainStack} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

Main stack — auth gate

MainStack renders either AuthStack or UnAuthStack depending on the loggedIn value from useAuthStore. React Navigation handles the transition automatically when the store value changes.
const MainStack = () => {
  const {loggedIn} = useAuthStore();
  return (
    <Stack.Navigator screenOptions={screenOptions}>
      {loggedIn ? (
        <Stack.Screen name={AUTHED_STACK} component={AuthStack} />
      ) : (
        <Stack.Screen name={UNAUTHED_STACK} component={UnAuthStack} />
      )}
    </Stack.Navigator>
  );
};
Swapping the single screen inside MainStack is the entire auth-gate mechanism. No explicit reset call is needed — React Navigation replaces the stack automatically.

Unauthenticated stack

All registration and login screens. The initial route is AUTH_INTRO.
Route constantScreen componentPurpose
AUTH_INTROAuthIntroWelcome / choose login or sign-up
LOGINLoginInputEnter email/phone
ENTER_PINEnterPinEnter PIN for login
EMAIL_INPUTEmailInputSign-up: enter email
EMAIL_VERIFYEmailVerifySign-up: verify email OTP
EMAIL_VERIFY_SUCCESSEmailVerifySuccessEmail verified confirmation
NUMBER_INPUTNumberInputSign-up: enter phone number
NUMBER_VERIFYNumberVerifySign-up: verify phone OTP
NUMBER_VERIFY_SUCCESSNumberVerifySuccessPhone verified confirmation
EMAIL_NUMBER_VERIFY_SUCCESSEmailNumberVerifySuccessBoth email and phone verified
CREATE_PIN_INPUTCreatePinInputSet new PIN
CONFIRM_PINConfirmPinConfirm PIN
CREATE_PIN_SUCCESSCreatePinSuccessPIN created
SIGN_UP_SUCCESSSignUpSuccessAccount created

Authenticated stack

All screens available after login. The initial route is TAB.
Route constantScreen componentPurpose
TABTabStackBottom tab navigator
SCANScanQR code scanner
PAYPayPayment amount entry
CONFIRM_PURCHASEConfirmPurchaseReview before paying
PAYMENT_SUCCESSFULPaymentSuccessPayment confirmed
SCANNER_PROFILEScannedProfileMerchant profile from QR
MERCHANTMerchantMerchant detail
IDENTITY_VERIFICATIONIdentityVerificationKYC / identity upload
DOSS_NOTIFICATIONSDossNotificationsIn-app DOSS notifications
ALERT_NOTIFICATIONSAlertNotificationsAlert / payment-request notifications
PREFERENCESPreferencesAccount preferences
DOCUMENTSDocumentsAccount security documents
CUSTOMER_SUPPORTCustomerSupportSupport chat
GROUPSGroupsPayment groups
ENTER_GROUP_PINEnterGroupPinPIN for group payment
RESET_PIN_INPUTResetPinInputEnter new PIN
RESET_CONFIRM_PINResetConfirmPinConfirm new PIN
RESET_PIN_SUCCESSResetPinSuccessPIN reset confirmed
UPDATE_NAMEUpdateNameChange display name
UPDATE_EMAILUpdateEmailChange email
UPDATE_NUMBERUpdateNumberChange phone number
PAYMENT_REQUESTEDPaymentRequestedIncoming payment request
PAY_ENTER_PINPayEnterPinPIN confirmation for payment
REQ_PAYMENT_SUCCESSReqPaymentSuccessRequest paid successfully
REQ_PAYMENT_FAILRequestedPaymentFailedRequest payment failed

Tab navigator

src/navigation/Tab/Tab.jsx renders a custom bottom bar. The first three tabs (HOME, TRANSACTION_HISTORY, PROFILE) sit inside a rounded pill-shaped container. The fourth tab (MERCHANT) is a standalone icon button outside the pill.
const TabStack = () => (
  <BottomTab.Navigator
    tabBar={props => <CustomTabBar {...props} />}
    initialRouteName={HOME}>
    <BottomTab.Screen name={HOME}               component={Home}               options={{tabBarLabel: 'Home'}} />
    <BottomTab.Screen name={TRANSACTION_HISTORY} component={TransactionHistory} options={{tabBarLabel: 'History'}} />
    <BottomTab.Screen name={PROFILE}             component={Profile}            options={{tabBarLabel: 'Profile'}} />
    <BottomTab.Screen name={MERCHANT}            component={Merchant} />
  </BottomTab.Navigator>
);

Tab icons

TabInactive iconActive icon
HOMETabHomeTabHomeFocused
TRANSACTION_HISTORYTabTransactionTabTransactionFocused
PROFILETabProfileTabProfileFocused
MERCHANTTabMapTabMap (always)
The active tab shows a white pill behind the icon and its label. Inactive tabs show only the icon.

Programmatic navigation

src/utils/helpers/navigate.js holds a module-level ref to the navigation container, enabling imperative navigation from outside React components (e.g. from interceptors or notification handlers).
let navigationRef;

export const setNavigationRef = ref => {
  navigationRef = ref;
};

// Hard reset — replaces the whole stack history
export const navigate = (routeName, params) => {
  navigationRef?.reset({
    index: 1,
    routes: [{ name: routeName }],
  });
};

// Standard push navigation
export const navigateRoute = async (routeName, params) => {
  await navigationRef?.navigate(routeName, { ...params });
};
The ref is attached in Root.jsx:
<NavigationContainer ref={setNavigationRef} ...>

Deep linking

The linking config passed to NavigationContainer maps doss:// URLs to screen paths.
const config = {
  screens: {
    [MAIN_STACK]: {
      screens: {
        [AUTHED_STACK]: {
          screens: {
            [DOSS_NOTIFICATIONS]: 'Notifications',
            [ALERT_NOTIFICATIONS]: 'AlertNotifications',
          },
        },
      },
    },
  },
};

const linking = {
  prefixes: ['doss://'],
  config,
};
Deep link URLScreen
doss://NotificationsDossNotifications
doss://AlertNotificationsAlertNotifications

FCM notification navigation

When a Firebase Cloud Messaging notification opens the app, the app navigates to the notifications screen via a deep link. This is handled at module level in Root.jsx so it runs before any component mounts:
// App opened from a notification while running in background
messaging().onNotificationOpenedApp(rm => {
  Linking.openURL(deep_link.notification);
});

// App opened from a notification while fully quit
messaging()
  .getInitialNotification()
  .then(remoteMessage => {
    if (remoteMessage) {
      Linking.openURL(deep_link.notification);
    }
  });
The deep_link.notification constant resolves to the doss://AlertNotifications (or doss://Notifications) URL defined in ~/utils/constants/country-picker-data/enums.

Build docs developers (and LLMs) love