Skip to content

Modal getting dismissed in iOS but persistent in Android #51467

@vinvijdev

Description

@vinvijdev

Description

In one of our scenarios, on a button click we open a web link and come back to the screen from where it was triggered. On coming back to the screen we display a modal which is working correctly in Android but in iOS it is displayed and then dismissed. This behaviour was observed after we upgraded react-native version to 0.78.

Steps to reproduce

Given below is the file in which we see the issue

import { useFocusEffect } from '@react-navigation/native'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { Alert } from '../../../components/alert/alert'
import { AlertContent } from '../../../components/alert/alert-content'
import { AlertTitle } from '../../../components/alert/alert-title'
import { Button } from '../../../components/button/button'
import { LinkText } from '../../../components/link-text/link-text'
import { TranslatedText } from '../../../components/translated-text/translated-text'
import useAccessibilityFocus from '../../../navigation/a11y/use-accessibility-focus'
import { ErrorWithCode } from '../../../services/errors/errors'
import { useFaqLink } from '../../../services/faq-configuration/hooks/use-faq-link'
import { useTestIdBuilder } from '../../../services/test-id/test-id'
import { useTranslation } from '../../../services/translation/translation'
import { useTextStyles } from '../../../theme/hooks/use-text-styles'
import { useTheme } from '../../../theme/hooks/use-theme'
import { spacing } from '../../../theme/spacing'
import {
  AA2AcceptTimeout,
  AA2BelowMinAge,
  AA2BelowMinYearOfBirth,
  AA2CardAuthenticityValidationFailed,
  AA2CardDeactivated,
  AA2CardRemoved,
  AA2CardValidationFailed,
  AA2ForeignResidency,
  AA2InitError,
  AA2PseudonymAlreadyInUse,
  AA2SetPinTimeout,
  AA2Timeout,
} from '../errors'
import { useCloseFlow } from '../hooks/use-close-flow'
import { useHandleErrors } from '../hooks/use-handle-errors'

export type EidErrorAlertProps = {
  error: ErrorWithCode | null
  onModalIsVisible?: (isVisible: boolean) => void
  cancelEidFlowAlertVisible?: boolean
  handleUserCancellation?: boolean
  inEidFlow?: boolean
  // Add Loading Animation to Alert as there is a react native issue with multuple modals
  isLoading?: boolean
}

