Skip to content

Modal getting dismissed in iOS but persistent in Android #51467

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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions