diff --git a/package.json b/package.json index fd4d0b1467a..8c95c272193 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "idpay_api": "https://raw.githubusercontent.com/pagopa/cstar-infrastructure/v6.9.1/src/domains/idpay-app/api/idpay_appio_full/openapi.appio.full.yml", "lollipop_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.25.1-RELEASE/api_lollipop_first_consumer.yaml", "fast_login_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.25.1-RELEASE/openapi/generated/api_fast_login.yaml", - "pagopa_api_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/4cd111e94432ff62580adc391de78a5462a7128e/src/domains/wallet-app/api/payment-wallet/v1/_openapi.json.tpl", - "pagopa_api_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/65878f9913fcc0eaff499ba8a1a20427a412c010/src/domains/ecommerce-app/api/ecommerce-io/v1/_openapi.json.tpl", + "pagopa_api_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/740e7dcc5ea2ea19639316fea6797bbd504dd0ae/src/domains/wallet-app/api/payment-wallet/v1/_openapi.json.tpl", + "pagopa_api_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/5190135ac34791cf66c1986735d4134bcaf4096f/src/domains/ecommerce-app/api/ecommerce-io/v1/_openapi.json.tpl", "private": true, "scripts": { "start": "react-native start", diff --git a/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx b/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx index ec0eebcd348..9ef0c1fd4cd 100644 --- a/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx +++ b/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx @@ -11,6 +11,7 @@ import { isError, isReady } from "../../../../../common/model/RemoteValue"; import { navigateBack } from "../../../../../store/actions/navigation"; import { cgnDetails } from "../../store/actions/details"; import { IOToast } from "../../../../../components/Toast"; +import { skipToastShowingDueToE2ECrash } from "./ToastPatch"; const CgnUnsubscribe = () => { const dispatch = useIODispatch(); @@ -38,7 +39,11 @@ const CgnUnsubscribe = () => { if (isReady(unsubscriptionStatus)) { navigateBack(); dispatch(cgnDetails.request()); - IOToast.success(I18n.t("bonus.cgn.activation.deactivate.toast")); + // This is needed to prevent a crash while running E2E tests. Showing + // the toast causes random crashes upon calling device.reloadReactNative + if (!skipToastShowingDueToE2ECrash) { + IOToast.success(I18n.t("bonus.cgn.activation.deactivate.toast")); + } } if (isError(unsubscriptionStatus) && !isFirstRender.current) { IOToast.error(I18n.t("global.genericError")); diff --git a/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts b/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts new file mode 100644 index 00000000000..cd3658899d7 --- /dev/null +++ b/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts @@ -0,0 +1 @@ +export const skipToastShowingDueToE2ECrash = true; diff --git a/ts/features/bonus/cgn/components/detail/ToastPatch.ts b/ts/features/bonus/cgn/components/detail/ToastPatch.ts new file mode 100644 index 00000000000..b6d723d80f9 --- /dev/null +++ b/ts/features/bonus/cgn/components/detail/ToastPatch.ts @@ -0,0 +1 @@ +export const skipToastShowingDueToE2ECrash = false; diff --git a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts index 27ec8188c1f..642a158fd29 100644 --- a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts +++ b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts @@ -17,16 +17,12 @@ describe("Credit Card onboarding", () => { // Footer, Wallet icon await element(by.text(I18n.t("global.navigator.wallet"))).tap(); - await waitFor( - element(by.text(I18n.t("wallet.newPaymentMethod.add").toUpperCase())) - ) + await waitFor(element(by.id("walletAddNewPaymentMethodTestId"))) .toBeVisible() .withTimeout(e2eWaitRenderTimeout); // Button "+ Add" - await element( - by.text(I18n.t("wallet.newPaymentMethod.add").toUpperCase()) - ).tap(); + await element(by.id("walletAddNewPaymentMethodTestId")).tap(); await waitFor(element(by.text(I18n.t("wallet.paymentMethod")))) .toBeVisible() diff --git a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx b/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx index 959c7e90a41..b3799b6f200 100644 --- a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx +++ b/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx @@ -19,7 +19,7 @@ type Props = { paymentMethod: WalletInfo }; * - global settings (payment capability, favourite, etc.) */ const WalletDetailsPaymentMethodFeatures = ({ paymentMethod }: Props) => { - const isMethodExpired = isPaymentMethodExpired(paymentMethod); + const isMethodExpired = isPaymentMethodExpired(paymentMethod.details); const isIdpayEnabled = useIOSelector(isIdPayEnabledSelector); if (isMethodExpired) { diff --git a/ts/features/walletV3/common/utils/index.ts b/ts/features/walletV3/common/utils/index.ts index 67866723415..812c3a3b7b6 100644 --- a/ts/features/walletV3/common/utils/index.ts +++ b/ts/features/walletV3/common/utils/index.ts @@ -1,25 +1,20 @@ -import _ from "lodash"; -import * as O from "fp-ts/lib/Option"; import { IOLogoPaymentType, IOPaymentLogos, ListItemTransactionStatusWithBadge } from "@pagopa/io-app-design-system"; -import I18n from "i18n-js"; +import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; - -import { isExpiredDate } from "../../../../utils/dates"; +import I18n from "i18n-js"; +import _ from "lodash"; +import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; import { ServiceNameEnum } from "../../../../../definitions/pagopa/walletv3/ServiceName"; -import { PaymentSupportStatus } from "../../../../types/paymentMethodCapabilities"; -import { - TypeEnum, - WalletInfoDetails, - WalletInfoDetails1 -} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; import { ServiceStatusEnum } from "../../../../../definitions/pagopa/walletv3/ServiceStatus"; import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; +import { PaymentSupportStatus } from "../../../../types/paymentMethodCapabilities"; +import { isExpiredDate } from "../../../../utils/dates"; import { findFirstCaseInsensitive } from "../../../../utils/object"; -import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; +import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; import { WalletPaymentPspSortType } from "../../payment/types"; /** @@ -50,16 +45,15 @@ export const getBadgeTextByTransactionStatus = ( * left if expiring date can't be evaluated * @param paymentMethod */ -export const isPaymentMethodExpired = (paymentMethod: WalletInfo): boolean => { - switch (paymentMethod.details?.type) { - case TypeEnum.PAYPAL: - return false; - case TypeEnum.CARDS: - const cardDetails = paymentMethod.details as WalletInfoDetails1; - return isExpiredDate(cardDetails.expiryDate); - } - return false; -}; +export const isPaymentMethodExpired = ( + details?: UIWalletInfoDetails +): boolean => + pipe( + details?.expiryDate, + O.fromNullable, + O.map(isExpiredDate), + O.getOrElse(() => false) + ); /** * true if the given paymentMethod supports the given walletFunction @@ -111,25 +105,25 @@ export const isPaymentSupported = ( }; export const getPaymentLogo = ( - selectedMethod: WalletInfoDetails + details: UIWalletInfoDetails ): IOLogoPaymentType | undefined => { - switch (selectedMethod.type) { - case TypeEnum.CARDS: - const cardsType = selectedMethod as WalletInfoDetails1; - const { brand } = cardsType; - return pipe( - brand, - findFirstCaseInsensitive(IOPaymentLogos), - O.fold( - () => undefined, - ([logoName, _]) => logoName - ) - ) as IOLogoPaymentType; - case TypeEnum.PAYPAL: - return "payPal"; - default: - return undefined; + if (details.maskedEmail !== undefined) { + return "payPal"; + } else if (details.maskedNumber !== undefined) { + return "bancomatPay"; + } else if (details.maskedPan !== undefined) { + return pipe( + details.brand, + O.fromNullable, + O.chain(findFirstCaseInsensitive(IOPaymentLogos)), + O.fold( + () => undefined, + ([logoName, _]) => logoName + ) + ) as IOLogoPaymentType; } + + return undefined; }; export const WALLET_PAYMENT_TERMS_AND_CONDITIONS_URL = diff --git a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx index ea637521529..64c923cf7b8 100644 --- a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx +++ b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx @@ -20,12 +20,7 @@ import { walletDetailsInstrumentSelector } from "../store"; import { walletDetailsGetInstrument } from "../store/actions"; -import { - TypeEnum, - WalletInfoDetails, - WalletInfoDetails1, - WalletInfoDetails2 -} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; +import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; export type WalletDetailsScreenNavigationParams = Readonly<{ walletId: string; @@ -36,44 +31,38 @@ export type WalletDetailsScreenRouteProps = RouteProp< "WALLET_DETAILS_SCREEN" >; -const generateCardComponent = (walletDetails: WalletInfoDetails) => { - switch (walletDetails.type) { - case TypeEnum.PAYPAL: - const paypalDetails = walletDetails as WalletInfoDetails2; - return ( - - ); - case TypeEnum.CARDS: - default: - const cardDetails = walletDetails as WalletInfoDetails1; - return ( - - ); +const generateCardComponent = (details: UIWalletInfoDetails) => { + if (details.maskedEmail !== undefined) { + return ( + + ); } + + return ( + + ); }; -const generateCardHeaderTitle = (walletDetails?: WalletInfoDetails) => { - switch (walletDetails?.type) { - case TypeEnum.CARDS: - const cardDetails = walletDetails as WalletInfoDetails1; - const capitalizedCardCircuit = capitalize( - cardDetails.brand.toLowerCase() ?? "" - ); - return `${capitalizedCardCircuit} ••${cardDetails.maskedPan}`; - default: - return ""; +const generateCardHeaderTitle = (details?: UIWalletInfoDetails) => { + if (details?.maskedPan !== undefined) { + const capitalizedCardCircuit = capitalize( + details.brand?.toLowerCase() ?? "" + ); + return `${capitalizedCardCircuit} ••${details.maskedPan}`; } + + return ""; }; /** diff --git a/ts/features/walletV3/details/types/UIWalletInfoDetails.ts b/ts/features/walletV3/details/types/UIWalletInfoDetails.ts new file mode 100644 index 00000000000..56f40ccbd47 --- /dev/null +++ b/ts/features/walletV3/details/types/UIWalletInfoDetails.ts @@ -0,0 +1,40 @@ +import * as t from "io-ts"; +import { + WalletInfoDetails1, + WalletInfoDetails2, + WalletInfoDetails3 +} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; + +/** + * Transforms all required props from WalletInfoDetails1 to partial + */ +export const UIWalletInfoDetails1 = t.partial({ + ...WalletInfoDetails1.types[0].props, + ...WalletInfoDetails1.types[0].props +}); + +/** + * Transforms all required props from WalletInfoDetails2 to partial + */ +export const UIWalletInfoDetails2 = t.partial({ + ...WalletInfoDetails2.types[0].props, + ...WalletInfoDetails2.types[1].props +}); + +/** + * Transforms all required props from WalletInfoDetails3 to partial + */ +export const UIWalletInfoDetails3 = t.partial({ + ...WalletInfoDetails3.types[0].props, + ...WalletInfoDetails3.types[1].props +}); + +/** + * This type is used to bypass the `type` props of {@see WalletInfoDetails} + */ +export const UIWalletInfoDetails = t.intersection( + [UIWalletInfoDetails1, UIWalletInfoDetails2, UIWalletInfoDetails3], + "UIWalletInfoDetails" +); + +export type UIWalletInfoDetails = t.TypeOf; diff --git a/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx b/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx index d7c8a22c6d2..456914c71e8 100644 --- a/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx +++ b/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Body, GradientScrollView, @@ -7,35 +6,29 @@ import { ModuleCheckout, VSpacer } from "@pagopa/io-app-design-system"; -import { useNavigation } from "@react-navigation/native"; import { openAuthenticationSession } from "@pagopa/io-react-native-login-utils"; +import { useNavigation } from "@react-navigation/native"; +import React from "react"; +import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; +import { PaymentRequestsGetResponse } from "../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; +import I18n from "../../../../i18n"; import { AppParamsList, IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; -import { WalletPaymentRoutes } from "../navigation/routes"; +import { format } from "../../../../utils/dates"; +import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import { capitalize } from "../../../../utils/strings"; import { WALLET_PAYMENT_TERMS_AND_CONDITIONS_URL, getPaymentLogo } from "../../common/utils"; -import { format } from "../../../../utils/dates"; -import { capitalize } from "../../../../utils/strings"; -import { PaymentRequestsGetResponse } from "../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; -import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; -import { - TypeEnum, - WalletInfoDetails, - WalletInfoDetails1, - WalletInfoDetails2, - WalletInfoDetails3 -} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; -import I18n from "../../../../i18n"; - -import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; +import { WalletPaymentRoutes } from "../navigation/routes"; import { WalletPaymentTotalAmount } from "./WalletPaymentTotalAmount"; export type WalletPaymentConfirmContentProps = { - paymentMethodDetails: WalletInfoDetails; + paymentMethodDetails: UIWalletInfoDetails; selectedPsp: Bundle; paymentDetails: PaymentRequestsGetResponse; isLoading?: boolean; @@ -127,33 +120,26 @@ export const WalletPaymentConfirmContent = ({ ); }; -const getPaymentSubtitle = (cardDetails: WalletInfoDetails) => { - switch (cardDetails.type) { - case TypeEnum.CARDS: - const cardsDetail = cardDetails as WalletInfoDetails1; - return `${format(cardsDetail.expiryDate, "MM/YY")}`; - case TypeEnum.PAYPAL: - return I18n.t("wallet.onboarding.paypal.name"); - case TypeEnum.BANCOMATPAY: - const bancomatpayDetail = cardDetails as WalletInfoDetails3; - return `${bancomatpayDetail.bankName}`; - default: - return ""; +const getPaymentSubtitle = (details: UIWalletInfoDetails): string => { + if (details.maskedPan !== undefined) { + return `${format(details.expiryDate, "MM/YY")}`; + } else if (details.maskedEmail !== undefined) { + return I18n.t("wallet.onboarding.paypal.name"); + } else if (details.maskedNumber !== undefined) { + return `${details.bankName}`; } + + return ""; }; -const getPaymentTitle = (cardDetails: WalletInfoDetails) => { - switch (cardDetails.type) { - case TypeEnum.CARDS: - const cardsDetail = cardDetails as WalletInfoDetails1; - return `${capitalize(cardsDetail.brand)} ••${cardsDetail.maskedPan}`; - case TypeEnum.PAYPAL: - const paypalDetail = cardDetails as WalletInfoDetails2; - return `${paypalDetail.maskedEmail}`; - case TypeEnum.BANCOMATPAY: - const bancomatpayDetail = cardDetails as WalletInfoDetails3; - return `${bancomatpayDetail.maskedNumber}`; - default: - return ""; +const getPaymentTitle = (details: UIWalletInfoDetails): string => { + if (details.maskedPan !== undefined) { + return `${capitalize(details.brand || "")} ••${details.maskedPan}`; + } else if (details.maskedEmail !== undefined) { + return `${details.maskedEmail}`; + } else if (details.maskedNumber !== undefined) { + return `${details.maskedNumber}`; } + + return ""; }; diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts index d634e782503..2a1a9614566 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts +++ b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts @@ -2,16 +2,17 @@ import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import { call, put } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; +import { + LanguageEnum, + RequestAuthorizationRequest +} from "../../../../../../definitions/pagopa/ecommerce/RequestAuthorizationRequest"; +import { WalletDetailTypeEnum } from "../../../../../../definitions/pagopa/ecommerce/WalletDetailType"; import { SagaCallReturnType } from "../../../../../types/utils"; import { getGenericError, getNetworkError } from "../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; import { PaymentClient } from "../../api/client"; import { walletPaymentAuthorization } from "../../store/actions/networking"; -import { - LanguageEnum, - RequestAuthorizationRequest -} from "../../../../../../definitions/pagopa/ecommerce/RequestAuthorizationRequest"; export function* handleWalletPaymentAuthorization( requestTransactionAuthorization: PaymentClient["requestTransactionAuthorization"], @@ -23,7 +24,10 @@ export function* handleWalletPaymentAuthorization( isAllCCP: true, language: LanguageEnum.IT, pspId: action.payload.pspId, - walletId: action.payload.walletId + details: { + detailType: WalletDetailTypeEnum.wallet, + walletId: action.payload.walletId + } }; const requestTransactionAuthorizationRequest = requestTransactionAuthorization({ diff --git a/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx b/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx index 23c53a5caa0..bed3a5835dc 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx +++ b/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx @@ -20,7 +20,6 @@ import React, { useEffect, useMemo } from "react"; import { View } from "react-native"; import { PaymentMethodResponse } from "../../../../../definitions/pagopa/walletv3/PaymentMethodResponse"; import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; -import { WalletInfoDetails1 } from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; import { @@ -43,6 +42,7 @@ import { } from "../store/selectors"; import { WalletPaymentMissingMethodsError } from "../components/WalletPaymentMissingMethodsError"; import { useWalletPaymentGoBackHandler } from "../hooks/useWalletPaymentGoBackHandler"; +import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; type SavedMethodState = { kind: "saved"; @@ -265,29 +265,29 @@ const mapGenericToRadioItem = ( const mapSavedToRadioItem = ( method: WalletInfo ): RadioItem | undefined => { - switch (method.details?.type) { - case "CARDS": - const cardDetails = method.details as WalletInfoDetails1; - return { - id: method.walletId, - value: `${capitalize(cardDetails.brand)} ••${cardDetails.maskedPan}`, - startImage: getIconWithFallback(cardDetails.brand) - }; - case "PAYPAL": - return { - id: method.walletId, - value: "PayPal", - startImage: getIconWithFallback("paypal") - }; - case "BANCOMATPAY": - return { - id: method.walletId, - value: "BANCOMAT Pay", - startImage: getIconWithFallback("bancomatpay") - }; - default: - return undefined; + const details = method.details as UIWalletInfoDetails; + + if (details.maskedPan !== undefined) { + return { + id: method.walletId, + value: `${capitalize(details.brand)} ••${details.maskedPan}`, + startImage: getIconWithFallback(details.brand) + }; + } else if (details.maskedEmail !== undefined) { + return { + id: method.walletId, + value: "PayPal", + startImage: getIconWithFallback("paypal") + }; + } else if (details.maskedNumber !== undefined) { + return { + id: method.walletId, + value: "BANCOMAT Pay", + startImage: getIconWithFallback("bancomatpay") + }; } + + return undefined; }; const isMethodDisabledForAmount = ( diff --git a/ts/navigation/components/HeaderFirstLevelHandler.tsx b/ts/navigation/components/HeaderFirstLevelHandler.tsx index 136922219fd..4e6186f6aaf 100644 --- a/ts/navigation/components/HeaderFirstLevelHandler.tsx +++ b/ts/navigation/components/HeaderFirstLevelHandler.tsx @@ -166,10 +166,12 @@ export const HeaderFirstLevelHandler = () => { type: "twoActions", firstAction: helpAction, backgroundColor: "dark", + testID: "wallet-home-header-title", secondAction: { icon: "add", accessibilityLabel: I18n.t("wallet.accessibility.addElement"), - onPress: presentWalletHomeHeaderBottomsheet + onPress: presentWalletHomeHeaderBottomsheet, + testID: "walletAddNewPaymentMethodTestId" } }; break; diff --git a/ts/utils/permission.ts b/ts/utils/permission.ts index 3837a443b45..098e301df12 100644 --- a/ts/utils/permission.ts +++ b/ts/utils/permission.ts @@ -11,6 +11,11 @@ export const requestIOPermission = async ( permission: RNPermissions.Permission, rationale?: RNPermissions.Rationale ): Promise => { + // Be aware that some permissions may return "unavailable" event if the library + // documents them as supported. One notorious case is the iOS PHOTO_LIBRARY_ADD_ONLY + // permission. If such permission is automatically handled by the system upon request + // (such as PHOTO_LIBRARY_ADD_ONLY is), then you should not use this function to + // check nor to request such permission const checkResult = await RNPermissions.check(permission); if (checkResult === "granted") { return true; @@ -83,8 +88,10 @@ export const requestSaveToGalleryPermission = async ( RNPermissions.PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE, rationale ), - ios: requestIOPermission( - RNPermissions.PERMISSIONS.IOS.PHOTO_LIBRARY_ADD_ONLY - ), + // on iOS the permission is handled by adding NSPhotoLibraryAddUsageDescription and NSPhotoLibraryUsageDescription + // into the Info.plist file and the permission request is automatically handled by the system when using the + // Cameraroll.save method. Asking for the PHOTO_LIBRARY_ADD_ONLY permission results in an "unavailable" response + // from the react-native-permissions library (even if the library documentation declares that it is supported) so + // it cannot be used to determine the permission status. default: Promise.resolve(true) });