export const EidErrorAlert: React.FC<EidErrorAlertProps> = ({
  error,
  onModalIsVisible,
  cancelEidFlowAlertVisible = false,
  handleUserCancellation = false,
  inEidFlow = true,
  isLoading,
}) => {
  const { buildTestId, addTestIdModifier } = useTestIdBuilder()
  const testID = buildTestId('eid_error_alert')
  const { colors } = useTheme()
  const { t } = useTranslation()
  const [textStyles] = useTextStyles()

  const [focusRef, setFocus] = useAccessibilityFocus()
  useFocusEffect(setFocus)

  const [intError, setIntError] = useState<ErrorWithCode | null>(null)

  useHandleErrors(setIntError, handleUserCancellation, cancelEidFlowAlertVisible, inEidFlow)

  const { closeFlow } = useCloseFlow(inEidFlow)

  useEffect(() => {
    if (error !== null) {
      setIntError(error)
    }
  }, [error])

  useEffect(() => {
    if (onModalIsVisible !== undefined) {
      onModalIsVisible(intError !== null)
    }
  }, [intError, onModalIsVisible])

  const eid_belowMinYearOfBirth_faq_link = useFaqLink('ENTITLED_USER_GROUP')

  const handleClose = useCallback(async () => {
    await closeFlow()
    setIntError(null)
  }, [closeFlow])

  const errorMessage: string | undefined = useMemo(() => {
    if (intError instanceof AA2InitError) {
      return t('eid_error_init_message')
    } else if (intError instanceof AA2BelowMinYearOfBirth) {
      return t('eid_error_belowMinYearOfBirth_message')
    } else if (intError instanceof AA2BelowMinAge) {
      return t('eid_error_belowMinAge_message')
    } else if (intError instanceof AA2ForeignResidency) {
      return t('eid_error_foreignResidency_message')
    } else if (intError instanceof AA2PseudonymAlreadyInUse) {
      return t('eid_error_pseudonymAlreadyInUse_message')
    } else if (intError instanceof AA2CardDeactivated) {
      return t('eid_error_cardDeactivated_message')
    } else if (intError instanceof AA2Timeout) {
      return t('eid_error_timeout_message')
    } else if (intError instanceof AA2CardRemoved) {
      return t('eid_error_cardRemoved_message')
    } else if (intError instanceof AA2CardValidationFailed) {
      return t('eid_error_cardValidationFailed_message')
    } else if (intError instanceof AA2CardAuthenticityValidationFailed) {
      return t('eid_error_cardAuthenticityValidationFailed_message')
    } else if (intError instanceof AA2AcceptTimeout) {
      return t('eid_error_acceptTimeout_message')
    } else if (intError instanceof AA2SetPinTimeout) {
      return t('eid_error_setPinTimeout_message')
    }
  }, [intError, t])

  const errorCode: string | undefined = useMemo(() => {
    if (intError === null) {
      return
    } else if (!intError.detailCode) {
      return intError.errorCode
    } else {
      return `${intError.errorCode} - ${intError.detailCode}`
    }
  }, [intError])

  console.log('🔍 Alert visible:', intError !== null, 'intError:', intError)

  return (
    <Alert visible={intError !== null} isLoading={isLoading} dismissable={false}>
      <AlertContent ref={focusRef}>
        <AlertTitle i18nKey="eid_error_title" testID={addTestIdModifier(testID, 'title')} />
        {!errorMessage && (
          <TranslatedText
            textStyle="BodyRegular"
            i18nKey="error_alert_message_fallback"
            testID={addTestIdModifier(testID, 'message')}
            textStyleOverrides={{ color: colors.labelColor }}
          />
        )}
        <View style={styles.content}>
          {errorMessage ? (
            <Text
              style={[textStyles.BodyRegular, styles.message, { color: colors.labelColor }]}
              testID={addTestIdModifier(testID, 'message_detail')}>
              {errorMessage}
            </Text>
          ) : (
            <TranslatedText
              i18nKey="eid_error_try_again_message"
              testID={addTestIdModifier(testID, 'try_again_message')}
              textStyle="BodyRegular"
              textStyleOverrides={[styles.message, { color: colors.labelColor }]}
            />
          )}
          <Text
            style={[textStyles.BodyRegular, styles.message, { color: colors.labelColor }]}
            testID={addTestIdModifier(testID, 'code')}>
            {errorCode}
          </Text>
          {intError?.errorDetails ? (
            <Text
              style={[textStyles.BodyRegular, styles.message, { color: colors.labelColor }]}
              testID={addTestIdModifier(testID, 'details')}>
              {intError.errorDetails}
            </Text>
          ) : null}
          {intError instanceof AA2BelowMinYearOfBirth && (
            <View style={styles.textPadding}>
              <LinkText
                testID={buildTestId('eid_belowMinYearOfBirth_faq_link')}
                i18nKey="eid_belowMinYearOfBirth_faq_link"
                link={eid_belowMinYearOfBirth_faq_link}
              />
            </View>
          )}
        </View>
        <Button
          widthOption="stretch"
          variant="primary"
          i18nKey="alert_cta"
          onPress={handleClose}
          testID={addTestIdModifier(testID, 'cta')}
        />
      </AlertContent>
    </Alert>
  )
}

const styles = StyleSheet.create({
  message: {
    textAlign: 'center',
  },
  content: {
    marginBottom: spacing[6],
    gap: spacing[4],
  },
  textPadding: {
    paddingTop: spacing[6],
    justifyContent: 'center',
  },
})

import React, { useCallback, useMemo } from 'react'
import type { PropsWithChildren } from 'react'
import { Modal, type ModalProps } from 'react-native'
import { LoadingIndicatorOverlay } from '../loading-indicator/loading-indicator-overlay'
import { AlertBackdrop } from './alert-backdrop'
import { AlertContainer } from './alert-container'
import { AlertContextImpl } from './alert-context'

export type AlertProps = ModalProps &
  PropsWithChildren<{
    visible: boolean
    onChange?: (visible: boolean) => void
    dismissable?: boolean
    // Add Loading Animation to Alert as there is a react native issue with multuple modals
    isLoading?: boolean
  }>

export const Alert = ({ visible, onChange, children, dismissable, isLoading, ...modalProps }: AlertProps) => {
  const onShow = useCallback(() => onChange?.(true), [onChange])
  const onHide = useCallback(() => onChange?.(false), [onChange])
  const providerValue = useMemo(() => ({ dismiss: onHide }), [onHide])

  return (
    <AlertContextImpl.Provider value={providerValue}>
      <Modal
        // DO NOT USE `animationType`
        // this leads to a ui issue
        // in which the refresh control is not hiding anymore when opening a modal in parallel
        // the workaround is to animate the modal on our own
        // see `AlertContainer`
        presentationStyle="overFullScreen"
        transparent={true}
        visible={visible || isLoading === true}
        onRequestClose={onHide}
        onShow={onShow}
        onDismiss={onHide}
        {...modalProps}>
        {isLoading ? (
          <LoadingIndicatorOverlay />
        ) : (
          <AlertContainer visible={visible}>
            <AlertBackdrop dismissable={dismissable} />
            {children}
          </AlertContainer>
        )}
      </Modal>
    </AlertContextImpl.Provider>
  )
}

React Native Version

0.78.0

Affected Platforms

Runtime - iOS

Areas

Fabric - The New Renderer

Output of npx @react-native-community/cli info

System:
  OS: macOS 15.3.1
  CPU: (10) arm64 Apple M1 Pro
  Memory: 85.89 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 18.18.0
    path: ~/.nvm/versions/node/v18.18.0/bin/node
  Yarn:
    version: 1.22.22
    path: ~/.nvm/versions/node/v18.18.0/bin/yarn
  npm:
    version: 9.8.1
    path: ~/.nvm/versions/node/v18.18.0/bin/npm
  Watchman:
    version: 2025.04.14.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /Users/I583816/.gem/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 24.2
      - iOS 18.2
      - macOS 15.2
      - tvOS 18.2
      - visionOS 2.2
      - watchOS 11.2
  Android SDK:
    API Levels:
      - "28"
      - "30"
      - "31"
      - "32"
      - "33"
      - "33"
      - "34"
      - "35"
    Build Tools:
      - 28.0.3
      - 29.0.2
      - 30.0.3
      - 31.0.0
      - 32.0.0
      - 33.0.0
      - 33.0.1
      - 33.0.2
      - 34.0.0
      - 34.0.0
      - 34.0.0
      - 35.0.0
    System Images:
      - android-29 | Google Play ARM 64 v8a
      - android-30 | Google APIs ARM 64 v8a
      - android-31 | Google APIs ARM 64 v8a
      - android-31 | Google Play ARM 64 v8a
      - android-33 | Google APIs ARM 64 v8a
      - android-34 | Google Play ARM 64 v8a
      - android-35 | Google APIs ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2022.3 AI-223.8836.35.2231.11005911
  Xcode:
    version: 16.2/16C5032a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.9
    path: /Users/I583816/Library/Java/JavaVirtualMachines/corretto-17.0.9/Contents/Home/bin/javac
  Ruby:
    version: 3.1.1
    path: /Users/I583816/.asdf/shims/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 15.0.1
    wanted: 15.0.1
  react:
    installed: 19.0.0
    wanted: 19.0.0
  react-native:
    installed: 0.78.0
    wanted: 0.78.0
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: true
  newArchEnabled: true

Stacktrace or Logs

N/A

MANDATORY Reproducer

https://github.com/vinvijdev/IssueReproducer

Screenshots and Videos

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions